mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 11:37:07 +00:00
Compare commits
100
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d5a406cc3 | ||
|
|
c0f4f855af | ||
|
|
7939339a65 | ||
|
|
4ea2db2eda | ||
|
|
f0a2ecddab | ||
|
|
ad73699c30 | ||
|
|
569a1aa674 | ||
|
|
45f63750bc | ||
|
|
00a547668b | ||
|
|
4b82a19493 | ||
|
|
ae6c4006d4 | ||
|
|
a0f04b4157 | ||
|
|
7c8044fa0c | ||
|
|
cb277e3512 | ||
|
|
bfc8b714a9 | ||
|
|
925f7a0127 | ||
|
|
8e082ffcf0 | ||
|
|
e592a33428 | ||
|
|
fa9efc6e17 | ||
|
|
bd8b57ba5f | ||
|
|
d4ffe32534 | ||
|
|
7354816f65 | ||
|
|
12cb3932f3 | ||
|
|
dc64a8daac | ||
|
|
dc524e9cc6 | ||
|
|
4060b1bcd4 | ||
|
|
bfbb440fc2 | ||
|
|
4087861411 | ||
|
|
dc43faafed | ||
|
|
6af97c07d9 | ||
|
|
4c7d920e66 | ||
|
|
8b59d9d307 | ||
|
|
b0fa295595 | ||
|
|
9af432cb0e | ||
|
|
4435ca4cbf | ||
|
|
cd72a11786 | ||
|
|
b398b88385 | ||
|
|
31f6f90206 | ||
|
|
1d8c2f9172 | ||
|
|
f105418bdb | ||
|
|
4a134eb883 | ||
|
|
ec3f251ad8 | ||
|
|
5faa861226 | ||
|
|
88fd1222d6 | ||
|
|
f1ce9449f2 | ||
|
|
7237b3321c | ||
|
|
84d980d883 | ||
|
|
791c96faad | ||
|
|
31473e7e6d | ||
|
|
aa70b368b5 | ||
|
|
d3db569b34 | ||
|
|
94d6b7daf8 | ||
|
|
e089abe982 | ||
|
|
d260b4be6d | ||
|
|
df4835d4f0 | ||
|
|
522689aea6 | ||
|
|
795eb2fdfb | ||
|
|
e1ce7f40d8 | ||
|
|
cac9d753e1 | ||
|
|
9696e9021a | ||
|
|
8e82bc8de1 | ||
|
|
21c4a3b02d | ||
|
|
6a1cb5fc1a | ||
|
|
346316e9a3 | ||
|
|
41aeb2149a | ||
|
|
489d2d84fa | ||
|
|
32c8893ee8 | ||
|
|
20b1c3efc2 | ||
|
|
59975e1742 | ||
|
|
0dc1ae9783 | ||
|
|
a5610d8443 | ||
|
|
0536d4f0dd | ||
|
|
41bcd23edd | ||
|
|
4dbffc8201 | ||
|
|
d2ac3a0b58 | ||
|
|
830bc0d1f3 | ||
|
|
9214e7d3fd | ||
|
|
0c0a0e8794 | ||
|
|
86b3835d54 | ||
|
|
7990cfb8a0 | ||
|
|
c01a0539fe | ||
|
|
cdfe9cb65d | ||
|
|
4258eca938 | ||
|
|
01919230a5 | ||
|
|
2ba6f4cc68 | ||
|
|
c5ae59c4a0 | ||
|
|
1ac1d40d06 | ||
|
|
fb10515cff | ||
|
|
3c85a0180f | ||
|
|
096fa967da | ||
|
|
1ced7dc8f3 | ||
|
|
df1458a136 | ||
|
|
cdd5ebf4c1 | ||
|
|
1a9a11fc31 | ||
|
|
a9430d4778 | ||
|
|
fed5093488 | ||
|
|
6c74fc1a7d | ||
|
|
939f8b1a49 | ||
|
|
1f8c462c42 | ||
|
|
47df026dcc |
No files matched your search
@@ -103,6 +103,7 @@ words:
|
||||
- dearmor
|
||||
- decryptor
|
||||
- dedented
|
||||
- dedup
|
||||
- deleteme
|
||||
- demultiplexer
|
||||
- deserializaton
|
||||
@@ -344,6 +345,7 @@ words:
|
||||
- unambiguity
|
||||
- unauthorizes
|
||||
- unauthorizing
|
||||
- undeserializable
|
||||
- unergonomic
|
||||
- unfetched
|
||||
- unfindable
|
||||
|
||||
@@ -62,6 +62,7 @@ libxrpl.tx > xrpl.protocol
|
||||
libxrpl.tx > xrpl.server
|
||||
libxrpl.tx > xrpl.tx
|
||||
test.app > test.jtx
|
||||
test.app > tests.libxrpl
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
test.app > xrpl.config
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
paths:
|
||||
- "crates/**/Cargo.toml"
|
||||
- "crates/Cargo.lock"
|
||||
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
- should-run
|
||||
- build-test
|
||||
# Only run when committing to a PR that targets a release branch.
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') }}
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# This workflow runs all workflows to build and test the code on various Linux
|
||||
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
|
||||
# the 'develop' or 'release*' branches, or when requested manually. Upon pushes
|
||||
# to the develop branch it also uploads the libxrpl recipe to the Conan remote.
|
||||
# the 'develop', 'release/*' or 'staging/*' branches, or when requested
|
||||
# manually. Upon pushes to the develop branch, it also uploads the libxrpl
|
||||
# recipe to the Conan remote.
|
||||
name: Trigger
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release*"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
paths:
|
||||
# These paths are unique to `on-trigger.yml`.
|
||||
- ".github/workflows/on-trigger.yml"
|
||||
@@ -62,9 +64,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# When a PR is merged into the develop branch it will be assigned a unique
|
||||
# When a PR is merged into the develop branch, it will be assigned a unique
|
||||
# group identifier, so execution will continue even if another PR is merged
|
||||
# while it is still running. In all other cases the group identifier is shared
|
||||
# while it is still running. In all other cases, the group identifier is shared
|
||||
# per branch, so that any in-progress runs are cancelled when a new commit is
|
||||
# pushed.
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
|
||||
@@ -98,7 +100,7 @@ jobs:
|
||||
# However, we do not enable ccache for events targeting a release branch,
|
||||
# to protect against the rare case that the output produced by ccache is
|
||||
# not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
os: ${{ matrix.os }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -8,7 +8,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release*"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -123,13 +123,19 @@ jobs:
|
||||
|
||||
- name: Fix style
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
# pre-commit installs every hook's environment, even for hooks that match
|
||||
# no files, and node-based hooks need libatomic, which this image lacks.
|
||||
# Exit code 1 just means hooks modified files or found issues.
|
||||
env:
|
||||
SKIP: prettier,cspell
|
||||
run: |
|
||||
pre-commit run --all-files || true
|
||||
git add --update
|
||||
pre-commit run || [ $? -eq 1 ]
|
||||
|
||||
- name: Generate git diff
|
||||
if: ${{ steps.files_changed.outcome != 'success' }}
|
||||
run: |
|
||||
git diff | tee "${DIFF_FILE}"
|
||||
git diff HEAD | tee "${DIFF_FILE}"
|
||||
|
||||
- name: Upload clang-tidy diff output
|
||||
if: ${{ github.event.repository.visibility == 'public' && steps.files_changed.outcome != 'success' }}
|
||||
|
||||
@@ -30,12 +30,18 @@ Version 3.5.0 is not yet released.
|
||||
|
||||
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive the same `transaction` message as the `transactions` stream for each validated transaction whose metadata affects a subscribed issuance. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
|
||||
- `ledger_entry`: Add full support for checks, NFT offers, payment channels, and signer lists. ([#6319](https://github.com/XRPLF/rippled/pull/6319))
|
||||
- `channel_authorize`, `channel_verify`: `amount` now also accepts the transaction `Amount` JSON object for a token payment channel (`currency`, `issuer` and `value` for a trust line token; `mpt_issuance_id` and `value` for an MPT), and the claim authorization is serialized for that token. A string of drops is still used for an XRP channel. An object that names XRP, is negative or is malformed returns `channelAmtMalformed`. On the command line, the amount argument may be that JSON object as a single argument. For a token channel the signed message is the `PaymentChannelClaim` hash prefix, the channel ID and the amount serialized as an `Amount` field value without its field header; the XRP message is unchanged. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
|
||||
- `PaymentChannelCreate`, `PaymentChannelFund`, `PaymentChannelClaim`: `Amount` (and `Balance` on `PaymentChannelClaim`) now accept a trust line or MPT value in addition to XRP, gated by the `TokenPaychan` amendment. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
|
||||
- `ledger_entry`, `account_objects`: The `PayChannel` ledger entry now includes optional `TransferRate` and `IssuerNode` fields, present on a channel funded with a trust line or MPT value. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
|
||||
- `account_channels`: For a token payment channel, `amount` and `balance` are `Amount` JSON objects (`currency`, `issuer` and `value` for a trust line token; `mpt_issuance_id` and `value` for an MPT). An XRP channel still returns strings of drops. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
- `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)
|
||||
- `deposit_authorized`: A `credentials` entry that is all zeros (for example `"0"` or 64 `0` characters) now returns a `badCredentials` error instead of looking up a credential that can never exist.
|
||||
- `account_info`, `account_lines`, `account_channels`: 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 a missing or invalid `account`, or `ident` for `account_info`. For `account_lines`, a `peer` that is not a string, or an invalid `limit` or `marker`, is reported before `lgrNotFound`, `actNotFound` or a malformed `account`, and an invalid `limit` or `marker` also before a malformed `peer`. For `account_channels`, a malformed `account` is reported before `lgrNotFound`, and an invalid `destination_account`, `limit` or `marker` before `lgrNotFound` or `actNotFound`. A request with a single problem returns the same error as before. [#8573](https://github.com/XRPLF/rippled/pull/8573)
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
|
||||
@@ -2,33 +2,12 @@
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| These instructions assume you have a C++ development environment ready with Git, Python, Conan, CMake, and a C++ compiler. For help setting one up on Linux, macOS, or Windows, [see this guide](./docs/build/environment.md).<br><br>These instructions also assume a basic familiarity with Conan and CMake. If you are unfamiliar with Conan, you can read our [crash course](./docs/build/conan.md) or the official [Getting Started][conan-getting-started] walkthrough. |
|
||||
|
||||
## Minimum Requirements
|
||||
## Supported platforms
|
||||
|
||||
For the hardware needed to run a node, see
|
||||
[System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
For the software needed to build xrpld, see the
|
||||
[environment setup guide](./docs/build/environment.md).
|
||||
|
||||
## Operating Systems
|
||||
|
||||
### Linux
|
||||
|
||||
The Ubuntu Linux distribution has received the highest level of quality
|
||||
assurance, testing, and support. We also support Red Hat and use Debian
|
||||
internally.
|
||||
Our Linux CI tooling is distro-independent and uses a Nix-based environment, so it should be possible to build on other Linux distributions as well, although we have not tested them.
|
||||
|
||||
### macOS
|
||||
|
||||
Many `xrpld` engineers use macOS for development.
|
||||
The minimum supported version is macOS 15 (Sequoia).
|
||||
CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
|
||||
|
||||
### Windows
|
||||
|
||||
Windows is used by some engineers for development only, and is not recommended
|
||||
for production use.
|
||||
Building and testing are supported on Linux, macOS, and (best effort) Windows;
|
||||
see [Supported development platforms](./docs/build/environment.md#supported-development-platforms).
|
||||
Running `xrpld` is officially supported on Linux only;
|
||||
see [Requirements](./docs/install.md#requirements).
|
||||
|
||||
## Steps
|
||||
|
||||
|
||||
+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)
|
||||
|
||||
+1
-1
@@ -332,7 +332,7 @@ This ensures that configuration changes don't introduce new warnings across the
|
||||
|
||||
### Installing clang-tidy
|
||||
|
||||
See the [environment setup guide](./docs/build/environment.md#clang-tidy) for how to get clang-tidy.
|
||||
The [Nix development shell](./docs/build/nix.md) provides `clang-tidy` at the [tested version](./docs/build/environment.md#tested-compiler-versions).
|
||||
|
||||
### Running clang-tidy locally
|
||||
|
||||
|
||||
@@ -1360,6 +1360,39 @@
|
||||
# Example:
|
||||
# owner_reserve = 200000 # 0.2 XRP
|
||||
#
|
||||
# gas_limit = <gas>
|
||||
#
|
||||
# The gas limit is the maximum amount of gas that can be
|
||||
# consumed by a single transaction. The gas limit is used to prevent
|
||||
# transactions from consuming too many resources.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# gas_limit = 1000000 # 1 million gas
|
||||
#
|
||||
# bytecode_size_limit = <bytes>
|
||||
#
|
||||
# The bytecode size limit is the maximum size of a WASM extension in
|
||||
# bytes. The size limit is used to prevent extensions from consuming
|
||||
# too many resources.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# bytecode_size_limit = 100000 # 100 kb
|
||||
#
|
||||
# gas_price = <micro-drops>
|
||||
#
|
||||
# The gas price is the conversion between WASM gas and its price in drops.
|
||||
#
|
||||
# If this parameter is unspecified, xrpld will use an internal
|
||||
# default. Don't change this without understanding the consequences.
|
||||
#
|
||||
# Example:
|
||||
# gas_price = 1000000 # 1 drop per gas
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 9. Misc Settings
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -297,7 +297,10 @@ if(xrpld)
|
||||
VALUE_TYPE "::json::Value"
|
||||
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
|
||||
HANDLERS
|
||||
account_channels
|
||||
account_currencies
|
||||
account_info
|
||||
account_lines
|
||||
account_nfts
|
||||
account_offers
|
||||
book_changes
|
||||
|
||||
@@ -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)
|
||||
|
||||
+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.23#56e8946f5b3fb949a3732e1a51135ca2%1790977482.197086",
|
||||
"xrpl-rpc-spec/0.1.25#2daa763e2d86ccc4ad0a0fa4d1983e9f%1791907455.579992",
|
||||
"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.23",
|
||||
"xrpl-rpc-spec/0.1.25",
|
||||
"zlib/1.3.2",
|
||||
]
|
||||
|
||||
|
||||
Vendored
+32
-77
@@ -4,6 +4,18 @@ This document explains how to set one up.
|
||||
|
||||
[BUILD.md]: ../../BUILD.md
|
||||
|
||||
## Supported development platforms
|
||||
|
||||
- **Linux**: the [Nix development shell](./nix.md),
|
||||
which provides the same toolchain CI uses on any distribution.
|
||||
- **macOS** 15 (Sequoia) or newer: the [Nix development shell](./nix.md),
|
||||
or the system [Apple Clang](#macos-apple-clang).
|
||||
CI runs on macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15.
|
||||
- **Windows**: best effort, see [Windows](#windows).
|
||||
|
||||
For the platforms supported for running `xrpld`,
|
||||
see [Requirements](../install.md#requirements).
|
||||
|
||||
## Tested compiler versions
|
||||
|
||||
`xrpld` is built in the **C++23** dialect by default, so your toolchain has to
|
||||
@@ -35,20 +47,16 @@ compiler building. Treat support for anything outside the table as best-effort.
|
||||
|
||||
## Required tools
|
||||
|
||||
Besides a compiler, building `xrpld` requires:
|
||||
|
||||
| Tool | Minimum version |
|
||||
| ------------------------------------------- | --------------- |
|
||||
| [Git](https://git-scm.com/downloads) | any recent |
|
||||
| [Python](https://www.python.org/downloads/) | 3.11 |
|
||||
| [Conan](https://conan.io/downloads.html) | 2.17 |
|
||||
| [CMake](https://cmake.org/download/) | 3.16 |
|
||||
Besides a compiler, building `xrpld` requires
|
||||
[Git](https://git-scm.com/downloads),
|
||||
[Python](https://www.python.org/downloads/),
|
||||
[Conan](https://conan.io/downloads.html),
|
||||
and [CMake](https://cmake.org/download/).
|
||||
Building with `-Drust=ON` additionally requires a Rust toolchain,
|
||||
see [Rust](#rust).
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides all of them
|
||||
(see below). On Windows they have to be installed manually.
|
||||
|
||||
Building with `-Drust=ON` additionally requires a Rust toolchain, see
|
||||
[Rust](#rust). A default build does not, so it is not in the table above.
|
||||
at the versions CI uses, recorded in [`nix/check-tools/`](../../nix/check-tools).
|
||||
|
||||
Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
@@ -58,60 +66,19 @@ Once they are in place, verify that everything is installed and runnable with:
|
||||
|
||||
## Linux and macOS
|
||||
|
||||
The **recommended way** to get a development environment on Linux and macOS is
|
||||
the Nix development shell. It provides the exact tooling used in CI — `git`,
|
||||
`python`, `conan`, `cmake`, `clang-tidy`, `clang-format`, and everything else —
|
||||
with a single command and without installing anything system-wide:
|
||||
Use the [Nix development shell](./nix.md):
|
||||
it provides the compiler and every tool CI uses,
|
||||
without installing anything system-wide.
|
||||
|
||||
```bash
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
### macOS: Apple Clang
|
||||
|
||||
On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides
|
||||
Clang. If you instead opt to use your system-wide Apple Clang (via
|
||||
`nix develop .#apple-clang`), you need to manage its version yourself (see
|
||||
below).
|
||||
|
||||
See [Using the Nix development shell](./nix.md) for installation and usage
|
||||
details, including how to select a different compiler and why we recommend Nix
|
||||
over a hand-maintained environment.
|
||||
|
||||
### macOS: managing the Apple Clang version
|
||||
|
||||
If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`),
|
||||
the compiler version is whatever your installed Xcode (or Command Line Tools)
|
||||
provides. The following command should return a version greater than or equal to
|
||||
the [tested one](#tested-compiler-versions):
|
||||
|
||||
```bash
|
||||
clang --version
|
||||
```
|
||||
|
||||
If you develop other applications using Xcode, you might be consistently
|
||||
updating to the newest version of Apple Clang, which will likely cause issues
|
||||
building xrpld. You may want to install and pin a specific version of Xcode:
|
||||
|
||||
1. **Download Xcode**
|
||||
- Visit [Apple Developer Downloads](https://developer.apple.com/download/more/)
|
||||
- Sign in with your Apple Developer account
|
||||
- Search for an Xcode version that includes the expected Apple Clang version
|
||||
- Download the `.xip` file
|
||||
|
||||
2. **Install and configure Xcode**
|
||||
|
||||
```bash
|
||||
# Extract the .xip file and rename for version management
|
||||
# Example: Xcode_16.2.app
|
||||
|
||||
# Move to Applications directory
|
||||
sudo mv Xcode_16.2.app /Applications/
|
||||
|
||||
# Set as default toolchain (persistent)
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
|
||||
|
||||
# Set as environment variable (temporary)
|
||||
export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer
|
||||
```
|
||||
You can also build with the system Apple Clang,
|
||||
installing the other [required tools](#required-tools) any way you like
|
||||
(or getting them from `nix develop .#apple-clang`).
|
||||
Its version is whatever your installed Xcode (or Command Line Tools) provides,
|
||||
so check that `clang --version` is at least the [tested one](#tested-compiler-versions).
|
||||
If several Xcode versions are installed,
|
||||
select one with `sudo xcode-select -s <path>` or the `DEVELOPER_DIR` environment variable.
|
||||
|
||||
## Windows
|
||||
|
||||
@@ -123,8 +90,7 @@ manually:
|
||||
"x64 Native Tools Command Prompt". CI configures CMake with the
|
||||
`Visual Studio 18 2026` generator.
|
||||
- [Git for Windows](https://git-scm.com/download/win)
|
||||
- Python, Conan, and CMake, at the versions listed in
|
||||
[Required tools](#required-tools).
|
||||
- recent releases of Python, Conan, and CMake
|
||||
- a [Rust toolchain](https://rustup.rs) — only needed to build with
|
||||
`-Drust=ON`, see [Rust](#rust)
|
||||
|
||||
@@ -145,16 +111,5 @@ version.
|
||||
Everything else the Rust build needs on the CMake side comes from Conan along
|
||||
with the rest of the dependencies, so there is nothing further to install.
|
||||
|
||||
## Clang-tidy
|
||||
|
||||
`clang-tidy` is required to run static analysis checks locally (see
|
||||
[CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the
|
||||
project. The version this project uses is listed in
|
||||
[Tested compiler versions](#tested-compiler-versions).
|
||||
|
||||
On Linux and macOS, the [Nix development shell](./nix.md) provides that exact
|
||||
version out of the box — run it via `run-clang-tidy`. No separate installation
|
||||
is needed.
|
||||
|
||||
[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23
|
||||
[packages.nix]: ../../nix/packages.nix
|
||||
Vendored
+2
-9
@@ -9,9 +9,7 @@ This guide explains how to use Nix to set up a reproducible development environm
|
||||
- **Reproducible environment**: Everyone gets the same versions of tools and compilers
|
||||
- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment, and CI builds some macOS configurations in it as well
|
||||
- **No system pollution**: Dependencies are isolated and don't affect your system packages
|
||||
- **Consistent compilers**: The GCC and Clang shells use the same versions as CI
|
||||
- **Quick setup**: Get started with a single command
|
||||
- **Works on Linux and macOS**: Consistent experience across platforms
|
||||
|
||||
## Install Nix
|
||||
|
||||
@@ -27,13 +25,8 @@ From the root of the xrpld repository, enter the default development shell:
|
||||
nix --experimental-features 'nix-command flakes' develop
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Download and set up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
- Configure the appropriate compiler for your platform:
|
||||
- **Linux**: GCC (provided by Nix)
|
||||
- **macOS**: Clang (provided by Nix)
|
||||
|
||||
This downloads and sets up all required development tools (CMake, Ninja, Conan, etc.)
|
||||
and the compiler for your platform (see [Platform notes](#platform-notes)).
|
||||
The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster.
|
||||
|
||||
### Platform notes
|
||||
|
||||
@@ -10,6 +10,19 @@ and DNF on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux,
|
||||
where `yum` is a symlink to `dnf`.
|
||||
To build from source instead, see [BUILD.md](../BUILD.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
Linux is the only operating system officially supported for running `xrpld`;
|
||||
macOS and Windows builds are for development only.
|
||||
|
||||
Any x86-64 Linux distribution with APT or DNF and glibc 2.31 or newer should work
|
||||
(see [How libc is handled](../nix/docker/README.md#how-libc-is-handled)).
|
||||
CI tests the packages on Debian 11–13, Ubuntu 20.04–26.04,
|
||||
RHEL (UBI) 9–10, AlmaLinux 9–10, and Rocky Linux 9–10.
|
||||
|
||||
For the hardware needed to run a node,
|
||||
see [System Requirements](https://xrpl.org/system-requirements.html).
|
||||
|
||||
## Release channels
|
||||
|
||||
Packages are published to four channels:
|
||||
|
||||
@@ -352,9 +352,9 @@ class Number final
|
||||
using rep = std::int64_t;
|
||||
using InternalRep = MantissaRange::rep;
|
||||
|
||||
bool negative_{false};
|
||||
InternalRep mantissa_{0};
|
||||
int exponent_{std::numeric_limits<int>::lowest()};
|
||||
bool negative_{false};
|
||||
|
||||
public:
|
||||
// The range for the exponent when normalized
|
||||
@@ -650,8 +650,12 @@ private:
|
||||
shiftExponent(int exponentDelta) const;
|
||||
};
|
||||
|
||||
// The number class currently has 3 tail padding bytes. If you are adding new
|
||||
// members, try to fit them there before expanding the size of the class.
|
||||
static_assert(sizeof(Number) == 16);
|
||||
|
||||
constexpr Number::Number(bool negative, InternalRep mantissa, int exponent, Unchecked) noexcept
|
||||
: negative_(negative), mantissa_{mantissa}, exponent_{exponent}
|
||||
: mantissa_{mantissa}, exponent_{exponent}, negative_(negative)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ struct Keys
|
||||
static constexpr auto kBbtOptions = "bbt_options";
|
||||
static constexpr auto kBgThreads = "bg_threads";
|
||||
static constexpr auto kBlockSize = "block_size";
|
||||
static constexpr auto kBytecodeSizeLimit = "bytecode_size_limit";
|
||||
static constexpr auto kCacheAge = "cache_age";
|
||||
static constexpr auto kCacheMb = "cache_mb";
|
||||
static constexpr auto kCacheSize = "cache_size";
|
||||
@@ -108,6 +109,8 @@ struct Keys
|
||||
static constexpr auto kFileSizeMult = "file_size_mult";
|
||||
static constexpr auto kFilterBits = "filter_bits";
|
||||
static constexpr auto kFilterFull = "filter_full";
|
||||
static constexpr auto kGasLimit = "gas_limit";
|
||||
static constexpr auto kGasPrice = "gas_price";
|
||||
static constexpr auto kHardSet = "hard_set";
|
||||
static constexpr auto kHighThreads = "high_threads";
|
||||
static constexpr auto kHoldTime = "hold_time";
|
||||
|
||||
@@ -257,13 +257,42 @@ public:
|
||||
header_.validated = true;
|
||||
}
|
||||
|
||||
void
|
||||
/**
|
||||
* Mark this ledger as accepted and attempt to make it immutable.
|
||||
*
|
||||
* The close-time fields are recorded first, since the ledger hash covers
|
||||
* them.
|
||||
*
|
||||
* @param closeTime The consensus-agreed close time.
|
||||
* @param closeResolution The close time resolution.
|
||||
* @param correctCloseTime Whether consensus agreed on the close time. If
|
||||
* false, kSLcfNoConsensusTime is recorded in closeFlags instead.
|
||||
* @return What setImmutable() returned.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
bool correctCloseTime);
|
||||
|
||||
void
|
||||
/**
|
||||
* Mark this ledger as immutable, so it can no longer be modified.
|
||||
*
|
||||
* A ledger built or loaded locally cannot have an invalid map, since only
|
||||
* a map syncing against hashes from outside can be proven impossible (see
|
||||
* SHAMap::addKnownNode), so a caller on such a path may treat a false
|
||||
* return as a broken internal invariant. A caller assembling a ledger from
|
||||
* peer data may not: for it, false is an outcome a peer can produce.
|
||||
*
|
||||
* @param rehash Whether to recompute the ledger hash from the header
|
||||
* fields. The transaction and account hashes are recomputed from the
|
||||
* maps too, but only the first time.
|
||||
* @return false if either map is Invalid, leaving the immutable flag as it
|
||||
* was and the header untouched. A map invalidated partway through can
|
||||
* still leave the other one immutable, so a false return means the
|
||||
* ledger must be discarded rather than retried.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setImmutable(bool rehash = true);
|
||||
|
||||
bool
|
||||
@@ -272,23 +301,28 @@ public:
|
||||
return immutable_;
|
||||
}
|
||||
|
||||
/* Mark this ledger as "should be full".
|
||||
/**
|
||||
* @return Whether both maps can still be the maps the header names. See
|
||||
* SHAMap::isValid().
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
mapsValid() const
|
||||
{
|
||||
return txMap_.isValid() && stateMap_.isValid();
|
||||
}
|
||||
|
||||
"Full" is metadata property of the ledger, it indicates
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
|
||||
This is marked `const` because it reflects metadata
|
||||
and not data that is in common with other nodes on the
|
||||
network.
|
||||
*/
|
||||
/**
|
||||
* Mark this ledger as "should be full", indicating that the local server
|
||||
* wants all the corresponding nodes in durable storage.
|
||||
*
|
||||
* Const because it reflects metadata, not data this ledger shares with
|
||||
* other nodes on the network.
|
||||
*/
|
||||
void
|
||||
setFull() const
|
||||
{
|
||||
txMap_.setFull();
|
||||
txMap_.setLedgerSeq(header_.seq);
|
||||
stateMap_.setFull();
|
||||
stateMap_.setLedgerSeq(header_.seq);
|
||||
txMap_.setFull(header_.seq);
|
||||
stateMap_.setFull(header_.seq);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -418,6 +452,22 @@ private:
|
||||
static std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
|
||||
deserializeTxPlusMeta(SHAMapItem const& item);
|
||||
|
||||
/**
|
||||
* Make both maps immutable.
|
||||
*
|
||||
* Each map is settled on its own, so neither is left mid-sync because the
|
||||
* other refused.
|
||||
*
|
||||
* @return Whether both maps are immutable.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setMapsImmutable()
|
||||
{
|
||||
bool const txImmutable = txMap_.setImmutable();
|
||||
bool const stateImmutable = stateMap_.setImmutable();
|
||||
return txImmutable && stateImmutable;
|
||||
}
|
||||
|
||||
bool immutable_;
|
||||
|
||||
// A SHAMap containing the transactions associated with this ledger.
|
||||
|
||||
@@ -265,6 +265,13 @@ canWithdraw(
|
||||
[[nodiscard]] TER
|
||||
canWithdraw(ReadView const& view, STTx const& tx);
|
||||
|
||||
/**
|
||||
* Pays out a withdrawal from a vault or loan broker pseudo-account.
|
||||
*
|
||||
* `sourceAmount` leaves `sourceAcct` and `destinationAmount` reaches `dstAcct`.
|
||||
* The two differ only when the withdrawal carries a transfer fee; the
|
||||
* difference is the fee and is settled through the issuer.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
doWithdraw(
|
||||
ApplyViewContext ctx,
|
||||
@@ -272,7 +279,8 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& amount,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#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/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -50,153 +40,22 @@ escrowLockPreclaimHelper(
|
||||
beast::Journal j);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowLockPreclaimHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const& issue = amount.get<Issue>();
|
||||
auto const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tecNO_PERMISSION
|
||||
if (issuer == account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
|
||||
auto const sleIssuer = view.read(keylet::account(issuer));
|
||||
if (!sleIssuer)
|
||||
return tecNO_ISSUER;
|
||||
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the account does not have a trustline to the issuer, return tecNO_LINE
|
||||
auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency));
|
||||
if (!sleRippleState)
|
||||
return tecNO_LINE;
|
||||
|
||||
STAmount const balance = (*sleRippleState)[sfBalance];
|
||||
|
||||
// If balance is positive, issuer must have higher address than account
|
||||
if (balance > beast::kZero && issuer < account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If balance is negative, issuer must have lower address than account
|
||||
if (balance < beast::kZero && issuer > account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is authorized
|
||||
if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecFROZEN
|
||||
if (isFrozen(view, account, issue))
|
||||
return tecFROZEN;
|
||||
|
||||
// If the issuer has frozen the destination, return tecFROZEN
|
||||
if (isFrozen(view, dest, issue))
|
||||
return tecFROZEN;
|
||||
|
||||
STAmount const spendableAmount =
|
||||
accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
|
||||
|
||||
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::kZero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the amount is not addable to the balance, return tecPRECISION_LOSS
|
||||
if (!canAdd(spendableAmount, amount))
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal j);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowLockPreclaimHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
AccountID const issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tecNO_PERMISSION
|
||||
if (issuer == account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
|
||||
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the issuer is not the same as the issuer of the mpt, return
|
||||
// tecNO_PERMISSION
|
||||
if (sleIssuance->getAccountID(sfIssuer) != issuer)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
|
||||
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is
|
||||
// authorized
|
||||
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecLOCKED
|
||||
if (isFrozen(view, account, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
// If the issuer has frozen the destination, return tecLOCKED
|
||||
if (isFrozen(view, dest, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
// If the mpt cannot be transferred, return tecNO_AUTH
|
||||
if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
view,
|
||||
account,
|
||||
amount.get<MPTIssue>(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j);
|
||||
|
||||
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::kZero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal j);
|
||||
|
||||
template <ValidIssueType T>
|
||||
TER
|
||||
@@ -208,43 +67,22 @@ escrowLockApplyHelper(
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowLockApplyHelper<Issue>(
|
||||
ApplyView& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
STAmount const& amount,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// Defensive: Issuer cannot create an escrow
|
||||
if (issuer == sender)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ter =
|
||||
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowLockApplyHelper<MPTIssue>(
|
||||
ApplyView& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
STAmount const& amount,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// Defensive: Issuer cannot create an escrow
|
||||
if (issuer == sender)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ter = lockEscrowMPT(view, sender, amount, journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal journal);
|
||||
|
||||
template <ValidIssueType T>
|
||||
TER
|
||||
@@ -255,62 +93,20 @@ escrowUnlockPreclaimHelper(
|
||||
bool checkFreeze = true);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowUnlockPreclaimHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
bool checkFreeze)
|
||||
{
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tesSUCCESS
|
||||
if (issuer == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has deep frozen the destination, return tecFROZEN
|
||||
if (checkFreeze &&
|
||||
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.getIssuer()))
|
||||
return tecFROZEN;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
bool checkFreeze);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowUnlockPreclaimHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
bool checkFreeze)
|
||||
{
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tesSUCCESS
|
||||
if (issuer == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecLOCKED
|
||||
if (checkFreeze && isFrozen(view, account, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
bool checkFreeze);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -329,7 +125,7 @@ escrowUnlockApplyHelper(
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowUnlockApplyHelper<Issue>(
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
@@ -340,138 +136,10 @@ escrowUnlockApplyHelper<Issue>(
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
|
||||
if (senderIssuer)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (receiverIssuer)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const& issue = amount.get<Issue>();
|
||||
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
|
||||
bool const recvLow = issuer > receiver;
|
||||
|
||||
if (!ctx.view.exists(trustLineKey) && createAsset)
|
||||
{
|
||||
// Can the account cover the trust line's reserve?
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx,
|
||||
sleDest,
|
||||
xrpBalance,
|
||||
*sponsorSle,
|
||||
{.ownerCountDelta = 1},
|
||||
journal,
|
||||
tecNO_LINE_INSUF_RESERVE);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
JLOG(journal.trace()) << "Trust line does not exist. "
|
||||
"Insufficient reserve to create line.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
Currency const currency = issue.currency;
|
||||
STAmount initialBalance(issue);
|
||||
initialBalance.get<Issue>().account = noAccount();
|
||||
|
||||
if (TER const ter = trustCreate(
|
||||
ctx.view, // payment sandbox
|
||||
recvLow, // is dest low?
|
||||
issuer, // source
|
||||
receiver, // destination
|
||||
trustLineKey.key, // ledger index
|
||||
sleDest, // Account to add to
|
||||
false, // authorize account
|
||||
!sleDest->isFlag(lsfDefaultRipple), //
|
||||
false, // freeze trust line
|
||||
false, // deep freeze trust line
|
||||
initialBalance, // zero initial balance
|
||||
Issue(currency, receiver), // limit of zero
|
||||
0, // quality in
|
||||
0, // quality out
|
||||
*sponsorSle, // sponsor
|
||||
journal); // journal
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
ctx.view.update(sleDest);
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(trustLineKey) && !receiverIssuer)
|
||||
return tecNO_LINE;
|
||||
|
||||
auto const xferRate = transferRate(ctx.view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
|
||||
// validate the line limit if the account submitting txn is not the receiver
|
||||
// of the funds
|
||||
if (!createAsset)
|
||||
{
|
||||
auto const sleRippleState = ctx.view.peek(trustLineKey);
|
||||
if (!sleRippleState)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// if the issuer is the high, then we use the low limit
|
||||
// otherwise we use the high limit
|
||||
STAmount const lineLimit =
|
||||
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
|
||||
|
||||
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
|
||||
|
||||
// flip the sign of the line balance if the issuer is not high
|
||||
if (!recvLow)
|
||||
lineBalance.negate();
|
||||
|
||||
// add the final amount to the line balance
|
||||
lineBalance += finalAmt;
|
||||
|
||||
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
|
||||
if (lineLimit < lineBalance)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
|
||||
// if destination is not the issuer then transfer funds
|
||||
if (!receiverIssuer)
|
||||
{
|
||||
auto const ter = directSendNoFee(ctx.view, issuer, receiver, finalAmt, true, journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowUnlockApplyHelper<MPTIssue>(
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
@@ -482,82 +150,56 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
beast::Journal journal);
|
||||
|
||||
/**
|
||||
* Smart escrow kill switches, driven by the voted FeeSettings.
|
||||
*
|
||||
* Zeroing `bytecodeSizeLimit` stops new uploads while leaving existing escrows
|
||||
* finishable without forcing holders to wait for `CancelAfter`.
|
||||
* Zeroing `gasLimit` stops both.
|
||||
*/
|
||||
/** @{ */
|
||||
inline bool
|
||||
isBytecodeUploadDisabled(Fees const& fees)
|
||||
{
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
return fees.bytecodeSizeLimit == 0 || fees.gasLimit == 0;
|
||||
}
|
||||
|
||||
auto const mptID = amount.get<MPTIssue>().getMptID();
|
||||
auto const issuanceKey = keylet::mptokenIssuance(mptID);
|
||||
auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver);
|
||||
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
|
||||
inline bool
|
||||
isBytecodeExecutionDisabled(Fees const& fees)
|
||||
{
|
||||
return fees.gasLimit == 0;
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* Owner count an escrow costs.
|
||||
*
|
||||
* An escrow costs one increment per started 500 bytes of bytecode, and one
|
||||
* increment if it carries no bytecode.
|
||||
*/
|
||||
template <class T>
|
||||
int32_t
|
||||
calculateAdditionalReserve(T const& finishFunction)
|
||||
{
|
||||
static auto constexpr kBytecodeReserveIncrement = 500;
|
||||
|
||||
if (!finishFunction)
|
||||
return 1;
|
||||
|
||||
auto const size = finishFunction->size();
|
||||
if (size == 0)
|
||||
{
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// update owner count.
|
||||
increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal);
|
||||
// LCOV_EXCL_START
|
||||
// Empty Bytecode is refused at create, so no escrow carries it. Ceiling
|
||||
// division would answer 0, which would subtract less than the create added.
|
||||
UNREACHABLE("xrpl::calculateAdditionalReserve : empty Bytecode");
|
||||
return 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(mptKeylet) && !receiverIssuer)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const xferRate = transferRate(ctx.view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
if (ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
lockedRate >= kParityRate,
|
||||
"xrpl::escrowUnlockApplyHelper<MPTIssue> : lockedRate is at least parity");
|
||||
// MPTs are integral, so round the delivered amount down and
|
||||
// charge any fractional transfer fee to the escrowed amount.
|
||||
auto const delivered =
|
||||
mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false);
|
||||
finalAmt = STAmount(amount.asset(), delivered.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
}
|
||||
return unlockEscrowMPT(
|
||||
ctx.view,
|
||||
sender,
|
||||
receiver,
|
||||
finalAmt,
|
||||
ctx.view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
|
||||
journal);
|
||||
return static_cast<int32_t>((size + kBytecodeReserveIncrement - 1) / kBytecodeReserveIncrement);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -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);
|
||||
@@ -254,28 +330,38 @@ adjustImpreciseNumber(
|
||||
value = 0;
|
||||
}
|
||||
|
||||
inline int
|
||||
getAssetsTotalScale(SLE::ConstRef vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::kMinExponent - 1; // LCOV_EXCL_LINE
|
||||
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// 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));
|
||||
}
|
||||
/**
|
||||
* 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 +465,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,
|
||||
|
||||
@@ -3,9 +3,16 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
@@ -13,17 +20,81 @@
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Close a payment channel and return its remaining funds to the channel owner.
|
||||
* Check that a payment channel transaction may carry a token amount.
|
||||
*
|
||||
* @param slep The SLE for the PayChannel object to close.
|
||||
* @param view The apply view in which ledger state modifications are made.
|
||||
* @param key The ledger key identifying the PayChannel entry.
|
||||
* @param j Journal used for fatal-level diagnostic messages.
|
||||
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
|
||||
* fails; tefINTERNAL if the source account SLE cannot be found.
|
||||
* Token channels rely on fixCleanup3_2_0 and fixTokenEscrowV1, so both must be
|
||||
* enabled alongside featureTokenPaychan.
|
||||
*
|
||||
* @param rules The current ledger rules used to check amendment status.
|
||||
* @return temBAD_AMOUNT if featureTokenPaychan is not enabled; temDISABLED if
|
||||
* fixCleanup3_2_0 or fixTokenEscrowV1 is not enabled; tesSUCCESS otherwise.
|
||||
*/
|
||||
NotTEC
|
||||
checkTokenPayChanRules(Rules const& rules);
|
||||
|
||||
/**
|
||||
* Validate the token amount of a PaymentChannelCreate or PaymentChannelFund
|
||||
* transaction during preflight.
|
||||
*
|
||||
* @param amount The channel or funding amount from the transaction.
|
||||
* @return tesSUCCESS if the amount is valid; temBAD_AMOUNT or temBAD_CURRENCY
|
||||
* otherwise.
|
||||
*/
|
||||
template <ValidIssueType T>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper(STAmount const& amount);
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper<Issue>(STAmount const& amount);
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper<MPTIssue>(STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Check that locking @p amount debits the source's trust line by exactly
|
||||
* @p amount.
|
||||
*
|
||||
* @param view The ledger view holding the source's trust line.
|
||||
* @param account The channel source whose balance is debited.
|
||||
* @param amount The amount to lock in the channel.
|
||||
* @param j Journal passed to the balance lookup.
|
||||
* @return tecPRECISION_LOSS if @p amount is an IOU and the source's balance
|
||||
* less @p amount is not exactly representable; tesSUCCESS otherwise,
|
||||
* including for XRP and MPT amounts.
|
||||
*/
|
||||
TER
|
||||
closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal j);
|
||||
payChanLockPrecisionHelper(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Close a payment channel and return its remaining funds to the channel owner.
|
||||
*
|
||||
* @param slep The SLE for the PayChannel object to close.
|
||||
* @param ctx The apply view context (view and transaction) in which ledger
|
||||
* state modifications are made.
|
||||
* @param key The ledger key identifying the PayChannel entry.
|
||||
* @param txAccount The account submitting the transaction that closes the
|
||||
* channel.
|
||||
* @param submitterPreFeeBalance The submitter's XRP balance before the
|
||||
* transaction fee; the source's balance when the source closes its own
|
||||
* channel and its trust line must be re-created for the refund.
|
||||
* @param j Journal used for fatal-level diagnostic messages.
|
||||
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
|
||||
* fails; tefINTERNAL if the source account SLE cannot be found.
|
||||
*/
|
||||
TER
|
||||
closeChannel(
|
||||
SLE::Ref slep,
|
||||
ApplyViewContext ctx,
|
||||
UInt256 const& key,
|
||||
AccountID const& txAccount,
|
||||
XRPAmount submitterPreFeeBalance,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Add two uint32_t values with saturation at UINT32_MAX.
|
||||
|
||||
@@ -316,6 +316,20 @@ transferRate(ReadView const& view, Asset const& asset);
|
||||
[[nodiscard]] Rate
|
||||
transferRate(ReadView const& view, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Returns the amount a sender must part with for `amount` to reach the
|
||||
* receiver once the issuer takes the transfer fee. Rounds up.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
addTransferFee(STAmount const& amount, Rate const& rate);
|
||||
|
||||
/**
|
||||
* Returns the amount a receiver gets when `amount` leaves the sender and the
|
||||
* issuer takes the transfer fee. Rounds down.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
subtractTransferFee(STAmount const& amount, Rate const& rate);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations (Asset-based dispatchers)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -80,6 +81,21 @@ struct VaultBalanceChange
|
||||
[[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.
|
||||
*
|
||||
@@ -148,18 +164,20 @@ posteriorAssetScale(
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* creditToPosteriorBrokerCoverScale: floors reference + raw at the sum's
|
||||
* own live scale (never finer than baseScale) 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. The sum and its scale
|
||||
* are computed once, in roundingMode, so a Downward credit is always
|
||||
* between zero and raw.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
int baseScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
@@ -333,6 +351,18 @@ vaultBaseScale(Asset const& asset, std::uint8_t scale);
|
||||
[[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.
|
||||
*/
|
||||
@@ -360,35 +390,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.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
|
||||
|
||||
/**
|
||||
* Returns the reference balance a cash outflow's dust check
|
||||
* (debitIsNonZeroDust) should compare against: FixedPrecision Vaults use
|
||||
* AssetsAvailable, the balance the cash actually leaves; Legacy/CashBasis
|
||||
* Vaults use the given AssetsTotal. Shared by VaultWithdraw and
|
||||
* VaultClawback.
|
||||
* Reject a cash outflow that is dust (see debitIsNonZeroDust) or whose check
|
||||
* overflows Number, which is easy to hit with a large Scale.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param assetsTotal The vault's current AssetsTotal, as already read by the
|
||||
* caller.
|
||||
* @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]] Number
|
||||
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal);
|
||||
[[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
|
||||
@@ -483,8 +502,9 @@ decodeVaultVersion(std::optional<std::uint8_t> leVersion);
|
||||
*
|
||||
* @param rules The active ledger rules.
|
||||
*
|
||||
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_2,
|
||||
* fixCleanup3_2_0 and fixCleanup3_4_0 are all enabled; VaultVersion::CashBasis
|
||||
* @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
|
||||
|
||||
@@ -348,16 +348,19 @@ isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's issuer mirror epoch to match the issuance's current issuer key epoch.
|
||||
* @brief Set the holder's issuer mirror epoch, and the issuer mirror key, to match the issuance's
|
||||
* current key.
|
||||
*
|
||||
* Call this after writing the issuer mirror ciphertext under the issuance's
|
||||
* currently registered issuer key, so that the mirror reads as current afterwards.
|
||||
* Also records sfIssuerMirrorEncryptionKey as the issuance's current issuer key
|
||||
* (both fields stay absent at epoch 0, where the original issuer key applies).
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's auditor mirror epoch to match the issuance's current auditor key epoch.
|
||||
@@ -372,16 +375,18 @@ void
|
||||
setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
|
||||
* @brief Set the holder's MPToken mirror epochs, and the issuer mirror key, to match the issuance's
|
||||
* current keys.
|
||||
*
|
||||
* Call this after writing mirror ciphertexts under the issuance's currently
|
||||
* registered keys, so that the mirrors read as current afterwards.
|
||||
* registered keys, so that the mirrors read as current afterwards. Also records
|
||||
* sfIssuerMirrorEncryptionKey via setIssuerMirrorCurrent.
|
||||
*
|
||||
* @param issuance The MPTokenIssuance ledger object.
|
||||
* @param mptoken The holder's MPToken ledger entry to update.
|
||||
*/
|
||||
void
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken);
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken);
|
||||
|
||||
/**
|
||||
* @brief Verifies revealed amount encryptions for all recipients.
|
||||
|
||||
@@ -10,6 +10,25 @@ namespace xrpl {
|
||||
// This was the reference fee units used in the old fee calculation.
|
||||
inline constexpr std::uint32_t kFeeUnitsDeprecated = 10;
|
||||
|
||||
// Number of micro-drops in one drop.
|
||||
inline constexpr std::uint32_t microDropsPerDrop{1'000'000};
|
||||
|
||||
/**
|
||||
* Hard protocol bounds on the Feature Extension fee settings. A voted value
|
||||
* can never fall outside these, so `preflight`, which has no view, may bound
|
||||
* against them. Gas and bytecode limits are capped; gas price has a floor
|
||||
* (zero would make WASM execution effectively free).
|
||||
*/
|
||||
inline constexpr std::uint32_t kMaxGasLimit{2'000'000};
|
||||
inline constexpr std::uint32_t kMaxBytecodeSizeLimit{200'000};
|
||||
inline constexpr std::uint32_t kMinGasPrice{1};
|
||||
|
||||
// The following default values of fee settings will seed into FeeSettings
|
||||
// and write to the ledger on featureSmartEscrow activation.
|
||||
inline constexpr std::uint32_t kDefaultGasLimit{1'000'000};
|
||||
inline constexpr std::uint32_t kDefaultBytecodeSizeLimit{100'000};
|
||||
inline constexpr std::uint32_t kDefaultGasPrice{1'000'000};
|
||||
|
||||
/**
|
||||
* Reflects the fee settings for a particular ledger.
|
||||
*
|
||||
@@ -33,13 +52,47 @@ struct Fees
|
||||
*/
|
||||
XRPAmount increment{0};
|
||||
|
||||
/**
|
||||
* @brief Gas limit for Feature Extensions (instructions).
|
||||
*
|
||||
* This and the one below default to the protocol values rather than 0,
|
||||
* because an explicit 0 is the voted kill switch.
|
||||
*/
|
||||
std::uint32_t gasLimit{kDefaultGasLimit};
|
||||
|
||||
/**
|
||||
* @brief Bytecode size limit for Feature Extensions (bytes).
|
||||
*/
|
||||
std::uint32_t bytecodeSizeLimit{kDefaultBytecodeSizeLimit};
|
||||
|
||||
/**
|
||||
* @brief Price of WASM gas (micro-drops).
|
||||
*/
|
||||
std::uint32_t gasPrice{kDefaultGasPrice};
|
||||
|
||||
explicit Fees() = default;
|
||||
Fees(Fees const&) = default;
|
||||
Fees&
|
||||
operator=(Fees const&) = default;
|
||||
|
||||
Fees(XRPAmount base, XRPAmount reserve, XRPAmount increment)
|
||||
: base(base), reserve(reserve), increment(increment)
|
||||
: base{base}, reserve{reserve}, increment{increment}
|
||||
{
|
||||
}
|
||||
|
||||
Fees(
|
||||
XRPAmount base,
|
||||
XRPAmount reserve,
|
||||
XRPAmount increment,
|
||||
std::uint32_t gasLimit,
|
||||
std::uint32_t bytecodeSizeLimit,
|
||||
std::uint32_t gasPrice)
|
||||
: base{base}
|
||||
, reserve{reserve}
|
||||
, increment{increment}
|
||||
, gasLimit{gasLimit}
|
||||
, bytecodeSizeLimit{bytecodeSizeLimit}
|
||||
, gasPrice{gasPrice}
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Serialize the PaymentChannelClaim authorization message:
|
||||
* HashPrefix::PaymentChannelClaim, the channel key and the drops as a
|
||||
* 64-bit integer.
|
||||
*/
|
||||
inline void
|
||||
serializePayChanAuthorization(Serializer& msg, UInt256 const& key, XRPAmount const& amt)
|
||||
{
|
||||
@@ -15,4 +21,23 @@ serializePayChanAuthorization(Serializer& msg, UInt256 const& key, XRPAmount con
|
||||
msg.add64(amt.drops());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the PaymentChannelClaim authorization message for any asset: a
|
||||
* native amount uses the XRP layout; a token amount follows
|
||||
* HashPrefix::PaymentChannelClaim and the channel key with STAmount::add, the
|
||||
* Amount field value without its field header.
|
||||
*/
|
||||
inline void
|
||||
serializePayChanAuthorization(Serializer& msg, UInt256 const& key, STAmount const& amt)
|
||||
{
|
||||
if (amt.native())
|
||||
{
|
||||
serializePayChanAuthorization(msg, key, amt.xrp());
|
||||
return;
|
||||
}
|
||||
msg.add32(HashPrefix::PaymentChannelClaim);
|
||||
msg.addBitString(key);
|
||||
amt.add(msg);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -317,12 +317,12 @@ constexpr std::uint8_t kVaultDefaultIouScale = 6;
|
||||
* Chosen so 1 IOU can always convert to shares:
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumIouScale = 18;
|
||||
constexpr std::uint8_t kLegacyVaultMaximumIouScale = 18;
|
||||
|
||||
/**
|
||||
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumFixedPrecisionIouScale = 10;
|
||||
constexpr std::uint8_t kFixedPrecisionVaultMaximumIouScale = 10;
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions, persisted as sfLEVersion.
|
||||
@@ -398,6 +398,11 @@ constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono:
|
||||
*/
|
||||
constexpr std::uint8_t kMaxAssetCheckDepth = 5;
|
||||
|
||||
/**
|
||||
* Maximum length of a Data field in Escrow object that can be updated by WASM code.
|
||||
*/
|
||||
constexpr std::size_t kMaxWasmDataLength = 1 * 1024; // 1KB
|
||||
|
||||
/**
|
||||
* A ledger index.
|
||||
*/
|
||||
|
||||
@@ -696,6 +696,16 @@ divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundU
|
||||
STAmount
|
||||
divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
|
||||
|
||||
/**
|
||||
* Returns amt * num / den, rounded in the requested direction.
|
||||
*
|
||||
* Dispatches to the XRPAmount, IOUAmount or MPTAmount overload. XRP and MPT
|
||||
* results are exact; an IOU result is rounded to the 16-digit mantissa in the
|
||||
* requested direction. Only Upward and Downward are supported.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding);
|
||||
|
||||
// Someone is offering X for Y, what is the rate?
|
||||
// Rate: smaller is better, the taker wants the most out: in/out
|
||||
// VFALCO TODO Return a Quality object
|
||||
@@ -795,6 +805,39 @@ canAdd(STAmount const& amt1, STAmount const& amt2);
|
||||
bool
|
||||
canSubtract(STAmount const& amt1, STAmount const& amt2);
|
||||
|
||||
/**
|
||||
* Determine whether `amt1 + amt2` equals the mathematical sum.
|
||||
*
|
||||
* XRP and MPT sums are integer additions, so this is canAdd(). An IOU sum is
|
||||
* rounded to the mantissa width and is exact only if subtracting each
|
||||
* operand from it gives back the other.
|
||||
*
|
||||
* @param amt1 The first addend.
|
||||
* @param amt2 The second addend.
|
||||
* @return true if the sum is exact; false if it rounds, exceeds the largest
|
||||
* amount the asset can represent, or the amounts are not comparable.
|
||||
* Never throws.
|
||||
*/
|
||||
bool
|
||||
isExactSum(STAmount const& amt1, STAmount const& amt2);
|
||||
|
||||
/**
|
||||
* Determine whether `amt1 - amt2` equals the mathematical difference.
|
||||
*
|
||||
* XRP and MPT differences are integer subtractions, so this is canSubtract().
|
||||
* An IOU difference is rounded to the mantissa width and is exact only if
|
||||
* adding the subtrahend back gives the minuend and subtracting the difference
|
||||
* from the minuend gives back the subtrahend.
|
||||
*
|
||||
* @param amt1 The minuend.
|
||||
* @param amt2 The subtrahend.
|
||||
* @return true if the difference is exact; false if it rounds, underflows,
|
||||
* exceeds the largest amount the asset can represent, or the amounts are
|
||||
* not comparable. Never throws.
|
||||
*/
|
||||
bool
|
||||
isExactDifference(STAmount const& amt1, STAmount const& amt2);
|
||||
|
||||
/**
|
||||
* Get the scale of a Number for a given asset.
|
||||
*
|
||||
|
||||
@@ -42,9 +42,9 @@ public:
|
||||
|
||||
STArray&
|
||||
operator=(STArray const&) = default;
|
||||
STArray(STArray&&) noexcept;
|
||||
STArray(STArray&&);
|
||||
STArray&
|
||||
operator=(STArray&&) noexcept;
|
||||
operator=(STArray&&);
|
||||
|
||||
STArray(SField const& f, std::size_t n);
|
||||
STArray(SerialIter& sit, SField const& f, int depth = 0);
|
||||
|
||||
@@ -85,9 +85,9 @@ public:
|
||||
|
||||
STObject&
|
||||
operator=(STObject const&) = default;
|
||||
STObject(STObject&&) noexcept;
|
||||
STObject(STObject&&);
|
||||
STObject&
|
||||
operator=(STObject&& other) noexcept;
|
||||
operator=(STObject&& other);
|
||||
|
||||
STObject(SOTemplate const& type, SField const& name);
|
||||
STObject(
|
||||
|
||||
@@ -133,7 +133,6 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temBAD_MPT,
|
||||
temBAD_CIPHERTEXT,
|
||||
temINVALID_BYTECODE,
|
||||
temTEMP_DISABLED,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -184,6 +183,8 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefBAD_PATH_COUNT,
|
||||
tefNO_BYTECODE,
|
||||
tefBYTECODE_NOT_INCLUDED,
|
||||
tefTEMP_DISABLED,
|
||||
tefEXCEEDS_LIMIT,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -45,11 +45,11 @@ private:
|
||||
public:
|
||||
~STVar();
|
||||
STVar(STVar const& other);
|
||||
STVar(STVar&& other) noexcept;
|
||||
STVar(STVar&& other);
|
||||
STVar&
|
||||
operator=(STVar const& rhs);
|
||||
STVar&
|
||||
operator=(STVar&& rhs) noexcept;
|
||||
operator=(STVar&& rhs);
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
|
||||
STVar(STBase&& t) : p_(t.move(kMaxSize, &d_))
|
||||
|
||||
@@ -23,6 +23,7 @@ XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultN
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (BatchV1_2, Supported::Yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(TokenPaychan, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocolV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -376,6 +376,8 @@ LEDGER_ENTRY(ltPAYCHAN, 0x0078, PayChannel, payment_channel, ({
|
||||
{sfPreviousTxnID, SoeRequired},
|
||||
{sfPreviousTxnLgrSeq, SoeRequired},
|
||||
{sfDestinationNode, SoeOptional},
|
||||
{sfTransferRate, SoeOptional},
|
||||
{sfIssuerNode, SoeOptional},
|
||||
}))
|
||||
|
||||
/** The ledger object which tracks the AMM.
|
||||
@@ -417,6 +419,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfAuditorEncryptionKey, SoeOptional},
|
||||
{sfIssuerKeyEpoch, SoeOptional},
|
||||
{sfAuditorKeyEpoch, SoeOptional},
|
||||
{sfInitialIssuerEncryptionKey, SoeOptional},
|
||||
{sfConfidentialOutstandingAmount, SoeDefault},
|
||||
}))
|
||||
|
||||
@@ -438,6 +441,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfAuditorEncryptedBalance, SoeOptional},
|
||||
{sfIssuerKeyMirrorEpoch, SoeOptional},
|
||||
{sfAuditorKeyMirrorEpoch, SoeOptional},
|
||||
{sfIssuerMirrorEncryptionKey, SoeOptional},
|
||||
{sfHolderEncryptionKey, SoeOptional},
|
||||
{sfRecoveryKey, SoeOptional},
|
||||
}))
|
||||
|
||||
@@ -333,6 +333,8 @@ TYPED_SFIELD(sfAmountCommitment, VL, 45)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBytecode, VL, 47)
|
||||
TYPED_SFIELD(sfRecoveryKey, VL, 48)
|
||||
TYPED_SFIELD(sfInitialIssuerEncryptionKey, VL, 49)
|
||||
TYPED_SFIELD(sfIssuerMirrorEncryptionKey, VL, 50)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -182,7 +182,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet,
|
||||
#endif
|
||||
TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegation::Delegable}), ({
|
||||
{sfDestination, SoeRequired},
|
||||
{sfAmount, SoeRequired},
|
||||
{sfAmount, SoeRequired, SoeMptSupported},
|
||||
{sfSettleDelay, SoeRequired},
|
||||
{sfPublicKey, SoeRequired},
|
||||
{sfCancelAfter, SoeOptional},
|
||||
@@ -195,7 +195,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegatio
|
||||
#endif
|
||||
TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::Delegable}), ({
|
||||
{sfChannel, SoeRequired},
|
||||
{sfAmount, SoeRequired},
|
||||
{sfAmount, SoeRequired, SoeMptSupported},
|
||||
{sfExpiration, SoeOptional},
|
||||
}))
|
||||
|
||||
@@ -205,8 +205,8 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::D
|
||||
#endif
|
||||
TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({.delegable = Delegation::Delegable}), ({
|
||||
{sfChannel, SoeRequired},
|
||||
{sfAmount, SoeOptional},
|
||||
{sfBalance, SoeOptional},
|
||||
{sfAmount, SoeOptional, SoeMptSupported},
|
||||
{sfBalance, SoeOptional, SoeMptSupported},
|
||||
{sfSignature, SoeOptional},
|
||||
{sfPublicKey, SoeOptional},
|
||||
{sfCredentialIDs, SoeOptional},
|
||||
|
||||
@@ -252,6 +252,9 @@ JSS(expected_date); // out: any (warnings)
|
||||
JSS(expected_date_UTC); // out: any (warnings)
|
||||
JSS(expected_ledger_size); // out: TxQ
|
||||
JSS(expiration); // out: AccountOffers, AccountChannels, ValidatorList, amm_info
|
||||
JSS(gas_limit); // out: NetworkOPs
|
||||
JSS(bytecode_size_limit); // out: NetworkOPs
|
||||
JSS(gas_price); // out: NetworkOPs
|
||||
JSS(fail_hard); // in: Sign, Submit
|
||||
JSS(failed); // out: InboundLedger
|
||||
JSS(feature); // in: Feature
|
||||
|
||||
@@ -316,6 +316,30 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerMirrorEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getIssuerMirrorEncryptionKey() const
|
||||
{
|
||||
if (hasIssuerMirrorEncryptionKey())
|
||||
return this->sle_->at(sfIssuerMirrorEncryptionKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerMirrorEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerMirrorEncryptionKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfIssuerMirrorEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfHolderEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -565,6 +589,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerMirrorEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenBuilder&
|
||||
setIssuerMirrorEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerMirrorEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfHolderEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -399,6 +399,30 @@ public:
|
||||
return this->sle_->isFieldPresent(sfAuditorKeyEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfInitialIssuerEncryptionKey (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VL::type::value_type>
|
||||
getInitialIssuerEncryptionKey() const
|
||||
{
|
||||
if (hasInitialIssuerEncryptionKey())
|
||||
return this->sle_->at(sfInitialIssuerEncryptionKey);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfInitialIssuerEncryptionKey is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasInitialIssuerEncryptionKey() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfInitialIssuerEncryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
@@ -670,6 +694,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfInitialIssuerEncryptionKey (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
MPTokenIssuanceBuilder&
|
||||
setInitialIssuerEncryptionKey(std::decay_t<typename SF_VL::type::value_type> const& value)
|
||||
{
|
||||
object_[sfInitialIssuerEncryptionKey] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -287,6 +287,54 @@ public:
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfDestinationNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfTransferRate (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getTransferRate() const
|
||||
{
|
||||
if (hasTransferRate())
|
||||
return this->sle_->at(sfTransferRate);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfTransferRate is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasTransferRate() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfTransferRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerNode (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT64::type::value_type>
|
||||
getIssuerNode() const
|
||||
{
|
||||
if (hasIssuerNode())
|
||||
return this->sle_->at(sfIssuerNode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerNode is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerNode() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfIssuerNode);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -508,6 +556,28 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfTransferRate (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PayChannelBuilder&
|
||||
setTransferRate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfTransferRate] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerNode (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PayChannelBuilder&
|
||||
setIssuerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerNode] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed PayChannel wrapper.
|
||||
* @param index The ledger entry index.
|
||||
|
||||
@@ -60,6 +60,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
@@ -86,6 +87,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get sfBalance (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
@@ -246,6 +248,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelClaimBuilder&
|
||||
@@ -257,6 +260,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set sfBalance (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelClaimBuilder&
|
||||
|
||||
@@ -60,6 +60,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
@@ -207,6 +208,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelCreateBuilder&
|
||||
|
||||
@@ -60,6 +60,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
@@ -155,6 +156,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelFundBuilder&
|
||||
|
||||
+250
-28
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/IntrusivePointer.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/SHAMapHash.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -17,6 +18,7 @@
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -40,7 +42,7 @@ class SHAMapSyncFilter;
|
||||
/**
|
||||
* Describes the current state of a given SHAMap
|
||||
*/
|
||||
enum class SHAMapState {
|
||||
enum class SHAMapState : std::uint8_t {
|
||||
/**
|
||||
* The map is in flux and objects can be added and removed.
|
||||
*
|
||||
@@ -120,16 +122,42 @@ private:
|
||||
*/
|
||||
std::uint32_t cowid_ = 1;
|
||||
|
||||
// ledgerSeq_, state_ and full_ are touched on the nodestore fetch path and on a
|
||||
// getMissingNodes() walk, neither of which may block, so pin them lock-free.
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(std::atomic<SHAMapState>::is_always_lock_free);
|
||||
static_assert(std::atomic<bool>::is_always_lock_free);
|
||||
|
||||
/**
|
||||
* The sequence of the ledger that this map references, if any.
|
||||
*
|
||||
* Written when a map's ledger sequence is established (Ledger::setFull(),
|
||||
* InboundLedger) while a nodestore reader thread reads it. Relaxed either
|
||||
* way: it serves as a lookup hint for a nodestore keyed by hash.
|
||||
*/
|
||||
std::uint32_t ledgerSeq_ = 0;
|
||||
std::atomic<std::uint32_t> ledgerSeq_ = 0;
|
||||
|
||||
SHAMapTreeNodePtr root_;
|
||||
mutable SHAMapState state_;
|
||||
|
||||
/**
|
||||
* The map's state.
|
||||
*
|
||||
* A getMissingNodes() walk writes it, through setInvalid() and
|
||||
* clearSynching(), while whatever drives the acquisition reads it.
|
||||
* The walk runs with the acquisition's lock released, so this is atomic
|
||||
* rather than guarded.
|
||||
*/
|
||||
mutable std::atomic<SHAMapState> state_;
|
||||
SHAMapType const type_;
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
mutable bool full_ = false; // Map is believed complete in database
|
||||
bool backed_ = true; // Map is backed by the database
|
||||
|
||||
/**
|
||||
* Map is believed complete in database.
|
||||
*
|
||||
* Atomic because finishFetch() clears it on any nodestore reader thread,
|
||||
* several of which can run at once.
|
||||
*/
|
||||
mutable std::atomic<bool> full_ = false;
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -152,7 +180,14 @@ public:
|
||||
SHAMap&
|
||||
operator=(SHAMap const&) = delete;
|
||||
|
||||
// Take a snapshot of the given map:
|
||||
/**
|
||||
* Take a snapshot of the given map.
|
||||
*
|
||||
* @param other The map to snapshot. An Invalid source yields an Invalid
|
||||
* snapshot.
|
||||
* @param isMutable Whether the snapshot may be modified. Ignored when other
|
||||
* is Invalid.
|
||||
*/
|
||||
SHAMap(SHAMap const& other, bool isMutable);
|
||||
|
||||
// build new map
|
||||
@@ -190,17 +225,33 @@ public:
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
// Returns a new map that's a snapshot of this one.
|
||||
// Handles copy on write for mutable snapshots.
|
||||
/**
|
||||
* Return a new map that is a snapshot of this one.
|
||||
*
|
||||
* Handles copy on write for mutable snapshots. An invalid map yields an
|
||||
* invalid snapshot.
|
||||
*
|
||||
* @param isMutable Whether the snapshot may be modified.
|
||||
* @return The snapshot.
|
||||
*/
|
||||
std::shared_ptr<SHAMap>
|
||||
snapShot(bool isMutable) const;
|
||||
|
||||
/* Mark this SHAMap as "should be full", indicating
|
||||
that the local server wants all the corresponding nodes
|
||||
in durable storage.
|
||||
*/
|
||||
/**
|
||||
* Mark this map as "should be full": the local server wants every node
|
||||
* it references in durable storage, so a nodestore miss is a gap to report.
|
||||
*
|
||||
* Publishes the ledger sequence before the flag. The sequence store is
|
||||
* relaxed and the flag store is release, and finishFetch() reads the
|
||||
* sequence only after acquiring the flag, so the one miss that withdraws
|
||||
* the flag always reports the sequence stored here. Keeping both stores in
|
||||
* one place is what makes that order impossible to get wrong.
|
||||
*
|
||||
* @param ledgerSeq Sequence of the ledger this map belongs to, which the
|
||||
* gap report names.
|
||||
*/
|
||||
void
|
||||
setFull();
|
||||
setFull(std::uint32_t ledgerSeq);
|
||||
|
||||
void
|
||||
setLedgerSeq(std::uint32_t lseq);
|
||||
@@ -296,9 +347,13 @@ public:
|
||||
* concurrency, to discover nodes referenced in the
|
||||
* SHAMap but not available locally.
|
||||
*
|
||||
* Only a leaf may occupy a position at or beyond kLeafDepth. A map that
|
||||
* breaks that is marked Invalid and the traversal is abandoned, so callers
|
||||
* ask isValid() to tell an empty result from a satisfied map.
|
||||
*
|
||||
* @param maxNodes The maximum number of found nodes to return
|
||||
* @param filter The filter to use when retrieving nodes
|
||||
* @param return The nodes known to be missing
|
||||
* @return The nodes known to be missing, or empty if the map is Invalid
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, UInt256>>
|
||||
getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter);
|
||||
@@ -341,6 +396,9 @@ public:
|
||||
* This function is used when receiving the root node of a SHAMap from a peer during ledger
|
||||
* synchronization. The node must already have been deserialized.
|
||||
*
|
||||
* A root offered under a hash the map does not hold names another tree and
|
||||
* is reported as invalid data. A root matching that hash is a duplicate.
|
||||
*
|
||||
* @param hash The expected hash of the root node.
|
||||
* @param rootNode A deserialized root node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
@@ -360,6 +418,10 @@ public:
|
||||
* is inserted at the position specified by nodeID. The node must already have been
|
||||
* deserialized.
|
||||
*
|
||||
* A node that no valid tree can hold makes the map Invalid, which is
|
||||
* terminal: the root hash committed to an impossible shape, so no peer
|
||||
* can satisfy it.
|
||||
*
|
||||
* @param nodeID The position in the tree where this node belongs.
|
||||
* @param treeNode A deserialized tree node to add.
|
||||
* @param filter Optional sync filter to track received nodes.
|
||||
@@ -375,16 +437,57 @@ public:
|
||||
SHAMapTreeNodePtr treeNode,
|
||||
SHAMapSyncFilter const* filter);
|
||||
|
||||
// status functions
|
||||
void
|
||||
/**
|
||||
* Mark this map as immutable, so it can no longer be modified.
|
||||
*
|
||||
* @return false if the map is Invalid and was left unchanged, true
|
||||
* otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
setImmutable();
|
||||
bool
|
||||
|
||||
/**
|
||||
* @return Whether the map is being synced against a hash it was given, so
|
||||
* its hash is fixed while nodes may still be added.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isSynching() const;
|
||||
|
||||
/**
|
||||
* @return Whether the map is settled, with its hash and nodes fixed.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isImmutable() const;
|
||||
|
||||
/**
|
||||
* Mark this map as syncing, fixing its hash while still allowing missing
|
||||
* nodes to be added.
|
||||
*
|
||||
* The map must be freshly constructed. The body asserts that the map is not
|
||||
* Invalid, so a caller that ignores the precondition stops a build with
|
||||
* assertions enabled.
|
||||
*/
|
||||
void
|
||||
setSynching();
|
||||
|
||||
/**
|
||||
* Mark this map as no longer syncing, so it can be modified again.
|
||||
*
|
||||
* Moves only a Synching map. An Immutable map stays settled, a Modifying
|
||||
* map has nothing to clear, and Invalid is terminal.
|
||||
*/
|
||||
void
|
||||
clearSynching();
|
||||
bool
|
||||
|
||||
/**
|
||||
* Whether the map can still be the map it claims to be.
|
||||
*
|
||||
* A map that is merely missing nodes is valid, and stays valid until
|
||||
* something proves the tree it is syncing against cannot exist.
|
||||
*
|
||||
* @return Whether the map has not been proven impossible.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isValid() const;
|
||||
|
||||
// caution: otherMap must be accessed only by this function
|
||||
@@ -524,6 +627,51 @@ private:
|
||||
using DeltaRef =
|
||||
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
|
||||
|
||||
/**
|
||||
* The sequence of the ledger this map references, read atomically.
|
||||
*
|
||||
* @return The sequence, or zero if the map references no ledger.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
ledgerSeq() const;
|
||||
|
||||
/**
|
||||
* The current state, read atomically.
|
||||
*
|
||||
* Orders state_ alone. The tree's nodes carry no ordering guarantees.
|
||||
*
|
||||
* @return The state as of the call, which a concurrent walk may already
|
||||
* have moved past.
|
||||
*/
|
||||
[[nodiscard]] SHAMapState
|
||||
state() const;
|
||||
|
||||
/**
|
||||
* Record that the map is provably not the one it claims to be.
|
||||
*
|
||||
* Private because only the map itself can prove that, from a node that
|
||||
* contradicts the hashes it is syncing against. Cannot fail, since
|
||||
* Invalid outranks every other state; see trySetState().
|
||||
*/
|
||||
void
|
||||
setInvalid();
|
||||
|
||||
/**
|
||||
* Move the map to a new state, atomically.
|
||||
*
|
||||
* With clearSynching(), the only writer of state_ past construction, so
|
||||
* the order between the states lives in one place: Invalid outranks all
|
||||
* of them and is always stored, while every other transition is refused
|
||||
* once the map is Invalid, which is what makes that verdict terminal.
|
||||
* clearSynching() is narrower and moves only Synching.
|
||||
*
|
||||
* @param desired The state to move to.
|
||||
* @return false if the map is Invalid and the requested state is not,
|
||||
* leaving it unchanged; true otherwise.
|
||||
*/
|
||||
bool
|
||||
trySetState(SHAMapState desired);
|
||||
|
||||
// tree node cache operations
|
||||
SHAMapTreeNodePtr
|
||||
cacheLookup(SHAMapHash const& hash) const;
|
||||
@@ -739,46 +887,120 @@ private:
|
||||
};
|
||||
|
||||
inline void
|
||||
SHAMap::setFull()
|
||||
SHAMap::setFull(std::uint32_t ledgerSeq)
|
||||
{
|
||||
full_ = true;
|
||||
ledgerSeq_.store(ledgerSeq, std::memory_order_relaxed);
|
||||
full_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setLedgerSeq(std::uint32_t lseq)
|
||||
{
|
||||
ledgerSeq_ = lseq;
|
||||
ledgerSeq_.store(lseq, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline void
|
||||
inline std::uint32_t
|
||||
SHAMap::ledgerSeq() const
|
||||
{
|
||||
return ledgerSeq_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline SHAMapState
|
||||
SHAMap::state() const
|
||||
{
|
||||
return state_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::trySetState(SHAMapState desired)
|
||||
{
|
||||
// Invalid is stored outright: a walk reaching that verdict has to win against a thread
|
||||
// settling the map.
|
||||
if (desired == SHAMapState::Invalid)
|
||||
{
|
||||
state_.store(SHAMapState::Invalid, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A failed exchange both reports the state and refreshes expected, so no load is needed
|
||||
// ahead of the loop.
|
||||
auto expected = SHAMapState::Modifying;
|
||||
while (expected != SHAMapState::Invalid)
|
||||
{
|
||||
if (state_.compare_exchange_weak(
|
||||
expected, desired, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::setImmutable()
|
||||
{
|
||||
XRPL_ASSERT(state_ != SHAMapState::Invalid, "xrpl::SHAMap::setImmutable : state is valid");
|
||||
state_ = SHAMapState::Immutable;
|
||||
SOMETIMES(!isValid(), "xrpl::SHAMap::setImmutable : map is invalid");
|
||||
return trySetState(SHAMapState::Immutable);
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isSynching() const
|
||||
{
|
||||
return state_ == SHAMapState::Synching;
|
||||
return state() == SHAMapState::Synching;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isImmutable() const
|
||||
{
|
||||
return state() == SHAMapState::Immutable;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setSynching()
|
||||
{
|
||||
state_ = SHAMapState::Synching;
|
||||
// Guarded, so this is not a way out of Invalid.
|
||||
if (!trySetState(SHAMapState::Synching))
|
||||
{
|
||||
// Only ever called on a freshly constructed map.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::setSynching : map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::clearSynching()
|
||||
{
|
||||
state_ = SHAMapState::Modifying;
|
||||
// Only Synching moves. A walk can end after the ledger settled, and then the map stays
|
||||
// Immutable; a Modifying map has nothing to clear; an invalid map stays invalid. Only that
|
||||
// last refusal is logged, since a concurrent walk is what produces it.
|
||||
auto expected = SHAMapState::Synching;
|
||||
if (state_.compare_exchange_strong(
|
||||
expected, SHAMapState::Modifying, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool const invalid = expected == SHAMapState::Invalid;
|
||||
SOMETIMES(invalid, "xrpl::SHAMap::clearSynching : map is invalid");
|
||||
if (invalid)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Refused to clear synching on an invalid map, root hash "
|
||||
<< root_->getHash();
|
||||
}
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMap::isValid() const
|
||||
{
|
||||
return state_ != SHAMapState::Invalid;
|
||||
return state() != SHAMapState::Invalid;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMap::setInvalid()
|
||||
{
|
||||
// Through trySetState() like every other transition, so state_ has one writer funnel.
|
||||
trySetState(SHAMapState::Invalid);
|
||||
}
|
||||
|
||||
inline void
|
||||
|
||||
@@ -31,7 +31,19 @@ private:
|
||||
*/
|
||||
TaggedPointer hashesAndChildren_;
|
||||
|
||||
std::uint32_t fullBelowGen_ = 0;
|
||||
// Pin that wrapping fullBelowGen_ in an atomic keeps the packed layout, and that the load
|
||||
// isFullBelow() does once per node of every walk stays lock-free.
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free);
|
||||
static_assert(sizeof(std::atomic<std::uint32_t>) == sizeof(std::uint32_t));
|
||||
static_assert(alignof(std::atomic<std::uint32_t>) == alignof(std::uint32_t));
|
||||
|
||||
/**
|
||||
* Written from more than one thread, since canonicalization shares a node
|
||||
* between maps and a walk can run with the acquisition lock released (see
|
||||
* SHAMap::state_). Relaxed both ways, since a generation is only compared
|
||||
* for equality and the children it vouches for are published through lock_.
|
||||
*/
|
||||
std::atomic<std::uint32_t> fullBelowGen_ = 0;
|
||||
std::uint16_t isBranch_ = 0;
|
||||
|
||||
/**
|
||||
@@ -204,13 +216,13 @@ SHAMapInnerNode::getBranchCount() const
|
||||
inline bool
|
||||
SHAMapInnerNode::isFullBelow(std::uint32_t generation) const
|
||||
{
|
||||
return fullBelowGen_ == generation;
|
||||
return fullBelowGen_.load(std::memory_order_relaxed) == generation;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMapInnerNode::setFullBelowGen(std::uint32_t gen)
|
||||
{
|
||||
fullBelowGen_ = gen;
|
||||
fullBelowGen_.store(gen, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ReadView;
|
||||
|
||||
/**
|
||||
* @brief Invariants for TransactionProposal ledger entries.
|
||||
*
|
||||
* - Only transaction types implementing proposal lifecycle operations may
|
||||
* create or modify proposals, and a failed one leaves no proposal effect.
|
||||
* - A proposal is deleted only by TransactionProposalCancel or together with
|
||||
* the Ticket it is keyed to, and a deleted Ticket takes the proposal keyed
|
||||
* to it along.
|
||||
* - Collected signer arrays remain sorted, unique, and bounded.
|
||||
*/
|
||||
class ValidTransactionProposal
|
||||
{
|
||||
// The Ticket each deleted proposal was keyed to; a proposal may go with
|
||||
// its Ticket, or by Cancel, and no other way.
|
||||
std::vector<uint256> ticketsOfDeletedProposals_;
|
||||
// The proposal each deleted Ticket was keyed to, whether or not one
|
||||
// existed; none may survive its Ticket.
|
||||
std::vector<Keylet> proposalsOfDeletedTickets_;
|
||||
std::set<uint256> deletedTickets_;
|
||||
std::uint32_t created_ = 0;
|
||||
std::uint32_t modified_ = 0;
|
||||
std::uint32_t deleted_ = 0;
|
||||
bool invalidSignerArrays_ = false;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalize(STTx const& tx, TER result, XRPAmount, ReadView const& view, beast::Journal const& j)
|
||||
const;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -1,12 +1,15 @@
|
||||
#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>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariant.h>
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
#include <xrpl/tx/invariants/DirectoryInvariant.h>
|
||||
#include <xrpl/tx/invariants/FreezeInvariant.h>
|
||||
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
|
||||
@@ -21,6 +24,7 @@
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -363,11 +367,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);
|
||||
@@ -464,6 +484,7 @@ using InvariantChecks = std::tuple<
|
||||
ValidAmounts,
|
||||
ValidMPTTransfer,
|
||||
ObjectHasPseudoAccount,
|
||||
ValidTransactionProposal,
|
||||
SponsorshipOwnerCountsMatch,
|
||||
SponsorshipAccountCountMatchesField>;
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ class ValidMPTIssuance
|
||||
|
||||
std::uint32_t mptokensCreated_ = 0;
|
||||
std::uint32_t mptokensDeleted_ = 0;
|
||||
// holder of the last MPToken created
|
||||
std::optional<AccountID> mptokenCreatedHolder_;
|
||||
// non-MPT transactions may attempt to create
|
||||
// MPToken by an issuer
|
||||
bool mptCreatedByIssuer_ = false;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,11 +20,14 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -45,9 +46,29 @@ public:
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief Compute the First-Loss Capital cover amount for a defaulted Loan on a FixedPrecision
|
||||
* Vault, capped at the LoanBroker's CoverAvailable and floored so that AssetsAvailable + cover
|
||||
* is exactly on its posterior grid.
|
||||
*
|
||||
* The LoanBroker absorbs the rounding. When CoverAvailable - cover needs finer digits than the
|
||||
* broker's posterior grid, it rounds to nearest, as the broker's trust line does, so
|
||||
* CoverAvailable can move by up to half a posterior ulp more or less than the cover. When the
|
||||
* two grids differ, no cover is exact on both sides; the Vault side is kept exact.
|
||||
*
|
||||
* The one exception: when AssetsAvailable has digits finer than the 16-digit cover can carry,
|
||||
* no cover cancels them, and the balance writer rounds AssetsAvailable + cover to nearest,
|
||||
* by at most half a posterior ulp. VaultDeposit and LoanPay credits have the same limit.
|
||||
* @param loanSle The Loan being defaulted.
|
||||
* @param brokerSle The Loan's LoanBroker.
|
||||
* @param vaultSle The LoanBroker's Vault.
|
||||
* @return The cover amount, in the Vault's Asset.
|
||||
*/
|
||||
static STAmount
|
||||
calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle);
|
||||
|
||||
/**
|
||||
* Helper function that might be needed by other transactors
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,9 @@ public:
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
|
||||
@@ -23,9 +23,15 @@ public:
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
|
||||
@@ -52,6 +52,13 @@ private:
|
||||
TER
|
||||
applyFee();
|
||||
|
||||
/**
|
||||
* Write the default Feature Extension fee settings into FeeSettings on
|
||||
* `featureSmartEscrow` activation.
|
||||
*/
|
||||
void
|
||||
initializeVMFees();
|
||||
|
||||
TER
|
||||
applyUNLModify();
|
||||
};
|
||||
|
||||
@@ -200,11 +200,23 @@ Ledger::Ledger(
|
||||
sle->at(sfReserveIncrement) = *f;
|
||||
sle->at(sfReferenceFeeUnits) = kFeeUnitsDeprecated;
|
||||
}
|
||||
if (std::ranges::find(amendments, featureSmartEscrow) != amendments.end())
|
||||
{
|
||||
sle->at(sfGasLimit) = fees.gasLimit;
|
||||
sle->at(sfBytecodeSizeLimit) = fees.bytecodeSizeLimit;
|
||||
sle->at(sfGasPrice) = fees.gasPrice;
|
||||
}
|
||||
rawInsert(sle);
|
||||
}
|
||||
|
||||
stateMap_.flushDirty(NodeObjectType::AccountNode);
|
||||
setImmutable();
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
logicError("Ledger::Ledger(CreateGenesisT, ...): genesis ledger map is invalid");
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
Ledger::Ledger(
|
||||
@@ -215,7 +227,7 @@ Ledger::Ledger(
|
||||
Fees const& fees,
|
||||
Family& family,
|
||||
beast::Journal j)
|
||||
: immutable_(true)
|
||||
: immutable_(false)
|
||||
, txMap_(SHAMapType::TRANSACTION, info.txHash, family)
|
||||
, stateMap_(SHAMapType::STATE, info.accountHash, family)
|
||||
, fees_(fees)
|
||||
@@ -238,8 +250,20 @@ Ledger::Ledger(
|
||||
JLOG(j.warn()) << "Don't have state data root for ledger" << header_.seq;
|
||||
}
|
||||
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
// Loaded locally. See Ledger::setImmutable().
|
||||
if (setMapsImmutable())
|
||||
{
|
||||
immutable_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "Invalid map for ledger " << header_.seq;
|
||||
UNREACHABLE("xrpl::Ledger::Ledger(LedgerHeader const&, ...) : map is invalid");
|
||||
// Treat it as a damaged ledger: the code below recomputes the hash and re-acquires.
|
||||
loaded = false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (!setup())
|
||||
loaded = false;
|
||||
@@ -310,27 +334,50 @@ Ledger::Ledger(
|
||||
setup();
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
Ledger::setImmutable(bool rehash)
|
||||
{
|
||||
// Force update, since this is the only
|
||||
// place the hash transitions to valid
|
||||
if (!immutable_ && rehash)
|
||||
// A map found structurally invalid during sync must never be made immutable: isValid() tests
|
||||
// only for Invalid, and an immutable ledger is treated as persistable. Asked before anything is
|
||||
// written, so a refusal leaves the header exactly as it was rather than half relabeled.
|
||||
if (!mapsValid())
|
||||
return false;
|
||||
|
||||
// Read here but written to the header below, once the maps are settled: getHash() can unshare a
|
||||
// dirty tree, so it has to run while the map is still mutable, while a write to the header must
|
||||
// wait until both maps have made it. Skipped once the ledger is immutable, since its maps can
|
||||
// no longer change.
|
||||
bool const deriveMapHashes = !immutable_ && rehash;
|
||||
UInt256 const txHash = deriveMapHashes ? txMap_.getHash().asUInt256() : UInt256{};
|
||||
UInt256 const accountHash = deriveMapHashes ? stateMap_.getHash().asUInt256() : UInt256{};
|
||||
|
||||
// Both were valid at the check above, but a concurrent walk can invalidate one in between (see
|
||||
// SHAMap::state_), so the result is checked. setInvalid() outranks Immutable and can land
|
||||
// after both maps have been settled, so this narrows the window it guards.
|
||||
bool const bothImmutable = setMapsImmutable();
|
||||
SOMETIMES(!bothImmutable, "xrpl::Ledger::setImmutable : map invalidated while going immutable");
|
||||
if (!bothImmutable)
|
||||
return false;
|
||||
|
||||
// Written only now, so losing the race above leaves the header describing what the ledger was
|
||||
// built from rather than a map that has since been abandoned. Forced rather than conditional,
|
||||
// since this is the only place the hash transitions to valid.
|
||||
if (deriveMapHashes)
|
||||
{
|
||||
header_.txHash = txMap_.getHash().asUInt256();
|
||||
header_.accountHash = stateMap_.getHash().asUInt256();
|
||||
header_.txHash = txHash;
|
||||
header_.accountHash = accountHash;
|
||||
}
|
||||
|
||||
if (rehash)
|
||||
header_.hash = calculateLedgerHash(header_);
|
||||
|
||||
// Set last, so isImmutable() reports only a ledger whose maps are both immutable.
|
||||
immutable_ = true;
|
||||
txMap_.setImmutable();
|
||||
stateMap_.setImmutable();
|
||||
setup();
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
Ledger::setAccepted(
|
||||
NetClock::time_point closeTime,
|
||||
NetClock::duration closeResolution,
|
||||
@@ -342,7 +389,17 @@ Ledger::setAccepted(
|
||||
header_.closeTime = closeTime;
|
||||
header_.closeTimeResolution = closeResolution;
|
||||
header_.closeFlags = correctCloseTime ? 0 : kSLcfNoConsensusTime;
|
||||
setImmutable();
|
||||
// Built locally. See Ledger::setImmutable().
|
||||
if (!setImmutable())
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "Invalid map for accepted ledger " << header_.seq;
|
||||
UNREACHABLE("xrpl::Ledger::setAccepted : map is invalid");
|
||||
return false;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -566,6 +623,7 @@ Ledger::setup()
|
||||
{
|
||||
bool oldFees = false;
|
||||
bool newFees = false;
|
||||
bool extensionFees = false;
|
||||
{
|
||||
auto const baseFee = sle->at(~sfBaseFee);
|
||||
auto const reserveBase = sle->at(~sfReserveBase);
|
||||
@@ -600,6 +658,20 @@ Ledger::setup()
|
||||
assign(fees_.increment, reserveIncrementXRP);
|
||||
newFees = baseFeeXRP || reserveBaseXRP || reserveIncrementXRP;
|
||||
}
|
||||
{
|
||||
auto const gasLimit = sle->at(~sfGasLimit);
|
||||
auto const bytecodeSizeLimit = sle->at(~sfBytecodeSizeLimit);
|
||||
auto const gasPrice = sle->at(~sfGasPrice);
|
||||
|
||||
// An absent field resolves to the protocol constant.
|
||||
if (rules_.enabled(featureSmartEscrow))
|
||||
{
|
||||
fees_.gasLimit = gasLimit.value_or(kDefaultGasLimit);
|
||||
fees_.bytecodeSizeLimit = bytecodeSizeLimit.value_or(kDefaultBytecodeSizeLimit);
|
||||
fees_.gasPrice = gasPrice.value_or(kDefaultGasPrice);
|
||||
}
|
||||
extensionFees = gasLimit || bytecodeSizeLimit || gasPrice;
|
||||
}
|
||||
if (oldFees && newFees)
|
||||
{
|
||||
// Should be all of one or the other, but not both
|
||||
@@ -610,6 +682,12 @@ Ledger::setup()
|
||||
// Can't populate the new fees before the amendment is enabled
|
||||
ret = false;
|
||||
}
|
||||
if (!rules_.enabled(featureSmartEscrow) && extensionFees)
|
||||
{
|
||||
// Can't populate the extension fees before the amendment is
|
||||
// enabled
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SHAMapMissingNode const&)
|
||||
|
||||
@@ -511,9 +511,14 @@ doWithdraw(
|
||||
AccountID const& dstAcct,
|
||||
AccountID const& sourceAcct,
|
||||
XRPAmount priorBalance,
|
||||
STAmount const& amount,
|
||||
STAmount const& destinationAmount,
|
||||
STAmount const& sourceAmount,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
destinationAmount.asset() == sourceAmount.asset(),
|
||||
"xrpl::doWithdraw : delivered and source amounts use the same asset");
|
||||
|
||||
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
|
||||
|
||||
// Create a trust line or MPToken for a self-destination only when there
|
||||
@@ -523,9 +528,10 @@ doWithdraw(
|
||||
// create+delete MPTokens in the same transaction.
|
||||
if (dstAcct == senderAcct)
|
||||
{
|
||||
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
|
||||
if (auto const ter =
|
||||
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
|
||||
!isTesSuccess(ter) && ter != tecDUPLICATE)
|
||||
return ter;
|
||||
}
|
||||
@@ -540,17 +546,33 @@ doWithdraw(
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
sourceAcct,
|
||||
amount.asset(),
|
||||
destinationAmount.asset(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j) < amount)
|
||||
j) < sourceAmount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
|
||||
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// A transfer fee applies. Redeem the gross amount with the issuer, then
|
||||
// issue the net amount to the destination.
|
||||
if (sourceAmount != destinationAmount)
|
||||
{
|
||||
auto const issuer = destinationAmount.getIssuer();
|
||||
XRPL_ASSERT(
|
||||
!destinationAmount.native() && sourceAcct != issuer && dstAcct != issuer,
|
||||
"xrpl::doWithdraw : transfer fee applies between token holders");
|
||||
|
||||
if (auto const ter = directSendNoFee(ctx.view, sourceAcct, issuer, sourceAmount, false, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
|
||||
return directSendNoFee(ctx.view, issuer, dstAcct, destinationAmount, false, j);
|
||||
}
|
||||
|
||||
// A reserve sponsor only covers tx.Account's own objects, so resolve the
|
||||
// sponsor against the destination. accountSend can auto-create a holding
|
||||
// for dstAcct; keying on the destination ensures a third-party destination's
|
||||
@@ -562,7 +584,7 @@ doWithdraw(
|
||||
// Move the funds directly from the broker's pseudo-account to the
|
||||
// dstAcct
|
||||
return accountSend(
|
||||
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#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/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowLockPreclaimHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const& issue = amount.get<Issue>();
|
||||
auto const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tecNO_PERMISSION
|
||||
if (issuer == account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
|
||||
auto const sleIssuer = view.read(keylet::account(issuer));
|
||||
if (!sleIssuer)
|
||||
return tecNO_ISSUER;
|
||||
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the account does not have a trustline to the issuer, return tecNO_LINE
|
||||
auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency));
|
||||
if (!sleRippleState)
|
||||
return tecNO_LINE;
|
||||
|
||||
STAmount const balance = (*sleRippleState)[sfBalance];
|
||||
|
||||
// If balance is positive, issuer must have higher address than account
|
||||
if (balance > beast::kZero && issuer < account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If balance is negative, issuer must have lower address than account
|
||||
if (balance < beast::kZero && issuer > account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is authorized
|
||||
if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecFROZEN
|
||||
if (isFrozen(view, account, issue))
|
||||
return tecFROZEN;
|
||||
|
||||
// If the issuer has frozen the destination, return tecFROZEN
|
||||
if (isFrozen(view, dest, issue))
|
||||
return tecFROZEN;
|
||||
|
||||
STAmount const spendableAmount =
|
||||
accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
|
||||
|
||||
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::kZero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the amount is not addable to the balance, return tecPRECISION_LOSS
|
||||
if (!canAdd(spendableAmount, amount))
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowLockPreclaimHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
AccountID const issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tecNO_PERMISSION
|
||||
if (issuer == account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
|
||||
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the issuer is not the same as the issuer of the mpt, return
|
||||
// tecNO_PERMISSION
|
||||
if (sleIssuance->getAccountID(sfIssuer) != issuer)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
|
||||
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is
|
||||
// authorized
|
||||
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecLOCKED
|
||||
if (isFrozen(view, account, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
// If the issuer has frozen the destination, return tecLOCKED
|
||||
if (isFrozen(view, dest, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
// If the mpt cannot be transferred, return tecNO_AUTH
|
||||
if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
view,
|
||||
account,
|
||||
amount.get<MPTIssue>(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j);
|
||||
|
||||
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::kZero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowLockApplyHelper<Issue>(
|
||||
ApplyView& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
STAmount const& amount,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// Defensive: Issuer cannot create an escrow
|
||||
if (issuer == sender)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ter =
|
||||
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowLockApplyHelper<MPTIssue>(
|
||||
ApplyView& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
STAmount const& amount,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// Defensive: Issuer cannot create an escrow
|
||||
if (issuer == sender)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ter = lockEscrowMPT(view, sender, amount, journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowUnlockPreclaimHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
bool checkFreeze)
|
||||
{
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tesSUCCESS
|
||||
if (issuer == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has deep frozen the destination, return tecFROZEN
|
||||
if (checkFreeze &&
|
||||
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.getIssuer()))
|
||||
return tecFROZEN;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowUnlockPreclaimHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
bool checkFreeze)
|
||||
{
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tesSUCCESS
|
||||
if (issuer == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecLOCKED
|
||||
if (checkFreeze && isFrozen(view, account, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowUnlockApplyHelper<Issue>(
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
SLE::Ref sleDest,
|
||||
XRPAmount xrpBalance,
|
||||
STAmount const& amount,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
|
||||
if (senderIssuer)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (receiverIssuer)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const& issue = amount.get<Issue>();
|
||||
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
|
||||
bool const recvLow = issuer > receiver;
|
||||
|
||||
if (!ctx.view.exists(trustLineKey) && createAsset)
|
||||
{
|
||||
// Can the account cover the trust line's reserve?
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx,
|
||||
sleDest,
|
||||
xrpBalance,
|
||||
*sponsorSle,
|
||||
{.ownerCountDelta = 1},
|
||||
journal,
|
||||
tecNO_LINE_INSUF_RESERVE);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
JLOG(journal.trace()) << "Trust line does not exist. "
|
||||
"Insufficient reserve to create line.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
Currency const currency = issue.currency;
|
||||
STAmount initialBalance(issue);
|
||||
initialBalance.get<Issue>().account = noAccount();
|
||||
|
||||
if (TER const ter = trustCreate(
|
||||
ctx.view, // payment sandbox
|
||||
recvLow, // is dest low?
|
||||
issuer, // source
|
||||
receiver, // destination
|
||||
trustLineKey.key, // ledger index
|
||||
sleDest, // Account to add to
|
||||
false, // authorize account
|
||||
!sleDest->isFlag(lsfDefaultRipple), //
|
||||
false, // freeze trust line
|
||||
false, // deep freeze trust line
|
||||
initialBalance, // zero initial balance
|
||||
Issue(currency, receiver), // limit of zero
|
||||
0, // quality in
|
||||
0, // quality out
|
||||
*sponsorSle, // sponsor
|
||||
journal); // journal
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
ctx.view.update(sleDest);
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(trustLineKey) && !receiverIssuer)
|
||||
return tecNO_LINE;
|
||||
|
||||
auto const xferRate = transferRate(ctx.view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
|
||||
// validate the line limit if the account submitting txn is not the receiver
|
||||
// of the funds
|
||||
if (!createAsset)
|
||||
{
|
||||
auto const sleRippleState = ctx.view.peek(trustLineKey);
|
||||
if (!sleRippleState)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// if the issuer is the high, then we use the low limit
|
||||
// otherwise we use the high limit
|
||||
STAmount const lineLimit =
|
||||
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
|
||||
|
||||
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
|
||||
|
||||
// flip the sign of the line balance if the issuer is not high
|
||||
if (!recvLow)
|
||||
lineBalance.negate();
|
||||
|
||||
// add the final amount to the line balance
|
||||
lineBalance += finalAmt;
|
||||
|
||||
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
|
||||
if (lineLimit < lineBalance)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
|
||||
// if destination is not the issuer then transfer funds
|
||||
if (!receiverIssuer)
|
||||
{
|
||||
auto const ter = directSendNoFee(ctx.view, issuer, receiver, finalAmt, true, journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowUnlockApplyHelper<MPTIssue>(
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
SLE::Ref sleDest,
|
||||
XRPAmount xrpBalance,
|
||||
STAmount const& amount,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
|
||||
auto const mptID = amount.get<MPTIssue>().getMptID();
|
||||
auto const issuanceKey = keylet::mptokenIssuance(mptID);
|
||||
auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver);
|
||||
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
|
||||
{
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// update owner count.
|
||||
increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal);
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(mptKeylet) && !receiverIssuer)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const xferRate = transferRate(ctx.view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
if (ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
lockedRate >= kParityRate,
|
||||
"xrpl::escrowUnlockApplyHelper<MPTIssue> : lockedRate is at least parity");
|
||||
// MPTs are integral, so round the delivered amount down and
|
||||
// charge any fractional transfer fee to the escrowed amount.
|
||||
auto const delivered =
|
||||
mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false);
|
||||
finalAmt = STAmount(amount.asset(), delivered.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
}
|
||||
return unlockEscrowMPT(
|
||||
ctx.view,
|
||||
sender,
|
||||
receiver,
|
||||
finalAmt,
|
||||
ctx.view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
|
||||
journal);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -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);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, getVaultBaseScale(vault), 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
|
||||
|
||||
@@ -3,16 +3,26 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.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/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -21,9 +31,76 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
TER
|
||||
closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal j)
|
||||
NotTEC
|
||||
checkTokenPayChanRules(Rules const& rules)
|
||||
{
|
||||
if (!rules.enabled(featureTokenPaychan))
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!rules.enabled(fixCleanup3_2_0) || !rules.enabled(fixTokenEscrowV1))
|
||||
return temDISABLED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper<Issue>(STAmount const& amount)
|
||||
{
|
||||
if (amount.native() || amount <= beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (badCurrency() == amount.get<Issue>().currency)
|
||||
return temBAD_CURRENCY;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper<MPTIssue>(STAmount const& amount)
|
||||
{
|
||||
if (amount.native() || amount <= beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
payChanLockPrecisionHelper(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (!amount.holds<Issue>())
|
||||
return tesSUCCESS;
|
||||
|
||||
// The trust line debit rounds to the mantissa, so require an exact
|
||||
// difference rather than canAdd's relative tolerance.
|
||||
STAmount const spendable = accountHolds(
|
||||
view,
|
||||
account,
|
||||
amount.get<Issue>().currency,
|
||||
amount.getIssuer(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
j);
|
||||
if (!isExactDifference(spendable, amount))
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
closeChannel(
|
||||
SLE::Ref slep,
|
||||
ApplyViewContext ctx,
|
||||
UInt256 const& key,
|
||||
AccountID const& txAccount,
|
||||
XRPAmount submitterPreFeeBalance,
|
||||
beast::Journal j)
|
||||
{
|
||||
ApplyView& view = ctx.view;
|
||||
AccountID const src = (*slep)[sfAccount];
|
||||
// Remove PayChan from owner directory
|
||||
{
|
||||
@@ -38,9 +115,9 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
|
||||
}
|
||||
|
||||
// Remove PayChan from recipient's owner directory, if present.
|
||||
AccountID const dst = (*slep)[sfDestination];
|
||||
if (auto const page = (*slep)[~sfDestinationNode])
|
||||
{
|
||||
auto const dst = (*slep)[sfDestination];
|
||||
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -50,15 +127,69 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer amount back to owner, decrement owner count
|
||||
auto const sle = view.peek(keylet::account(src));
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Release the channel's reserve first: a token refund can re-create a
|
||||
// holding the owner deleted while the channel was open.
|
||||
decreaseOwnerCountForObject(view, sle, slep, 1, j);
|
||||
|
||||
XRPL_ASSERT(
|
||||
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
|
||||
decreaseOwnerCountForObject(view, sle, slep, 1, j);
|
||||
XRPL_ASSERT(
|
||||
isExactDifference((*slep)[sfAmount], (*slep)[sfBalance]),
|
||||
"xrpl::closeChannel : exact refund");
|
||||
|
||||
auto const reqDelta = (*slep)[sfAmount] - (*slep)[sfBalance];
|
||||
auto const& issuer = reqDelta.getIssuer();
|
||||
|
||||
// Only update the balance if there is a positive delta.
|
||||
if (reqDelta > beast::kZero)
|
||||
{
|
||||
if (isXRP(reqDelta))
|
||||
{
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] + reqDelta;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = reqDelta.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(view, src, reqDelta, false);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
bool const createAsset = src == txAccount;
|
||||
if (auto const ret = reqDelta.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx,
|
||||
kParityRate,
|
||||
sle,
|
||||
submitterPreFeeBalance,
|
||||
reqDelta,
|
||||
issuer,
|
||||
src,
|
||||
src,
|
||||
createAsset,
|
||||
j);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove PayChan from issuer's owner directory, if present.
|
||||
if (auto const optPage = (*slep)[~sfIssuerNode])
|
||||
{
|
||||
if (!view.dirRemove(keylet::ownerDir(issuer), *optPage, key, true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Could not remove paychan from issuer owner directory";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
view.update(sle);
|
||||
|
||||
// Remove PayChan from ledger
|
||||
|
||||
@@ -43,6 +43,9 @@ constexpr auto kProposalTxTypes = std::to_array<TxType>({
|
||||
bool
|
||||
isValidProposalTxnType(STObject const& proposedTx)
|
||||
{
|
||||
if (!proposedTx.isFieldPresent(sfTransactionType))
|
||||
return false;
|
||||
|
||||
auto const type = proposedTx.getFieldU16(sfTransactionType);
|
||||
|
||||
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/OracleHelpers.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -319,6 +320,8 @@ getLedgerEntryOwnerCount(SLE const& sle)
|
||||
UNREACHABLE("AccountRoots are not supported by object sponsorship.");
|
||||
return 0;
|
||||
// LCOV_EXCL_STOP
|
||||
case ltESCROW:
|
||||
return calculateAdditionalReserve(sle[~sfBytecode]);
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/MathUtilities.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -551,6 +552,22 @@ transferRate(ReadView const& view, STAmount const& amount)
|
||||
return transferRate(view, amount.asset());
|
||||
}
|
||||
|
||||
STAmount
|
||||
addTransferFee(STAmount const& amount, Rate const& rate)
|
||||
{
|
||||
if (rate == kParityRate)
|
||||
return amount;
|
||||
return mulRatio(amount, rate.value, QUALITY_ONE, Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
STAmount
|
||||
subtractTransferFee(STAmount const& amount, Rate const& rate)
|
||||
{
|
||||
if (rate == kParityRate)
|
||||
return amount;
|
||||
return mulRatio(amount, QUALITY_ONE, rate.value, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Holding operations
|
||||
@@ -1347,9 +1364,8 @@ directSendNoLimitMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the cost
|
||||
// exactly and round it up, matching the payment engine.
|
||||
auto const cost = mulRatio(saAmount.mpt(), rate.value, QUALITY_ONE, true);
|
||||
saActual = STAmount(saAmount.asset(), cost.value());
|
||||
// exactly, matching the payment engine.
|
||||
saActual = addTransferFee(saAmount, rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1474,9 +1490,8 @@ directSendNoLimitMultiMPT(
|
||||
{
|
||||
// Number math loses precision on large MPT amounts, which can
|
||||
// overcharge the sender. MPTs are integral, so compute the
|
||||
// cost exactly and round it up, matching the payment engine.
|
||||
auto const cost = mulRatio(amount.mpt(), rate.value, QUALITY_ONE, true);
|
||||
actualSend = STAmount(amount.asset(), cost.value());
|
||||
// cost exactly, matching the payment engine.
|
||||
actualSend = addTransferFee(amount, rate);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -46,7 +48,7 @@ fixedBaseScale(SLE::ConstRef vault)
|
||||
}
|
||||
|
||||
// Thin, vault-specific wrapper over posteriorAssetScale: used by
|
||||
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
|
||||
// getPosteriorVaultScale and the cash outflow rounding below.
|
||||
[[nodiscard]] int
|
||||
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
|
||||
{
|
||||
@@ -134,6 +136,18 @@ syncAssetsTotal(SLE::Ref vault)
|
||||
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 {
|
||||
@@ -181,11 +195,11 @@ posteriorAssetScale(
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
int baseScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
// Floor the SUM (reference + raw), not just raw, at atScale. See
|
||||
// Floor the SUM (reference + raw), not just raw. 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.
|
||||
@@ -194,9 +208,21 @@ creditToPosteriorScale(
|
||||
// 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.
|
||||
//
|
||||
// Take the grid from this same sum. A sum just under a power of ten that
|
||||
// rounds up to it in another mode would pick the coarser grid, and
|
||||
// flooring the unrounded sum on it would land below reference.
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
Number const flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
|
||||
return STAmount{asset, flooredSum - reference};
|
||||
Number const sum = reference + raw;
|
||||
int const atScale = liveScale(sum, asset, baseScale);
|
||||
Number const flooredSum = roundToAsset(asset, sum, atScale, roundingMode);
|
||||
STAmount const credit{asset, flooredSum - reference};
|
||||
XRPL_ASSERT_PARTS(
|
||||
roundingMode != Number::RoundingMode::Downward ||
|
||||
(credit >= beast::kZero && Number(credit) <= raw),
|
||||
"xrpl::detail::creditToPosteriorScale",
|
||||
"credit is between zero and raw");
|
||||
return credit;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
@@ -302,6 +328,17 @@ adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Jou
|
||||
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)
|
||||
{
|
||||
@@ -374,8 +411,8 @@ creditToPosteriorAvailableScale(
|
||||
}
|
||||
|
||||
Number const reference = vault->at(sfAssetsAvailable);
|
||||
int const scale = posteriorScale(vault, reference, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
return detail::creditToPosteriorScale(
|
||||
asset, reference, fixedBaseScale(vault), raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
@@ -428,6 +465,36 @@ vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
|
||||
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)
|
||||
{
|
||||
@@ -575,6 +642,14 @@ isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
|
||||
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)
|
||||
{
|
||||
@@ -584,22 +659,6 @@ assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
|
||||
return assetTotal;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
return false;
|
||||
return STAmount{asset, total - amount} == STAmount{asset, total};
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal)
|
||||
{
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? Number(vault->at(sfAssetsAvailable))
|
||||
: assetsTotal;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesWithdraw(
|
||||
SLE::ConstRef vault,
|
||||
@@ -695,9 +754,10 @@ getVaultVersion(SLE::ConstRef vault)
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules)
|
||||
{
|
||||
// FixedPrecision requires both cleanups, so a FixedPrecision Vault always
|
||||
// sees them enabled.
|
||||
if (rules.enabled(featureLendingProtocolV1_2) && rules.enabled(fixCleanup3_2_0) &&
|
||||
// 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))
|
||||
|
||||
@@ -435,18 +435,25 @@ areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
setIssuerMirrorCurrent(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
issuance.getType() == ltMPTOKEN_ISSUANCE,
|
||||
"xrpl::setIssuerMirrorEpoch : issuance MPTokenIssuance object");
|
||||
"xrpl::setIssuerMirrorCurrent : issuance MPTokenIssuance object");
|
||||
XRPL_ASSERT(
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorEpoch : mptoken MPToken object");
|
||||
mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorCurrent : mptoken MPToken object");
|
||||
|
||||
// Unlike the auditor mirror, the issuer mirror is not optional: every
|
||||
// confidential MPToken carries one, so there is no existence check here.
|
||||
//
|
||||
// Record the key and epoch under which the mirror is now encrypted. Both fields remain absent
|
||||
// at epoch 0: such mirrors use the original issuer key. The original key is stored in
|
||||
// sfInitialIssuerEncryptionKey after the first rotation.
|
||||
if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0)
|
||||
{
|
||||
mptoken[sfIssuerKeyMirrorEpoch] = epoch;
|
||||
mptoken[sfIssuerMirrorEncryptionKey] = issuance[sfIssuerEncryptionKey];
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -466,9 +473,9 @@ setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken)
|
||||
}
|
||||
|
||||
void
|
||||
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
|
||||
setMirrorsCurrent(SLE const& issuance, SLE& mptoken)
|
||||
{
|
||||
setIssuerMirrorEpoch(issuance, mptoken);
|
||||
setIssuerMirrorCurrent(issuance, mptoken);
|
||||
setAuditorMirrorEpoch(issuance, mptoken);
|
||||
}
|
||||
|
||||
|
||||
@@ -522,6 +522,64 @@ canAdd(STAmount const& a, STAmount const& b)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
/**
|
||||
* Integral types defer to canAdd(). For IOU amounts the rounding error of the
|
||||
* sum is a multiple of the finer operand's unit in the last place, so
|
||||
* subtracting the coarser operand back from the sum returns the finer one
|
||||
* only when that error is zero. Both differences are checked because either
|
||||
* operand may be the finer. IOUAmount throws std::overflow_error when a sum
|
||||
* or difference exceeds the largest IOU value; the exact result is then not
|
||||
* representable, so the sum is not exact.
|
||||
*/
|
||||
bool
|
||||
isExactSum(STAmount const& a, STAmount const& b)
|
||||
{
|
||||
if (!areComparable(a, b))
|
||||
return false;
|
||||
|
||||
if (a.integral())
|
||||
return canAdd(a, b);
|
||||
|
||||
try
|
||||
{
|
||||
STAmount const sum = a + b;
|
||||
return sum - a == b && sum - b == a;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Integral types defer to canSubtract(). For IOU amounts each check misses
|
||||
* the case where one operand is dust against the other. With a dust
|
||||
* subtrahend, a - b rounds to a and adding b back rounds to a again, so only
|
||||
* a - diff != b shows the loss. With a dust minuend, a - b rounds to -b and
|
||||
* a - diff rounds to b, so only diff + b != a shows it. Both are checked.
|
||||
* IOUAmount throws std::overflow_error when a step rounds past the largest
|
||||
* IOU value; the difference is then not exact.
|
||||
*/
|
||||
bool
|
||||
isExactDifference(STAmount const& a, STAmount const& b)
|
||||
{
|
||||
if (!areComparable(a, b))
|
||||
return false;
|
||||
|
||||
if (a.integral())
|
||||
return canSubtract(a, b);
|
||||
|
||||
try
|
||||
{
|
||||
STAmount const diff = a - b;
|
||||
return diff + b == a && a - diff == b;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Determines if it is safe to subtract one STAmount from another.
|
||||
*
|
||||
@@ -1763,6 +1821,24 @@ divRoundStrict(STAmount const& num, STAmount const& den, Asset const& asset, boo
|
||||
return divRoundImpl<NumberRoundModeGuard>(num, den, asset, roundUp);
|
||||
}
|
||||
|
||||
STAmount
|
||||
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
rounding == Number::RoundingMode::Upward || rounding == Number::RoundingMode::Downward,
|
||||
"xrpl::mulRatio(STAmount) : rounding is Upward or Downward");
|
||||
bool const roundUp = rounding == Number::RoundingMode::Upward;
|
||||
return amt.asset().visit(
|
||||
[&](Issue const& issue) -> STAmount {
|
||||
if (amt.native())
|
||||
return mulRatio(amt.xrp(), num, den, roundUp);
|
||||
return {mulRatio(amt.iou(), num, den, roundUp), issue};
|
||||
},
|
||||
[&](MPTIssue const& issue) -> STAmount {
|
||||
return {mulRatio(amt.mpt(), num, den, roundUp), issue};
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
STAmount::isZeroAtScale(int scale) const
|
||||
{
|
||||
|
||||
@@ -11,20 +11,16 @@
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Lets std::vector move, not copy, elements when it grows.
|
||||
static_assert(std::is_nothrow_move_constructible_v<STArray>);
|
||||
|
||||
STArray::STArray(STArray&& other) noexcept : STBase(other.getFName()), v_(std::move(other.v_))
|
||||
STArray::STArray(STArray&& other) : STBase(other.getFName()), v_(std::move(other.v_))
|
||||
{
|
||||
}
|
||||
|
||||
STArray&
|
||||
STArray::operator=(STArray&& other) noexcept
|
||||
STArray::operator=(STArray&& other)
|
||||
{
|
||||
setFName(other.getFName());
|
||||
v_ = std::move(other.v_);
|
||||
|
||||
@@ -37,16 +37,12 @@
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Lets std::vector move, not copy, elements when it grows.
|
||||
static_assert(std::is_nothrow_move_constructible_v<STObject>);
|
||||
|
||||
STObject::STObject(STObject&& other) noexcept
|
||||
STObject::STObject(STObject&& other)
|
||||
: STBase(other.getFName()), v_(std::move(other.v_)), type_(other.type_)
|
||||
{
|
||||
}
|
||||
@@ -130,7 +126,7 @@ STObject::add(Serializer& s) const
|
||||
}
|
||||
|
||||
STObject&
|
||||
STObject::operator=(STObject&& other) noexcept
|
||||
STObject::operator=(STObject&& other)
|
||||
{
|
||||
setFName(other.getFName());
|
||||
type_ = other.type_;
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
|
||||
namespace xrpl::detail {
|
||||
|
||||
// Lets std::vector move, not copy, elements when it grows.
|
||||
static_assert(std::is_nothrow_move_constructible_v<STVar>);
|
||||
|
||||
DefaultObjectT gDefaultObject;
|
||||
NonPresentObjectT gNonPresentObject;
|
||||
|
||||
@@ -44,7 +41,7 @@ STVar::STVar(STVar const& other)
|
||||
p_ = other.p_->copy(kMaxSize, &d_);
|
||||
}
|
||||
|
||||
STVar::STVar(STVar&& other) noexcept
|
||||
STVar::STVar(STVar&& other)
|
||||
{
|
||||
if (other.onHeap())
|
||||
{
|
||||
@@ -77,7 +74,7 @@ STVar::operator=(STVar const& rhs)
|
||||
}
|
||||
|
||||
STVar&
|
||||
STVar::operator=(STVar&& rhs) noexcept
|
||||
STVar::operator=(STVar&& rhs)
|
||||
{
|
||||
if (&rhs != this)
|
||||
{
|
||||
|
||||
@@ -137,6 +137,8 @@ transResults()
|
||||
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
|
||||
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
|
||||
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
|
||||
MAKE_ERROR(tefTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
MAKE_ERROR(tefEXCEEDS_LIMIT, "The transaction exceeds a limit set by fee voting."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
@@ -209,7 +211,6 @@ transResults()
|
||||
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
||||
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
|
||||
MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."),
|
||||
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
|
||||
|
||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#include <boost/smart_ptr/intrusive_ptr.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
@@ -77,14 +78,23 @@ SHAMap::SHAMap(SHAMap const& other, bool isMutable)
|
||||
: f_(other.f_)
|
||||
, journal_(other.f_.journal())
|
||||
, cowid_(other.cowid_ + 1)
|
||||
, ledgerSeq_(other.ledgerSeq_)
|
||||
, ledgerSeq_(other.ledgerSeq())
|
||||
, root_(other.root_)
|
||||
, state_(isMutable ? SHAMapState::Modifying : SHAMapState::Immutable)
|
||||
, type_(other.type_)
|
||||
, backed_(other.backed_)
|
||||
{
|
||||
// A snapshot shares the source's root, so Invalid carries over. Read once into a local, since
|
||||
// the source's state can change while this runs.
|
||||
auto const otherState = other.state();
|
||||
auto const ownState = [&] {
|
||||
if (otherState == SHAMapState::Invalid)
|
||||
return SHAMapState::Invalid;
|
||||
return isMutable ? SHAMapState::Modifying : SHAMapState::Immutable;
|
||||
}();
|
||||
state_.store(ownState, std::memory_order_release);
|
||||
|
||||
// If either map may change, they cannot share nodes
|
||||
if ((state_ != SHAMapState::Immutable) || (other.state_ != SHAMapState::Immutable))
|
||||
if ((ownState != SHAMapState::Immutable) || (otherState != SHAMapState::Immutable))
|
||||
{
|
||||
unshare();
|
||||
}
|
||||
@@ -105,7 +115,7 @@ SHAMap::dirtyUp(NodePathStack& stack, UInt256 const& target, SHAMapTreeNodePtr c
|
||||
// child can be an inner node or a leaf
|
||||
|
||||
XRPL_ASSERT(
|
||||
(state_ != SHAMapState::Synching) && (state_ != SHAMapState::Immutable),
|
||||
(state() != SHAMapState::Synching) && (state() != SHAMapState::Immutable),
|
||||
"xrpl::SHAMap::dirtyUp : valid state");
|
||||
XRPL_ASSERT(child && (child->cowid() == cowid_), "xrpl::SHAMap::dirtyUp : valid child input");
|
||||
|
||||
@@ -170,7 +180,7 @@ SHAMapTreeNodePtr
|
||||
SHAMap::fetchNodeFromDB(SHAMapHash const& hash) const
|
||||
{
|
||||
XRPL_ASSERT(backed_, "xrpl::SHAMap::fetchNodeFromDB : is backed");
|
||||
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq_);
|
||||
auto const obj = f_.db().fetchNodeObject(hash.asUInt256(), ledgerSeq());
|
||||
return finishFetch(hash, obj);
|
||||
}
|
||||
|
||||
@@ -183,10 +193,17 @@ SHAMap::finishFetch(SHAMapHash const& hash, std::shared_ptr<NodeObject> const& o
|
||||
{
|
||||
if (!object)
|
||||
{
|
||||
if (full_)
|
||||
// A missing node disproves full_, so withdraw it and report the gap. The exchange
|
||||
// makes exactly one reader report it, and its acquire pairs with the release store in
|
||||
// setFull(). It needs no release: the only other reader of full_ is this same path,
|
||||
// and a reader that loses the exchange does nothing with what the winner did. The load
|
||||
// ahead of it is relaxed, since all it decides is whether to try the exchange: a stale
|
||||
// true costs one exchange that loses, and a stale false means another reader won, or
|
||||
// setFull() is not visible yet and the gap goes unreported.
|
||||
if (full_.load(std::memory_order_relaxed) &&
|
||||
full_.exchange(false, std::memory_order_acquire))
|
||||
{
|
||||
full_ = false;
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq_, hash.asUInt256());
|
||||
f_.missingNodeAcquireBySeq(ledgerSeq(), hash.asUInt256());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -219,7 +236,7 @@ SHAMap::checkFilter(SHAMapHash const& hash, SHAMapSyncFilter const* filter) cons
|
||||
auto node = SHAMapTreeNode::makeFromPrefix(makeSlice(*nodeData), hash);
|
||||
if (node)
|
||||
{
|
||||
filter->gotNode(true, hash, ledgerSeq_, std::move(*nodeData), node->getType());
|
||||
filter->gotNode(true, hash, ledgerSeq(), std::move(*nodeData), node->getType());
|
||||
if (backed_)
|
||||
canonicalize(hash, node);
|
||||
}
|
||||
@@ -399,7 +416,7 @@ SHAMap::descendAsync(
|
||||
{
|
||||
f_.db().asyncFetch(
|
||||
hash.asUInt256(),
|
||||
ledgerSeq_,
|
||||
ledgerSeq(),
|
||||
[this, hash, cb{std::move(callback)}](std::shared_ptr<NodeObject> const& object) {
|
||||
auto const node = finishFetch(hash, object);
|
||||
cb(node, hash);
|
||||
@@ -424,7 +441,7 @@ SHAMap::unshareNode(intr_ptr::SharedPtr<Node> node, SHAMapNodeID const& nodeID)
|
||||
if (node->cowid() != cowid_)
|
||||
{
|
||||
// have a CoW
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::unshareNode : not immutable");
|
||||
node = intr_ptr::staticPointerCast<Node>(node->clone(cowid_));
|
||||
if (nodeID.isRoot())
|
||||
root_ = node;
|
||||
@@ -637,7 +654,7 @@ bool
|
||||
SHAMap::delItem(UInt256 const& id)
|
||||
{
|
||||
// delete the item with this ID
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::delItem : not immutable");
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(id, &stack);
|
||||
@@ -719,7 +736,7 @@ SHAMap::delItem(UInt256 const& id)
|
||||
bool
|
||||
SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const> item)
|
||||
{
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::addGiveItem : not immutable");
|
||||
XRPL_ASSERT(type != SHAMapNodeType::TnInner, "xrpl::SHAMap::addGiveItem : valid type input");
|
||||
|
||||
// add the specified item, does not update
|
||||
@@ -810,7 +827,7 @@ SHAMap::updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem cons
|
||||
// can't change the tag but can change the hash
|
||||
UInt256 const tag = item->key();
|
||||
|
||||
XRPL_ASSERT(state_ != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
|
||||
XRPL_ASSERT(state() != SHAMapState::Immutable, "xrpl::SHAMap::updateGiveItem : not immutable");
|
||||
|
||||
NodePathStack stack;
|
||||
walkTowardsKey(tag, &stack);
|
||||
@@ -901,7 +918,7 @@ SHAMap::writeNode(NodeObjectType t, SHAMapTreeNodePtr node) const
|
||||
|
||||
Serializer s;
|
||||
node->serializeWithPrefix(s);
|
||||
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq_);
|
||||
f_.db().store(t, std::move(s.modData()), node->getHash().asUInt256(), ledgerSeq());
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <xrpl/shamap/detail/TaggedPointer.h>
|
||||
#include <xrpl/shamap/detail/TaggedPointer.ipp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
@@ -77,7 +78,9 @@ SHAMapInnerNode::clone(std::uint32_t cowid) const
|
||||
auto p = intr_ptr::makeShared<SHAMapInnerNode>(cowid, branchCount);
|
||||
p->hash_ = hash_;
|
||||
p->isBranch_ = isBranch_;
|
||||
p->fullBelowGen_ = fullBelowGen_;
|
||||
// Relaxed, as everywhere. p is not reachable by another thread until this returns.
|
||||
p->fullBelowGen_.store(
|
||||
fullBelowGen_.load(std::memory_order_relaxed), std::memory_order_relaxed);
|
||||
SHAMapHash* cloneHashes = nullptr;
|
||||
SHAMapHash* thisHashes = nullptr;
|
||||
SHAMapTreeNodePtr* cloneChildren = nullptr;
|
||||
|
||||
@@ -31,6 +31,25 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Whether a depth is at or past the deepest an inner node may occupy.
|
||||
*
|
||||
* Nibbles run out at SHAMap::kLeafDepth, so only a leaf may sit there. True for
|
||||
* every deeper position too, which lies past the end of a key.
|
||||
*
|
||||
* @param depth The depth to judge.
|
||||
* @return Whether an inner node at that depth makes the map impossible.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isLeafDepth(unsigned int depth)
|
||||
{
|
||||
return depth >= SHAMap::kLeafDepth;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
SHAMap::visitLeaves(
|
||||
std::function<void(boost::intrusive_ptr<SHAMapItem const> const& item)> const& leafFunction)
|
||||
@@ -143,13 +162,10 @@ SHAMap::visitDifferences(
|
||||
if (!function(*node))
|
||||
return;
|
||||
|
||||
// Nibbles run out at kLeafDepth, so only a leaf belongs there. A well-formed map never
|
||||
// holds an inner node at that depth: addKnownNode marks the map invalid rather than hooking
|
||||
// one in, and fetch-pack data is hash-verified against a validated root, so reaching this
|
||||
// means a defect or a corrupt store, not something a peer can provoke. Report the node
|
||||
// anyway - the wire form carries no depth, and the recipient hooks blobs in by hash - but
|
||||
// skip the children rather than letting getChildNodeID throw on them.
|
||||
if (nodeID.getDepth() >= kLeafDepth)
|
||||
// Only a leaf belongs at kLeafDepth. The node is still reported, since the wire form
|
||||
// carries no depth and the recipient hooks blobs by hash. Its children are skipped, since
|
||||
// getChildNodeID has no answer past kLeafDepth.
|
||||
if (isLeafDepth(nodeID.getDepth()))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::visitDifferences : inner node at leaf depth");
|
||||
@@ -207,7 +223,12 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
// we already know this child node is missing
|
||||
fullBelow = false;
|
||||
}
|
||||
else if (!backed_ || !f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
// The depth test precedes the cache lookup for the same reason it does in addKnownNode():
|
||||
// the cache is keyed by node hash and shared across maps, and a hash covers a node's
|
||||
// children but not its depth. Skipping the shortcut forgoes an optimization only.
|
||||
else if (
|
||||
!backed_ || isLeafDepth(nodeID.getDepth() + 1) ||
|
||||
!f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
{
|
||||
bool pending = false;
|
||||
auto d = descendAsync(
|
||||
@@ -238,6 +259,17 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
|
||||
if (--mn.max <= 0)
|
||||
return;
|
||||
}
|
||||
else if (d->isInner() && isLeafDepth(nodeID.getDepth() + 1))
|
||||
{
|
||||
// Only a leaf belongs that deep (see isLeafDepth and SHAMap::addKnownNode). A node
|
||||
// resolved locally reaches the walk without passing through addKnownNode(), so the
|
||||
// walk reaches this verdict itself. Ordered ahead of the full-below test below,
|
||||
// which canonicalization shares across maps.
|
||||
JLOG(journal_.warn()) << "Inner node at branch " << branch << " below " << nodeID
|
||||
<< " makes the map invalid";
|
||||
setInvalid();
|
||||
return;
|
||||
}
|
||||
else if (d->isInner() && !safeDowncast<SHAMapInnerNode*>(d)->isFullBelow(mn.generation))
|
||||
{
|
||||
mn.stack.push(se);
|
||||
@@ -301,6 +333,8 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
|
||||
}
|
||||
else if ((mn.max > 0) && mn.missingHashes.insert(nodeHash).second)
|
||||
{
|
||||
// getChildNodeID() is safe here: gmnProcessNodes refuses to descend into an inner node
|
||||
// at kLeafDepth, so a deferred parent sits at most one level above it.
|
||||
mn.missingNodes.emplace_back(parentID.getChildNodeID(branch), nodeHash.asUInt256());
|
||||
--mn.max;
|
||||
}
|
||||
@@ -311,17 +345,20 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
|
||||
mn.deferred = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of node IDs and hashes for nodes that are part of this SHAMap
|
||||
* but not available locally. The filter can hold alternate sources of
|
||||
* nodes that are not permanently stored locally
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, UInt256>>
|
||||
SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
{
|
||||
XRPL_ASSERT(root_->getHash().isNonZero(), "xrpl::SHAMap::getMissingNodes : nonzero root hash");
|
||||
XRPL_ASSERT(max > 0, "xrpl::SHAMap::getMissingNodes : valid max input");
|
||||
|
||||
if (!isValid())
|
||||
{
|
||||
// The root node's own hash, since getHash() unshares the tree on a zero hash.
|
||||
JLOG(journal_.warn()) << "getMissingNodes called on an invalid map, root hash "
|
||||
<< root_->getHash() << " seq " << ledgerSeq();
|
||||
return {};
|
||||
}
|
||||
|
||||
MissingNodes mn(
|
||||
max,
|
||||
filter,
|
||||
@@ -354,6 +391,11 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
{
|
||||
gmnProcessNodes(mn, pos);
|
||||
|
||||
// The walk just invalidated the map. The loop stops descending here but falls through
|
||||
// to the drain below, since every posted read must be drained while `mn` is alive.
|
||||
if (!isValid())
|
||||
break;
|
||||
|
||||
if (mn.max <= 0)
|
||||
break;
|
||||
|
||||
@@ -383,6 +425,11 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
if (mn.deferred != 0)
|
||||
gmnProcessDeferredReads(mn);
|
||||
|
||||
// Reads are drained, so the map can be abandoned. What was collected belongs to a tree
|
||||
// that cannot exist.
|
||||
if (!isValid())
|
||||
return {};
|
||||
|
||||
if (mn.max <= 0)
|
||||
return std::move(mn.missingNodes);
|
||||
|
||||
@@ -416,6 +463,11 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter)
|
||||
|
||||
} while (node != nullptr);
|
||||
|
||||
// addKnownNode() on another thread can write the verdict after the loop's own test, so the map
|
||||
// is judged once more before the result is returned.
|
||||
if (!isValid())
|
||||
return {}; // LCOV_EXCL_LINE: only that other thread reaches this, so no test does
|
||||
|
||||
if (mn.missingNodes.empty())
|
||||
clearSynching();
|
||||
|
||||
@@ -527,11 +579,19 @@ SHAMap::addRootNode(
|
||||
XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid");
|
||||
XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node");
|
||||
|
||||
// we already have a root_ node
|
||||
// A map syncs against one hash and installs a root once, so a root already held is a duplicate
|
||||
// only once it hashes to the hash asked for.
|
||||
if (root_->getHash().isNonZero())
|
||||
{
|
||||
JLOG(journal_.trace()) << "Got root node, already have one";
|
||||
XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash");
|
||||
|
||||
if (root_->getHash() != hash)
|
||||
{
|
||||
JLOG(journal_.warn()) << "Root node offered under hash " << hash
|
||||
<< ", but the map holds " << root_->getHash();
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
|
||||
@@ -555,7 +615,7 @@ SHAMap::addRootNode(
|
||||
Serializer s;
|
||||
root_->serializeWithPrefix(s);
|
||||
filter->gotNode(
|
||||
false, root_->getHash(), ledgerSeq_, std::move(s.modData()), root_->getType());
|
||||
false, root_->getHash(), ledgerSeq(), std::move(s.modData()), root_->getType());
|
||||
}
|
||||
|
||||
return SHAMapAddNode::useful();
|
||||
@@ -598,7 +658,13 @@ SHAMap::addKnownNode(
|
||||
}
|
||||
|
||||
auto childHash = inner->getChildHash(branch);
|
||||
if (f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
|
||||
// Depth before the cache: the cache is keyed by node hash and shared across the family, and
|
||||
// a hash covers a node's children but not its depth, so the same subtree can be cached as
|
||||
// complete at one depth and reached at another. Skipping the shortcut only forgoes an
|
||||
// optimization.
|
||||
if (!isLeafDepth(currNodeID.getDepth() + 1) &&
|
||||
f_.getFullBelowCache()->touchIfExists(childHash.asUInt256()))
|
||||
{
|
||||
return SHAMapAddNode::duplicate();
|
||||
}
|
||||
@@ -616,25 +682,32 @@ SHAMap::addKnownNode(
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
// Inner nodes must be at a level strictly less than 64
|
||||
// but leaf nodes (while notionally at level 64) can be
|
||||
// at any depth up to and including 64:
|
||||
if ((currNodeID.getDepth() > kLeafDepth) ||
|
||||
(treeNode->isInner() && currNodeID.getDepth() == kLeafDepth))
|
||||
// Only leaves may sit at kLeafDepth (see isLeafDepth), so an inner node there makes the map
|
||||
// impossible. The node is reported as bad data.
|
||||
//
|
||||
// Every node from the root down hash-verified to get here, so it is the requested root hash
|
||||
// itself that commits to a shape no valid tree can have. The verdict belongs to that hash
|
||||
// rather than to our copy of the tree.
|
||||
bool const badDepth = treeNode->isInner() && isLeafDepth(currNodeID.getDepth());
|
||||
SOMETIMES(badDepth, "xrpl::SHAMap::addKnownNode : map is invalid");
|
||||
if (badDepth)
|
||||
{
|
||||
// Map is provably invalid
|
||||
state_ = SHAMapState::Invalid;
|
||||
return SHAMapAddNode::useful();
|
||||
JLOG(journal_.warn()) << "Node " << nodeID << " makes the map invalid at "
|
||||
<< currNodeID;
|
||||
setInvalid();
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
if (currNodeID != nodeID)
|
||||
// The data hashes to the child at currNodeID but claims to belong at nodeID, so it is not
|
||||
// the node that was asked for. Only the label is wrong, so the map stays sound and the node
|
||||
// is still obtainable from another sender.
|
||||
bool const badPosition = (currNodeID != nodeID);
|
||||
SOMETIMES(badPosition, "xrpl::SHAMap::addKnownNode : node ID does not match its position");
|
||||
if (badPosition)
|
||||
{
|
||||
// Either this node is broken or we didn't request it (yet)
|
||||
JLOG(journal_.warn()) << "unable to hook node " << nodeID;
|
||||
JLOG(journal_.info()) << " stuck at " << currNodeID;
|
||||
JLOG(journal_.info()) << "got depth=" << nodeID.getDepth()
|
||||
<< ", walked to= " << currNodeID.getDepth();
|
||||
return SHAMapAddNode::useful();
|
||||
JLOG(journal_.warn()) << "Unable to hook node " << nodeID << ", stuck at "
|
||||
<< currNodeID;
|
||||
return SHAMapAddNode::invalid();
|
||||
}
|
||||
|
||||
if (backed_)
|
||||
@@ -647,7 +720,7 @@ SHAMap::addKnownNode(
|
||||
Serializer s;
|
||||
treeNode->serializeWithPrefix(s);
|
||||
filter->gotNode(
|
||||
false, childHash, ledgerSeq_, std::move(s.modData()), treeNode->getType());
|
||||
false, childHash, ledgerSeq(), std::move(s.modData()), treeNode->getType());
|
||||
}
|
||||
|
||||
return SHAMapAddNode::useful();
|
||||
@@ -766,7 +839,7 @@ SHAMap::hasLeafNode(UInt256 const& tag, SHAMapHash const& targetNodeHash) const
|
||||
// Same kLeafDepth hazard as in visitDifferences above. That guard bounds the caller's own
|
||||
// traversal, not the map queried here, and the loop below descends from this map's root
|
||||
// independently, so this check is what keeps a malformed map from reaching getChildNodeID.
|
||||
if (nodeID.getDepth() >= kLeafDepth)
|
||||
if (isLeafDepth(nodeID.getDepth()))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::SHAMap::hasLeafNode : inner node at leaf depth");
|
||||
@@ -849,8 +922,8 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
|
||||
// there. These nodes come off the wire, so a peer can still claim an inner one;
|
||||
// reject it rather than passing this depth to selectBranch.
|
||||
SOMETIMES(
|
||||
depth >= kLeafDepth, "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
||||
if (depth >= kLeafDepth)
|
||||
isLeafDepth(depth), "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
|
||||
if (isLeafDepth(depth))
|
||||
return false;
|
||||
|
||||
auto const nodeId = SHAMapNodeID::createID(depth, key);
|
||||
@@ -859,10 +932,9 @@ SHAMap::verifyProofPath(UInt256 const& rootHash, UInt256 const& key, std::vector
|
||||
}
|
||||
else
|
||||
{
|
||||
// The hash chain up to rootHash only proves this leaf sits where the path claims,
|
||||
// not that it is the leaf for `key`: a peer could substitute any other leaf whose
|
||||
// subtree hashes to the same value at every level above it. Checking the terminal
|
||||
// leaf's own key is what ties the proof to `key` specifically.
|
||||
// The hash chain up to rootHash proves this leaf sits where the path claims. Any
|
||||
// leaf whose subtree hashes the same at every level above satisfies that chain, so
|
||||
// the terminal leaf's own key is what ties the proof to `key`.
|
||||
if (leafKey(*node) != key)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
|
||||
#undef TRANSACTION
|
||||
#undef TRANSACTION_INCLUDE
|
||||
|
||||
#pragma pop_macro("TRANSACTION")
|
||||
|
||||
// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE.
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#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/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.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/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
bool
|
||||
validSignerArray(STArray const& signers, std::size_t limit, SField const& elementName)
|
||||
{
|
||||
if (signers.size() > limit)
|
||||
return false;
|
||||
|
||||
std::optional<AccountID> previous;
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
if (signer.getFName() != elementName || !signer.isFieldPresent(sfAccount))
|
||||
return false;
|
||||
|
||||
auto const account = signer.getAccountID(sfAccount);
|
||||
if (previous && account <= *previous)
|
||||
return false;
|
||||
previous = account;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
validNestedSigners(STObject const& signature)
|
||||
{
|
||||
return !signature.isFieldPresent(sfSigners) ||
|
||||
validSignerArray(signature.getFieldArray(sfSigners), STTx::kMaxMultiSigners, sfSigner);
|
||||
}
|
||||
|
||||
bool
|
||||
validSignerArrays(STObject const& proposedTx)
|
||||
{
|
||||
if (!validNestedSigners(proposedTx))
|
||||
return false;
|
||||
|
||||
for (auto const* field :
|
||||
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
|
||||
{
|
||||
if (proposedTx.isFieldPresent(*field) &&
|
||||
!validNestedSigners(proposedTx.getFieldObject(*field)))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!proposedTx.isFieldPresent(sfBatchSigners))
|
||||
return true;
|
||||
|
||||
auto const& batchSigners = proposedTx.getFieldArray(sfBatchSigners);
|
||||
if (!validSignerArray(batchSigners, kMaxBatchSigners, sfBatchSigner))
|
||||
return false;
|
||||
|
||||
return std::ranges::all_of(
|
||||
batchSigners, [](auto const& batchSigner) { return validNestedSigners(batchSigner); });
|
||||
}
|
||||
|
||||
// The Ticket a proposal is keyed to: the one its proposed transaction spends.
|
||||
uint256
|
||||
proposalTicketKey(SLE const& proposal)
|
||||
{
|
||||
auto const& proposedTx = proposal.getFieldObject(sfProposedTransaction);
|
||||
return keylet::ticket(
|
||||
proposedTx.getAccountID(sfAccount),
|
||||
SeqProxy::rawTicket(proposedTx.getFieldU32(sfTicketSequence)))
|
||||
.key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
ValidTransactionProposal::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
|
||||
{
|
||||
// `after` is always present (on deletion it holds the final state), so it
|
||||
// alone determines the entry type. LedgerEntryTypesMatch owns malformed
|
||||
// type transitions; skip them rather than read one side with the other's
|
||||
// schema.
|
||||
if (!after || (before && before->getType() != after->getType()))
|
||||
return;
|
||||
|
||||
auto const type = after->getType();
|
||||
|
||||
// Consuming a Ticket deletes the proposal keyed to it (XLS-0103 §4.5).
|
||||
// Remember each Ticket that went, so finalize can tell a proposal deleted
|
||||
// with its Ticket from one that simply vanished, and the proposal each
|
||||
// Ticket was keyed to, so finalize can check that none stayed behind.
|
||||
if (type == ltTICKET)
|
||||
{
|
||||
if (isDelete)
|
||||
{
|
||||
deletedTickets_.insert(after->key());
|
||||
proposalsOfDeletedTickets_.push_back(
|
||||
keylet::txProposal(
|
||||
after->getAccountID(sfAccount), after->getFieldU32(sfTicketSequence)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != ltTRANSACTION_PROPOSAL)
|
||||
return;
|
||||
|
||||
if (!before)
|
||||
{
|
||||
++created_;
|
||||
}
|
||||
else if (isDelete)
|
||||
{
|
||||
++deleted_;
|
||||
ticketsOfDeletedProposals_.push_back(proposalTicketKey(*before));
|
||||
}
|
||||
else
|
||||
{
|
||||
++modified_;
|
||||
}
|
||||
|
||||
// A live proposal's signer lists must stay submittable: within size
|
||||
// limits, sorted by account, and free of duplicates.
|
||||
if (!isDelete && !validSignerArrays(after->getFieldObject(sfProposedTransaction)))
|
||||
invalidSignerArrays_ = true;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidTransactionProposal::finalize(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) const
|
||||
{
|
||||
if (!view.rules().enabled(featureCosign))
|
||||
return true;
|
||||
|
||||
if (invalidSignerArrays_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TransactionProposal signer arrays are not canonical.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A proposal leaves the ledger in two ways (XLS-0103 §4.5): a
|
||||
// TransactionProposalCancel deletes the proposal it names, and consuming
|
||||
// the Ticket a proposal is keyed to deletes the proposal with it, whether
|
||||
// the proposed transaction executed (or failed with a claimed-fee tec) or
|
||||
// AccountDelete swept the target's Tickets.
|
||||
std::uint32_t deletedWithTicket = 0;
|
||||
for (auto const& ticketKey : ticketsOfDeletedProposals_)
|
||||
{
|
||||
if (deletedTickets_.contains(ticketKey))
|
||||
++deletedWithTicket;
|
||||
}
|
||||
|
||||
bool const succeeded = isTesSuccess(result);
|
||||
bool effectsMatch = false;
|
||||
if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CREATE)
|
||||
{
|
||||
effectsMatch = created_ == (succeeded ? 1u : 0u) && modified_ == 0 && deleted_ == 0;
|
||||
}
|
||||
else if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CANCEL)
|
||||
{
|
||||
// A Cancel deletes one proposal and nothing else. It cannot
|
||||
// spend a reserved Ticket, so no proposal goes with its Ticket here.
|
||||
effectsMatch = created_ == 0 && modified_ == 0 && deletedWithTicket == 0 &&
|
||||
deleted_ == (succeeded ? 1u : 0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Any other transaction deletes a proposal only together with its
|
||||
// Ticket, and modifies at most one, only as a successful
|
||||
// SponsorshipTransfer.
|
||||
// TODO: extend this when TransactionProposalSign is implemented.
|
||||
std::uint32_t const modifiable =
|
||||
tx.getTxnType() == ttSPONSORSHIP_TRANSFER && succeeded ? 1 : 0;
|
||||
effectsMatch = created_ == 0 && modified_ <= modifiable && deleted_ == deletedWithTicket;
|
||||
}
|
||||
|
||||
if (!effectsMatch)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: TransactionProposal changes do not match transaction result.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The other direction of the deletion rule: ticketDelete deletes the
|
||||
// proposal keyed to a Ticket along with it (XLS-0103 §4.5), so no
|
||||
// proposal may outlive its Ticket, orphaned on a key that can no longer
|
||||
// be spent.
|
||||
bool const orphaned = std::ranges::any_of(
|
||||
proposalsOfDeletedTickets_, [&view](Keylet const& keylet) { return view.exists(keylet); });
|
||||
if (orphaned)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: TransactionProposal outlived the Ticket it is keyed to.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -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>
|
||||
@@ -148,7 +150,8 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef aft
|
||||
drops_ -= (*before)[sfBalance].xrp().drops();
|
||||
break;
|
||||
case ltPAYCHAN:
|
||||
drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
|
||||
if (isXRP((*before)[sfAmount]))
|
||||
drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
|
||||
break;
|
||||
case ltESCROW:
|
||||
if (isXRP((*before)[sfAmount]))
|
||||
@@ -181,7 +184,7 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef aft
|
||||
drops_ += (*after)[sfBalance].xrp().drops();
|
||||
break;
|
||||
case ltPAYCHAN:
|
||||
if (!isDelete)
|
||||
if (!isDelete && isXRP((*after)[sfAmount]))
|
||||
drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops();
|
||||
break;
|
||||
case ltESCROW:
|
||||
@@ -933,8 +936,12 @@ ValidClawback::finalize(
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
// IOU balances keep 16 significant digits, so a clawback
|
||||
// much smaller than the balance changes it by a rounded
|
||||
// amount, possibly zero. Compare with the same rounded
|
||||
// subtraction the balance update performs.
|
||||
if (*afterBalance > *beforeBalance ||
|
||||
(*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
|
||||
*afterBalance != *beforeBalance - std::min(*beforeBalance, clawAmount))
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: trustline clawback balance change is invalid";
|
||||
@@ -1010,6 +1017,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 +1098,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 +1206,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())
|
||||
|
||||
@@ -125,6 +125,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
|
||||
else if (!before)
|
||||
{
|
||||
mptokensCreated_++;
|
||||
mptokenCreatedHolder_ = after->at(sfAccount);
|
||||
MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)};
|
||||
if (mptIssue.getIssuer() == after->at(sfAccount))
|
||||
mptCreatedByIssuer_ = true;
|
||||
@@ -339,6 +340,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 +353,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())
|
||||
@@ -434,6 +440,13 @@ ValidMPTIssuance::finalize(
|
||||
return true;
|
||||
}
|
||||
|
||||
// A token paychan claim may auto-create the submitter's MPToken.
|
||||
if (rules.enabled(featureTokenPaychan) && txnType == ttPAYCHAN_CLAIM &&
|
||||
mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensDeleted_ == 0 &&
|
||||
(mptokensCreated_ == 0 ||
|
||||
(mptokensCreated_ == 1 && mptokenCreatedHolder_ == tx[sfAccount])))
|
||||
return true;
|
||||
|
||||
if (hasPrivilege(tx, Privilege::MayDeleteMpt) &&
|
||||
((txnType == ttAMM_DELETE && mptokensDeleted_ <= 2) || mptokensDeleted_ == 1) &&
|
||||
mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -63,6 +65,9 @@ ValidVault::Vault::make(SLE const& from)
|
||||
self.assetsAvailable = from.at(sfAssetsAvailable);
|
||||
self.assetsMaximum = from.at(sfAssetsMaximum);
|
||||
self.lossUnrealized = from.at(sfLossUnrealized);
|
||||
self.assetsDeployed = from[~sfAssetsDeployed].value_or(Number{0});
|
||||
self.yieldUnrealized = from[~sfYieldUnrealized].value_or(Number{0});
|
||||
self.version = decodeVaultVersion(from[~sfLEVersion]);
|
||||
self.vaultKind = from[~sfVaultKind];
|
||||
self.subscriptionDate = from[~sfSubscriptionDate];
|
||||
self.redemptionDate = from[~sfRedemptionDate];
|
||||
@@ -408,6 +413,70 @@ ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules
|
||||
return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
|
||||
}
|
||||
|
||||
bool
|
||||
ValidVault::checkTotalsAddUp(
|
||||
Vault const& afterVault,
|
||||
Vault const& beforeVault,
|
||||
Asset const& vaultAsset,
|
||||
bool fix340Enabled,
|
||||
std::int32_t minScale,
|
||||
Number const& roundedVaultDelta,
|
||||
Number const& exactVaultDelta,
|
||||
char const* verb,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
bool result = true;
|
||||
|
||||
// FixedPrecision: the stored AssetsTotal is a Downward-floored cache
|
||||
// (AA + AD), not an exact running total, so comparing its delta against
|
||||
// the real transfer can be off by more than one unit purely from
|
||||
// independent floor-rounding of the two snapshots -- not an accounting
|
||||
// bug. The stored-cache-equals-derived-total check elsewhere in finalize,
|
||||
// plus the AssetsDeployed-only-changed-by-lending-transactors check,
|
||||
// already cover AssetsTotal; only compare it here on Legacy/CashBasis,
|
||||
// where AssetsTotal is written exactly alongside AA.
|
||||
if (afterVault.version != VaultVersion::FixedPrecision)
|
||||
{
|
||||
auto const assetTotalDelta =
|
||||
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(assetTotalDelta, roundedVaultDelta, vaultAsset, minScale)
|
||||
: assetTotalDelta == roundedVaultDelta;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << verb
|
||||
<< " and assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
// FixedPrecision: AssetsAvailable is the exact, single writer
|
||||
// (adjustVaultBalances)-managed balance, so its delta must match the real
|
||||
// transfer exactly, with no rounding tolerance -- compare the exact
|
||||
// deltas, not the values rounded to the posterior AssetsTotal scale.
|
||||
bool const availableAddsUp = [&] {
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
return (afterVault.assetsAvailable - beforeVault.assetsAvailable) == exactVaultDelta;
|
||||
}
|
||||
if (fix340Enabled)
|
||||
{
|
||||
return agreesWithinOneUnit(
|
||||
assetAvailableDelta, roundedVaultDelta, vaultAsset, minScale);
|
||||
}
|
||||
return assetAvailableDelta == roundedVaultDelta;
|
||||
}();
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << verb << " and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidVault::finalize(
|
||||
STTx const& tx,
|
||||
@@ -418,6 +487,7 @@ ValidVault::finalize(
|
||||
{
|
||||
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
||||
bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
|
||||
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
|
||||
|
||||
if (!isTesSuccess(ret))
|
||||
return true; // Do not perform checks
|
||||
@@ -585,6 +655,13 @@ ValidVault::finalize(
|
||||
"vault must have no assets available";
|
||||
result = false;
|
||||
}
|
||||
if (afterVault.version == VaultVersion::FixedPrecision &&
|
||||
afterVault.assetsDeployed != kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
|
||||
"vault must have no AssetsDeployed";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
|
||||
{
|
||||
@@ -609,6 +686,14 @@ ValidVault::finalize(
|
||||
else
|
||||
{
|
||||
bool const gapExceeded = [&] {
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// FixedPrecision: both LossUnrealized and AssetsDeployed are exact
|
||||
// (never rounded), so compare them directly. AssetsTotal is
|
||||
// just a rounded cache of AssetsAvailable + AssetsDeployed and
|
||||
// is not the reference this check needs.
|
||||
return afterVault.lossUnrealized > afterVault.assetsDeployed;
|
||||
}
|
||||
if (!fix340Enabled)
|
||||
{
|
||||
return afterVault.lossUnrealized >
|
||||
@@ -631,6 +716,43 @@ ValidVault::finalize(
|
||||
}
|
||||
}
|
||||
|
||||
if (afterVault.version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (afterVault.assetsDeployed < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: AssetsDeployed must not be negative";
|
||||
result = false;
|
||||
}
|
||||
else if (
|
||||
Number(STAmount{afterVault.asset, afterVault.assetsDeployed}) !=
|
||||
afterVault.assetsDeployed)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: AssetsDeployed must be exactly representable "
|
||||
"at the vault asset's precision";
|
||||
result = false;
|
||||
}
|
||||
|
||||
Number const expectedAssetsTotal = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return Number(
|
||||
STAmount{afterVault.asset, afterVault.assetsAvailable + afterVault.assetsDeployed});
|
||||
}();
|
||||
if (afterVault.assetsTotal != expectedAssetsTotal)
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: stored AssetsTotal must equal AssetsAvailable "
|
||||
"plus AssetsDeployed, rounded Downward";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.yieldUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: YieldUnrealized must not be negative";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (fix340Enabled && afterVault.lossUnrealized < kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
|
||||
@@ -659,6 +781,18 @@ ValidVault::finalize(
|
||||
return !enforce; // That's all we can do here
|
||||
}
|
||||
|
||||
// ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE are the only transactors allowed to
|
||||
// write AssetsDeployed and YieldUnrealized; any new transactor that writes
|
||||
// either field must be added to both allow-lists below.
|
||||
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
|
||||
afterVault.assetsDeployed != beforeVault_[0].assetsDeployed && txnType != ttLOAN_SET &&
|
||||
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: vault transaction must not change AssetsDeployed";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
|
||||
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
|
||||
{
|
||||
@@ -668,6 +802,16 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
|
||||
afterVault.yieldUnrealized != beforeVault_[0].yieldUnrealized && txnType != ttLOAN_SET &&
|
||||
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: vault transaction must not change yield "
|
||||
"unrealized";
|
||||
result = false;
|
||||
}
|
||||
|
||||
// Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by
|
||||
// NoModifiedUnmodifiableFields in InvariantCheck.cpp.
|
||||
|
||||
@@ -716,7 +860,9 @@ ValidVault::finalize(
|
||||
}
|
||||
|
||||
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
|
||||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
|
||||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0 ||
|
||||
(afterVault.version == VaultVersion::FixedPrecision &&
|
||||
(afterVault.assetsDeployed != kZero || afterVault.yieldUnrealized != kZero)))
|
||||
{
|
||||
JLOG(j.fatal()) //
|
||||
<< "Invariant failed: created vault must be empty";
|
||||
@@ -880,7 +1026,17 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (vaultDeltaAssets <= kZero)
|
||||
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
|
||||
// delta is exact; rounding either one down to the posterior
|
||||
// AssetsTotal scale before the sign check can turn a legitimate
|
||||
// sub-unit deposit into a false zero. Compare the exact deltas
|
||||
// instead; Legacy/CashBasis keep the rounded comparison.
|
||||
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const exactVaultDeltaAssets = maybeVaultDeltaAssets->delta;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? exactVaultDeltaAssets : vaultDeltaAssets;
|
||||
|
||||
if (signCheckDeltaAssets <= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must increase vault balance";
|
||||
@@ -976,29 +1132,17 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(assetTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetTotalDelta == vaultDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: deposit and assets outstanding must add up";
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultDeltaAssets,
|
||||
exactVaultDeltaAssets,
|
||||
"deposit",
|
||||
j))
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1054,7 +1198,17 @@ ValidVault::finalize(
|
||||
auto const vaultPseudoDeltaAssets =
|
||||
roundToAsset(vaultAsset, vaultDeltaAssets.delta, minScale);
|
||||
|
||||
if (!zeroDeltaIsLegitimate && vaultPseudoDeltaAssets >= kZero)
|
||||
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
|
||||
// delta is exact; rounding either one down to the posterior
|
||||
// AssetsTotal scale before the sign check can turn a legitimate
|
||||
// sub-unit withdrawal into a false zero and reject it. Compare
|
||||
// the exact deltas instead; Legacy/CashBasis keep the rounded
|
||||
// comparison.
|
||||
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? vaultDeltaAssets.delta : vaultPseudoDeltaAssets;
|
||||
|
||||
if (!zeroDeltaIsLegitimate && signCheckDeltaAssets >= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
|
||||
result = false;
|
||||
@@ -1144,6 +1298,18 @@ ValidVault::finalize(
|
||||
|
||||
auto const localPseudoDeltaAssets =
|
||||
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
|
||||
auto const rate = transferRate(view, vaultAsset);
|
||||
bool const feeAdjustedWithdrawal = fix350Enabled && distinctDestination &&
|
||||
!vaultAsset.native() && tx[sfAccount] != vaultAsset.getIssuer() &&
|
||||
rate != kParityRate;
|
||||
auto expectedDestinationDelta = localPseudoDeltaAssets * -1;
|
||||
if (feeAdjustedWithdrawal)
|
||||
{
|
||||
expectedDestinationDelta =
|
||||
subtractTransferFee(
|
||||
STAmount{vaultAsset, expectedDestinationDelta}, rate)
|
||||
.value();
|
||||
}
|
||||
// For IOU assets near a precision boundary the destination's STAmount
|
||||
// exponent can shift, making part of the sent value unrepresentable at
|
||||
// the receiver's new scale — that portion is irreversibly absorbed by the
|
||||
@@ -1152,7 +1318,8 @@ ValidVault::finalize(
|
||||
// the destination's scale. Floor rounding is used so that values exactly
|
||||
// at the step boundary are not mistakenly dismissed. Any representable
|
||||
// discrepancy indicates a real accounting bug and must be caught.
|
||||
auto const destroyedIsSubUlp = tolerateZeroDelta &&
|
||||
auto const destroyedIsSubUlp = !feeAdjustedWithdrawal &&
|
||||
tolerateZeroDelta &&
|
||||
roundToAsset(
|
||||
vaultAsset,
|
||||
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
|
||||
@@ -1160,11 +1327,11 @@ ValidVault::finalize(
|
||||
Number::RoundingMode::Downward) == kZero;
|
||||
bool const withdrawAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
localPseudoDeltaAssets * -1,
|
||||
expectedDestinationDelta,
|
||||
roundedDestinationDelta,
|
||||
vaultAsset,
|
||||
localMinScale)
|
||||
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
|
||||
: expectedDestinationDelta == roundedDestinationDelta;
|
||||
if (!destroyedIsSubUlp && !withdrawAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
@@ -1205,33 +1372,17 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
// Note, vaultBalance is negative (see check above)
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetTotalDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
|
||||
: assetTotalDelta == vaultPseudoDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: withdrawal and assets outstanding must add up";
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultPseudoDeltaAssets,
|
||||
vaultDeltaAssets.delta,
|
||||
"withdrawal",
|
||||
j))
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultPseudoDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: withdrawal and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1263,39 +1414,33 @@ ValidVault::finalize(
|
||||
computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
|
||||
auto const vaultDeltaAssets =
|
||||
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
|
||||
if (vaultDeltaAssets >= kZero)
|
||||
// FixedPrecision: AssetsAvailable is exact and the
|
||||
// pseudo-account delta is exact; rounding either one down
|
||||
// to the posterior AssetsTotal scale before the sign
|
||||
// check can turn a legitimate sub-unit clawback into a
|
||||
// false zero and reject it. Compare the exact deltas
|
||||
// instead; Legacy/CashBasis keep the rounded comparison.
|
||||
bool const isFixedPrecision =
|
||||
afterVault.version == VaultVersion::FixedPrecision;
|
||||
Number const signCheckDeltaAssets =
|
||||
isFixedPrecision ? maybeVaultDeltaAssets->delta : vaultDeltaAssets;
|
||||
if (signCheckDeltaAssets >= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: clawback must decrease vault balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetsTotalDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
bool const totalAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetsTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetsTotalDelta == vaultDeltaAssets;
|
||||
if (!totalAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets outstanding must add up";
|
||||
if (!checkTotalsAddUp(
|
||||
afterVault,
|
||||
beforeVault,
|
||||
vaultAsset,
|
||||
fix340Enabled,
|
||||
minScale,
|
||||
vaultDeltaAssets,
|
||||
maybeVaultDeltaAssets->delta,
|
||||
"clawback",
|
||||
j))
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset,
|
||||
afterVault.assetsAvailable - beforeVault.assetsAvailable,
|
||||
minScale);
|
||||
bool const availableAddsUp = fix340Enabled
|
||||
? agreesWithinOneUnit(
|
||||
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
|
||||
: assetAvailableDelta == vaultDeltaAssets;
|
||||
if (!availableAddsUp)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets available must add up";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else if (!isVaultEmpty(beforeVault))
|
||||
{
|
||||
|
||||
@@ -254,6 +254,15 @@ private:
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const;
|
||||
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
tryConsumeOffer(
|
||||
PaymentSandbox& sb,
|
||||
Offer<TIn, TOut>& offer,
|
||||
TAmounts<TIn, TOut> const& ofrAmt,
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const;
|
||||
|
||||
// If clobQuality is available and has a better quality then return nullopt,
|
||||
// otherwise if amm liquidity is available return AMM offer adjusted based
|
||||
// on clobQuality.
|
||||
@@ -810,10 +819,11 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
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.
|
||||
// crossing limits are applied, here or in the callback's
|
||||
// limitStepIn()/limitStepOut(); remove that unusable offer instead
|
||||
// of letting it persist as a tecINTERNAL source. Overflows while
|
||||
// consuming the offer are handled by tryConsumeOffer().
|
||||
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
|
||||
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
|
||||
@@ -941,16 +951,9 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
removeOffer("Removing offer with overflowing amount calculation");
|
||||
return true;
|
||||
}
|
||||
// An overflow can only be produced by a crafted MPT offer, and MPT
|
||||
// offers require featureMPTokensV2 (enforced at OfferCreate
|
||||
// preflight). So the amendment is always enabled when we get here
|
||||
// and this legacy re-throw is unreachable in practice.
|
||||
// LCOV_EXCL_START
|
||||
XRPL_ASSERT(
|
||||
sb.rules().enabled(featureMPTokensV2),
|
||||
"xrpl::BookStep::forEachOffer : overflow implies MPTokensV2");
|
||||
// Legacy behavior: the exception escapes flow() and the
|
||||
// transaction fails with tecINTERNAL.
|
||||
throw;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
};
|
||||
|
||||
@@ -991,6 +994,32 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
return {offers.permToRemove(), counter.count()};
|
||||
}
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
BookStep<TIn, TOut, TDerived>::tryConsumeOffer(
|
||||
PaymentSandbox& sb,
|
||||
Offer<TIn, TOut>& offer,
|
||||
TAmounts<TIn, TOut> const& ofrAmt,
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const
|
||||
{
|
||||
try
|
||||
{
|
||||
// Under MPTokensV2, an overflow while consuming the offer fails the
|
||||
// strand instead of being handled by forEachOffer().
|
||||
consumeOffer(sb, offer, ofrAmt, stepAmt, ownerGives);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
if (sb.rules().enabled(featureMPTokensV2))
|
||||
{
|
||||
Throw<FlowException>(tecINTERNAL, "Overflow while consuming offer");
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
@@ -1192,7 +1221,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
savedOuts.insert(stpAmt.out);
|
||||
result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts));
|
||||
remainingOut = out - result.out;
|
||||
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
this->tryConsumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
// return true b/c even if the payment is satisfied,
|
||||
// we need to consume the offer. An offer limited by what the
|
||||
// issuer can issue is not consumed, though: its remainder is still
|
||||
@@ -1224,7 +1253,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
savedOuts.insert(remainingOut);
|
||||
result.in = sum(savedIns);
|
||||
result.out = out;
|
||||
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
|
||||
// Explicitly check whether the offer is funded. Given that we have
|
||||
// (stpAmt.out > remainingOut), it's natural to assume the offer
|
||||
@@ -1408,7 +1437,7 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
savedOuts = std::move(savedOutsAdj);
|
||||
result = resultAdj;
|
||||
remainingIn = in - result.in;
|
||||
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
|
||||
// When the mantissas of two iou amounts differ by less than ten, then
|
||||
// subtracting them leaves a result of zero. This can cause the check
|
||||
@@ -1512,6 +1541,18 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
|
||||
return temBAD_PATH;
|
||||
}
|
||||
|
||||
// An issuer paying with its own MPT issues it into the first book. A
|
||||
// holder-sourced strand records the MPT in the first MPTEndpointStep and
|
||||
// so rejects a later book that outputs it again; do the same here. The
|
||||
// step after such a book issues the MPT a second time and the payment
|
||||
// engine cannot keep the two within MaximumAmount once StrandFlow
|
||||
// rebuilds the sandbox on a limiting step.
|
||||
if (ctx.isFirst && book_.in.holds<MPTIssue>() && book_.in.getIssuer() == ctx.strandSrc &&
|
||||
ctx.view.rules().enabled(featureMPTokensV2))
|
||||
{
|
||||
ctx.seenDirectAssets[0].insert(book_.in);
|
||||
}
|
||||
|
||||
// Do not allow two books to output the same issue. This may cause offers on
|
||||
// one step to unfund offers in another step.
|
||||
if (!ctx.seenBookOuts.insert(book_.out).second ||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -874,15 +875,18 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
|
||||
|
||||
// pure issue/redeem can't be frozen (issuer/holder)
|
||||
// For the first step: check global freeze of the step's own asset.
|
||||
// For the last step: check only the per-holder MPToken lock.
|
||||
// For the last step: check only the per-holder lock.
|
||||
// Global freeze of the deliver asset is not checked here
|
||||
// because MPT semantics allow issuer<->holder transfers even when globally
|
||||
// locked — only holder-to-holder DEX paths are restricted.
|
||||
// For both steps: a vault share inherits the freeze of the vault's
|
||||
// underlying asset.
|
||||
if (!(ctx.isLast && ctx.isFirst))
|
||||
{
|
||||
auto const& account = ctx.isFirst ? src_ : dst_;
|
||||
bool const frozen = (ctx.isFirst && isGlobalFrozen(ctx.view, mptIssue_)) ||
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_);
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_) ||
|
||||
isVaultPseudoAccountFrozen(ctx.view, account, mptIssue_, 0);
|
||||
if (frozen)
|
||||
return terLOCKED;
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
@@ -45,11 +43,9 @@ EscrowCancel::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, account, amount, false);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, account, amount, false);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -110,11 +106,16 @@ EscrowCancel::doApply()
|
||||
auto const sle = ctx_.view().peek(keylet::account(account));
|
||||
STAmount const amount = escrow->getFieldAmount(sfAmount);
|
||||
|
||||
auto const reserveToSubtract = calculateAdditionalReserve((*escrow)[~sfBytecode]);
|
||||
|
||||
// The return can re-create a holding the owner deleted while the escrow
|
||||
// was pending; the removed escrow must not be counted against its reserve.
|
||||
bool const recycleReserve = ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), sle, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
|
||||
// Transfer amount back to the owner
|
||||
if (isXRP(amount))
|
||||
@@ -128,21 +129,19 @@ EscrowCancel::doApply()
|
||||
|
||||
auto const issuer = amount.getIssuer();
|
||||
bool const createAsset = account == accountID_;
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
kParityRate,
|
||||
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : escrow.mutableRawSle(),
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account, // sender and receiver are the same
|
||||
account,
|
||||
createAsset,
|
||||
j_);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
kParityRate,
|
||||
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : escrow.mutableRawSle(),
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account, // sender and receiver are the same
|
||||
account,
|
||||
createAsset,
|
||||
j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -160,7 +159,10 @@ EscrowCancel::doApply()
|
||||
}
|
||||
|
||||
if (!recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), sle, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), sle, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
|
||||
// Remove escrow from ledger
|
||||
escrow.erase();
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/conditions/Condition.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/entries/EscrowEntry.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -36,7 +37,6 @@
|
||||
|
||||
#include <memory>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -130,6 +130,19 @@ escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
EscrowCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
XRPAmount txnFees{Transactor::calculateBaseFee(view, tx)};
|
||||
if (tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
// Ten base fees in all: `Transactor::calculateBaseFee` charges one,
|
||||
// this adds nine. Plus 5 drops per byte.
|
||||
txnFees += 9 * view.fees().base + 5 * tx[sfBytecode].size();
|
||||
}
|
||||
return txnFees;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -139,9 +152,8 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
if (!ctx.rules.enabled(featureTokenEscrow))
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) { return escrowCreatePreflightHelper<T>(ctx); },
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit(
|
||||
[&]<typename T>(T const&) { return escrowCreatePreflightHelper<T>(ctx); });
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -161,12 +173,19 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfCancelAfter))
|
||||
return temBAD_EXPIRATION;
|
||||
|
||||
// In the absence of a FinishAfter, the escrow can be finished
|
||||
// immediately, which can be confusing. When creating an escrow,
|
||||
// we want to ensure that either a FinishAfter time is explicitly
|
||||
// specified or a completion condition is attached.
|
||||
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
|
||||
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition] && !ctx.tx[~sfBytecode])
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Must have at least one of FinishAfter, "
|
||||
"Condition, or Bytecode.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const cb = ctx.tx[~sfCondition])
|
||||
{
|
||||
@@ -182,6 +201,33 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfData))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate with Data requires Bytecode";
|
||||
return temMALFORMED;
|
||||
}
|
||||
auto const data = ctx.tx.getFieldVL(sfData);
|
||||
if (data.size() > kMaxWasmDataLength)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Data bad size " << data.size();
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
// amendment was checked by checkExtraFeatures
|
||||
if (ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
auto const code = ctx.tx.getFieldVL(sfBytecode);
|
||||
// Protocol ceiling only; `preflight` has no view.
|
||||
if (code.empty() || code.size() > kMaxBytecodeSizeLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode bad size " << code.size();
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -208,14 +254,36 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (!ctx.view.rules().enabled(featureTokenEscrow))
|
||||
return temDISABLED; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The limits are voted, so they can change between ledgers. A `tef`, unlike
|
||||
// a `tem`, does not mark the transaction bad for the hold time.
|
||||
if (ctx.tx.isFieldPresent(sfBytecode))
|
||||
{
|
||||
auto const& fees = ctx.view.fees();
|
||||
if (isBytecodeUploadDisabled(fees))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
|
||||
return tefTEMP_DISABLED;
|
||||
}
|
||||
|
||||
auto const code = ctx.tx.getFieldVL(sfBytecode);
|
||||
if (code.size() > fees.bytecodeSizeLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode over voted limit " << code.size();
|
||||
return tefEXCEEDS_LIMIT;
|
||||
}
|
||||
|
||||
// TODO(SmartEscrow): screen the module itself here - that it compiles,
|
||||
// and that it imports and exports what the engine can serve - once the
|
||||
// WASM engine lands. Until then any blob of the right size is taken.
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -236,6 +304,7 @@ EscrowCreate::doApply()
|
||||
|
||||
// Check reserve and funds availability
|
||||
STAmount const amount{ctx_.tx[sfAmount]};
|
||||
auto const reserveToAdd = calculateAdditionalReserve(ctx_.tx[~sfBytecode]);
|
||||
|
||||
auto const balance = sle->getFieldAmount(sfBalance).xrp();
|
||||
// First check: whoever is on the hook for the new owner increment
|
||||
@@ -243,8 +312,8 @@ EscrowCreate::doApply()
|
||||
// validates the sponsor's reserve + remaining credit. When
|
||||
// unsponsored this hits the source branch and validates the
|
||||
// source's pre-lock balance against base + (currentOC+1)*increment.
|
||||
if (auto const ret =
|
||||
checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = reserveToAdd}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -258,11 +327,15 @@ EscrowCreate::doApply()
|
||||
// checkReserve) because that helper diverts to the sponsor's balance
|
||||
// when a sponsor is present and would ignore the source's post-lock
|
||||
// balance entirely. ownerCountDelta differs by case:
|
||||
// - sponsored: 0 — sponsor covers the new owner increment, so the
|
||||
// source only owes reserve for its current owners.
|
||||
// - unsponsored: 1 — source owes reserve including the new increment.
|
||||
// - sponsored: 0 — sponsor covers the new owner increment, so
|
||||
// the source only owes reserve for its current owners.
|
||||
// - unsponsored: reserveToAdd — source owes reserve including the new
|
||||
// increment, which Bytecode can make larger than one unit.
|
||||
auto const sourceReserve = accountReserve(
|
||||
ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1});
|
||||
ctx_.view(),
|
||||
sle,
|
||||
j_,
|
||||
{.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : reserveToAdd});
|
||||
if (balance - STAmount(amount).xrp() < sourceReserve)
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
@@ -289,6 +362,8 @@ EscrowCreate::doApply()
|
||||
(*escrow)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
|
||||
(*escrow)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
|
||||
(*escrow)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
|
||||
(*escrow)[~sfBytecode] = ctx_.tx[~sfBytecode];
|
||||
(*escrow)[~sfData] = ctx_.tx[~sfData];
|
||||
|
||||
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
|
||||
{
|
||||
@@ -344,11 +419,9 @@ EscrowCreate::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
return ret; // LCOV_EXCL_LINE
|
||||
@@ -356,7 +429,7 @@ EscrowCreate::doApply()
|
||||
}
|
||||
|
||||
// increment owner count
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, reserveToAdd, ctx_.journal);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), escrow.mutableRawSle());
|
||||
ctx_.view().update(sle);
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -27,8 +28,9 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -79,7 +81,24 @@ EscrowFinish::preflight(PreflightContext const& ctx)
|
||||
// If you specify a condition, then you must also specify
|
||||
// a fulfillment.
|
||||
if (static_cast<bool>(cb) != static_cast<bool>(fb))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Condition != Fulfillment";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const allowance = ctx.tx[~sfGas]; allowance)
|
||||
{
|
||||
// Protocol ceiling only; `preflight` has no view.
|
||||
if (*allowance == 0)
|
||||
{
|
||||
return temBAD_LIMIT;
|
||||
}
|
||||
if (*allowance > kMaxGasLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Gas too large: " << *allowance;
|
||||
return temBAD_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -148,7 +167,15 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
extraFee += view.fees().base * (32 + (fb->size() / 16));
|
||||
}
|
||||
|
||||
if (auto const allowance = tx[~sfGas]; allowance)
|
||||
{
|
||||
// The extra fee is the allowance in drops, rounded up to the nearest
|
||||
// whole drop. Widen before multiplying: both factors are 32-bit.
|
||||
std::uint64_t const allowanceFee =
|
||||
((std::uint64_t{*allowance} * view.fees().gasPrice) + microDropsPerDrop - 1) /
|
||||
microDropsPerDrop;
|
||||
extraFee += allowanceFee;
|
||||
}
|
||||
return Transactor::calculateBaseFee(view, tx) + extraFee;
|
||||
}
|
||||
|
||||
@@ -162,25 +189,64 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
|
||||
return err;
|
||||
}
|
||||
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow))
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow) ||
|
||||
ctx.view.rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// this check is done in doApply before this amendment is enabled
|
||||
auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]);
|
||||
EscrowEntryR const escrow(ctx.tx[sfOwner], seqProxy, ctx.view);
|
||||
if (!escrow)
|
||||
return tecNO_TARGET;
|
||||
|
||||
AccountID const dest = (*escrow)[sfDestination];
|
||||
STAmount const amount = (*escrow)[sfAmount];
|
||||
|
||||
if (!isXRP(amount))
|
||||
if (ctx.view.rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
if (escrow->isFieldPresent(sfBytecode))
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfGas))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Bytecode requires Gas";
|
||||
return tefBYTECODE_NOT_INCLUDED;
|
||||
}
|
||||
|
||||
// The limits are voted, so they can change between ledgers.
|
||||
// A `tef`, unlike a `tem`, does not mark the transaction bad
|
||||
// for the hold time.
|
||||
auto const& fees = ctx.view.fees();
|
||||
if (isBytecodeExecutionDisabled(fees))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "WASM runtime deactivated by fee voting";
|
||||
return tefTEMP_DISABLED;
|
||||
}
|
||||
|
||||
if (ctx.tx[sfGas] > fees.gasLimit)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Gas over voted limit: " << ctx.tx[sfGas];
|
||||
return tefEXCEEDS_LIMIT;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ctx.tx.isFieldPresent(sfGas))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Bytecode not present, "
|
||||
"Gas present";
|
||||
return tefNO_BYTECODE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ctx.view.rules().enabled(featureTokenEscrow))
|
||||
{
|
||||
AccountID const dest = (*escrow)[sfDestination];
|
||||
STAmount const amount = (*escrow)[sfAmount];
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
|
||||
},
|
||||
amount.asset().value());
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
@@ -193,7 +259,8 @@ EscrowFinish::doApply()
|
||||
EscrowEntryW escrow(ctx_.tx[sfOwner], seqProxy, ctx_.view(), j_);
|
||||
if (!escrow)
|
||||
{
|
||||
if (ctx_.view().rules().enabled(featureTokenEscrow))
|
||||
if (ctx_.view().rules().enabled(featureTokenEscrow) ||
|
||||
ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
return tecNO_TARGET;
|
||||
@@ -211,6 +278,20 @@ EscrowFinish::doApply()
|
||||
if ((*escrow)[~sfCancelAfter] && after(now, (*escrow)[sfCancelAfter]))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
AccountID const destID = (*escrow)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
if (ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
// Check cryptocondition fulfillment
|
||||
{
|
||||
auto const id = ctx_.tx.getTransactionID();
|
||||
@@ -264,16 +345,33 @@ EscrowFinish::doApply()
|
||||
return tecCRYPTOCONDITION_ERROR;
|
||||
}
|
||||
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
AccountID const destID = (*escrow)[sfDestination];
|
||||
auto const sled = ctx_.view().peek(keylet::account(destID));
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
if (!ctx_.view().rules().enabled(featureSmartEscrow))
|
||||
{
|
||||
// NOTE: Escrow payments cannot be used to fund accounts.
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, destID, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
// Execute custom release function
|
||||
if ((*escrow)[~sfBytecode])
|
||||
{
|
||||
// TODO(SmartEscrow): run the escrow's contract here once the WASM
|
||||
// engine lands. That change also brings with it the reporting the run
|
||||
// produces - the gas it consumed and the code it returned, in the
|
||||
// transaction metadata - and the tecBYTECODE_REJECTED result that lets
|
||||
// a rejected escrow survive with the data the contract wrote.
|
||||
//
|
||||
// Until then an escrow carrying bytecode can be created, funded and
|
||||
// cancelled, but not finished. Reachable only under featureSmartEscrow,
|
||||
// which is Supported::No.
|
||||
JLOG(j_.debug()) << "EscrowFinish: no WASM engine to run the escrow's bytecode";
|
||||
return tecFAILED_PROCESSING;
|
||||
}
|
||||
|
||||
AccountID const account = (*escrow)[sfAccount];
|
||||
|
||||
@@ -301,13 +399,18 @@ EscrowFinish::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
auto const reserveToSubtract = calculateAdditionalReserve((*escrow)[~sfBytecode]);
|
||||
|
||||
// Delivery can auto-create the destination's holding; the removed escrow
|
||||
// must not be counted against its reserve. The two share a reserve payer
|
||||
// for a self-escrow, or when one sponsor covers both.
|
||||
bool const recycleReserve =
|
||||
ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
|
||||
if (recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), account, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
|
||||
STAmount const amount = escrow->getFieldAmount(sfAmount);
|
||||
// Transfer amount to destination
|
||||
@@ -325,21 +428,19 @@ EscrowFinish::doApply()
|
||||
: kParityRate;
|
||||
auto const issuer = amount.getIssuer();
|
||||
bool const createAsset = destID == accountID_;
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
lockedRate,
|
||||
sled,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account,
|
||||
destID,
|
||||
createAsset,
|
||||
j_);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
lockedRate,
|
||||
sled,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account,
|
||||
destID,
|
||||
createAsset,
|
||||
j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -359,7 +460,10 @@ EscrowFinish::doApply()
|
||||
ctx_.view().update(sled);
|
||||
|
||||
if (!recycleReserve)
|
||||
decreaseOwnerCountForObject(ctx_.view(), account, escrow.mutableRawSle(), 1, ctx_.journal);
|
||||
{
|
||||
decreaseOwnerCountForObject(
|
||||
ctx_.view(), account, escrow.mutableRawSle(), reserveToSubtract, ctx_.journal);
|
||||
}
|
||||
|
||||
// Remove escrow from ledger
|
||||
escrow.erase();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
@@ -158,7 +159,7 @@ determineAsset(
|
||||
|
||||
std::expected<STAmount, TER>
|
||||
determineClawAmount(
|
||||
SLE const& sleBroker,
|
||||
SLE::ConstRef sleBroker,
|
||||
Asset const& vaultAsset,
|
||||
std::optional<STAmount> const& amount,
|
||||
SLE::ConstRef vaultSle,
|
||||
@@ -169,17 +170,19 @@ determineClawAmount(
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
return minimumBrokerCover(
|
||||
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]), vaultSle);
|
||||
sleBroker->at(sfDebtTotal),
|
||||
TenthBips32(sleBroker->at(sfCoverRateMinimum)),
|
||||
vaultSle);
|
||||
}
|
||||
|
||||
// Always round the minimum required up
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
return tenthBipsOfValue(
|
||||
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
|
||||
sleBroker->at(sfDebtTotal), TenthBips32(sleBroker->at(sfCoverRateMinimum)));
|
||||
}();
|
||||
// The subtraction probably won't round, but round down if it does.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
|
||||
return sleBroker[sfCoverAvailable] - minRequiredCover;
|
||||
return sleBroker->at(sfCoverAvailable) - minRequiredCover;
|
||||
}();
|
||||
if (maxClawAmount <= beast::kZero)
|
||||
return std::unexpected(tecINSUFFICIENT_FUNDS);
|
||||
@@ -187,12 +190,23 @@ determineClawAmount(
|
||||
// Use the vaultAsset here, because it will be the right type in all
|
||||
// circumstances. The amount may be an IOU indicating the pseudo-account's
|
||||
// asset, which is correct, but not what is needed here.
|
||||
if (!amount || *amount == beast::kZero)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
Number const magnitude{*amount};
|
||||
if (magnitude > maxClawAmount)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
return STAmount{vaultAsset, magnitude};
|
||||
STAmount requested = [&] {
|
||||
if (!amount || *amount == beast::kZero)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
Number const magnitude{*amount};
|
||||
if (magnitude > maxClawAmount)
|
||||
return STAmount{vaultAsset, maxClawAmount};
|
||||
return STAmount{vaultAsset, magnitude};
|
||||
}();
|
||||
|
||||
if (getVaultVersion(vaultSle) != VaultVersion::FixedPrecision)
|
||||
return requested;
|
||||
|
||||
STAmount rounded = debitToPosteriorBrokerCoverScale(
|
||||
vaultSle, sleBroker, requested, Number::RoundingMode::TowardsZero);
|
||||
if (rounded == beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
return rounded;
|
||||
}
|
||||
|
||||
template <ValidIssueType T>
|
||||
@@ -294,17 +308,25 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
|
||||
auto const findClawAmount =
|
||||
determineClawAmount(*sleBroker, vaultAsset, amount, vault, ctx.view.rules());
|
||||
determineClawAmount(sleBroker, vaultAsset, amount, vault, ctx.view.rules());
|
||||
if (!findClawAmount)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
|
||||
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum, or the claw amount "
|
||||
"rounds to zero: "
|
||||
<< transToken(findClawAmount.error());
|
||||
return findClawAmount.error();
|
||||
}
|
||||
STAmount const& clawAmount = *findClawAmount;
|
||||
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
|
||||
return ret;
|
||||
// FixedPrecision outflows already rounded at the posterior exponent; the
|
||||
// live CoverAvailable scale used by canApplyToBrokerCover would reject a
|
||||
// re-fining clawback as sub-ULP.
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
{
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Explicitly check the balance of the trust line / MPT to make sure the
|
||||
// balance is actually there. It should always match `sfCoverAvailable`, so
|
||||
@@ -357,7 +379,7 @@ LoanBrokerCoverClawback::doApply()
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
|
||||
auto const findClawAmount =
|
||||
determineClawAmount(*sleBroker, vaultAsset, amount, vault, view().rules());
|
||||
determineClawAmount(sleBroker, vaultAsset, amount, vault, view().rules());
|
||||
if (!findClawAmount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
STAmount const& clawAmount = *findClawAmount;
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -104,22 +106,35 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// here in preclaim lets us reject sub-cover-scale dust early with tecPRECISION_LOSS instead of
|
||||
// failing only in doApply.
|
||||
auto const roundedAmount = [&] -> STAmount {
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Cover deposit is an inflow to CoverAvailable, so it must floor
|
||||
// the sum, not just the delta.
|
||||
return creditToPosteriorBrokerCoverScale(
|
||||
vault, sleBroker, amount, Number::RoundingMode::Downward);
|
||||
}
|
||||
if (!fix320Enabled)
|
||||
return tx[sfAmount];
|
||||
return STAmount{amount};
|
||||
|
||||
return roundToScale(
|
||||
tx[sfAmount],
|
||||
amount,
|
||||
scale(sleBroker->at(sfCoverAvailable), vaultAsset),
|
||||
Number::RoundingMode::Downward);
|
||||
}();
|
||||
|
||||
if (fix320Enabled && roundedAmount == beast::kZero)
|
||||
// fixCleanup3_2_0 is enabled on the network, so this also covers the
|
||||
// FixedPrecision zero-credit case.
|
||||
if (fix320Enabled && roundedAmount <= beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
|
||||
<< " is zero at loan broker scale";
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << amount
|
||||
<< " is zero or negative at loan broker scale";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
if (auto const ter = checkOptionalBrokerCoverInflow(vault, sleBroker, roundedAmount);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
account,
|
||||
@@ -155,6 +170,13 @@ LoanBrokerCoverDeposit::doApply()
|
||||
// see the rationale comment in preclaim.
|
||||
bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
|
||||
auto const amount = [&] -> STAmount {
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// Cover deposit is an inflow to CoverAvailable, so it must floor
|
||||
// the sum, not just the delta.
|
||||
return creditToPosteriorBrokerCoverScale(
|
||||
vault, broker, tx[sfAmount], Number::RoundingMode::Downward);
|
||||
}
|
||||
if (!fix320Enabled)
|
||||
return tx[sfAmount];
|
||||
|
||||
@@ -164,12 +186,12 @@ LoanBrokerCoverDeposit::doApply()
|
||||
Number::RoundingMode::Downward);
|
||||
}();
|
||||
|
||||
// We validated zero-amount in preclaim, if we ended up with zero now, fail hard.
|
||||
if (amount == beast::kZero)
|
||||
// We validated zero-or-negative amount in preclaim, if we ended up with one now, fail hard.
|
||||
if (amount <= beast::kZero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
|
||||
<< " is zero";
|
||||
<< " is zero or negative";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -103,10 +104,28 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
if (amount.asset() != vaultAsset)
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
// Helper handles both IOU and MPT correctly without explicit branching.
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverWithdraw"))
|
||||
return ret;
|
||||
STAmount roundedAmount{amount};
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
{
|
||||
// FixedPrecision outflows are rounded at the posterior exponent here
|
||||
// instead of going through canApplyToBrokerCover: the live
|
||||
// CoverAvailable scale it uses would reject a re-fining withdrawal as
|
||||
// sub-ULP.
|
||||
roundedAmount = debitToPosteriorBrokerCoverScale(
|
||||
vault, sleBroker, amount, Number::RoundingMode::TowardsZero);
|
||||
if (roundedAmount == beast::kZero)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "LoanBrokerCoverWithdraw: withdraw amount: " << amount
|
||||
<< " is zero at loan broker scale";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = canApplyToBrokerCover(
|
||||
ctx.view, sleBroker, vaultAsset, roundedAmount, ctx.j, "LoanBrokerCoverWithdraw"))
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The broker's pseudo-account is the source of funds.
|
||||
auto const pseudoAccountID = sleBroker->at(sfAccount);
|
||||
@@ -129,7 +148,15 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
AuthType authType = AuthType::WeakAuth;
|
||||
if (account != dstAcct)
|
||||
{
|
||||
if (auto const ret = canWithdraw(ctx.view, tx))
|
||||
// Check the amount actually sent: on FixedPrecision it is the rounded
|
||||
// amount, not sfAmount.
|
||||
if (auto const ret = canWithdraw(
|
||||
ctx.view,
|
||||
account,
|
||||
dstAcct,
|
||||
roundedAmount,
|
||||
tx.isFieldPresent(sfDestinationTag),
|
||||
tx[~sfCredentialIDs]))
|
||||
return ret;
|
||||
|
||||
// The destination account must have consented to receive the asset by
|
||||
@@ -184,9 +211,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
|
||||
scale(currentDebtTotal, vaultAsset));
|
||||
}();
|
||||
if (coverAvail < amount)
|
||||
if (coverAvail < roundedAmount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
if ((coverAvail - amount) < minimumCover)
|
||||
if ((coverAvail - roundedAmount) < minimumCover)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
auto const freezeHandling = fix330Enabled && dstAcct == vaultAsset.getIssuer()
|
||||
@@ -199,7 +226,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
vaultAsset,
|
||||
freezeHandling,
|
||||
AuthHandling::ZeroIfUnauthorized,
|
||||
ctx.j) < amount)
|
||||
ctx.j) < roundedAmount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -211,7 +238,7 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
auto const& tx = ctx_.tx;
|
||||
|
||||
auto const brokerID = tx[sfLoanBrokerID];
|
||||
auto const amount = tx[sfAmount];
|
||||
auto const requestedAmount = tx[sfAmount];
|
||||
auto const dstAcct = tx[~sfDestination].value_or(accountID_);
|
||||
|
||||
auto const broker = view().peek(keylet::loanBroker(brokerID));
|
||||
@@ -223,6 +250,10 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const vaultAsset = vault->at(sfAsset);
|
||||
auto const amount = getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? debitToPosteriorBrokerCoverScale(
|
||||
vault, broker, requestedAmount, Number::RoundingMode::TowardsZero)
|
||||
: requestedAmount;
|
||||
|
||||
auto const brokerPseudoID = *broker->at(sfAccount);
|
||||
|
||||
@@ -239,6 +270,7 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
brokerPseudoID,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
amount,
|
||||
j_);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -77,7 +78,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// Any remaining debt should have been wiped out by the last Loan
|
||||
// Delete. This check is purely defensive.
|
||||
auto const scale = getAssetsTotalScale(vault);
|
||||
auto const scale = getVaultScale(vault);
|
||||
|
||||
auto const rounded =
|
||||
roundToAsset(asset, debtTotal, scale, Number::RoundingMode::TowardsZero);
|
||||
@@ -94,21 +95,38 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
auto const coverAvailable = STAmount{asset, sleBroker->at(sfCoverAvailable)};
|
||||
// If there are assets in the cover, broker will receive them on deletion.
|
||||
// So we need to check if the broker owner is deep frozen for that asset.
|
||||
if (coverAvailable > beast::kZero)
|
||||
{
|
||||
auto const brokerPseudo = sleBroker->at(sfAccount);
|
||||
|
||||
// Pre-fixCleanup3_5_0: only freeze checks apply to the cover payout.
|
||||
// Post-fixCleanup3_5_0: apply the cover-withdraw transfer and authorization checks too.
|
||||
if (ctx.view.rules().enabled(fixCleanup3_5_0))
|
||||
{
|
||||
// Like a cover withdrawal, the payout skips the lsfMPTCanTransfer check so that an
|
||||
// issuer cannot trap the broker's cover.
|
||||
if (auto const ret = canTransfer(
|
||||
ctx.view, asset, brokerPseudo, brokerOwner, WaiveMPTCanTransfer::Yes))
|
||||
return ret;
|
||||
|
||||
if (auto const ret = requireAuth(ctx.view, asset, brokerOwner, AuthType::WeakAuth))
|
||||
return ret;
|
||||
|
||||
if (!holdingExists(ctx.view, brokerOwner, asset))
|
||||
{
|
||||
if (auto const ret = canAddHolding(ctx.view, asset); !isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Broker owner account is frozen.";
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.view.rules().enabled(fixCleanup3_2_0))
|
||||
{
|
||||
if (coverAvailable > beast::kZero)
|
||||
if (ctx.view.rules().enabled(fixCleanup3_2_0))
|
||||
{
|
||||
auto const brokerPseudo = sleBroker->at(sfAccount);
|
||||
if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen/locked.";
|
||||
@@ -151,8 +169,36 @@ LoanBrokerDelete::doApply()
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
auto const owner = view().peek(keylet::account(accountID_));
|
||||
if (!owner)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
// Pre-fixCleanup3_5_0: the owner count drops after the pseudo-account is erased.
|
||||
// Post-fixCleanup3_5_0: it drops first, so the owner's holding created below can use the
|
||||
// reserve that the broker frees.
|
||||
bool const fix350Enabled = view().rules().enabled(fixCleanup3_5_0);
|
||||
if (fix350Enabled)
|
||||
{
|
||||
// Decreases the owner count by two: one for the LoanBroker object, and one
|
||||
// for the pseudo-account.
|
||||
decreaseOwnerCountForObject(view(), owner, broker, 2, j_);
|
||||
}
|
||||
|
||||
{
|
||||
auto const coverAvailable = STAmount{vaultAsset, broker->at(sfCoverAvailable)};
|
||||
|
||||
// Pre-fixCleanup3_5_0: the cover goes straight to accountSend, so an MPT payout to an
|
||||
// owner with no MPToken fails with tecNO_AUTH.
|
||||
// Post-fixCleanup3_5_0: a nonzero cover first gets the owner a holding if it has none, as a
|
||||
// withdrawal to self does.
|
||||
if (fix350Enabled && coverAvailable > beast::kZero &&
|
||||
!holdingExists(view(), accountID_, vaultAsset))
|
||||
{
|
||||
if (auto const ter = addEmptyHolding(
|
||||
ctx_.getApplyViewContext(), accountID_, preFeeBalance_, vaultAsset, j_))
|
||||
return ter;
|
||||
}
|
||||
|
||||
if (auto const ter = accountSend(
|
||||
view(), brokerPseudoID, accountID_, coverAvailable, j_, {}, WaiveTransferFee::Yes))
|
||||
return ter;
|
||||
@@ -185,15 +231,8 @@ LoanBrokerDelete::doApply()
|
||||
|
||||
view().erase(brokerPseudoSLE);
|
||||
|
||||
{
|
||||
auto const owner = view().peek(keylet::account(accountID_));
|
||||
if (!owner)
|
||||
return tefBAD_LEDGER; // LCOV_EXCL_LINE
|
||||
|
||||
// Decreases the owner count by two: one for the LoanBroker object, and
|
||||
// one for the pseudo-account.
|
||||
if (!fix350Enabled)
|
||||
decreaseOwnerCountForObject(view(), owner, broker, 2, j_);
|
||||
}
|
||||
|
||||
view().erase(broker);
|
||||
|
||||
|
||||
@@ -232,7 +232,9 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (auto const value = tx[field]; value &&
|
||||
((fix350Enabled && !isRepresentable(asset, *value)) ||
|
||||
STAmount{asset, *value} != *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");
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#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>
|
||||
@@ -131,15 +133,130 @@ LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
STAmount
|
||||
LoanManage::calculateDefaultCover(SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle)
|
||||
{
|
||||
auto const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)};
|
||||
|
||||
Number const rawCover = [&] {
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
|
||||
// Compute minimum cover
|
||||
Number const minimumCover =
|
||||
tenthBipsOfValue(brokerSle->at(sfDebtTotal).value(), coverRateMinimum);
|
||||
|
||||
// Apply coverRateLiquidation
|
||||
return std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), principalOutstanding);
|
||||
}();
|
||||
|
||||
// rawCover is a product of multiplication and can carry more digits than STAmount's. Keep the
|
||||
// cap as an exact Number: rounding it to 16 digits first could drop digits that change
|
||||
// floor16(AssetsAvailable + cover).
|
||||
Number const cappedCover = std::min(rawCover, *brokerSle->at(sfCoverAvailable));
|
||||
|
||||
if (Asset{vaultAsset}.integral())
|
||||
{
|
||||
// Fix the mode so the STAmount conversion truncates the fractional part.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero);
|
||||
return STAmount{vaultAsset, cappedCover};
|
||||
}
|
||||
|
||||
// Floor AssetsAvailable + cover at its posterior grid so the Vault side is exact, unless
|
||||
// AssetsAvailable has digits finer than the 16-digit cover can carry. The LoanBroker absorbs
|
||||
// the rounding: CoverAvailable - cover may need finer digits than its posterior grid and
|
||||
// rounds to nearest, as the broker's trust line does. Downward also keeps the 19-digit
|
||||
// AssetsAvailable + cover sum from rounding up past cappedCover.
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
|
||||
return creditToPosteriorAvailableScale(vaultSle, cappedCover, Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Defaults a Loan on a FixedPrecision Vault. Cover is the raw XLS-66
|
||||
// First-Loss Capital amount, capped at the LoanBroker's CoverAvailable and
|
||||
// floored so AssetsAvailable + cover lands exactly on its posterior grid (a
|
||||
// no-op for integral assets). AssetsAvailable rises by cover through the cash writer;
|
||||
// AssetsDeployed drops by the Loan's full PrincipalOutstanding exactly, and
|
||||
// AssetsTotal is re-derived from AssetsAvailable + AssetsDeployed by the
|
||||
// writer's sync -- this function never touches sfAssetsTotal directly.
|
||||
[[nodiscard]] TER
|
||||
defaultLoanFixedPrecision(
|
||||
ApplyView& view,
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
beast::Journal j)
|
||||
{
|
||||
Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
Number const scheduledInterest = loanSle->at(sfTotalValueOutstanding) - principalOutstanding -
|
||||
loanSle->at(sfManagementFeeOutstanding);
|
||||
|
||||
STAmount const coverAmount = LoanManage::calculateDefaultCover(loanSle, brokerSle, vaultSle);
|
||||
|
||||
// The broker's CoverAvailable decrease and the broker-to-vault transfer
|
||||
// both use this amount. AssetsAvailable + cover is exact unless
|
||||
// AssetsAvailable has digits finer than the 16-digit cover can carry; then
|
||||
// adjustVaultBalances rounds the sum. CoverAvailable - cover may need finer
|
||||
// digits than the broker's posterior grid; it rounds to nearest, as the
|
||||
// broker's trust line does.
|
||||
|
||||
Number const loss = loanSle->isFlag(lsfLoanImpaired) ? -principalOutstanding : 0;
|
||||
if (auto const ter = adjustVaultBalances(
|
||||
vaultSle,
|
||||
{
|
||||
.cash = coverAmount,
|
||||
.deployed = -principalOutstanding,
|
||||
.yield = -scheduledInterest,
|
||||
.loss = loss,
|
||||
},
|
||||
j);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
view.update(vaultSle);
|
||||
|
||||
brokerSle->at(sfDebtTotal) -= principalOutstanding;
|
||||
brokerSle->at(sfCoverAvailable) -= coverAmount;
|
||||
view.update(brokerSle);
|
||||
|
||||
loanSle->setFlag(lsfLoanDefault);
|
||||
loanSle->at(sfTotalValueOutstanding) = 0;
|
||||
loanSle->at(sfPaymentRemaining) = 0;
|
||||
loanSle->at(sfPrincipalOutstanding) = 0;
|
||||
loanSle->at(sfManagementFeeOutstanding) = 0;
|
||||
loanSle->at(sfNextPaymentDueDate) = 0;
|
||||
view.update(loanSle);
|
||||
|
||||
if (coverAmount == beast::kZero)
|
||||
return tesSUCCESS;
|
||||
|
||||
return accountSend(
|
||||
view,
|
||||
brokerSle->at(sfAccount),
|
||||
vaultSle->at(sfAccount),
|
||||
coverAmount,
|
||||
j,
|
||||
{},
|
||||
WaiveTransferFee::Yes);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TER
|
||||
LoanManage::defaultLoan(
|
||||
ApplyView& view,
|
||||
SLE::Ref loanSle,
|
||||
SLE::Ref brokerSle,
|
||||
SLE::Ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
|
||||
return defaultLoanFixedPrecision(view, loanSle, brokerSle, vaultSle, j);
|
||||
|
||||
auto const vaultAsset = *vaultSle->at(sfAsset);
|
||||
// Calculate the amount of the Default that First-Loss Capital covers:
|
||||
|
||||
std::int32_t const loanScale = loanSle->at(sfLoanScale);
|
||||
@@ -176,7 +293,7 @@ LoanManage::defaultLoan(
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
|
||||
{
|
||||
// Decrease the Total Value of the Vault:
|
||||
@@ -299,22 +416,39 @@ LoanManage::impairLoan(
|
||||
return tecTOO_SOON;
|
||||
}
|
||||
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
Number const lossUnrealized = loanVaultExposure(vaultSle, loanSle);
|
||||
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Update the Vault object(set "paper loss")
|
||||
auto const vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
|
||||
if (vaultLossUnrealizedProxy > vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
|
||||
// Update the Vault object (set "paper loss")
|
||||
if (fixedPrecision)
|
||||
{
|
||||
// Having a loss greater than the vault's unavailable assets
|
||||
// will leave the vault in an invalid / inconsistent state.
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
// adjustVaultBalances's own LU' > AD' check covers the comparison
|
||||
// against AssetsDeployed.
|
||||
if (auto const ter = adjustVaultBalances(vaultSle, {.loss = lossUnrealized}, j);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return ter;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
|
||||
if (vaultLossUnrealizedProxy >
|
||||
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
|
||||
{
|
||||
// Having a loss greater than the vault's unavailable assets
|
||||
// will leave the vault in an invalid / inconsistent state.
|
||||
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
|
||||
return tecLIMIT_EXCEEDED; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
view.update(vaultSle);
|
||||
|
||||
@@ -343,23 +477,32 @@ LoanManage::unimpairLoan(
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Update the Vault object(clear "paper loss")
|
||||
auto const vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
// Update the Vault object (clear "paper loss")
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
Number const lossReversed = loanVaultExposure(vaultSle, loanSle);
|
||||
if (vaultLossUnrealizedProxy < lossReversed)
|
||||
if (fixedPrecision)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is less than the amount to be cleared";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
// adjustVaultBalances's own LU' < 0 check covers the comparison.
|
||||
if (auto const ter = adjustVaultBalances(vaultSle, {.loss = -lossReversed}, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
if (vaultLossUnrealizedProxy < lossReversed)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.warn()) << "Vault unrealized loss is less than the amount to be cleared";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
}
|
||||
// Reverse the "paper loss"
|
||||
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
|
||||
view.update(vaultSle);
|
||||
|
||||
@@ -414,7 +557,7 @@ LoanManage::doApply()
|
||||
// Valid flag combinations are checked in preflight. No flags is valid -
|
||||
// just a noop.
|
||||
if (tx.isFlag(tfLoanDefault))
|
||||
return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
|
||||
return defaultLoan(view, loanSle, brokerSle, vaultSle, j_);
|
||||
if (tx.isFlag(tfLoanImpair))
|
||||
return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
|
||||
if (tx.isFlag(tfLoanUnimpair))
|
||||
@@ -427,9 +570,27 @@ LoanManage::doApply()
|
||||
// path. Post-amendment, we call associateAsset on all successful paths.
|
||||
if (view.rules().enabled(fixCleanup3_1_3) && isTesSuccess(result))
|
||||
{
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
[[maybe_unused]] bool const assetsEqualBeforeStore =
|
||||
fixedPrecision && vaultSle->at(sfAssetsAvailable) == vaultSle->at(sfAssetsTotal);
|
||||
|
||||
associateAsset(*loanSle, vaultAsset);
|
||||
associateAsset(*brokerSle, vaultAsset);
|
||||
associateAsset(*vaultSle, vaultAsset);
|
||||
|
||||
if (fixedPrecision)
|
||||
{
|
||||
[[maybe_unused]] Number const assetsAvailableAfter = *vaultSle->at(sfAssetsAvailable);
|
||||
[[maybe_unused]] Number const assetsTotalAfter = *vaultSle->at(sfAssetsTotal);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= assetsTotalAfter,
|
||||
"xrpl::LoanManage::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
XRPL_ASSERT_PARTS(
|
||||
!assetsEqualBeforeStore || assetsAvailableAfter == assetsTotalAfter,
|
||||
"xrpl::LoanManage::doApply",
|
||||
"equal assets remain equal after storage");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -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>
|
||||
@@ -45,14 +46,12 @@ isPaymentAmountInvalid(STAmount const& amount)
|
||||
return amount <= beast::kZero;
|
||||
}
|
||||
|
||||
// Returns the account's true, unclamped balance in `asset`, for use only in
|
||||
// fund-conservation checks. accountHolds(..., SpendableHandling::FullBalance)
|
||||
// cannot be used for this: for XRP it always defers to xrpLiquid, which
|
||||
// subtracts the account's reserve, so a payee sitting below its own reserve
|
||||
// would appear to receive nothing even though its raw ledger balance grew.
|
||||
// That mismatch is exactly what a conservation check must not see.
|
||||
// Returns the balance stored on the ledger for a holder of `asset`: the raw
|
||||
// XRP balance (not xrpLiquid, which subtracts the reserve), or the signed
|
||||
// trust line or MPToken balance with no trust limit added. Not meaningful for
|
||||
// the issuer, which holds no balance of its own.
|
||||
STAmount
|
||||
conservationBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
|
||||
storedBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
|
||||
{
|
||||
if (isXRP(asset))
|
||||
{
|
||||
@@ -61,6 +60,25 @@ conservationBalance(ReadView const& view, AccountID const& id, Asset const& asse
|
||||
return STAmount{asset}; // LCOV_EXCL_LINE
|
||||
return view.balanceHookIOU(id, xrpAccount(), sle->getFieldAmount(sfBalance));
|
||||
}
|
||||
return accountHolds(
|
||||
view,
|
||||
id,
|
||||
asset,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j,
|
||||
SpendableHandling::SimpleBalance);
|
||||
}
|
||||
|
||||
// Returns the account's balance in `asset`, for use only in fund-conservation
|
||||
// checks. A holder's is its stored balance: a spendable balance would add the
|
||||
// opposite trust limit, which can hide a small credit in rounding. An issuer
|
||||
// has no stored balance, so its spendable balance is used instead.
|
||||
STAmount
|
||||
conservationBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
|
||||
{
|
||||
if (id != asset.getIssuer())
|
||||
return storedBalance(view, id, asset, j);
|
||||
return accountHolds(
|
||||
view,
|
||||
id,
|
||||
@@ -356,11 +374,11 @@ LoanPay::doApply()
|
||||
auto const asset = *vaultSle->at(sfAsset);
|
||||
|
||||
// Determine where to send the broker's fee
|
||||
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
|
||||
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
bool const fixedPrecision = getVaultVersion(vaultSle) == VaultVersion::FixedPrecision;
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
|
||||
// Send the broker fee to the owner if they have sufficient cover available,
|
||||
// _and_ if the owner can receive funds
|
||||
@@ -386,7 +404,8 @@ LoanPay::doApply()
|
||||
return roundToAsset(
|
||||
asset, tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), loanScale);
|
||||
}();
|
||||
return coverAvailableProxy >= minCover && !isDeepFrozen(view, brokerOwner, asset) &&
|
||||
return brokerSle->at(sfCoverAvailable) >= minCover &&
|
||||
!isDeepFrozen(view, brokerOwner, asset) &&
|
||||
!requireAuth(view, asset, brokerOwner, AuthType::StrongAuth);
|
||||
}();
|
||||
|
||||
@@ -419,6 +438,12 @@ LoanPay::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
auto const scheduledInterest = [&loanSle] {
|
||||
return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding) -
|
||||
loanSle->at(sfManagementFeeOutstanding);
|
||||
};
|
||||
Number const scheduledInterestBefore = fixedPrecision ? scheduledInterest() : kNumZero;
|
||||
|
||||
LoanPaymentType const paymentType = [&tx] {
|
||||
// preflight already checked that at most one flag is set.
|
||||
if (tx.isFlag(tfLoanLatePayment))
|
||||
@@ -432,7 +457,6 @@ LoanPay::doApply()
|
||||
|
||||
std::expected<LoanPaymentParts, TER> const paymentParts =
|
||||
loanMakePayment(asset, view, loanSle, brokerSle, amount, paymentType, j_);
|
||||
|
||||
if (!paymentParts)
|
||||
{
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -440,10 +464,12 @@ LoanPay::doApply()
|
||||
return paymentParts.error();
|
||||
}
|
||||
|
||||
// If the payment computation completed without error, the loanSle object
|
||||
// has been modified.
|
||||
// If the payment computation completed without error, the loanSle object has been modified.
|
||||
view.update(loanSle);
|
||||
|
||||
Number const scheduledInterestDelta =
|
||||
fixedPrecision ? scheduledInterest() - scheduledInterestBefore : kNumZero;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
// It is possible to pay 0 principal
|
||||
paymentParts->principalPaid >= 0,
|
||||
@@ -470,35 +496,49 @@ LoanPay::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const [assetsTotalDelta, debtTotalDelta] = loanPaymentDeltas(vaultSle, *paymentParts);
|
||||
//------------------------------------------------------
|
||||
// LoanBroker object state changes
|
||||
view.update(brokerSle);
|
||||
|
||||
Number const assetsAvailableBefore = *vaultSle->at(sfAssetsAvailable);
|
||||
Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
|
||||
auto const deltas = loanPaymentDeltas(vaultSle, *paymentParts);
|
||||
// FixedPrecision never writes sfAssetsTotal directly, so assetsTotalDelta
|
||||
// (cash_basis's interestPaid) is unused here and only meaningful for
|
||||
// Legacy/CashBasis below.
|
||||
Number const assetsTotalDelta = fixedPrecision ? kNumZero : deltas.assetsTotalDelta;
|
||||
Number const debtTotalDelta = deltas.debtTotalDelta;
|
||||
STAmount const totalPaidToVaultRounded = fixedPrecision
|
||||
// Pass the exact Number sum: rounding it to 16 digits first could drop
|
||||
// a tail that moves floor16(AssetsAvailable + sum).
|
||||
? creditToPosteriorAvailableScale(
|
||||
vaultSle, totalPaidToVaultRaw, Number::RoundingMode::Downward)
|
||||
: STAmount{
|
||||
asset,
|
||||
roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward)};
|
||||
XRPL_ASSERT_PARTS(
|
||||
!asset.integral() || totalPaidToVaultRaw == Number(totalPaidToVaultRounded),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"rounding does nothing for integral asset");
|
||||
Number const totalPaidToBrokerRaw = paymentParts->feePaid;
|
||||
// Fee redirect into cover is an inflow to CoverAvailable, so it must
|
||||
// floor the sum (see creditToPosteriorBrokerCoverScale's doc), not just
|
||||
// the delta. Legacy/CashBasis keeps the fee as the original, unrounded
|
||||
// Number: only the FixedPrecision path is 16-digit-rounded via STAmount.
|
||||
Number const totalPaidToBroker = fixedPrecision && !sendBrokerFeeToOwner
|
||||
// The exact fee, for the same reason as totalPaidToVaultRounded.
|
||||
? Number(creditToPosteriorBrokerCoverScale(
|
||||
vaultSle, brokerSle, totalPaidToBrokerRaw, Number::RoundingMode::Downward))
|
||||
: totalPaidToBrokerRaw;
|
||||
|
||||
JLOG(j_.debug()) << "Loan Pay: principal paid: " << paymentParts->principalPaid
|
||||
<< ", interest paid: " << paymentParts->interestPaid
|
||||
<< ", fee paid: " << paymentParts->feePaid
|
||||
<< ", assets total delta: " << assetsTotalDelta
|
||||
<< ", debt total delta: " << debtTotalDelta;
|
||||
|
||||
//------------------------------------------------------
|
||||
// LoanBroker object state changes
|
||||
view.update(brokerSle);
|
||||
|
||||
auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto assetsTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid;
|
||||
auto const totalPaidToVaultRounded =
|
||||
roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::RoundingMode::Downward);
|
||||
XRPL_ASSERT_PARTS(
|
||||
!asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"rounding does nothing for integral asset");
|
||||
auto const totalPaidToBroker = paymentParts->feePaid;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
(totalPaidToVaultRaw + totalPaidToBroker) ==
|
||||
(paymentParts->principalPaid + paymentParts->interestPaid + paymentParts->feePaid),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"payments add up");
|
||||
<< ", debt total delta: " << debtTotalDelta
|
||||
<< ", scheduled interest delta: " << scheduledInterestDelta;
|
||||
|
||||
// Decrease LoanBroker Debt by the amount paid, add the Loan value change
|
||||
// (which might be negative). debtTotalDelta may be negative, increasing the
|
||||
@@ -508,45 +548,35 @@ LoanPay::doApply()
|
||||
"xrpl::LoanPay::doApply",
|
||||
"debtTotalDelta rounding good");
|
||||
// Despite our best efforts, it's possible for rounding errors to accumulate
|
||||
// in the loan broker's debt total. This is because the broker may have more
|
||||
// than one loan with significantly different scales.
|
||||
adjustImpreciseNumber(debtTotalProxy, -debtTotalDelta, asset, vaultScale);
|
||||
// in the loan broker's debt total on Legacy/CashBasis Vaults. This is because
|
||||
// the broker may have more than one loan with significantly different scales.
|
||||
adjustBrokerDebtTotal(brokerSle, vaultSle, -debtTotalDelta, vaultScale);
|
||||
|
||||
//------------------------------------------------------
|
||||
// Vault object state changes
|
||||
view.update(vaultSle);
|
||||
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const assetsTotalBefore = *assetsTotalProxy;
|
||||
#if !NDEBUG
|
||||
if (fixedPrecision)
|
||||
{
|
||||
Number const pseudoAccountBalanceBefore = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableBefore == pseudoAccountBalanceBefore,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees before");
|
||||
// AssetsDeployed drops by exactly the principal paid this payment,
|
||||
// taken from the Loan's own payment split rather than the
|
||||
// broker-side debtTotalDelta.
|
||||
if (auto const ter = adjustVaultBalances(
|
||||
vaultSle,
|
||||
{
|
||||
.cash = totalPaidToVaultRounded,
|
||||
.deployed = -paymentParts->principalPaid,
|
||||
.yield = scheduledInterestDelta,
|
||||
},
|
||||
j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
else
|
||||
{
|
||||
vaultSle->at(sfAssetsAvailable) += totalPaidToVaultRounded;
|
||||
vaultSle->at(sfAssetsTotal) += assetsTotalDelta;
|
||||
}
|
||||
#endif
|
||||
|
||||
assetsAvailableProxy += totalPaidToVaultRounded;
|
||||
assetsTotalProxy += assetsTotalDelta;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
*assetsAvailableProxy <= *assetsTotalProxy,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
|
||||
JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
|
||||
<< ", total paid to vault rounded: " << totalPaidToVaultRounded
|
||||
<< ", total paid to broker: " << totalPaidToBroker
|
||||
<< ", amount from transaction: " << amount;
|
||||
|
||||
// Move funds
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -557,10 +587,11 @@ LoanPay::doApply()
|
||||
if (!sendBrokerFeeToOwner)
|
||||
{
|
||||
// If there is not enough first-loss capital, add the fee to First Loss
|
||||
// Cover Pool. Note that this moves the entire fee - it does not attempt
|
||||
// to split it. The broker can Withdraw it later if they want, or leave
|
||||
// it for future needs.
|
||||
coverAvailableProxy += totalPaidToBroker;
|
||||
// Cover Pool. FixedPrecision rounds the redirected fee at the
|
||||
// posterior cover scale; any sub-unit remainder is forgiven. The
|
||||
// broker can Withdraw the credited amount later or leave it for future
|
||||
// needs.
|
||||
brokerSle->at(sfCoverAvailable) += totalPaidToBroker;
|
||||
}
|
||||
|
||||
associateAsset(*loanSle, asset);
|
||||
@@ -568,28 +599,48 @@ LoanPay::doApply()
|
||||
associateAsset(*vaultSle, asset);
|
||||
|
||||
// Duplicate some checks after rounding
|
||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||
Number const assetsTotalAfter = *assetsTotalProxy;
|
||||
Number const assetsAvailableAfter = vaultSle->at(sfAssetsAvailable);
|
||||
Number const assetsTotalAfter = vaultSle->at(sfAssetsTotal);
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= assetsTotalAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
if (assetsAvailableAfter == assetsAvailableBefore)
|
||||
{
|
||||
// An unchanged assetsAvailable indicates that the amount paid to the
|
||||
// vault was zero, or rounded to zero. That should be impossible, but I
|
||||
// can't rule it out for extreme edge cases, so fail gracefully if it
|
||||
// happens.
|
||||
//
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
|
||||
<< "Before: " << assetsAvailableBefore //
|
||||
<< ", After: " << assetsAvailableAfter;
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
// Credit rounded to zero. On FixedPrecision, if this payment also
|
||||
// closes the Loan (XLS-66 section 3.2.8's terminal exception), let
|
||||
// it through: AssetsDeployed (vault) and DebtTotal (broker) already
|
||||
// dropped by the exact remaining principal, and YieldUnrealized was already
|
||||
// reduced by this payment's scheduled interest, above. No cash moves for the sub-unit
|
||||
// remainder; the Vault forgives it. A non-terminal zero-credit payment is still rejected:
|
||||
// letting it through would change the Loan's schedule with no cash movement at all.
|
||||
if (fixedPrecision && loanSle->at(sfPaymentRemaining) == 0)
|
||||
{
|
||||
JLOG(j_.debug()) << "LoanPay: terminal payment closed the Loan with a zero cash "
|
||||
"credit; the sub-unit remainder is forgiven. Principal: "
|
||||
<< paymentParts->principalPaid
|
||||
<< ", Interest: " << paymentParts->interestPaid;
|
||||
|
||||
// Nothing moves for this terminal close when the sub-unit remainder is forgiven and
|
||||
// either no fee is owed or the fee goes to the broker owner who is also the borrower,
|
||||
// i.e. the owner would pay itself. Return before the transfer and its conservation
|
||||
// checks, since they assume some transfer happens.
|
||||
if (totalPaidToVaultRounded == beast::kZero &&
|
||||
(totalPaidToBroker == beast::kZero || brokerPayee == accountID_))
|
||||
{
|
||||
return tesSUCCESS;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
|
||||
<< "Before: " << assetsAvailableBefore //
|
||||
<< ", After: " << assetsAvailableAfter //
|
||||
<< ", Credit: " << totalPaidToVaultRounded //
|
||||
<< ", Principal: " << paymentParts->principalPaid //
|
||||
<< ", Interest: " << paymentParts->interestPaid;
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
if (assetsTotalDelta != beast::kZero && assetsTotalAfter == assetsTotalBefore)
|
||||
if (!fixedPrecision && assetsTotalDelta != beast::kZero &&
|
||||
assetsTotalAfter == assetsTotalBefore)
|
||||
{
|
||||
// Non-zero assetsTotalDelta with an unchanged assetsTotal indicates that
|
||||
// the actual value change rounded to zero. That should be impossible, but
|
||||
@@ -605,7 +656,8 @@ LoanPay::doApply()
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (assetsTotalDelta == beast::kZero && assetsTotalAfter != assetsTotalBefore)
|
||||
if (!fixedPrecision && assetsTotalDelta == beast::kZero &&
|
||||
assetsTotalAfter != assetsTotalBefore)
|
||||
{
|
||||
// A change in assetsTotal when there was no assetsTotalDelta indicates
|
||||
// that something really weird happened. That should be flat out
|
||||
@@ -684,22 +736,6 @@ LoanPay::doApply()
|
||||
WaiveTransferFee::Yes))
|
||||
return ter;
|
||||
|
||||
#if !NDEBUG
|
||||
{
|
||||
Number const pseudoAccountBalanceAfter = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter == pseudoAccountBalanceAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees after");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check that funds are conserved
|
||||
auto const accountBalanceAfter = conservationBalance(view, accountID_, asset, j_);
|
||||
auto const vaultBalanceAfter = accountID_ == vaultPseudoAccount
|
||||
@@ -748,10 +784,6 @@ LoanPay::doApply()
|
||||
}();
|
||||
|
||||
// No object changes are made below this point
|
||||
XRPL_ASSERT_PARTS(
|
||||
Number::getround() == Number::RoundingMode::ToNearest,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"Number rounding ToNearest");
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::ToNearest);
|
||||
|
||||
auto const accountBalanceBeforeRounded = roundToScale(accountBalanceBefore, balanceScale);
|
||||
@@ -802,8 +834,26 @@ LoanPay::doApply()
|
||||
<< ", total " << to_string(totalBalanceChangeRounded) << " ("
|
||||
<< Number(totalBalanceChange) << ")";
|
||||
|
||||
// Each trust line rounds its own change to 16 digits, so a side can move by up to one unit
|
||||
// of its balance less or more than was sent. Integral assets move exactly.
|
||||
auto const roundingUnit = [&](STAmount const& before, STAmount const& after) {
|
||||
Number unit{0};
|
||||
if (asset.integral())
|
||||
return unit;
|
||||
for (auto const& balance : {before, after})
|
||||
{
|
||||
if (balance != beast::kZero)
|
||||
unit = std::max(unit, Number{1, balance.exponent()});
|
||||
}
|
||||
return unit;
|
||||
};
|
||||
Number const accountUnit = roundingUnit(accountBalanceBefore, accountBalanceAfter);
|
||||
Number const vaultUnit = roundingUnit(vaultBalanceBefore, vaultBalanceAfter);
|
||||
Number const brokerUnit = roundingUnit(brokerBalanceBefore, brokerBalanceAfter);
|
||||
|
||||
bool const goodRounding = totalBalanceBeforeRounded == totalBalanceAfterRounded ||
|
||||
totalBalanceChangeRounded == beast::kZero;
|
||||
totalBalanceChangeRounded == beast::kZero ||
|
||||
abs(Number(totalBalanceChange)) <= accountUnit + vaultUnit + brokerUnit;
|
||||
if (totalBalanceBeforeRounded != totalBalanceAfterRounded)
|
||||
{
|
||||
JLOG((goodRounding ? j_.debug() : j_.warn()))
|
||||
@@ -824,11 +874,20 @@ LoanPay::doApply()
|
||||
goodRounding, "xrpl::LoanPay::doApply", "funds are conserved (with rounding)");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
accountBalanceAfter < accountBalanceBefore || accountID_ == asset.getIssuer(),
|
||||
// A payment below one unit of the payer's balance can round away on its trust line. A fee
|
||||
// the owner-borrower pays to itself does not leave its balance.
|
||||
accountBalanceAfter < accountBalanceBefore || accountID_ == asset.getIssuer() ||
|
||||
(accountBalanceAfter == accountBalanceBefore &&
|
||||
totalPaidToVaultRounded +
|
||||
(brokerPayee == accountID_ ? Number{0} : totalPaidToBroker) <=
|
||||
accountUnit),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"account balance decreased");
|
||||
XRPL_ASSERT_PARTS(
|
||||
vaultBalanceAfter >= beast::kZero && brokerBalanceAfter >= beast::kZero,
|
||||
// A broker owner may owe the issuer on a reciprocal IOU trust line, so
|
||||
// its stored balance can be negative. Pseudo-accounts cannot.
|
||||
vaultBalanceAfter >= beast::kZero &&
|
||||
(brokerBalanceAfter >= beast::kZero || (sendBrokerFeeToOwner && !asset.integral())),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"non-negative vault and broker balances");
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -840,7 +899,11 @@ LoanPay::doApply()
|
||||
"xrpl::LoanPay::doApply",
|
||||
"broker balance did not decrease");
|
||||
XRPL_ASSERT_PARTS(
|
||||
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
|
||||
// An IOU issuer's spendable balance does not change when it is paid, and a credit below
|
||||
// one unit of the payee's balance can round away on its trust line.
|
||||
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore ||
|
||||
brokerPayee == asset.getIssuer() ||
|
||||
(Number(totalPaidToVaultRounded) <= vaultUnit && totalPaidToBroker <= brokerUnit),
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault and/or broker balance increased");
|
||||
|
||||
|
||||
Loaded 100 of 228 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user