mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 23:30:54 +00:00
Compare commits
69 Commits
tapanito/v
...
ripple/was
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7ac58ffb | ||
|
|
959a186a0f | ||
|
|
3b1c9e4320 | ||
|
|
a3a2c85c41 | ||
|
|
b1ac891210 | ||
|
|
579c44bb22 | ||
|
|
226ec160bb | ||
|
|
4d685d4bad | ||
|
|
9a8ec56d42 | ||
|
|
8950530b04 | ||
|
|
f6472347e9 | ||
|
|
9e427ae3ba | ||
|
|
ef7aeca6bf | ||
|
|
eec1d29b92 | ||
|
|
ecee732187 | ||
|
|
1259c1d5ca | ||
|
|
9917f96166 | ||
|
|
2cc9439fde | ||
|
|
52af9582e2 | ||
|
|
46e88dc732 | ||
|
|
bc24f2e211 | ||
|
|
7a7c993b15 | ||
|
|
9733ca8f91 | ||
|
|
18d5e3e226 | ||
|
|
b30b4e1d65 | ||
|
|
d435893602 | ||
|
|
00b0cf50f6 | ||
|
|
1338062be7 | ||
|
|
1d4a3c00b8 | ||
|
|
2930ef217f | ||
|
|
913e4b919e | ||
|
|
bce5d91e45 | ||
|
|
654338fa66 | ||
|
|
3a825a41e1 | ||
|
|
61bcb7621f | ||
|
|
a3f71b1774 | ||
|
|
b08bcf5d21 | ||
|
|
77dfd56ace | ||
|
|
9c3c0280b1 | ||
|
|
bd16f7989d | ||
|
|
2ec4a1114e | ||
|
|
b2627039f6 | ||
|
|
e85e7b1b1a | ||
|
|
72fffb6e51 | ||
|
|
f7ee580f01 | ||
|
|
122d405750 | ||
|
|
d7ed6d6512 | ||
|
|
8bc6f9cd70 | ||
|
|
ed5139d4e3 | ||
|
|
7a9d245950 | ||
|
|
d83ec96848 | ||
|
|
419d53ec4c | ||
|
|
d4d70d5675 | ||
|
|
bbc28b3b1c | ||
|
|
5aab274b7a | ||
|
|
2c30e41191 | ||
|
|
8ea5106b0b | ||
|
|
1977df9c2e | ||
|
|
6c95548df5 | ||
|
|
90e0bbd0fc | ||
|
|
b57df290de | ||
|
|
8a403f1241 | ||
|
|
6d2640871d | ||
|
|
500bb68831 | ||
|
|
16087c9680 | ||
|
|
25c3060fef | ||
|
|
ce9f0b38a4 | ||
|
|
35f7cbf772 | ||
|
|
0db564d261 |
38
.github/actions/cargo-cache/action.yml
vendored
Normal file
38
.github/actions/cargo-cache/action.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Use cargo artifacts cache
|
||||
description: >
|
||||
Cache the cargo build artifacts with rust-cache. Never caches ~/.cargo/bin:
|
||||
when saving the cache, rust-cache deletes all binaries that were already
|
||||
present there, which on persistent self-hosted runners wipes the tools
|
||||
installed by prepare-runner. Harmless on ephemeral runners, but kept
|
||||
consistent everywhere.
|
||||
|
||||
inputs:
|
||||
workspaces:
|
||||
description: "Workspaces to cache, as 'workspace -> target' lines."
|
||||
required: false
|
||||
default: crates
|
||||
key:
|
||||
description: "Additional part of the cache key."
|
||||
required: false
|
||||
default: ""
|
||||
cache-directories:
|
||||
description: "Additional non-workspace directories to cache."
|
||||
required: false
|
||||
default: ""
|
||||
save-if:
|
||||
description: "Condition for saving the cache after the job."
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
|
||||
steps:
|
||||
- name: Use cargo artifacts cache
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
cache-bin: "false"
|
||||
cache-directories: ${{ inputs.cache-directories }}
|
||||
key: ${{ inputs.key }}
|
||||
save-if: ${{ inputs.save-if }}
|
||||
workspaces: ${{ inputs.workspaces }}
|
||||
1
.github/dependabot.yml
vendored
1
.github/dependabot.yml
vendored
@@ -4,6 +4,7 @@ updates:
|
||||
directories:
|
||||
- /
|
||||
- .github/actions/build-deps/
|
||||
- .github/actions/cargo-cache/
|
||||
- .github/actions/release-info/
|
||||
- .github/actions/set-compiler-env/
|
||||
- .github/actions/setup-conan/
|
||||
|
||||
2
.github/workflows/build-nix-images.yml
vendored
2
.github/workflows/build-nix-images.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
base_image: debian:bookworm
|
||||
- name: rhel
|
||||
base_image: registry.access.redhat.com/ubi9/ubi:latest
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
with:
|
||||
image_name: xrpld/nix-${{ matrix.distro.name }}
|
||||
dockerfile: nix/docker/Dockerfile
|
||||
|
||||
2
.github/workflows/build-packaging-images.yml
vendored
2
.github/workflows/build-packaging-images.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
# AlmaLinux rather than UBI9, which does not ship rpm-sign.
|
||||
- name: rhel
|
||||
base_image: almalinux:9
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
with:
|
||||
image_name: xrpld/packaging-${{ matrix.distro.name }}
|
||||
dockerfile: package/Dockerfile
|
||||
|
||||
2
.github/workflows/build-pre-commit-image.yml
vendored
2
.github/workflows/build-pre-commit-image.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@9e7e4e80af9e684c116b38369add8eea64451f32
|
||||
uses: XRPLF/actions/.github/workflows/build-multiarch-image.yml@65d5a0bd72be4ecea95cff0673a6e0672ab5243a
|
||||
with:
|
||||
image_name: xrpld/pre-commit
|
||||
dockerfile: bin/pre-commit/Dockerfile
|
||||
|
||||
2
.github/workflows/check-tools.yml
vendored
2
.github/workflows/check-tools.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
|
||||
run-hooks:
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@3ba08d6ddf114092891d48491fc2e26c3ba15552
|
||||
uses: XRPLF/actions/.github/workflows/pre-commit.yml@f1952595d212e86169935135efc66294b4574131
|
||||
with:
|
||||
runs_on: ubuntu-latest
|
||||
container: '{ "image": "ghcr.io/xrplf/xrpld/pre-commit:sha-f56b79f" }'
|
||||
|
||||
2
.github/workflows/publish-docs.yml
vendored
2
.github/workflows/publish-docs.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: ${{ inputs.ccache_enabled }}
|
||||
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
compiler: ${{ inputs.compiler }}
|
||||
|
||||
- name: Use cargo artifacts cache
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
uses: ./.github/actions/cargo-cache
|
||||
with:
|
||||
cache-directories: ${{ env.BUILD_DIR }}/corrosion
|
||||
key: ${{ inputs.config_name }}
|
||||
|
||||
6
.github/workflows/reusable-clang-tidy.yml
vendored
6
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
determine-files:
|
||||
permissions:
|
||||
contents: read
|
||||
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@d041ac9f1fa9f07a4ba335eb4c1c82233fb3fef6
|
||||
uses: XRPLF/actions/.github/workflows/determine-tidy-files.yml@70145243b905dc3e040a61d39c00e178cfb96f71
|
||||
|
||||
run-clang-tidy:
|
||||
name: Run clang tidy
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
compiler: ${{ env.COMPILER }}
|
||||
|
||||
- name: Use cargo artifacts cache
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
uses: ./.github/actions/cargo-cache
|
||||
with:
|
||||
cache-directories: ${{ env.BUILD_DIR }}/corrosion
|
||||
save-if: ${{ github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release') }}
|
||||
|
||||
12
.github/workflows/reusable-rust.yml
vendored
12
.github/workflows/reusable-rust.yml
vendored
@@ -33,9 +33,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Use cargo artifacts cache
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
workspaces: crates
|
||||
uses: ./.github/actions/cargo-cache
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
@@ -48,9 +46,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Use cargo artifacts cache
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
workspaces: crates
|
||||
uses: ./.github/actions/cargo-cache
|
||||
|
||||
- name: Generate coverage report
|
||||
run: cargo llvm-cov nextest --workspace --all-features --locked --no-tests=warn --lcov --output-path lcov.info
|
||||
@@ -76,9 +72,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Use cargo artifacts cache
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
workspaces: crates
|
||||
uses: ./.github/actions/cargo-cache
|
||||
|
||||
- name: Build documentation
|
||||
env:
|
||||
|
||||
2
.github/workflows/upload-conan-deps.yml
vendored
2
.github/workflows/upload-conan-deps.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f
|
||||
uses: XRPLF/actions/prepare-runner@7bf7ceca5932114abdd0d43493c3c30c5a654e13
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ find_package(OpenSSL REQUIRED)
|
||||
find_package(secp256k1 REQUIRED)
|
||||
find_package(SOCI REQUIRED)
|
||||
find_package(SQLite3 REQUIRED)
|
||||
find_package(wasmi REQUIRED)
|
||||
find_package(xxHash REQUIRED)
|
||||
|
||||
target_link_libraries(
|
||||
|
||||
@@ -120,7 +120,10 @@ if(MSVC)
|
||||
_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS
|
||||
$<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:Debug>>:_CRTDBG_MAP_ALLOC>
|
||||
)
|
||||
target_link_libraries(common INTERFACE -errorreport:none -machine:X64)
|
||||
target_link_libraries(
|
||||
common
|
||||
INTERFACE -errorreport:none -machine:X64 -ignore:4099
|
||||
)
|
||||
else()
|
||||
target_compile_options(
|
||||
common
|
||||
|
||||
@@ -69,6 +69,7 @@ target_link_libraries(
|
||||
Xrpl::opts
|
||||
Xrpl::syslibs
|
||||
secp256k1::secp256k1
|
||||
wasmi::wasmi
|
||||
xrpl.libpb
|
||||
xxHash::xxhash
|
||||
$<$<BOOL:${voidstar}>:antithesis-sdk-cpp>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"requires": [
|
||||
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
|
||||
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
|
||||
"wasmi/1.0.9#1fecdab9b90c96698eb35ea99ca4f5cb%1782307153.343419",
|
||||
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
|
||||
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
|
||||
|
||||
@@ -36,6 +36,7 @@ class Xrpl(ConanFile):
|
||||
"nudb/2.0.9",
|
||||
"openssl/3.6.3",
|
||||
"soci/4.0.3",
|
||||
"wasmi/1.0.9",
|
||||
"zlib/1.3.2",
|
||||
]
|
||||
|
||||
@@ -224,6 +225,7 @@ class Xrpl(ConanFile):
|
||||
"soci::soci",
|
||||
"secp256k1::secp256k1",
|
||||
"sqlite3::sqlite",
|
||||
"wasmi::wasmi",
|
||||
"xxhash::xxhash",
|
||||
"zlib::zlib",
|
||||
]
|
||||
|
||||
20
crates/Cargo.lock
generated
20
crates/Cargo.lock
generated
@@ -57,9 +57,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291"
|
||||
checksum = "824894a4a85dca76d4c95c2b9098c036f5a29f627b30c12780774f6654e60974"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxx-build",
|
||||
@@ -72,9 +72,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2"
|
||||
checksum = "f1ae0b651ea5b0000b19513aef5a03f194d7e3486f2d9258b658da8677fe9036"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
@@ -87,9 +87,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-cmd"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d"
|
||||
checksum = "fb05f91d3fb8435d9bab6ac5ce6ac1868be774325fb7fb2a91be39393b21388e"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codespan-reporting",
|
||||
@@ -101,15 +101,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89"
|
||||
checksum = "bf293202e0e3e98495785745389e8d0755b217e66f19194a5c695c25e03282ef"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c"
|
||||
checksum = "ca001d746947c7249ed9d332a10f7a59daedbafeb0ec68c5c18a7db7a93f6ccc"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
|
||||
@@ -65,7 +65,7 @@ wherever it appears in the repository configuration.
|
||||
4. Add the repository, using the channel you picked in [Release channels](#release-channels):
|
||||
|
||||
```bash
|
||||
echo "deb [signed-by=/etc/apt/keyrings/xrplf.asc] https://packages.xrplf.org/repository/deb-stable focal main" | \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/xrplf.asc] https://packages.xrplf.org/repository/deb-stable any main" | \
|
||||
sudo tee /etc/apt/sources.list.d/xrplf.list
|
||||
```
|
||||
|
||||
@@ -98,13 +98,13 @@ wherever it appears in the repository configuration.
|
||||
enabled=1
|
||||
baseurl=https://packages.xrplf.org/repository/rpm-stable/
|
||||
gpgcheck=1
|
||||
repo_gpgcheck=0
|
||||
repo_gpgcheck=1
|
||||
gpgkey=https://packages.xrplf.org/xrplf.asc
|
||||
REPOFILE
|
||||
```
|
||||
|
||||
`gpgcheck=1` verifies each package against the key above.
|
||||
`repo_gpgcheck` is off because the repository metadata is generated by the server and is not signed.
|
||||
`repo_gpgcheck=1` verifies the repository metadata, which the server signs with the same key.
|
||||
|
||||
3. Install the `xrpld` package:
|
||||
|
||||
|
||||
@@ -43,32 +43,6 @@ assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& shares);
|
||||
|
||||
/**
|
||||
* Returns the effective change to sfAssetsTotal after canonicalizing
|
||||
* `sfAssetsTotal + delta` under `mode`, as a non-negative magnitude.
|
||||
* `delta` is positive when crediting the vault and negative when debiting
|
||||
* it; only its sign is used to select the rounding direction. This is not
|
||||
* the same as simply rounding `delta`'s magnitude to sfAssetsTotal's
|
||||
* scale: both the before and after totals are canonicalized under `mode`
|
||||
* before subtracting, so the result also accounts for sfAssetsTotal
|
||||
* itself sitting mid-grid. The caller adds the returned magnitude to
|
||||
* sfAssetsTotal for credits, or subtracts it for debits, and applies it
|
||||
* the same way to any related field (for example sfAssetsAvailable) so
|
||||
* all rails stay in sync; the other fields' scale is at least as fine as
|
||||
* sfAssetsTotal's.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param delta The signed amount by which sfAssetsTotal will change; only
|
||||
* its sign selects the rounding direction. The magnitude is
|
||||
* what is quantized and returned.
|
||||
* @param mode The rounding mode to apply when quantizing to the
|
||||
* sfAssetsTotal scale.
|
||||
*
|
||||
* @return The rounded magnitude, always non-negative.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
clampToAssetsTotalScale(SLE::const_ref vault, STAmount const& delta, Number::RoundingMode mode);
|
||||
|
||||
/**
|
||||
* Controls whether to truncate shares instead of rounding.
|
||||
*/
|
||||
@@ -84,30 +58,33 @@ enum class TruncateShares : bool { No = false, Yes = true };
|
||||
enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
|
||||
|
||||
/**
|
||||
* Returns the assets backing outstanding shares for a withdrawal:
|
||||
* sfAssetsTotal minus sfLossUnrealized, or sfAssetsTotal alone when the
|
||||
* unrealized loss is waived. Used by assetsToSharesWithdraw and
|
||||
* sharesToAssetsWithdraw as the numerator of the share/asset exchange rate.
|
||||
* Returns the effective total of assets backing outstanding shares for the
|
||||
* purposes of a withdrawal, i.e. sfAssetsTotal, discounted by sfLossUnrealized
|
||||
* unless waived. This is the numerator used by both withdraw conversion
|
||||
* helpers (assetsToSharesWithdraw and sharesToAssetsWithdraw) to compute the
|
||||
* share/asset exchange rate.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param waive Whether to skip subtracting the unrealized loss.
|
||||
* @param waive Whether to waive (i.e. not subtract) the vault's unrealized
|
||||
* loss.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::const_ref 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.
|
||||
* Returns whether debiting `amount` from `total` — the current value of a
|
||||
* vault's sfAssetsTotal or sfAssetsAvailable field — would canonicalize back
|
||||
* to the exact same STAmount value it started at. This happens when a
|
||||
* genuinely non-zero debit is dust relative to a `total` large enough to
|
||||
* exceed STAmount's significant-digit precision: the shares still move, but
|
||||
* the stored total doesn't change, which otherwise trips the ValidVault
|
||||
* invariant after the fact instead of failing cleanly upfront.
|
||||
*
|
||||
* @param asset The vault's underlying asset, used to canonicalize both
|
||||
* sides the same way the ledger will when the field is stored.
|
||||
* @param asset The vault's underlying asset, used to canonicalize both sides
|
||||
* the same way the ledger will when the field is stored.
|
||||
* @param total The field's current value.
|
||||
* @param amount The amount to debit. Zero always returns false; that case
|
||||
* is rejected separately.
|
||||
* @param amount The amount to debit. A value of zero always returns false;
|
||||
* that case is rejected separately and unconditionally.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
|
||||
|
||||
@@ -126,15 +126,15 @@ release defaults to 1 and is overridable with `-Dpkg_release=N`.
|
||||
|
||||
Packages are published to the XRPLF repositories on Sonatype Nexus at
|
||||
`https://packages.xrplf.org`. The `release-info` action decides the channel from
|
||||
the event, and `publish_pkg.sh` maps that channel to a repository pair:
|
||||
the event, and `publish_pkg.sh` maps that channel to its repositories:
|
||||
|
||||
| Event | Version | Channel | DEB repository | RPM repository |
|
||||
| ------------------------ | ----------------- | -------------- | ------------------ | ------------------ |
|
||||
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable` |
|
||||
| tag | `X.Y.Z-rcN` | `unstable` | `deb-unstable` | `rpm-unstable` |
|
||||
| tag | `X.Y.Z-bN` | `experimental` | `deb-experimental` | `rpm-experimental` |
|
||||
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop` |
|
||||
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private` |
|
||||
| Event | Version | Channel | DEB repository | RPM upload repository |
|
||||
| ------------------------ | ----------------- | -------------- | ------------------ | ------------------------- |
|
||||
| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` |
|
||||
| tag | `X.Y.Z-rcN` | `unstable` | `deb-unstable` | `rpm-unstable-hosted` |
|
||||
| tag | `X.Y.Z-bN` | `experimental` | `deb-experimental` | `rpm-experimental-hosted` |
|
||||
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
|
||||
| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` |
|
||||
|
||||
Only a tag names a channel — do not extend that to `develop`, where
|
||||
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
|
||||
@@ -155,12 +155,15 @@ Conan remote.
|
||||
|
||||
Nexus owns the repository metadata; nothing here indexes anything. Worth knowing:
|
||||
|
||||
- Each apt-hosted repository needs a distribution and a PGP signing keypair
|
||||
configured in Nexus, which rejects one created without a keypair. Nexus signs
|
||||
the apt metadata with it, never the packages.
|
||||
- Hosted yum repositories cannot be signed by Nexus at all, so `sign_rpm.sh`
|
||||
signs the RPMs before they are uploaded, and rpm clients verify with
|
||||
`gpgcheck=1` rather than `repo_gpgcheck=1`.
|
||||
- Each apt-hosted repository needs a distribution (ours use `any`) and a PGP
|
||||
signing keypair configured in Nexus, which rejects one created without a
|
||||
keypair. Nexus signs the apt metadata with it, never the packages.
|
||||
- Hosted yum repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
|
||||
repository sits behind a `rpm-<channel>` yum group repository whose metadata
|
||||
Nexus signs. Uploads go to the hosted repository; clients point at the group
|
||||
and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs
|
||||
themselves, so `sign_rpm.sh` signs them before they are uploaded, and clients
|
||||
verify them with `gpgcheck=1`.
|
||||
- yum metadata is rebuilt asynchronously, so a successful publish is not
|
||||
immediately installable.
|
||||
- Each job uploads only what it built, and uploads are not transactional, so a
|
||||
|
||||
@@ -7,10 +7,13 @@ set -euo pipefail
|
||||
# Usage: publish_pkg.sh <channel> [package-dir]
|
||||
#
|
||||
# channel release channel, selecting the 'deb-<channel>' and
|
||||
# 'rpm-<channel>' repository pair
|
||||
# 'rpm-<channel>-hosted' repositories
|
||||
# package-dir searched recursively for *.deb, *.ddeb and *.rpm ('build' by
|
||||
# default)
|
||||
#
|
||||
# RPMs are uploaded to the hosted repository, but yum clients install from the
|
||||
# 'rpm-<channel>' group repository in front of it, which serves signed metadata.
|
||||
#
|
||||
# NEXUS_USERNAME and NEXUS_PASSWORD are required. NEXUS_URL overrides the target
|
||||
# instance, and DRY_RUN=1 lists the uploads without performing them.
|
||||
|
||||
@@ -24,7 +27,7 @@ if [[ -z "${channel}" ]]; then
|
||||
fi
|
||||
|
||||
deb_repo="deb-${channel}"
|
||||
rpm_repo="rpm-${channel}"
|
||||
rpm_repo="rpm-${channel}-hosted"
|
||||
|
||||
if [[ -z "${DRY_RUN:-}" ]]; then
|
||||
: "${NEXUS_USERNAME:?is required}" "${NEXUS_PASSWORD:?is required}"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Sign the RPMs built by build_pkg.sh. Nexus cannot sign hosted yum metadata, so
|
||||
# the packages carry the signature themselves and rpm clients verify them with
|
||||
# gpgcheck=1.
|
||||
# Sign the RPMs built by build_pkg.sh. Nexus signs the yum repository metadata
|
||||
# (via the 'rpm-<channel>' group repository), but never the packages themselves,
|
||||
# so they carry their own signature. Clients verify the packages with gpgcheck=1
|
||||
# and the metadata with repo_gpgcheck=1.
|
||||
#
|
||||
# Usage: sign_rpm.sh [package-dir]
|
||||
#
|
||||
@@ -12,8 +13,9 @@ set -euo pipefail
|
||||
# PKG_SIGNING_KEY must hold an armoured PGP private key. It has no flag, to keep
|
||||
# the key out of the process list.
|
||||
#
|
||||
# There is no DEB equivalent: apt trusts the repository metadata, which Nexus
|
||||
# signs, rather than the packages themselves.
|
||||
# The DEBs are deliberately not signed: embedded DEB signatures exist (debsigs),
|
||||
# but apt does not verify them by default and trusts the repository metadata,
|
||||
# which Nexus signs, instead.
|
||||
|
||||
pkg_dir="${1:-build}"
|
||||
|
||||
|
||||
@@ -67,22 +67,6 @@ sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
|
||||
return assets;
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
clampToAssetsTotalScale(SLE::const_ref vault, STAmount const& delta, Number::RoundingMode mode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::clampToAssetsTotalScale : delta and vault asset match");
|
||||
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
// Canonicalize both endpoints under the same rounding mode so the subtraction
|
||||
// reflects only the effect of `delta`, never a rounding difference between them.
|
||||
NumberRoundModeGuard const mg(mode);
|
||||
STAmount const totalBefore{asset, vault->at(sfAssetsTotal)};
|
||||
STAmount const totalAfter{asset, vault->at(sfAssetsTotal) + delta};
|
||||
return delta.negative() ? totalBefore - totalAfter : totalAfter - totalBefore;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::const_ref vault, WaiveUnrealizedLoss waive)
|
||||
{
|
||||
|
||||
@@ -256,13 +256,10 @@ VaultClawback::assetsToClawback(
|
||||
STAmount sharesDestroyed;
|
||||
STAmount assetsRecovered;
|
||||
|
||||
// Number arithmetic can throw overflow_error when Scale and totals are large. Caught below.
|
||||
try
|
||||
{
|
||||
if (clawbackAmount == beast::kZero)
|
||||
{
|
||||
// Zero amount means clawback all shares the holder has; derive the corresponding asset
|
||||
// amount from the share balance.
|
||||
sharesDestroyed = accountHolds(
|
||||
view(), holder, share, FreezeHandling::IgnoreFreeze, AuthHandling::IgnoreAuth, j_);
|
||||
auto const maybeAssets =
|
||||
@@ -274,8 +271,6 @@ VaultClawback::assetsToClawback(
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-zero amount: convert to shares, then convert shares back to assets so the
|
||||
// recovery reflects exactly the shares to be burned, not the requested amount.
|
||||
auto const maybeShares =
|
||||
assetsToSharesWithdraw(vault, sleShareIssuance, clawbackAmount);
|
||||
if (!maybeShares)
|
||||
@@ -288,11 +283,13 @@ VaultClawback::assetsToClawback(
|
||||
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
assetsRecovered = *maybeAssets;
|
||||
}
|
||||
// Clamp assetsRecovered to sfAssetsAvailable, then re-derive shares and assets so the pair
|
||||
// stays consistent;.
|
||||
// Clamp to maximum.
|
||||
if (assetsRecovered > *assetsAvailable)
|
||||
{
|
||||
assetsRecovered = *assetsAvailable;
|
||||
// Note, it is important to truncate the number of shares,
|
||||
// otherwise the corresponding assets might breach the
|
||||
// AssetsAvailable
|
||||
{
|
||||
auto const maybeShares = assetsToSharesWithdraw(
|
||||
vault, sleShareIssuance, assetsRecovered, TruncateShares::Yes);
|
||||
@@ -306,8 +303,6 @@ VaultClawback::assetsToClawback(
|
||||
if (!maybeAssets)
|
||||
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
assetsRecovered = *maybeAssets;
|
||||
// Truncation should guarantee the invariant holds. If it does not, a conversion
|
||||
// helper is broken; refuse rather than over-recover.
|
||||
if (assetsRecovered > *assetsAvailable)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -316,20 +311,6 @@ VaultClawback::assetsToClawback(
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
// Post-fixCleanup3_4_0: round the recovery to the sfAssetsTotal scale so all rails change
|
||||
// by the same representable delta. sharesDestroyed is intentionally NOT re-derived here:
|
||||
// the holder's shares are burned for their pre-clamp value, so any sub-ULP trimmed off
|
||||
// stays in the vault for the remaining shareholders.
|
||||
if (ctx_.view().rules().enabled(fixCleanup3_4_0) && assetsRecovered > beast::kZero)
|
||||
{
|
||||
assetsRecovered =
|
||||
clampToAssetsTotalScale(vault, -assetsRecovered, Number::RoundingMode::Upward);
|
||||
// Recovery collapsed to zero. Return tecPRECISION_LOSS rather than burning shares for
|
||||
// no asset return.
|
||||
if (assetsRecovered <= beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
}
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
@@ -341,8 +322,6 @@ VaultClawback::assetsToClawback(
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleShareIssuance->at(sfOutstandingAmount)
|
||||
<< ", amount=" << clawbackAmount.value();
|
||||
// Overflow means this transaction cannot apply, but ledger state is still consistent.
|
||||
// Return tecPATH_DRY rather than a hard internal error.
|
||||
return std::unexpected(tecPATH_DRY);
|
||||
}
|
||||
|
||||
@@ -401,54 +380,23 @@ VaultClawback::doApply()
|
||||
sharesDestroyed = clawbackParts->second;
|
||||
}
|
||||
|
||||
// The holder has no shares (or the recovery clamped to zero). Nothing to burn; refuse rather
|
||||
// than modifying vault state.
|
||||
if (sharesDestroyed == beast::kZero)
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
// Even a non-zero recovery can be too small to change the stored sfAssetsTotal or
|
||||
// sfAssetsAvailable at STAmount's precision. Shares would still be burned, so ValidVault
|
||||
// would fail after apply with "clawback must decrease vault balance"; reject here instead.
|
||||
// On the issuer-clawback path this is effectively unreachable once fixCleanup3_4_0 is
|
||||
// active, since assetsToClawback's own clampToAssetsTotalScale already snapped
|
||||
// assetsRecovered to the grid; kept as defense in depth and to cover the owner-burn path,
|
||||
// where assetsRecovered is not put through that clamp.
|
||||
//
|
||||
// Number arithmetic can throw overflow_error when Scale and totals are large. Caught
|
||||
// below. debitIsNonZeroDust converts assetsTotal/assetsAvailable to STAmount, which is
|
||||
// exactly what a sufficiently abused sfScale can push out of STAmount's representable
|
||||
// range.
|
||||
if (view().rules().enabled(fixCleanup3_4_0))
|
||||
// A recovered amount can be genuinely non-zero yet still be dust relative to a
|
||||
// sfAssetsTotal/sfAssetsAvailable large enough to exceed STAmount's significant-digit
|
||||
// precision: subtracting it below rounds the stored total right back to where it started.
|
||||
// The shares still move, so ValidVault would fail after the fact with "clawback must
|
||||
// decrease vault balance" instead of a clean upfront rejection.
|
||||
if (view().rules().enabled(fixCleanup3_4_0) &&
|
||||
(debitIsNonZeroDust(vaultAsset, assetsTotal, assetsRecovered) ||
|
||||
debitIsNonZeroDust(vaultAsset, assetsAvailable, assetsRecovered)))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (debitIsNonZeroDust(vaultAsset, assetsTotal, assetsRecovered) ||
|
||||
debitIsNonZeroDust(vaultAsset, assetsAvailable, assetsRecovered))
|
||||
{
|
||||
JLOG(j_.debug())
|
||||
<< "VaultClawback: clawback amount too small to change stored vault"
|
||||
" balance";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j_.debug()) //
|
||||
<< "VaultClawback: overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale).value() //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
|
||||
<< ", amount=" << amount.value();
|
||||
// Overflow means this transaction cannot apply, but ledger state is still
|
||||
// consistent. Return tecPATH_DRY rather than a hard internal error.
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
JLOG(j_.debug()) << "VaultClawback: clawback amount too small to change stored vault"
|
||||
" balance";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
// Debit both rails by the same delta so sfAssetsTotal and sfAssetsAvailable stay in step,
|
||||
// as required by the ValidVault invariant.
|
||||
assetsTotal -= assetsRecovered;
|
||||
assetsAvailable -= assetsRecovered;
|
||||
view().update(vault);
|
||||
|
||||
@@ -208,7 +208,6 @@ TER
|
||||
VaultDeposit::doApply()
|
||||
{
|
||||
bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
|
||||
bool const fix340Enabled = view().rules().enabled(fixCleanup3_4_0);
|
||||
auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
|
||||
auto applyViewContext = ctx_.getApplyViewContext();
|
||||
if (!vault)
|
||||
@@ -285,8 +284,6 @@ VaultDeposit::doApply()
|
||||
}
|
||||
|
||||
STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited;
|
||||
|
||||
// Number arithmetic can throw overflow_error when Scale and totals are large. Caught below.
|
||||
try
|
||||
{
|
||||
// Compute exchange before transferring any amounts.
|
||||
@@ -296,20 +293,14 @@ VaultDeposit::doApply()
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
sharesCreated = *maybeShares;
|
||||
}
|
||||
|
||||
if (sharesCreated == beast::kZero)
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
// Convert shares back to assets so the depositor is debited for the amount actually minted.
|
||||
// The truncated share count is worth <= amount; without this the difference would be
|
||||
// credited to the vault for free.
|
||||
auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated);
|
||||
if (!maybeAssets)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
// The round-trip must never return more than the original amount. If it does, a conversion
|
||||
// helper is broken. Reject rather than overcharge the depositor.
|
||||
if (*maybeAssets > amount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -318,35 +309,6 @@ VaultDeposit::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
assetsDeposited = *maybeAssets;
|
||||
|
||||
// Post-fixCleanup3_4_0: round the deposit to the sfAssetsTotal scale so all accounting
|
||||
// fields (trust line / MPT, sfAssetsAvailable, sfAssetsTotal) change by the same
|
||||
// representable delta.
|
||||
if (fix340Enabled)
|
||||
{
|
||||
// Round Downward so the vault is credited by at most what the depositor paid.
|
||||
assetsDeposited =
|
||||
clampToAssetsTotalScale(vault, assetsDeposited, Number::RoundingMode::Downward);
|
||||
|
||||
// Return tecPRECISION_LOSS instead of minting shares against a zero credit.
|
||||
if (assetsDeposited <= beast::kZero)
|
||||
{
|
||||
JLOG(j_.warn()) << "VaultDeposit: deposit rounds to zero at "
|
||||
"assets outstanding scale.";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
// The pre-clamp share count would over-issue by the trimmed ULP and give the depositor
|
||||
// more value than they credited.
|
||||
auto const maybeReShares = assetsToSharesDeposit(vault, sleIssuance, assetsDeposited);
|
||||
if (!maybeReShares)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
sharesCreated = *maybeReShares;
|
||||
|
||||
if (sharesCreated == beast::kZero)
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <xrpl/tx/transactors/vault/VaultWithdraw.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -204,7 +203,6 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
TER
|
||||
VaultWithdraw::doApply()
|
||||
{
|
||||
bool const fix340Enabled = view().rules().enabled(fixCleanup3_4_0);
|
||||
auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
|
||||
auto applyViewContext = ctx_.getApplyViewContext();
|
||||
if (!vault)
|
||||
@@ -236,7 +234,6 @@ VaultWithdraw::doApply()
|
||||
// We waive the unrealized-loss subtraction in this case to avoid user withdrawing all of their
|
||||
// shares but keeping future value in the vault.
|
||||
auto const waiveUnrealizedLoss = shouldWaiveWithdrawal(view(), accountID_, sleIssuance);
|
||||
// Number arithmetic can throw overflow_error when Scale and totals are large. Caught below.
|
||||
try
|
||||
{
|
||||
if (amount.asset() == vaultAsset)
|
||||
@@ -250,12 +247,8 @@ VaultWithdraw::doApply()
|
||||
sharesRedeemed = *maybeShares;
|
||||
}
|
||||
|
||||
// Shares are MPT (integer). Small requested amounts truncate to zero; refuse rather
|
||||
// than burn nothing while paying out assets.
|
||||
if (sharesRedeemed == beast::kZero)
|
||||
return tecPRECISION_LOSS;
|
||||
// Convert shares back to assets so the payout matches the shares actually burned, not
|
||||
// the requested amount. The extra would otherwise be paid from the vault for free.
|
||||
auto const maybeAssets =
|
||||
sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed, waiveUnrealizedLoss);
|
||||
if (!maybeAssets)
|
||||
@@ -264,8 +257,7 @@ VaultWithdraw::doApply()
|
||||
}
|
||||
else if (amount.asset() == share)
|
||||
{
|
||||
// Fixed shares, variable assets. No round-trip: the share count is exactly what the
|
||||
// caller specified; only the payout amount is derived.
|
||||
// Fixed shares, variable assets.
|
||||
sharesRedeemed = amount;
|
||||
auto const maybeAssets =
|
||||
sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed, waiveUnrealizedLoss);
|
||||
@@ -288,8 +280,6 @@ VaultWithdraw::doApply()
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
|
||||
<< ", amount=" << amount.value();
|
||||
// Overflow means this transaction cannot apply, but ledger state is still consistent.
|
||||
// Return tecPATH_DRY rather than a hard internal error.
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
|
||||
@@ -305,11 +295,12 @@ VaultWithdraw::doApply()
|
||||
lossUnrealized <= (assetsTotal - assetsAvailable),
|
||||
"xrpl::VaultWithdraw::doApply : loss and assets do balance");
|
||||
|
||||
if (fix340Enabled && !isFinalWithdrawal)
|
||||
if (view().rules().enabled(fixCleanup3_4_0) && !isFinalWithdrawal)
|
||||
{
|
||||
// Fixed-shares path: a small share count can round to zero assets even though the vault
|
||||
// still has backing value. Reject rather than burn shares for a zero payout. The
|
||||
// fixed-assets branch above has already rejected zero via the sharesRedeemed check.
|
||||
// A withdrawal for a fixed share amount (variable assets) has no requested-asset
|
||||
// amount to check for rounding, unlike the fixed-assets branch above: a small enough
|
||||
// share amount can round down to an exact zero even though the vault still holds
|
||||
// positive effective value backing outstanding shares.
|
||||
if (amount.asset() == share && assetsWithdrawn == beast::kZero &&
|
||||
assetsTotalForWithdrawal(vault, waiveUnrealizedLoss) != beast::kZero)
|
||||
{
|
||||
@@ -317,36 +308,16 @@ VaultWithdraw::doApply()
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
// Number arithmetic can throw overflow_error when Scale and totals are large. Caught
|
||||
// below. debitIsNonZeroDust converts assetsTotal/assetsAvailable to STAmount, which is
|
||||
// exactly what a sufficiently abused sfScale can push out of STAmount's representable
|
||||
// range.
|
||||
try
|
||||
// assetsWithdrawn can also be genuinely non-zero and still too small to move
|
||||
// sfAssetsTotal or sfAssetsAvailable once canonicalized to STAmount's precision. Either
|
||||
// way the shares still move, so ValidVault would otherwise fail after the fact instead
|
||||
// of a clean upfront rejection.
|
||||
if (debitIsNonZeroDust(vaultAsset, assetsTotal, assetsWithdrawn) ||
|
||||
debitIsNonZeroDust(vaultAsset, assetsAvailable, assetsWithdrawn))
|
||||
{
|
||||
// Even a non-zero withdrawal can be too small to change the stored sfAssetsTotal or
|
||||
// sfAssetsAvailable at STAmount's precision. Shares would still move, so ValidVault
|
||||
// would fail after apply; reject here instead.
|
||||
if (debitIsNonZeroDust(vaultAsset, assetsTotal, assetsWithdrawn) ||
|
||||
debitIsNonZeroDust(vaultAsset, assetsAvailable, assetsWithdrawn))
|
||||
{
|
||||
JLOG(j_.debug()) << "VaultWithdraw: withdrawal amount too small to change stored"
|
||||
" vault balance";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j_.debug()) //
|
||||
<< "VaultWithdraw: overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale).value() //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
|
||||
<< ", amount=" << amount.value();
|
||||
// Overflow means this transaction cannot apply, but ledger state is still consistent.
|
||||
// Return tecPATH_DRY rather than a hard internal error.
|
||||
return tecPATH_DRY;
|
||||
JLOG(j_.debug()) << "VaultWithdraw: withdrawal amount too small to change stored"
|
||||
" vault balance";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,51 +335,6 @@ VaultWithdraw::doApply()
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// Post-fixCleanup3_4_0: round the payout to the sfAssetsTotal scale so all three rails
|
||||
// (trust line / MPT, sfAssetsAvailable, sfAssetsTotal) change by the same representable delta.
|
||||
// Skip when assetsWithdrawn is already zero: the earlier fix340 guard above deliberately
|
||||
// permits fixed-share zero-asset withdrawals in a fully-impaired vault (where
|
||||
// assetsTotalForWithdrawal == 0), and clamping-then-rejecting would undo that. Also skip on
|
||||
// the final-withdrawal path, which overwrites assetsWithdrawn with sfAssetsAvailable below.
|
||||
if (fix340Enabled && !isFinalWithdrawal && assetsWithdrawn > beast::kZero)
|
||||
{
|
||||
// Number arithmetic can throw overflow_error when Scale and totals are large. The
|
||||
// debitIsNonZeroDust check above already performs the same STAmount conversion of
|
||||
// assetsTotal/assetsAvailable under the ambient rounding mode and would have thrown
|
||||
// (and been caught) first for any value that overflows under that mode. Only reachable
|
||||
// if RoundingMode::Upward -- forced below to keep the clamp conservative -- carries a
|
||||
// value that was in range under the ambient mode just past the max representable
|
||||
// exponent. Kept for defense in depth; not realistically triggerable from a test.
|
||||
try
|
||||
{
|
||||
// Round Upward on the negative delta: the stored total is decremented by no more than
|
||||
// it can represent, so the payout is trimmed downward and the vault never pays out
|
||||
// more than it can account for.
|
||||
assetsWithdrawn =
|
||||
clampToAssetsTotalScale(vault, -assetsWithdrawn, Number::RoundingMode::Upward);
|
||||
// Payout collapsed to zero. Return tecPRECISION_LOSS instead of burning shares for
|
||||
// nothing.
|
||||
if (assetsWithdrawn <= beast::kZero)
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j_.debug()) //
|
||||
<< "VaultWithdraw: overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale).value() //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
|
||||
<< ", amount=" << amount.value();
|
||||
// Overflow means this transaction cannot apply, but ledger state is still consistent.
|
||||
// Return tecPATH_DRY rather than a hard internal error.
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// The vault must have enough assets on hand.
|
||||
if (*assetsAvailable < assetsWithdrawn)
|
||||
{
|
||||
@@ -416,12 +342,14 @@ VaultWithdraw::doApply()
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// Post-fixCleanup3_2_0: burning every outstanding share is only allowed when the vault has no
|
||||
// unrealized loss. Otherwise the resulting (shares == 0, assetsTotal > 0) state would violate
|
||||
// the zero-sized-vault invariant.
|
||||
// Post-fixCleanup3_2_0 "final withdrawal" rule:
|
||||
// a transaction that would burn every outstanding share is only permitted when the vault is in
|
||||
// a clean state — no outstanding receivables and no unrealized loss. Otherwise the resulting
|
||||
// (shares == 0, assetsTotal > 0) state would violate the zero-sized-vault invariant.
|
||||
//
|
||||
// The payout is set to the remaining sfAssetsAvailable. The helper result should already
|
||||
// equal that value in a clean vault; any mismatch is a rounding artifact and is logged.
|
||||
// When the rule applies, the payout is the remaining sfAssetsAvailable; in a clean vault
|
||||
// the helper result should already equal that value, and any mismatch is a rounding artifact
|
||||
// worth logging.
|
||||
if (view().rules().enabled(fixCleanup3_2_0) && isFinalWithdrawal)
|
||||
{
|
||||
// Unreachable: a final withdrawal with lossUnrealized > 0 has
|
||||
@@ -455,8 +383,6 @@ VaultWithdraw::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Debit both rails by the same delta so sfAssetsTotal and sfAssetsAvailable stay in step,
|
||||
// as required by the ValidVault invariant.
|
||||
assetsTotal -= assetsWithdrawn;
|
||||
assetsAvailable -= assetsWithdrawn;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,9 @@
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -413,11 +411,7 @@ private:
|
||||
testcase(
|
||||
"bug: VaultDeposit below Vault precision canonicalized to zero "
|
||||
"(pre-fixCleanup3_2_0)");
|
||||
// Also remove fixCleanup3_4_0 so the VaultDeposit clamp
|
||||
// introduced by that amendment does not short-circuit this
|
||||
// pre-fixCleanup3_2_0 scenario with tecPRECISION_LOSS.
|
||||
runScenario(
|
||||
testableAmendments() - fixCleanup3_2_0 - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
@@ -623,164 +617,6 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Bug: an IOU VaultDeposit can credit the vault (and its pseudo-account trust line) with
|
||||
// more than the depositor paid, when the exact new total needs 17 significant digits but
|
||||
// STAmount only keeps 16. The pseudo-account trust line (accountSend) and sfAssetsTotal
|
||||
// (associateAsset) both independently round the 17-digit sum UP to the nearest 16-digit
|
||||
// value -- the same direction on both rails, so no invariant catches the mismatch.
|
||||
//
|
||||
// seed 9.999999999999999 at scale 15 (1 share = 1e-15 asset); depositing 5 buys exactly
|
||||
// 5e15 shares.
|
||||
// exact new total 9.999999999999999 + 5 = 14.999999999999999 (17 digits)
|
||||
// stored 15 (rounds UP to the nearest 16-digit value)
|
||||
// credited more than the depositor actually paid in -- value issued out of
|
||||
// nothing
|
||||
//
|
||||
// Fix (fixCleanup3_4_0): VaultDeposit rounds the credited amount DOWN to what's exactly
|
||||
// representable at the vault's sfAssetsTotal scale before storing it, so the vault (and
|
||||
// its pseudo-account) can never be credited more than the depositor paid.
|
||||
void
|
||||
testBugVaultDepositOvercreditsAcrossScaleBoundary()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
auto runScenario = [this](FeatureBitset features, bool expectOvercredit) {
|
||||
Env env(*this, features);
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const depositor{"depositor"};
|
||||
env.fund(XRP(1'000'000), owner, issuer, depositor);
|
||||
env.close();
|
||||
|
||||
PrettyAsset const usd{issuer["USD"]};
|
||||
Number const seed{9'999'999'999'999'999LL, -15};
|
||||
Number const deposit{5};
|
||||
|
||||
env(trust(depositor, usd(1'000'000'000)));
|
||||
env.close();
|
||||
env(pay(issuer, depositor, usd(deposit)));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = usd.raw()});
|
||||
tx[sfScale] = 15;
|
||||
env(tx);
|
||||
env.close();
|
||||
env(vault.deposit({.depositor = issuer, .id = keylet.key, .amount = usd(seed)}));
|
||||
env.close();
|
||||
|
||||
Number const totalBefore = env.le(keylet)->at(sfAssetsTotal);
|
||||
Number const depositorBefore = env.balance(depositor, usd.raw()).number();
|
||||
|
||||
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = usd(deposit)}));
|
||||
env.close();
|
||||
|
||||
Number const totalAfter = env.le(keylet)->at(sfAssetsTotal);
|
||||
Number const depositorAfter = env.balance(depositor, usd.raw()).number();
|
||||
Number const paid = depositorBefore - depositorAfter;
|
||||
Number const credited = totalAfter - totalBefore;
|
||||
|
||||
if (expectOvercredit)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
credited > paid,
|
||||
"AssetsTotal credited " + to_string(credited) + " for a payment of " +
|
||||
to_string(paid) + ", expected an overcredit");
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
credited <= paid,
|
||||
"AssetsTotal credited " + to_string(credited) + " for a payment of " +
|
||||
to_string(paid));
|
||||
}
|
||||
};
|
||||
|
||||
// Also remove fixCleanup3_2_0: its roundToVaultScale trims the requested amount
|
||||
// down to the vault's current scale before this bug's own 17-digit-sum boundary is
|
||||
// ever reached, which would otherwise mask the overcredit behind a different
|
||||
// (already-fixed) rounding path instead of reproducing it.
|
||||
testcase(
|
||||
"bug: VaultDeposit overcredits across an IOU scale boundary "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_2_0 - fixCleanup3_4_0, true);
|
||||
|
||||
testcase(
|
||||
"bug: VaultDeposit no longer overcredits across an IOU scale boundary "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runScenario(all_, false);
|
||||
}
|
||||
|
||||
// Bug: a partial VaultWithdraw can permanently lock a large IOU vault. IOU amounts hold
|
||||
// 16 significant digits, so once sfAssetsTotal grows large enough it's stored in steps of
|
||||
// 1 ULP (e.g. steps of 100 at 1e17). If a single share is worth less than that step,
|
||||
// withdrawing all-but-one share overpays: the payout rounds UP to the entire remaining
|
||||
// balance, draining sfAssetsTotal to exactly zero while the last share stays outstanding.
|
||||
// The vault is then insolvent (AssetsTotal == 0, sharesTotal > 0) and permanently stuck --
|
||||
// withdrawing the last share pays 0 and can't change the balance (tecINVARIANT_FAILED),
|
||||
// depositing to refill is blocked because the vault is insolvent (tecLOCKED), and deleting
|
||||
// is impossible while a share is outstanding (tecHAS_OBLIGATIONS). A full withdrawal from
|
||||
// the start would have emptied the vault cleanly; once this partial-withdrawal state is
|
||||
// reached, no sequence of ordinary transactions recovers it.
|
||||
//
|
||||
// Fix (fixCleanup3_4_0): VaultWithdraw rounds the payout DOWN to what's exactly
|
||||
// representable at the vault's sfAssetsTotal scale, so a partial withdrawal can never
|
||||
// drain the vault to zero while shares remain outstanding.
|
||||
void
|
||||
testBugVaultLockedByPartialWithdraw()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
auto runScenario = [this](FeatureBitset features, TER expected) {
|
||||
Env env(*this, features);
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const holder{"holder"};
|
||||
env.fund(XRP(1'000'000), owner, issuer, holder);
|
||||
env.close();
|
||||
|
||||
PrettyAsset const usd{issuer["USD"]};
|
||||
env(trust(holder, usd(Number{1, 18})));
|
||||
env.close();
|
||||
env(pay(issuer, holder, usd(Number{1, 17})));
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
// scale=0: 1 share == 1 asset unit, so sharesTotal == assetsTotal == 1e17.
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = usd.raw()});
|
||||
tx[sfScale] = 0;
|
||||
env(tx);
|
||||
env.close();
|
||||
env(vault.deposit(
|
||||
{.depositor = holder, .id = keylet.key, .amount = usd(Number{1, 17})}));
|
||||
env.close();
|
||||
|
||||
MPTIssue const share{env.le(keylet)->at(sfShareMPTID)};
|
||||
std::int64_t const allButOne = 100'000'000'000'000'000LL - 1;
|
||||
env(vault.withdraw(
|
||||
{.depositor = holder, .id = keylet.key, .amount = STAmount{share, allButOne}}));
|
||||
env.close();
|
||||
|
||||
// The holder now tries to withdraw the one remaining share. Pre-fix, the vault is
|
||||
// already insolvent (AssetsTotal == 0, one share outstanding) and this fails with
|
||||
// tecINVARIANT_FAILED forever. Post-fix, the earlier partial withdrawal never
|
||||
// drained the vault, so this succeeds and empties it cleanly.
|
||||
env(vault.withdraw(
|
||||
{.depositor = holder, .id = keylet.key, .amount = STAmount{share, 1}}),
|
||||
Ter(expected));
|
||||
env.close();
|
||||
};
|
||||
|
||||
testcase(
|
||||
"bug: VaultWithdraw permanently locks a large IOU vault "
|
||||
"(pre-fixCleanup3_4_0)");
|
||||
runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
|
||||
testcase(
|
||||
"bug: VaultWithdraw no longer locks a large IOU vault "
|
||||
"(post-fixCleanup3_4_0)");
|
||||
runScenario(all_, tesSUCCESS);
|
||||
}
|
||||
|
||||
// VaultDeposit::preclaim uses accountHolds(..., SpendableHandling::
|
||||
// shFULL_BALANCE), which for an IOU asset adds the counterparty's
|
||||
// LowLimit/HighLimit to the depositor's raw balance (TokenHelpers.cpp:
|
||||
@@ -967,8 +803,6 @@ public:
|
||||
testVaultDepositCanonicalizeToZero();
|
||||
testVaultWithdrawCanonicalizeToZero();
|
||||
testBugVaultDustDebitCanonicalizesToNoOp();
|
||||
testBugVaultDepositOvercreditsAcrossScaleBoundary();
|
||||
testBugVaultLockedByPartialWithdraw();
|
||||
testVaultDepositNegativeBalanceFromOppositeLimit();
|
||||
testBug6LimitBypassWithShares();
|
||||
}
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/app/lending/LoanTestBase.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/sig.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/Units.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// Shared fixture for VaultInvariantPrecision_test and VaultTransactorPrecision_test.
|
||||
// Also landed identically on the companion invariant-fix branch; reconcile this copy
|
||||
// against that branch's version on rebase.
|
||||
//
|
||||
// Layout:
|
||||
// - A-1 (impairAndPaySibling=false): 1000 USD vault + one ordinary loan.
|
||||
// assetsTotal ~= 1000.353..., assetsAvailable == 993, lossUnrealized == 0.
|
||||
// - A-3 (impairAndPaySibling=true): add a second loan of principal 11,
|
||||
// impair the first loan, and pay off the second in full. This drives
|
||||
// the vault to the lossUnrealized == (assetsTotal - assetsAvailable)
|
||||
// boundary where the loss invariant used to spuriously fire.
|
||||
class VaultPrecisionFixture : public LoanTestBase
|
||||
{
|
||||
protected:
|
||||
static constexpr std::uint32_t kFixturePaymentInterval = 86400u * 30u;
|
||||
static constexpr std::uint32_t kFixtureGracePeriod = 86400u * 30u;
|
||||
static constexpr std::uint32_t kFixturePaymentTotal = 120u;
|
||||
// 10% APR, expressed in tenth-bips (1000 = 10.00 %).
|
||||
static constexpr std::uint32_t kFixtureInterestTenthBips = 1000u;
|
||||
|
||||
struct Fixture
|
||||
{
|
||||
// Every account is initialised with a placeholder name because
|
||||
// jtx::Account has no default constructor; setupSingleLoanVault
|
||||
// overwrites them.
|
||||
jtx::Account issuer{"vp_issuer_placeholder"};
|
||||
jtx::Account lender{"vp_lender_placeholder"};
|
||||
jtx::Account borrower{"vp_borrower_placeholder"};
|
||||
// Distinct account used to deposit into the vault. Keeps share
|
||||
// ownership independent of the initial vault seeding.
|
||||
jtx::Account depositor{"vp_depositor_placeholder"};
|
||||
// Optional so callers can BEAST_EXPECT(f.asset && f.broker)
|
||||
// after setup; both are populated in the happy path.
|
||||
std::optional<jtx::PrettyAsset> asset;
|
||||
std::optional<BrokerInfo> broker;
|
||||
// Keylet has no default constructor. Fill with an obviously
|
||||
// meaningless placeholder; setupSingleLoanVault overwrites the
|
||||
// fields that matter.
|
||||
Keylet vaultKeylet{ltACCOUNT_ROOT, uint256{}};
|
||||
Keylet loan1Keylet{ltACCOUNT_ROOT, uint256{}};
|
||||
// Only meaningful when impairAndPaySibling == true.
|
||||
Keylet loan2Keylet{ltACCOUNT_ROOT, uint256{}};
|
||||
jtx::Account vaultAccount{"vp_vault_pseudo_placeholder"};
|
||||
MPTID share;
|
||||
};
|
||||
|
||||
// Read-only snapshot of the vault + share issuance at a point in time.
|
||||
// Uses Number for exact arithmetic (no re-quantization).
|
||||
struct Numbers
|
||||
{
|
||||
Asset asset;
|
||||
MPTIssue share;
|
||||
Number assetsTotal{}; // sfAssetsTotal
|
||||
Number assetsAvailable{}; // sfAssetsAvailable
|
||||
Number lossUnrealized{}; // sfLossUnrealized
|
||||
Number pseudo{}; // vault pseudo-account balance in the asset
|
||||
Number sharesTotal{}; // sfOutstandingAmount on the share MPT
|
||||
};
|
||||
|
||||
static Numbers
|
||||
read(jtx::Env const& env, Fixture const& f)
|
||||
{
|
||||
Numbers n{.asset = f.asset ? f.asset->raw() : Asset{}, .share = MPTIssue{f.share}};
|
||||
if (auto const vaultSle = env.le(f.vaultKeylet))
|
||||
{
|
||||
n.assetsTotal = vaultSle->at(sfAssetsTotal);
|
||||
n.assetsAvailable = vaultSle->at(sfAssetsAvailable);
|
||||
n.lossUnrealized = vaultSle->at(sfLossUnrealized);
|
||||
}
|
||||
if (auto const issuanceSle = env.le(keylet::mptokenIssuance(f.share)))
|
||||
{
|
||||
n.sharesTotal = issuanceSle->at(sfOutstandingAmount);
|
||||
}
|
||||
if (f.asset)
|
||||
n.pseudo = env.balance(f.vaultAccount, *f.asset).number();
|
||||
return n;
|
||||
}
|
||||
|
||||
// One unit at the STAmount scale of `assetsTotalAfter`. Used as the
|
||||
// tolerance in one-unit-band assertions.
|
||||
static Number
|
||||
oneUnit(Asset const& asset, Number const& assetsTotalAfter)
|
||||
{
|
||||
return Number{1, scale(assetsTotalAfter, asset)};
|
||||
}
|
||||
|
||||
// Build the shared vault + loan(s) layout. The caller constructs
|
||||
// `env` with whatever FeatureBitset they want to exercise; this helper
|
||||
// just uses it. If `allowClawback` is true, the issuer's
|
||||
// asfAllowTrustLineClawback flag is set BEFORE any trust line is
|
||||
// established for that issuer. A separate env.close() runs so the
|
||||
// flag lands in the ledger before the trust lines are set up.
|
||||
static Fixture
|
||||
setupSingleLoanVault(jtx::Env& env, bool impairAndPaySibling, bool allowClawback = false)
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace jtx::loan;
|
||||
using namespace jtx::loan_broker;
|
||||
|
||||
Fixture f;
|
||||
f.issuer = Account{"vp_issuer"};
|
||||
f.lender = Account{"vp_lender"};
|
||||
f.borrower = Account{"vp_borrower"};
|
||||
f.depositor = Account{"vp_depositor"};
|
||||
|
||||
env.fund(XRP(1'000'000), f.issuer, f.lender, f.borrower, f.depositor);
|
||||
env.close();
|
||||
|
||||
// Must be set BEFORE any trust line to `issuer` is created.
|
||||
if (allowClawback)
|
||||
{
|
||||
env(fset(f.issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
}
|
||||
|
||||
PrettyAsset const asset = f.issuer["USD"];
|
||||
f.asset = asset;
|
||||
|
||||
env.trust(asset(1'000'000'000), f.lender);
|
||||
env.trust(asset(1'000'000'000), f.borrower);
|
||||
env.trust(asset(1'000'000'000), f.depositor);
|
||||
env(pay(f.issuer, f.lender, asset(100'000'000)));
|
||||
env(pay(f.issuer, f.borrower, asset(100'000'000)));
|
||||
env(pay(f.issuer, f.depositor, asset(100'000'000)));
|
||||
env.close();
|
||||
|
||||
BrokerParameters const brokerParams{
|
||||
.vaultDeposit = 1'000,
|
||||
.debtMax = 0,
|
||||
.coverRateMin = percentageToTenthBips(1),
|
||||
.coverDeposit = 10'000,
|
||||
.managementFeeRate = TenthBips16{100},
|
||||
.coverRateLiquidation = xrpl::lending::kMaxCoverRate};
|
||||
|
||||
// Build the vault + broker manually (rather than calling
|
||||
// createVaultAndBroker) so we can seed only the lender/depositor
|
||||
// trust lines we set up above, and skip the LoanTestBase auto
|
||||
// funding that assumes an XRP asset.
|
||||
Vault const vault{env};
|
||||
auto [createTx, vaultKeylet] = vault.create({.owner = f.lender, .asset = asset});
|
||||
env(createTx);
|
||||
env.close();
|
||||
f.vaultKeylet = vaultKeylet;
|
||||
|
||||
env(vault.deposit(
|
||||
{.depositor = f.lender,
|
||||
.id = vaultKeylet.key,
|
||||
.amount = asset(brokerParams.vaultDeposit)}));
|
||||
env.close();
|
||||
|
||||
auto const brokerKeylet =
|
||||
keylet::loanBroker(f.lender.id(), SeqProxy::rawSequence(env.seq(f.lender)));
|
||||
|
||||
env(set(f.lender, vaultKeylet.key, brokerParams.flags),
|
||||
kManagementFeeRate(brokerParams.managementFeeRate),
|
||||
kDebtMaximum(asset(brokerParams.debtMax).value()),
|
||||
kCoverRateMinimum(brokerParams.coverRateMin),
|
||||
kCoverRateLiquidation(TenthBips32(brokerParams.coverRateLiquidation)));
|
||||
env(coverDeposit(f.lender, brokerKeylet.key, asset(brokerParams.coverDeposit).value()));
|
||||
env.close();
|
||||
|
||||
f.broker = BrokerInfo{asset, brokerKeylet, vaultKeylet, brokerParams};
|
||||
|
||||
auto const vaultSle = env.le(vaultKeylet);
|
||||
f.vaultAccount = Account{"vp_vault_pseudo", vaultSle->at(sfAccount)};
|
||||
f.share = vaultSle->at(sfShareMPTID);
|
||||
|
||||
Fee const bigFee{env.current()->fees().base * 200};
|
||||
|
||||
auto const setLoan = [&](Number const& principal) -> Keylet {
|
||||
auto const brokerSle = env.le(brokerKeylet);
|
||||
auto const loanKeylet = keylet::loan(
|
||||
brokerKeylet.key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
|
||||
env(loan::set(f.borrower, brokerKeylet.key, asset(principal).number()),
|
||||
Sig(sfCounterpartySignature, f.lender),
|
||||
jtx::loan::kInterestRate(TenthBips32{kFixtureInterestTenthBips}),
|
||||
jtx::loan::kPaymentTotal(kFixturePaymentTotal),
|
||||
jtx::loan::kPaymentInterval(kFixturePaymentInterval),
|
||||
jtx::loan::kGracePeriod(kFixtureGracePeriod),
|
||||
bigFee);
|
||||
env.close();
|
||||
return loanKeylet;
|
||||
};
|
||||
|
||||
// Loan 1: principal 7, the one ordinary loan in both fixtures.
|
||||
// With vault deposit 1000, this leaves A ≈ 993 (see plan).
|
||||
f.loan1Keylet = setLoan(Number{7});
|
||||
|
||||
if (!impairAndPaySibling)
|
||||
return f;
|
||||
|
||||
// Loan 2: sibling loan of principal 11.
|
||||
f.loan2Keylet = setLoan(Number{11});
|
||||
|
||||
// Impair loan 1 → drives sfLossUnrealized to loan 1's value.
|
||||
env(jtx::loan::manage(f.lender, f.loan1Keylet.key, tfLoanImpair), bigFee);
|
||||
env.close();
|
||||
|
||||
// Pay off loan 2 in full so its total value flows into the vault
|
||||
// and pushes T-A upward, meeting the residual loss. Generous
|
||||
// upper bound; the transactor takes only what is due.
|
||||
auto const payoff = asset(Number{50}).value();
|
||||
env(pay(f.borrower, f.loan2Keylet.key, payoff, tfLoanFullPayment), bigFee);
|
||||
env.close();
|
||||
|
||||
return f;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1,785 +0,0 @@
|
||||
#include <test/app/vault/VaultPrecisionFixture.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
// PR 2 tests: with fixCleanup3_4_0 enabled the transactor clamps make
|
||||
// deposit / withdraw / clawback exact on the sfAssetsTotal grid. Every
|
||||
// successful withdrawal and clawback satisfies STRICT equality of the
|
||||
// assetsTotal / assetsAvailable / pseudo-account deltas -- no tolerance.
|
||||
// Deposit is exact on the T side; A stays within one unit at the
|
||||
// sfAssetsTotal grid.
|
||||
class VaultTransactorPrecision_test : public VaultPrecisionFixture
|
||||
{
|
||||
static Number
|
||||
absDiff(Number const& a, Number const& b)
|
||||
{
|
||||
return a > b ? a - b : b - a;
|
||||
}
|
||||
|
||||
// ---- deposit ------------------------------------------------------
|
||||
|
||||
// A-1 magnitude sweep. Post-fix every successful deposit satisfies
|
||||
// T_delta <= requested amount
|
||||
// |T_delta - A_delta| <= oneUnit(asset, T_after)
|
||||
// and the plan's three boundary amounts {1, 7, 10'000'000} now succeed.
|
||||
// Pre-fix those three boundary amounts fail with tecINVARIANT_FAILED.
|
||||
void
|
||||
testDepositNeverOverCredited(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
testcase(
|
||||
std::string("A-1 deposit never over-credited") +
|
||||
(fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
|
||||
|
||||
std::array<int, 17> const kAmounts{
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
7,
|
||||
10,
|
||||
50,
|
||||
100,
|
||||
500,
|
||||
1'000,
|
||||
5'000,
|
||||
10'000,
|
||||
50'000,
|
||||
100'000,
|
||||
500'000,
|
||||
1'000'000,
|
||||
5'000'000,
|
||||
10'000'000};
|
||||
|
||||
// Plan's three boundary amounts that Part 1 alone closes on A-1.
|
||||
std::array<int, 3> const kPlanBoundaryAmounts{1, 7, 10'000'000};
|
||||
|
||||
for (auto const amount : kAmounts)
|
||||
{
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
continue;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
auto const before = read(env, f);
|
||||
|
||||
Vault const v{env};
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(amount).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
TER const actual = env.ter();
|
||||
bool const isBoundary =
|
||||
std::ranges::find(kPlanBoundaryAmounts, amount) != kPlanBoundaryAmounts.end();
|
||||
|
||||
if (fixEnabled)
|
||||
{
|
||||
if (isBoundary)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
actual == tesSUCCESS,
|
||||
"plan boundary amount=" + std::to_string(amount) +
|
||||
" expected tesSUCCESS, got " + transToken(actual));
|
||||
}
|
||||
if (actual != tesSUCCESS)
|
||||
continue;
|
||||
|
||||
auto const after = read(env, f);
|
||||
Number const tDelta = after.assetsTotal - before.assetsTotal;
|
||||
Number const aDelta = after.assetsAvailable - before.assetsAvailable;
|
||||
Number const requested = asset(amount).number();
|
||||
|
||||
BEAST_EXPECTS(
|
||||
tDelta <= requested,
|
||||
"amount=" + std::to_string(amount) + " tDelta exceeds requested");
|
||||
Number const gap = absDiff(tDelta, aDelta);
|
||||
BEAST_EXPECTS(
|
||||
gap <= oneUnit(asset, after.assetsTotal),
|
||||
"amount=" + std::to_string(amount) + " |tDelta-aDelta| exceeds oneUnit");
|
||||
}
|
||||
else if (isBoundary)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
actual == tecINVARIANT_FAILED,
|
||||
"pre-fix amount=" + std::to_string(amount) +
|
||||
" expected tecINVARIANT_FAILED, got " + transToken(actual));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-fix: the depositor never gets shares worth more than the assets
|
||||
// they paid. Any overpay is bounded by the pre-deposit per-share value.
|
||||
void
|
||||
testDepositorNeverUnderpays(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
if (!fixEnabled)
|
||||
return;
|
||||
|
||||
testcase("A-1 depositor never underpays (fixCleanup3_4_0)");
|
||||
|
||||
std::array<int, 8> const kAmounts{1, 7, 100, 1'000, 10'000, 100'000, 1'000'000, 10'000'000};
|
||||
|
||||
for (auto const amount : kAmounts)
|
||||
{
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
continue;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
auto const before = read(env, f);
|
||||
|
||||
Vault const v{env};
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(amount).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
if (env.ter() != tesSUCCESS)
|
||||
continue;
|
||||
|
||||
auto const after = read(env, f);
|
||||
Number const sharesMinted = after.sharesTotal - before.sharesTotal;
|
||||
Number const assetsTaken = after.assetsTotal - before.assetsTotal;
|
||||
if (before.sharesTotal == Number{0})
|
||||
continue;
|
||||
Number const shareValue = (before.assetsTotal * sharesMinted) / before.sharesTotal;
|
||||
|
||||
BEAST_EXPECTS(
|
||||
shareValue <= assetsTaken,
|
||||
"amount=" + std::to_string(amount) + " shareValue > assetsTaken");
|
||||
|
||||
Number const perShareOverpay = before.assetsTotal / before.sharesTotal;
|
||||
Number const overpay = assetsTaken > shareValue ? assetsTaken - shareValue : Number{0};
|
||||
BEAST_EXPECTS(
|
||||
overpay <= perShareOverpay,
|
||||
"amount=" + std::to_string(amount) + " overpay exceeds per-share bound");
|
||||
}
|
||||
}
|
||||
|
||||
// Push T up to ~1e8, then attempt Number{1,-10} deposit. Post-fix:
|
||||
// tecPRECISION_LOSS with the vault state unchanged. Confirms the
|
||||
// deposit clamp cannot mint uncovered shares.
|
||||
void
|
||||
testZeroCreditRejected(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
if (!fixEnabled)
|
||||
return;
|
||||
|
||||
testcase("A-1 zero credit rejected (fixCleanup3_4_0)");
|
||||
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
return;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
Vault const v{env};
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(99'000'000).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
auto const before = read(env, f);
|
||||
Number const kLowerBound{1, 6};
|
||||
BEAST_EXPECT(before.assetsTotal > kLowerBound);
|
||||
|
||||
auto const tinyAmount = asset(Number{1, -10}).value();
|
||||
env(v.deposit({.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = tinyAmount}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECTS(
|
||||
env.ter() == tecPRECISION_LOSS,
|
||||
std::string{"expected tecPRECISION_LOSS, got "} + transToken(env.ter()));
|
||||
|
||||
auto const after = read(env, f);
|
||||
BEAST_EXPECT(after.assetsTotal == before.assetsTotal);
|
||||
BEAST_EXPECT(after.assetsAvailable == before.assetsAvailable);
|
||||
BEAST_EXPECT(after.sharesTotal == before.sharesTotal);
|
||||
}
|
||||
|
||||
// XRP and MPT (integral) vaults: the clamp is a no-op because
|
||||
// STAmount(asset, N) already truncates to whole units. Every amount
|
||||
// in the sweep succeeds pre- and post-amendment.
|
||||
void
|
||||
testIntegralAssetsUnchanged(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
testcase(
|
||||
std::string("integral asset deposit sweep") +
|
||||
(fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
|
||||
|
||||
std::array<int, 8> const kAmounts{1, 7, 100, 1'000, 10'000, 100'000, 1'000'000, 10'000'000};
|
||||
|
||||
auto runXrp = [&]() {
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
Account const owner{"xrp_owner"};
|
||||
Account const depositor{"xrp_depositor"};
|
||||
env.fund(XRP(1'000'000'000), owner, depositor);
|
||||
env.close();
|
||||
|
||||
Vault const v{env};
|
||||
auto [createTx, vaultKeylet] = v.create({.owner = owner, .asset = xrpIssue()});
|
||||
env(createTx);
|
||||
env.close();
|
||||
|
||||
env(v.deposit(
|
||||
{.depositor = owner, .id = vaultKeylet.key, .amount = XRP(1'000).value()}));
|
||||
env.close();
|
||||
|
||||
for (auto const amount : kAmounts)
|
||||
{
|
||||
env(v.deposit(
|
||||
{.depositor = depositor,
|
||||
.id = vaultKeylet.key,
|
||||
.amount = XRP(amount).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
BEAST_EXPECTS(
|
||||
env.ter() == tesSUCCESS,
|
||||
"XRP amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
|
||||
transToken(env.ter()));
|
||||
}
|
||||
};
|
||||
|
||||
auto runMpt = [&]() {
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
Account const issuer{"mpt_issuer"};
|
||||
Account const owner{"mpt_owner"};
|
||||
Account const depositor{"mpt_depositor"};
|
||||
env.fund(XRP(1'000'000), issuer, owner, depositor);
|
||||
env.close();
|
||||
|
||||
MPTTester mptt{env, issuer, kMptInitNoFund};
|
||||
mptt.create({.flags = tfMPTCanTransfer});
|
||||
PrettyAsset const asset = mptt.issuanceID();
|
||||
mptt.authorize({.account = owner});
|
||||
mptt.authorize({.account = depositor});
|
||||
env(pay(issuer, depositor, asset(1'000'000'000)));
|
||||
env.close();
|
||||
|
||||
Vault const v{env};
|
||||
auto [createTx, vaultKeylet] = v.create({.owner = owner, .asset = asset});
|
||||
env(createTx);
|
||||
env.close();
|
||||
|
||||
env(pay(issuer, owner, asset(1'000)));
|
||||
env.close();
|
||||
env(v.deposit(
|
||||
{.depositor = owner, .id = vaultKeylet.key, .amount = asset(1'000).value()}));
|
||||
env.close();
|
||||
|
||||
for (auto const amount : kAmounts)
|
||||
{
|
||||
env(v.deposit(
|
||||
{.depositor = depositor,
|
||||
.id = vaultKeylet.key,
|
||||
.amount = asset(amount).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
BEAST_EXPECTS(
|
||||
env.ter() == tesSUCCESS,
|
||||
"MPT amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
|
||||
transToken(env.ter()));
|
||||
}
|
||||
};
|
||||
|
||||
runXrp();
|
||||
runMpt();
|
||||
}
|
||||
|
||||
// ---- withdraw -----------------------------------------------------
|
||||
|
||||
// A-1 fixture withdrawals in both asset-fixed and share-fixed modes.
|
||||
// Post-fix: never tecINVARIANT_FAILED and every successful withdrawal
|
||||
// satisfies STRICT equality
|
||||
// beforeT - afterT == beforeA - afterA == beforePseudo - afterPseudo
|
||||
// in Number space. This is the strong claim of Part 1b of the plan.
|
||||
void
|
||||
testWithdrawDeltas(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
testcase(
|
||||
std::string("A-1 withdraw delta exactness") +
|
||||
(fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
|
||||
|
||||
std::array<std::uint64_t, 6> const kShareCounts{
|
||||
99'999u, 100'001u, 333'333u, 1'234'567u, 142'857'142u, 333'333'333u};
|
||||
|
||||
std::array<int, 5> const kAssetAmounts{1, 7, 99, 333, 993};
|
||||
|
||||
auto runOnce = [&](bool useShares) {
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
return;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
Vault const v{env};
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(1'000'000).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
auto step = [&](STAmount const& amount, std::string const& tag) {
|
||||
auto const before = read(env, f);
|
||||
env(v.withdraw(
|
||||
{.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = amount}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
TER const actual = env.ter();
|
||||
if (fixEnabled)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
actual != tecINVARIANT_FAILED, tag + " unexpected invariant failure");
|
||||
if (actual == tesSUCCESS)
|
||||
{
|
||||
auto const after = read(env, f);
|
||||
Number const tDelta = before.assetsTotal - after.assetsTotal;
|
||||
Number const aDelta = before.assetsAvailable - after.assetsAvailable;
|
||||
Number const pDelta = before.pseudo - after.pseudo;
|
||||
BEAST_EXPECTS(tDelta == aDelta, tag + " tDelta != aDelta");
|
||||
BEAST_EXPECTS(tDelta == pDelta, tag + " tDelta != pDelta");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (useShares)
|
||||
{
|
||||
for (auto const count : kShareCounts)
|
||||
{
|
||||
auto const before = read(env, f);
|
||||
if (before.sharesTotal < count)
|
||||
continue;
|
||||
STAmount const shareAmount{
|
||||
MPTIssue{f.share}, Number{static_cast<std::int64_t>(count)}};
|
||||
step(shareAmount, "shares=" + std::to_string(count));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto const amount : kAssetAmounts)
|
||||
{
|
||||
step(asset(amount).value(), "assets=" + std::to_string(amount));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
runOnce(/*useShares=*/true);
|
||||
runOnce(/*useShares=*/false);
|
||||
}
|
||||
|
||||
// Post-fix: withdrawer never receives more than the burned share value;
|
||||
// any shortfall is bounded by one unit at the sfAssetsTotal scale.
|
||||
void
|
||||
testWithdrawNeverOverpays(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
if (!fixEnabled)
|
||||
return;
|
||||
|
||||
testcase("A-1 withdraw never overpays (fixCleanup3_4_0)");
|
||||
|
||||
std::array<std::uint64_t, 5> const kShareCounts{
|
||||
99'999u, 100'001u, 333'333u, 1'234'567u, 142'857'142u};
|
||||
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
return;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
Vault const v{env};
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(1'000'000).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
for (auto const count : kShareCounts)
|
||||
{
|
||||
auto const before = read(env, f);
|
||||
if (before.sharesTotal < count)
|
||||
continue;
|
||||
STAmount const shareAmount{MPTIssue{f.share}, Number{static_cast<std::int64_t>(count)}};
|
||||
env(v.withdraw(
|
||||
{.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = shareAmount}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
if (env.ter() != tesSUCCESS)
|
||||
continue;
|
||||
|
||||
auto const after = read(env, f);
|
||||
Number const sharesBurned = before.sharesTotal - after.sharesTotal;
|
||||
if (before.sharesTotal == Number{0})
|
||||
continue;
|
||||
Number const shareValue = (before.assetsTotal * sharesBurned) / before.sharesTotal;
|
||||
Number const payout = before.assetsTotal - after.assetsTotal;
|
||||
|
||||
BEAST_EXPECTS(
|
||||
payout <= shareValue, "shares=" + std::to_string(count) + " payout > shareValue");
|
||||
Number const shortfall = shareValue > payout ? shareValue - payout : Number{0};
|
||||
BEAST_EXPECTS(
|
||||
shortfall <= oneUnit(asset, after.assetsTotal),
|
||||
"shares=" + std::to_string(count) + " shortfall exceeds oneUnit");
|
||||
}
|
||||
}
|
||||
|
||||
// Sub-ULP withdrawal from a ~1e8 vault; post-fix must return
|
||||
// tecPRECISION_LOSS after the Upward clamp rounds the amount to zero.
|
||||
void
|
||||
testWithdrawSubUlpRejected(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
if (!fixEnabled)
|
||||
return;
|
||||
|
||||
testcase("sub-ULP withdraw rejected (fixCleanup3_4_0)");
|
||||
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
return;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
Vault const v{env};
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(99'000'000).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
auto const before = read(env, f);
|
||||
Number const kLowerBound{1, 6};
|
||||
BEAST_EXPECT(before.assetsTotal > kLowerBound);
|
||||
|
||||
auto const tinyAmount = asset(Number{1, -10}).value();
|
||||
env(v.withdraw({.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = tinyAmount}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECTS(
|
||||
env.ter() == tecPRECISION_LOSS,
|
||||
std::string{"expected tecPRECISION_LOSS, got "} + transToken(env.ter()));
|
||||
}
|
||||
|
||||
// Depositor burns every share they hold, exercising the final-
|
||||
// withdrawal branch of VaultWithdraw (line 336-368 -- deliberately
|
||||
// untouched by this amendment). Must return tesSUCCESS: the clamp
|
||||
// does not spuriously reject a legitimate full-share withdrawal.
|
||||
//
|
||||
// The plan describes reaching a "dust-only gap state" via the A-3
|
||||
// fixture; empirically the A-3 fixture retains a non-zero
|
||||
// sfLossUnrealized which blocks the final-withdrawal branch through
|
||||
// the insufficient-funds guard. Verifying the branch from a clean
|
||||
// A-1 state still exercises the amendment's non-interference claim.
|
||||
void
|
||||
testFinalWithdrawalDust(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
if (!fixEnabled)
|
||||
return;
|
||||
|
||||
testcase("A-1 final withdrawal (fixCleanup3_4_0)");
|
||||
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
return;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
Vault const v{env};
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = asset(500).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
auto const depositorMptSle = env.le(keylet::mptoken(f.share, f.depositor.id()));
|
||||
if (!BEAST_EXPECT(depositorMptSle != nullptr))
|
||||
return;
|
||||
|
||||
auto const held = depositorMptSle->at(sfMPTAmount);
|
||||
if (held == 0)
|
||||
return;
|
||||
|
||||
STAmount const shareAmount{MPTIssue{f.share}, Number{static_cast<std::int64_t>(held)}};
|
||||
env(v.withdraw({.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = shareAmount}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
TER const actual = env.ter();
|
||||
BEAST_EXPECTS(
|
||||
actual == tesSUCCESS, std::string{"expected tesSUCCESS, got "} + transToken(actual));
|
||||
}
|
||||
|
||||
// ---- deposit + withdraw under impairment (A-3) --------------------
|
||||
|
||||
// A-3 fixture drives sfLossUnrealized to the boundary
|
||||
// lossUnrealized == assetsTotal - assetsAvailable
|
||||
// A companion invariant-fix branch found that independent per-field
|
||||
// rounding of assetsTotal/assetsAvailable can spuriously trip the
|
||||
// XRPL_ASSERT (and, pre-fix, tecINVARIANT_FAILED) that compares
|
||||
// lossUnrealized against assetsTotal - assetsAvailable in
|
||||
// VaultWithdraw::doApply. This branch's clamp keeps assetsTotal and
|
||||
// assetsAvailable exact on the sfAssetsTotal grid, which should keep
|
||||
// that comparison stable even while churning the vault through this
|
||||
// boundary state. Only meaningful post-fix -- the pre-fix boundary
|
||||
// behaviour at A-3 is out of scope for this branch (it is the subject
|
||||
// of the companion invariant-fix branch, not this one) -- so mirror
|
||||
// testWithdrawNeverOverpays and early-return before the amendment.
|
||||
void
|
||||
testDepositWithdrawUnderImpairment(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
if (!fixEnabled)
|
||||
return;
|
||||
|
||||
testcase("A-3 deposit/withdraw under impairment boundary (fixCleanup3_4_0)");
|
||||
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/true);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
return;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
Vault const v{env};
|
||||
|
||||
// Seed the depositor with an initial stake so later withdrawals
|
||||
// have shares/assets to draw against.
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(5'000).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
std::array<int, 12> const kAmounts{1, 3, 7, 13, 29, 51, 97, 137, 251, 499, 991, 1'999};
|
||||
|
||||
auto checkInvariant = [&](std::string const& tag) {
|
||||
TER const actual = env.ter();
|
||||
BEAST_EXPECTS(actual != tecINVARIANT_FAILED, tag + " unexpected invariant failure");
|
||||
if (actual == tesSUCCESS)
|
||||
{
|
||||
auto const after = read(env, f);
|
||||
BEAST_EXPECTS(
|
||||
after.lossUnrealized <= after.assetsTotal - after.assetsAvailable,
|
||||
tag + " lossUnrealized exceeds assetsTotal - assetsAvailable");
|
||||
}
|
||||
};
|
||||
|
||||
// Alternate deposit then withdraw of a different amount so the
|
||||
// vault's assetsTotal / assetsAvailable / lossUnrealized state
|
||||
// churns through several transactions at the A-3 boundary.
|
||||
for (std::size_t i = 0; i + 1 < kAmounts.size(); i += 2)
|
||||
{
|
||||
int const depositAmount = kAmounts[i];
|
||||
int const withdrawAmount = kAmounts[i + 1];
|
||||
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(depositAmount).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
checkInvariant("deposit=" + std::to_string(depositAmount));
|
||||
|
||||
env(v.withdraw(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(withdrawAmount).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
checkInvariant("withdraw=" + std::to_string(withdrawAmount));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- clawback -----------------------------------------------------
|
||||
|
||||
// A-1 fixture with clawback enabled. Sweep amounts including
|
||||
// sfAmount-absent (claw back everything) and amount-exceeds-available.
|
||||
// Post-fix: never tecINVARIANT_FAILED and each success satisfies
|
||||
// T_delta == A_delta == pseudo_delta in Number space.
|
||||
// Owner force-burn against a vault with a live loan: must return
|
||||
// tecNO_PERMISSION regardless of amendment (never enters
|
||||
// assetsToClawback so the clamp is unreachable there).
|
||||
void
|
||||
testClawbackDeltas(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
testcase(
|
||||
std::string("A-1 clawback delta exactness") +
|
||||
(fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
|
||||
|
||||
std::array<int, 6> const kAmounts{1, 7, 99, 333, 993, 5'000'000};
|
||||
|
||||
Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
|
||||
auto f = setupSingleLoanVault(
|
||||
env,
|
||||
/*impairAndPaySibling=*/false,
|
||||
/*allowClawback=*/true);
|
||||
if (!BEAST_EXPECT(f.asset && f.broker) || !f.asset)
|
||||
return;
|
||||
jtx::PrettyAsset const& asset = *f.asset;
|
||||
|
||||
Vault const v{env};
|
||||
env(v.deposit(
|
||||
{.depositor = f.depositor,
|
||||
.id = f.vaultKeylet.key,
|
||||
.amount = asset(2'000).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
auto stepAmount = [&](int amount) {
|
||||
auto const before = read(env, f);
|
||||
if (before.sharesTotal == Number{0})
|
||||
return;
|
||||
|
||||
env(v.clawback(
|
||||
{.issuer = f.issuer,
|
||||
.id = f.vaultKeylet.key,
|
||||
.holder = f.depositor,
|
||||
.amount = asset(amount).value()}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
TER const actual = env.ter();
|
||||
if (fixEnabled)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
actual != tecINVARIANT_FAILED,
|
||||
"amount=" + std::to_string(amount) + " unexpected invariant failure");
|
||||
if (actual == tesSUCCESS)
|
||||
{
|
||||
auto const after = read(env, f);
|
||||
Number const tDelta = before.assetsTotal - after.assetsTotal;
|
||||
Number const aDelta = before.assetsAvailable - after.assetsAvailable;
|
||||
Number const pDelta = before.pseudo - after.pseudo;
|
||||
BEAST_EXPECTS(
|
||||
tDelta == aDelta, "amount=" + std::to_string(amount) + " tDelta != aDelta");
|
||||
BEAST_EXPECTS(
|
||||
tDelta == pDelta, "amount=" + std::to_string(amount) + " tDelta != pDelta");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (auto const amount : kAmounts)
|
||||
stepAmount(amount);
|
||||
|
||||
{
|
||||
auto const before = read(env, f);
|
||||
if (before.sharesTotal > Number{0})
|
||||
{
|
||||
env(v.clawback(
|
||||
{.issuer = f.issuer, .id = f.vaultKeylet.key, .holder = f.depositor}),
|
||||
Ter(std::ignore));
|
||||
env.close();
|
||||
|
||||
TER const actual = env.ter();
|
||||
if (fixEnabled)
|
||||
{
|
||||
BEAST_EXPECTS(
|
||||
actual != tecINVARIANT_FAILED,
|
||||
"sfAmount-absent unexpected invariant failure");
|
||||
if (actual == tesSUCCESS)
|
||||
{
|
||||
auto const after = read(env, f);
|
||||
Number const tDelta = before.assetsTotal - after.assetsTotal;
|
||||
Number const aDelta = before.assetsAvailable - after.assetsAvailable;
|
||||
Number const pDelta = before.pseudo - after.pseudo;
|
||||
BEAST_EXPECT(tDelta == aDelta);
|
||||
BEAST_EXPECT(tDelta == pDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env(v.clawback({.issuer = f.lender, .id = f.vaultKeylet.key, .holder = f.depositor}),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
for (auto const& features : {all_ - fixCleanup3_4_0, all_})
|
||||
{
|
||||
testDepositNeverOverCredited(features);
|
||||
testDepositorNeverUnderpays(features);
|
||||
testZeroCreditRejected(features);
|
||||
testIntegralAssetsUnchanged(features);
|
||||
testWithdrawDeltas(features);
|
||||
testWithdrawNeverOverpays(features);
|
||||
testWithdrawSubUlpRejected(features);
|
||||
testFinalWithdrawalDust(features);
|
||||
testDepositWithdrawUnderImpairment(features);
|
||||
testClawbackDeltas(features);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(VaultTransactorPrecision, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
Reference in New Issue
Block a user