Merge branch 'develop' into ximinez/number-fix-maxrepcusp

This commit is contained in:
Ed Hennis
2026-05-26 16:01:40 -04:00
committed by GitHub
21 changed files with 998 additions and 126 deletions

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -8,12 +8,12 @@ set -e
SED_COMMAND=sed
HEAD_COMMAND=head
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi
SED_COMMAND=gsed
if ! command -v ghead &> /dev/null; then
if ! command -v ghead &>/dev/null; then
echo "Error: ghead is not installed. Please install it using 'brew install coreutils'."
exit 1
fi
@@ -74,10 +74,10 @@ if grep -q '"xrpld"' cmake/XrplCore.cmake; then
# The script has been rerun, so just restore the name of the binary.
${SED_COMMAND} -i 's/"xrpld"/"rippled"/' cmake/XrplCore.cmake
elif ! grep -q '"rippled"' cmake/XrplCore.cmake; then
${HEAD_COMMAND} -n -1 cmake/XrplCore.cmake > cmake.tmp
echo ' # For the time being, we will keep the name of the binary as it was.' >> cmake.tmp
echo ' set_target_properties(xrpld PROPERTIES OUTPUT_NAME "rippled")' >> cmake.tmp
tail -1 cmake/XrplCore.cmake >> cmake.tmp
${HEAD_COMMAND} -n -1 cmake/XrplCore.cmake >cmake.tmp
echo ' # For the time being, we will keep the name of the binary as it was.' >>cmake.tmp
echo ' set_target_properties(xrpld PROPERTIES OUTPUT_NAME "rippled")' >>cmake.tmp
tail -1 cmake/XrplCore.cmake >>cmake.tmp
mv cmake.tmp cmake/XrplCore.cmake
fi

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi
@@ -62,37 +62,37 @@ done
# restoring the verbiage that is already present in LICENSE.md. Ensure that if
# the script is run multiple times, duplicate notices are not added.
if ! grep -q 'Raw Material Software' include/xrpl/beast/core/CurrentThreadName.h; then
echo -e "// Portions of this file are from JUCE (http://www.juce.com).\n// Copyright (c) 2013 - Raw Material Software Ltd.\n// Please visit http://www.juce.com\n\n$(cat include/xrpl/beast/core/CurrentThreadName.h)" > include/xrpl/beast/core/CurrentThreadName.h
echo -e "// Portions of this file are from JUCE (http://www.juce.com).\n// Copyright (c) 2013 - Raw Material Software Ltd.\n// Please visit http://www.juce.com\n\n$(cat include/xrpl/beast/core/CurrentThreadName.h)" >include/xrpl/beast/core/CurrentThreadName.h
fi
if ! grep -q 'Dev Null' src/test/app/NetworkID_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/app/NetworkID_test.cpp)" > src/test/app/NetworkID_test.cpp
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/app/NetworkID_test.cpp)" >src/test/app/NetworkID_test.cpp
fi
if ! grep -q 'Dev Null' src/test/app/tx/apply_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/app/tx/apply_test.cpp)" > src/test/app/tx/apply_test.cpp
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/app/tx/apply_test.cpp)" >src/test/app/tx/apply_test.cpp
fi
if ! grep -q 'Dev Null' src/test/rpc/ManifestRPC_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ManifestRPC_test.cpp)" > src/test/rpc/ManifestRPC_test.cpp
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ManifestRPC_test.cpp)" >src/test/rpc/ManifestRPC_test.cpp
fi
if ! grep -q 'Dev Null' src/test/rpc/ValidatorInfo_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ValidatorInfo_test.cpp)" > src/test/rpc/ValidatorInfo_test.cpp
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ValidatorInfo_test.cpp)" >src/test/rpc/ValidatorInfo_test.cpp
fi
if ! grep -q 'Dev Null' src/xrpld/rpc/handlers/server_info/Manifest.cpp; then
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/server_info/Manifest.cpp)" > src/xrpld/rpc/handlers/server_info/Manifest.cpp
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/server_info/Manifest.cpp)" >src/xrpld/rpc/handlers/server_info/Manifest.cpp
fi
if ! grep -q 'Dev Null' src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp; then
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp)" > src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp)" >src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp
fi
if ! grep -q 'Bougalis' include/xrpl/basics/SlabAllocator.h; then
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/SlabAllocator.h)" > include/xrpl/basics/SlabAllocator.h # cspell: ignore Nikolaos Bougalis nikb
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/SlabAllocator.h)" >include/xrpl/basics/SlabAllocator.h # cspell: ignore Nikolaos Bougalis nikb
fi
if ! grep -q 'Bougalis' include/xrpl/basics/spinlock.h; then
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/spinlock.h)" > include/xrpl/basics/spinlock.h # cspell: ignore Nikolaos Bougalis nikb
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/spinlock.h)" >include/xrpl/basics/spinlock.h # cspell: ignore Nikolaos Bougalis nikb
fi
if ! grep -q 'Bougalis' include/xrpl/basics/tagged_integer.h; then
echo -e "// Copyright (c) 2014, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/tagged_integer.h)" > include/xrpl/basics/tagged_integer.h # cspell: ignore Nikolaos Bougalis nikb
echo -e "// Copyright (c) 2014, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/tagged_integer.h)" >include/xrpl/basics/tagged_integer.h # cspell: ignore Nikolaos Bougalis nikb
fi
if ! grep -q 'Ritchford' include/xrpl/beast/utility/Zero.h; then
echo -e "// Copyright (c) 2014, Tom Ritchford <tom@swirly.com>\n\n$(cat include/xrpl/beast/utility/Zero.h)" > include/xrpl/beast/utility/Zero.h # cspell: ignore Ritchford
echo -e "// Copyright (c) 2014, Tom Ritchford <tom@swirly.com>\n\n$(cat include/xrpl/beast/utility/Zero.h)" >include/xrpl/beast/utility/Zero.h # cspell: ignore Ritchford
fi
# Restore newlines and tabs in string literals in the affected file.

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -37,37 +37,37 @@ repos:
exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: cd481d7b0bfb5c7b3090c21846317f9a8262e891 # frozen: v22.1.0
rev: dd18dad857d6133e90bbe478f4f2f22ec0030269 # frozen: v22.1.5
hooks:
- id: clang-format
args: [--style=file]
"types_or": [c++, c, proto]
exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/
- repo: https://github.com/BlankSpruce/gersemi
rev: 0.26.0
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
rev: faadd6a9d852369ca94f4d15b2404c967ba8cb01 # frozen: 0.27.6
hooks:
- id: gersemi
- repo: https://github.com/rbubley/mirrors-prettier
rev: c2bc67fe8f8f549cc489e00ba8b45aa18ee713b1 # frozen: v3.8.1
rev: 515f543f5718ebfd6ce22e16708bb32c68ff96e1 # frozen: v3.8.3
hooks:
- id: prettier
args: [--end-of-line=auto]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: ea488cebbfd88a5f50b8bd95d5c829d0bb76feb8 # frozen: 26.1.0
rev: 4160603246a6b365d4a2af661c6d71b0a0f50478 # frozen: 26.5.1
hooks:
- id: black
- repo: https://github.com/openstack/bashate
rev: 5798d24d571676fc407e81df574c1ef57b520f23 # frozen: 2.1.1
- repo: https://github.com/scop/pre-commit-shfmt
rev: 05c1426671b9237fb5e1444dd63aa5731bec0dfb # frozen: v3.13.1-1
hooks:
- id: bashate
args: ["--ignore=E006"]
- id: shfmt
args: [--write, --indent=4, --case-indent=true]
- repo: https://github.com/streetsidesoftware/cspell-cli
rev: a42085ade523f591dca134379a595e7859986445 # frozen: v9.7.0
rev: 4643f154907327ee0a2c7038f0296e0dd77d9776 # frozen: v10.0.0
hooks:
- id: cspell # Spell check changed files
exclude: |

View File

@@ -1,8 +1,8 @@
#!/bin/bash
if [[ $# -ne 1 || "$1" == "--help" || "$1" == "-h" ]]; then
name=$( basename $0 )
cat <<- USAGE
name=$(basename $0)
cat <<-USAGE
Usage: $name <username>
Where <username> is the Github username of the upstream repo. e.g. XRPLF
@@ -14,7 +14,7 @@ fi
shift
user="$1"
# Get the origin URL. Expect it be an SSH-style URL
origin=$( git remote get-url origin )
origin=$(git remote get-url origin)
if [[ "${origin}" == "" ]]; then
echo Invalid origin remote >&2
exit 1
@@ -22,11 +22,11 @@ fi
# echo "Origin: ${origin}"
# Parse the origin
ifs_orig="${IFS}"
IFS=':' read remote originpath <<< "${origin}"
IFS=':' read remote originpath <<<"${origin}"
# echo "Remote: ${remote}, Originpath: ${originpath}"
IFS='@' read sshuser server <<< "${remote}"
IFS='@' read sshuser server <<<"${remote}"
# echo "SSHUser: ${sshuser}, Server: ${server}"
IFS='/' read originuser repo <<< "${originpath}"
IFS='/' read originuser repo <<<"${originpath}"
# echo "Originuser: ${originuser}, Repo: ${repo}"
if [[ "${sshuser}" == "" || "${server}" == "" || "${originuser}" == "" || "${repo}" == "" ]]; then
echo "Can't parse origin URL: ${origin}" >&2
@@ -35,9 +35,9 @@ fi
upstream="https://${server}/${user}/${repo}"
upstreampush="${remote}:${user}/${repo}"
upstreamgroup="upstream upstream-push"
current=$( git remote get-url upstream 2>/dev/null )
currentpush=$( git remote get-url upstream-push 2>/dev/null )
currentgroup=$( git config remotes.upstreams )
current=$(git remote get-url upstream 2>/dev/null)
currentpush=$(git remote get-url upstream-push 2>/dev/null)
currentgroup=$(git config remotes.upstreams)
if [[ "${current}" == "${upstream}" ]]; then
echo "Upstream already set up correctly. Skip"
elif [[ -n "${current}" && "${current}" != "${upstream}" && "${current}" != "${upstreampush}" ]]; then
@@ -45,9 +45,9 @@ elif [[ -n "${current}" && "${current}" != "${upstream}" && "${current}" != "${u
else
if [[ "${current}" == "${upstreampush}" ]]; then
echo "Upstream set to dangerous push URL. Update."
_run git remote rename upstream upstream-push || \
_run git remote remove upstream
currentpush=$( git remote get-url upstream-push 2>/dev/null )
_run git remote rename upstream upstream-push ||
_run git remote remove upstream
currentpush=$(git remote get-url upstream-push 2>/dev/null)
fi
_run git remote add upstream "${upstream}"
fi

View File

@@ -1,8 +1,8 @@
#!/bin/bash
if [[ $# -lt 3 || "$1" == "--help" || "$1" = "-h" ]]; then
name=$( basename $0 )
cat <<- USAGE
name=$(basename $0)
cat <<-USAGE
Usage: $name workbranch base/branch user/branch [user/branch [...]]
* workbranch will be created locally from base/branch
@@ -16,7 +16,7 @@ fi
work="$1"
shift
branches=( $( echo "${@}" | sed "s/:/\//" ) )
branches=($(echo "${@}" | sed "s/:/\//"))
base="${branches[0]}"
unset branches[0]
@@ -24,10 +24,10 @@ set -e
users=()
for b in "${branches[@]}"; do
users+=( $( echo $b | cut -d/ -f1 ) )
users+=($(echo $b | cut -d/ -f1))
done
users=( $( printf '%s\n' "${users[@]}" | sort -u ) )
users=($(printf '%s\n' "${users[@]}" | sort -u))
git fetch --multiple upstreams "${users[@]}"
git checkout -B "$work" --no-track "$base"
@@ -40,7 +40,7 @@ done
# Make sure the commits look right
git log --show-signature "$base..HEAD"
parts=( $( echo $base | sed "s/\// /" ) )
parts=($(echo $base | sed "s/\// /"))
repo="${parts[0]}"
b="${parts[1]}"
push=$repo
@@ -50,7 +50,7 @@ fi
if [[ "$repo" == "upstream" ]]; then
repo="upstreams"
fi
cat << PUSH
cat <<PUSH
-------------------------------------------------------------------
This script will not push. Verify everything is correct, then push

View File

@@ -1,8 +1,8 @@
#!/bin/bash
if [[ $# -ne 3 || "$1" == "--help" || "$1" = "-h" ]]; then
name=$( basename $0 )
cat <<- USAGE
name=$(basename $0)
cat <<-USAGE
Usage: $name workbranch base/branch version
* workbranch will be created locally from base/branch. If it exists,
@@ -16,7 +16,7 @@ fi
work="$1"
shift
base=$( echo "$1" | sed "s/:/\//" )
base=$(echo "$1" | sed "s/:/\//")
shift
version=$1
@@ -28,16 +28,16 @@ git fetch upstreams
git checkout -B "${work}" --no-track "${base}"
push=$( git rev-parse --abbrev-ref --symbolic-full-name '@{push}' \
2>/dev/null ) || true
push=$(git rev-parse --abbrev-ref --symbolic-full-name '@{push}' \
2>/dev/null) || true
if [[ "${push}" != "" ]]; then
echo "Warning: ${push} may already exist."
fi
build=$( find -name BuildInfo.cpp )
sed 's/\(^.*versionString =\).*$/\1 "'${version}'"/' ${build} > version.cpp && \
diff "${build}" version.cpp && exit 1 || \
mv -vi version.cpp ${build}
build=$(find -name BuildInfo.cpp)
sed 's/\(^.*versionString =\).*$/\1 "'${version}'"/' ${build} >version.cpp &&
diff "${build}" version.cpp && exit 1 ||
mv -vi version.cpp ${build}
git diff
@@ -47,7 +47,7 @@ git commit -S -m "Set version to ${version}"
git log --oneline --first-parent ${base}^..
cat << PUSH
cat <<PUSH
-------------------------------------------------------------------
This script will not push. Verify everything is correct, then push

View File

@@ -0,0 +1,13 @@
# Python dependencies for XRP Ledger code generation scripts
#
# These packages are required to run the code generation scripts that
# parse macro files and generate C++ wrapper classes.
# C preprocessor for Python - used to preprocess macro files
pcpp>=1.30
# Parser combinator library - used to parse the macro DSL
pyparsing>=3.0.0
# Template engine - used to generate C++ code from templates
Mako>=1.2.2

View File

@@ -1,13 +1,105 @@
# Python dependencies for XRP Ledger code generation scripts
#
# These packages are required to run the code generation scripts that
# parse macro files and generate C++ wrapper classes.
# C preprocessor for Python - used to preprocess macro files
pcpp>=1.30
# Parser combinator library - used to parse the macro DSL
pyparsing>=3.0.0
# Template engine - used to generate C++ code from templates
Mako>=1.2.2
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in --generate-hashes --output-file requirements.txt
mako==1.3.12 \
--hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \
--hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a
# via -r requirements.in
markupsafe==3.0.3 \
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
--hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \
--hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \
--hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \
--hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \
--hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \
--hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \
--hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \
--hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \
--hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \
--hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \
--hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \
--hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \
--hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \
--hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \
--hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \
--hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \
--hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \
--hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \
--hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \
--hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \
--hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \
--hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \
--hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \
--hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \
--hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \
--hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \
--hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \
--hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \
--hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \
--hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \
--hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \
--hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \
--hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \
--hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \
--hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \
--hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \
--hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \
--hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \
--hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \
--hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
--hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \
--hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \
--hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \
--hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \
--hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \
--hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \
--hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \
--hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \
--hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \
--hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \
--hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \
--hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \
--hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \
--hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \
--hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \
--hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \
--hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \
--hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \
--hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \
--hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \
--hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \
--hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \
--hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \
--hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \
--hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \
--hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \
--hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \
--hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \
--hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \
--hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \
--hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \
--hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \
--hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \
--hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \
--hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \
--hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \
--hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \
--hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \
--hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \
--hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \
--hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \
--hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \
--hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \
--hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \
--hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \
--hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \
--hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \
--hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50
# via mako
pcpp==1.30 \
--hash=sha256:05fe08292b6da57f385001c891a87f40d6aa7f46787b03e8ba326d20a3297c6e \
--hash=sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1
# via -r requirements.in
pyparsing==3.3.2 \
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
# via -r requirements.in

View File

@@ -256,6 +256,7 @@ words:
- sfields
- shamap
- shamapitem
- shfmt
- shlibs
- sidechain
- SIGGOOD

View File

@@ -9,9 +9,12 @@ set -eo pipefail
cpp_files_dir="${1:?usage: $0 <cpp_files_dir>}"
case "$(uname -m)" in
x86_64) loader=/lib64/ld-linux-x86-64.so.2 ;;
x86_64) loader=/lib64/ld-linux-x86-64.so.2 ;;
aarch64) loader=/lib/ld-linux-aarch64.so.1 ;;
*) echo "Unsupported arch: $(uname -m)" >&2; exit 1 ;;
*)
echo "Unsupported arch: $(uname -m)" >&2
exit 1
;;
esac
declare -A sanitize=(
@@ -35,8 +38,11 @@ for compiler in g++ clang++; do
echo "=== Run ${name}-${compiler} ==="
output=$("$bin" 2>&1) || true
echo "$output"
echo "$output" | grep -q "${expect[$name]}" \
|| { echo "expected '${expect[$name]}' from $bin"; exit 1; }
echo "$output" | grep -q "${expect[$name]}" ||
{
echo "expected '${expect[$name]}' from $bin"
exit 1
}
rm -f "$bin"
done
done

View File

@@ -1,5 +1,7 @@
#pragma once
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -43,6 +45,14 @@ sharesToAssetsDeposit(
/** Controls whether to truncate shares instead of rounding. */
enum class TruncateShares : bool { No = false, Yes = true };
/** Controls whether the withdraw conversion helpers
(assetsToSharesWithdraw and sharesToAssetsWithdraw) subtract
sfLossUnrealized from sfAssetsTotal before computing the exchange rate.
The default (No) applies the standard discounted rate; Yes is used when
the redeemer is the sole remaining shareholder.
*/
enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
/** From the perspective of a vault, return the number of shares to demand from
the depositor when they ask to withdraw a fixed amount of assets. Since
shares are MPT this number is integral, and it will be rounded to nearest
@@ -52,6 +62,8 @@ enum class TruncateShares : bool { No = false, Yes = true };
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param assets The amount of assets to convert.
@param truncate Whether to truncate instead of rounding.
@param waive Whether to waive the unrealized-loss discount when computing
the exchange rate.
@return The number of shares, or nullopt on error.
*/
@@ -60,7 +72,8 @@ assetsToSharesWithdraw(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& assets,
TruncateShares truncate = TruncateShares::No);
TruncateShares truncate = TruncateShares::No,
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
/** From the perspective of a vault, return the number of assets to give the
depositor when they redeem a fixed amount of shares. Note, since shares are
@@ -69,6 +82,8 @@ assetsToSharesWithdraw(
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param shares The amount of shares to convert.
@param waive Whether to waive (i.e. not subtract) the vault's unrealized
loss when computing the exchange rate.
@return The number of assets, or nullopt on error.
*/
@@ -76,6 +91,22 @@ assetsToSharesWithdraw(
sharesToAssetsWithdraw(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& shares);
STAmount const& shares,
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
/** Returns true iff `account` holds all of the vault's outstanding shares —
i.e. is the sole remaining shareholder. Returns false if the account
holds no shares or fewer than the total outstanding.
@param view The ledger view.
@param account The candidate sole shareholder.
@param issuance The MPTokenIssuance SLE for the vault's shares; provides
both the share MPTID and the outstanding-amount total.
*/
[[nodiscard]] bool
isSoleShareholder(
ReadView const& view,
AccountID const& account,
std::shared_ptr<SLE const> const& issuance);
} // namespace xrpl

View File

@@ -36,12 +36,35 @@ SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--src-dir) need_arg "$@"; SRC_DIR="$2"; shift 2 ;;
--build-dir) need_arg "$@"; BUILD_DIR="$2"; shift 2 ;;
--pkg-version) need_arg "$@"; PKG_VERSION="$2"; shift 2 ;;
--pkg-release) need_arg "$@"; PKG_RELEASE="$2"; shift 2 ;;
--source-date-epoch) need_arg "$@"; SOURCE_DATE_EPOCH="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--src-dir)
need_arg "$@"
SRC_DIR="$2"
shift 2
;;
--build-dir)
need_arg "$@"
BUILD_DIR="$2"
shift 2
;;
--pkg-version)
need_arg "$@"
PKG_VERSION="$2"
shift 2
;;
--pkg-release)
need_arg "$@"
PKG_RELEASE="$2"
shift 2
;;
--source-date-epoch)
need_arg "$@"
SOURCE_DATE_EPOCH="$2"
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
@@ -109,20 +132,20 @@ stage_common() {
local dest="$1"
mkdir -p "${dest}"
cp "${BUILD_DIR}/xrpld" "${dest}/xrpld"
cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg"
cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt"
cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md"
cp "${SRC_DIR}/README.md" "${dest}/README.md"
cp "${BUILD_DIR}/xrpld" "${dest}/xrpld"
cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg"
cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt"
cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md"
cp "${SRC_DIR}/README.md" "${dest}/README.md"
cp "${SHARED}/xrpld.service" "${dest}/xrpld.service"
cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers"
cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles"
cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate"
cp "${SHARED}/update-xrpld" "${dest}/update-xrpld"
cp "${SHARED}/update-xrpld.service" "${dest}/update-xrpld.service"
cp "${SHARED}/update-xrpld.timer" "${dest}/update-xrpld.timer"
cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset"
cp "${SHARED}/xrpld.service" "${dest}/xrpld.service"
cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers"
cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles"
cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate"
cp "${SHARED}/update-xrpld" "${dest}/update-xrpld"
cp "${SHARED}/update-xrpld.service" "${dest}/update-xrpld.service"
cp "${SHARED}/update-xrpld.timer" "${dest}/update-xrpld.timer"
cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset"
}
build_rpm() {
@@ -159,12 +182,12 @@ build_deb() {
cp -r "${DEBIAN_DIR}" "${staging}/debian"
# Debhelper auto-discovers these only from debian/.
cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service"
cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers"
cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles"
cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate"
cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service"
cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers"
cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles"
cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate"
cp "${staging}/update-xrpld.service" "${staging}/debian/xrpld.update-xrpld.service"
cp "${staging}/update-xrpld.timer" "${staging}/debian/xrpld.update-xrpld.timer"
cp "${staging}/update-xrpld.timer" "${staging}/debian/xrpld.update-xrpld.timer"
# Debian '~' marks a pre-release; 3.2.0~b1 sorts before 3.2.0.
local deb_full_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}-${PKG_RELEASE}"
@@ -175,12 +198,12 @@ build_deb() {
# b<N>, rc<N> -> unstable (pre-release)
local deb_distribution
case "${VER_SUFFIX}" in
"") deb_distribution="stable" ;;
b0) deb_distribution="develop" ;;
*) deb_distribution="unstable" ;;
"") deb_distribution="stable" ;;
b0) deb_distribution="develop" ;;
*) deb_distribution="unstable" ;;
esac
cat > "${staging}/debian/changelog" <<EOF
cat >"${staging}/debian/changelog" <<EOF
xrpld (${deb_full_version}) ${deb_distribution}; urgency=medium
* Release ${VERSION}.
@@ -190,7 +213,7 @@ EOF
chmod +x "${staging}/debian/rules"
set -x
( cd "${staging}" && dpkg-buildpackage -b --no-sign -d )
(cd "${staging}" && dpkg-buildpackage -b --no-sign -d)
}
"build_${pkg_type}"

View File

@@ -22,7 +22,7 @@ PATH=/usr/sbin:/usr/bin:/sbin:/bin
PKG_NAME=${PKG_NAME:-xrpld}
log() {
# If running under systemd/journald, let it handle timestamps.
# If running under systemd/journald, let it handle timestamps.
if [[ -n "${JOURNAL_STREAM:-}" ]]; then
printf '%s\n' "$*"
else
@@ -33,7 +33,7 @@ log() {
require_root() {
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
log "RESULT: failed reason=not-root"
exit 1
exit 1
fi
}

View File

@@ -2,11 +2,16 @@
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <cstdint>
#include <memory>
#include <optional>
@@ -70,7 +75,8 @@ assetsToSharesWithdraw(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& assets,
TruncateShares truncate)
TruncateShares truncate,
WaiveUnrealizedLoss waive)
{
XRPL_ASSERT(!assets.negative(), "xrpl::assetsToSharesWithdraw : non-negative assets");
XRPL_ASSERT(
@@ -80,7 +86,8 @@ assetsToSharesWithdraw(
return std::nullopt; // LCOV_EXCL_LINE
Number assetTotal = vault->at(sfAssetsTotal);
assetTotal -= vault->at(sfLossUnrealized);
if (waive == WaiveUnrealizedLoss::No)
assetTotal -= vault->at(sfLossUnrealized);
STAmount shares{vault->at(sfShareMPTID)};
if (assetTotal == 0)
return shares;
@@ -96,7 +103,8 @@ assetsToSharesWithdraw(
sharesToAssetsWithdraw(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& shares)
STAmount const& shares,
WaiveUnrealizedLoss waive)
{
XRPL_ASSERT(!shares.negative(), "xrpl::sharesToAssetsWithdraw : non-negative shares");
XRPL_ASSERT(
@@ -106,7 +114,8 @@ sharesToAssetsWithdraw(
return std::nullopt; // LCOV_EXCL_LINE
Number assetTotal = vault->at(sfAssetsTotal);
assetTotal -= vault->at(sfLossUnrealized);
if (waive == WaiveUnrealizedLoss::No)
assetTotal -= vault->at(sfLossUnrealized);
STAmount assets{vault->at(sfAsset)};
if (assetTotal == 0)
return assets;
@@ -115,4 +124,24 @@ sharesToAssetsWithdraw(
return assets;
}
[[nodiscard]] bool
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance)
{
XRPL_ASSERT(
issuance && issuance->getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isSoleShareholder : valid issuance SLE");
std::uint64_t const outstanding = issuance->at(sfOutstandingAmount);
if (outstanding == 0)
return false;
auto const shareMPTID =
makeMptID(issuance->getFieldU32(sfSequence), issuance->getAccountID(sfIssuer));
auto const sleToken = view.read(keylet::mptoken(shareMPTID, account));
if (!sleToken)
return false; // LCOV_EXCL_LINE
return sleToken->getFieldU64(sfMPTAmount) == outstanding;
}
} // namespace xrpl

View File

@@ -4,12 +4,14 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
@@ -26,6 +28,18 @@
namespace xrpl {
static WaiveUnrealizedLoss
shouldWaiveWithdrawal(ReadView const& view, AccountID const& account, SLE::const_ref issuance)
{
XRPL_ASSERT(
issuance && issuance->getType() == ltMPTOKEN_ISSUANCE,
"xrpl::shouldWaiveWithdrawal : valid issuance sle");
return view.rules().enabled(fixCleanup3_2_0) && isSoleShareholder(view, account, issuance)
? WaiveUnrealizedLoss::Yes
: WaiveUnrealizedLoss::No;
}
NotTEC
VaultWithdraw::preflight(PreflightContext const& ctx)
{
@@ -102,9 +116,14 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
// LCOV_EXCL_STOP
}
// When the user is the sole shareholder they own both the available and future value.
// 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(ctx.view, account, sleIssuance);
try
{
auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, amount);
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, amount, waiveUnrealizedLoss);
if (!maybeAssets)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -182,13 +201,19 @@ VaultWithdraw::doApply()
MPTIssue const share{mptIssuanceID};
STAmount sharesRedeemed = {share};
STAmount assetsWithdrawn;
// When the user is the sole shareholder they own both the available and future value.
// 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);
try
{
if (amount.asset() == vaultAsset)
{
// Fixed assets, variable shares.
{
auto const maybeShares = assetsToSharesWithdraw(vault, sleIssuance, amount);
auto const maybeShares = assetsToSharesWithdraw(
vault, sleIssuance, amount, TruncateShares::No, waiveUnrealizedLoss);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesRedeemed = *maybeShares;
@@ -196,7 +221,8 @@ VaultWithdraw::doApply()
if (sharesRedeemed == beast::kZero)
return tecPRECISION_LOSS;
auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed);
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed, waiveUnrealizedLoss);
if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE
assetsWithdrawn = *maybeAssets;
@@ -205,7 +231,8 @@ VaultWithdraw::doApply()
{
// Fixed shares, variable assets.
sharesRedeemed = amount;
auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed);
auto const maybeAssets =
sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed, waiveUnrealizedLoss);
if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE
assetsWithdrawn = *maybeAssets;
@@ -238,22 +265,64 @@ VaultWithdraw::doApply()
auto assetsAvailable = vault->at(sfAssetsAvailable);
auto assetsTotal = vault->at(sfAssetsTotal);
[[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
auto const lossUnrealized = vault->at(sfLossUnrealized);
XRPL_ASSERT(
lossUnrealized <= (assetsTotal - assetsAvailable),
"xrpl::VaultWithdraw::doApply : loss and assets do balance");
// The vault must have enough assets on hand. The vault may hold assets
// that it has already pledged. That is why we look at AssetAvailable
// instead of the pseudo-account balance.
// The vault must have enough assets on hand.
if (*assetsAvailable < assetsWithdrawn)
{
JLOG(j_.debug()) << "VaultWithdraw: vault doesn't hold enough assets";
return tecINSUFFICIENT_FUNDS;
}
assetsTotal -= assetsWithdrawn;
assetsAvailable -= assetsWithdrawn;
// 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.
//
// 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.
bool const isFinalWithdrawal =
sharesRedeemed == STAmount{share, sleIssuance->at(sfOutstandingAmount)};
if (view().rules().enabled(fixCleanup3_2_0) && isFinalWithdrawal)
{
// Unreachable: a final withdrawal with lossUnrealized > 0 has
// assetsWithdrawn == assetsTotal > assetsAvailable, which the
// insufficient-funds guard above already rejected.
if (*lossUnrealized != beast::kZero)
{
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::VaultWithdraw::doApply : final withdrawal with non-zero unrealized loss");
JLOG(j_.fatal())
<< "VaultWithdraw: " //
"Cannot burn all outstanding shares while unrealized loss is non-zero";
return tefINTERNAL;
// LCOV_EXCL_END
}
STAmount const allAvailable{vaultAsset, *assetsAvailable};
if (assetsWithdrawn != allAvailable)
{
JLOG(j_.error()) //
<< "VaultWithdraw: final withdrawal share-value mismatch;"
<< " computed=" << assetsWithdrawn.getText()
<< " assetsAvailable=" << allAvailable.getText();
}
assetsWithdrawn = allAvailable;
// Do not let dust accumulate in the Vault.
assetsTotal = 0;
assetsAvailable = 0;
}
else
{
assetsTotal -= assetsWithdrawn;
assetsAvailable -= assetsWithdrawn;
}
view().update(vault);
auto const& vaultAccount = vault->at(sfAccount);

View File

@@ -6457,6 +6457,604 @@ class Vault_test : public beast::unit_test::Suite
runTest(amendments);
}
// -----------------------------------------------------------------------
// Helpers and tests: sole-shareholder / stuck-depositor (XLS-0065 +
// fixCleanup3_2_0). The vault-level withdraw behavior is tested here;
// the loan-protocol setup is incidental.
// -----------------------------------------------------------------------
FeatureBitset const all_{test::jtx::testableAmendments()};
std::string const iouCurrency_{"IOU"};
// design doc:
// AssetsAvailable ≈ 3,333.50
// AssetsTotal ≈ 6,666.50 (3,333.50 cash + 3,333 receivable)
// LossUnrealized = 3,333
// OutstandingShares = sharesLender (5e9 at IOU scale 1e6)
struct StuckDepositorFixture
{
test::jtx::Account issuer{"issuer"};
test::jtx::Account lender{"lender"};
test::jtx::Account bob{"bob"};
test::jtx::Account borrower{"borrower"};
std::optional<PrettyAsset> asset;
std::optional<Keylet> vaultKeylet;
uint256 brokerID;
std::optional<Keylet> loanKeylet;
MPTID shareAsset;
std::uint64_t sharesLender = 0;
};
static constexpr std::int64_t kStuckFunding = 1'000'000;
static constexpr std::int64_t kStuckDepositorIOU = 1'000'000;
static constexpr std::int64_t kStuckBorrowerIOU = 100'000;
static constexpr std::int64_t kStuckDeposit = 5'000;
static constexpr std::int64_t kStuckPrincipal = 3'333;
static constexpr std::uint32_t kStuckPayInterval = 600;
static constexpr std::uint32_t kStuckPayTotal = 2;
[[nodiscard]] StuckDepositorFixture
setupStuckDepositor(test::jtx::Env& env)
{
using namespace test::jtx;
StuckDepositorFixture f;
f.asset = f.issuer[iouCurrency_];
env.fund(XRP(kStuckFunding), f.issuer, f.lender, f.bob, f.borrower);
env.close();
env(trust(f.lender, (*f.asset)(10'000'000)));
env(trust(f.bob, (*f.asset)(10'000'000)));
env(trust(f.borrower, (*f.asset)(10'000'000)));
env.close();
env(pay(f.issuer, f.lender, (*f.asset)(kStuckDepositorIOU)));
env(pay(f.issuer, f.bob, (*f.asset)(kStuckDepositorIOU)));
env(pay(f.issuer, f.borrower, (*f.asset)(kStuckBorrowerIOU)));
env.close();
// Vault: Lender creates and seeds it; Bob matches the deposit for a
// clean 50/50 split.
Vault const v{env};
auto [createTx, vaultKeylet] = v.create({.owner = f.lender, .asset = *f.asset});
env(createTx);
env.close();
if (!BEAST_EXPECT(env.le(vaultKeylet)))
return f;
f.vaultKeylet = vaultKeylet;
env(v.deposit({
.depositor = f.lender,
.id = vaultKeylet.key,
.amount = (*f.asset)(kStuckDeposit),
}),
Ter(tesSUCCESS));
env(v.deposit({
.depositor = f.bob,
.id = vaultKeylet.key,
.amount = (*f.asset)(kStuckDeposit),
}),
Ter(tesSUCCESS));
env.close();
// Loan broker: no cover, no management fee, debt cap 10x principal.
f.brokerID = keylet::loanbroker(f.lender.id(), env.seq(f.lender)).key;
{
using namespace loanBroker;
env(set(f.lender, vaultKeylet.key),
kDebtMaximum((*f.asset)(kStuckPrincipal * 10).value()));
env.close();
}
// Loan: 3,333 USD principal, impaired immediately.
auto const sleBroker = env.le(keylet::loanbroker(f.brokerID));
if (!BEAST_EXPECT(sleBroker))
return f;
f.loanKeylet = keylet::loan(f.brokerID, sleBroker->at(sfLoanSequence));
{
using namespace loan;
env(set(f.borrower, f.brokerID, kStuckPrincipal),
Sig(sfCounterpartySignature, f.lender),
kPaymentTotal(kStuckPayTotal),
kPaymentInterval(kStuckPayInterval),
Fee(env.current()->fees().base * 2),
Ter(tesSUCCESS));
env.close();
env(manage(f.lender, f.loanKeylet->key, tfLoanImpair), Ter(tesSUCCESS));
env.close();
}
auto const vaultSle = env.le(vaultKeylet);
if (!BEAST_EXPECT(vaultSle))
return f;
BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == (*f.asset)(kStuckPrincipal).value());
f.shareAsset = vaultSle->at(sfShareMPTID);
auto const tokenBob = env.le(keylet::mptoken(f.shareAsset, f.bob.id()));
if (!BEAST_EXPECT(tokenBob))
return f;
std::uint64_t const sharesBob = tokenBob->getFieldU64(sfMPTAmount);
// Bob (non-sole) exits at the discounted rate. Always succeeds.
STAmount const bobShareAmt{MPTIssue{f.shareAsset}, Number(sharesBob)};
env(v.withdraw({
.depositor = f.bob,
.id = vaultKeylet.key,
.amount = bobShareAmt,
}),
Ter(tesSUCCESS));
env.close();
auto const tokenLender = env.le(keylet::mptoken(f.shareAsset, f.lender.id()));
if (!BEAST_EXPECT(tokenLender))
return f;
f.sharesLender = tokenLender->getFieldU64(sfMPTAmount);
auto const sleIssuance = env.le(keylet::mptIssuance(f.shareAsset));
if (!BEAST_EXPECT(sleIssuance))
return f;
BEAST_EXPECT(sleIssuance->getFieldU64(sfOutstandingAmount) == f.sharesLender);
auto const vaultAfterBob = env.le(vaultKeylet);
if (!BEAST_EXPECT(vaultAfterBob))
return f;
// After Bob's exit: loss is unchanged (3,333 receivable), and the
// gap between assetsTotal and assetsAvailable equals exactly that
// receivable.
BEAST_EXPECT(vaultAfterBob->at(sfLossUnrealized) == (*f.asset)(kStuckPrincipal).value());
BEAST_EXPECT(
vaultAfterBob->at(sfAssetsTotal) - vaultAfterBob->at(sfAssetsAvailable) ==
vaultAfterBob->at(sfLossUnrealized));
return f;
}
// Reproduces the worked example from the XLS-0065 design doc. The sole
// remaining shareholder asks (via fixed-asset input) for the vault's
// entire AssetsAvailable. Pre-fix this fails with the zero-sized-vault
// invariant violation. Post-fix the full-price exchange rate burns
// only a portion of the shares, the depositor receives all of
// AssetsAvailable, and the residual shares remain backed by the
// impaired-loan receivable.
void
testWithdrawSoleShareholderFixedAssetExit(FeatureBitset features)
{
using namespace test::jtx;
bool const withFix = features[fixCleanup3_2_0];
testcase(
std::string{"Vault withdraw: sole shareholder exits via "
"fixed-asset amount with impaired loan"} +
(withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
Env env(*this, features);
auto const f = setupStuckDepositor(env);
if (!f.vaultKeylet || !f.asset || f.sharesLender == 0)
{
BEAST_EXPECT(false);
return;
}
Keylet const& vaultKey = *f.vaultKeylet;
PrettyAsset const& asset = *f.asset;
auto const vaultBefore = env.le(vaultKey);
if (!BEAST_EXPECT(vaultBefore))
return;
Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
Number const totalBefore = vaultBefore->at(sfAssetsTotal);
Number const lossBefore = vaultBefore->at(sfLossUnrealized);
STAmount const lenderBalanceBefore = env.balance(f.lender, asset);
// The requested amount differs between feature regimes because
// the two regimes are testing different behaviors:
//
// - Pre-fix: request the full AssetsAvailable (3,333.50). Under
// the discounted formula this would burn every outstanding
// share, hitting the zero-sized-vault invariant. The
// transaction is rejected with tecINVARIANT_FAILED — the
// stuck-depositor bug.
//
// - Post-fix: request a strictly smaller amount (1,000 USD).
// The full-price formula burns only ~30% of the outstanding
// shares; the vault retains the rest, backed by the impaired
// receivable. Requesting *exactly* AssetsAvailable post-fix
// would currently fail with tecINSUFFICIENT_FUNDS due to the
// round-to-nearest used by assetsToSharesWithdraw (the
// recomputed payout can overshoot the request by a few ULPs).
// The "force payout to AssetsAvailable" branch in doApply
// only triggers when every share is burned, which is covered
// by the loan-repayment test.
STAmount const requestAssets =
withFix ? asset(1000).value() : STAmount{asset.raw(), availableBefore};
Vault const v{env};
env(v.withdraw({
.depositor = f.lender,
.id = vaultKey.key,
.amount = requestAssets,
}),
Ter(withFix ? TER{tesSUCCESS} : TER{tecINVARIANT_FAILED}));
env.close();
auto const vaultAfter = env.le(vaultKey);
if (!BEAST_EXPECT(vaultAfter))
return;
auto const issuanceAfter = env.le(keylet::mptIssuance(f.shareAsset));
if (!BEAST_EXPECT(issuanceAfter))
return;
std::uint64_t const sharesAfter = issuanceAfter->getFieldU64(sfOutstandingAmount);
Number const availableAfter = vaultAfter->at(sfAssetsAvailable);
Number const totalAfter = vaultAfter->at(sfAssetsTotal);
Number const lossAfter = vaultAfter->at(sfLossUnrealized);
if (!withFix)
{
// Pre-fix: rejected — vault state unchanged.
BEAST_EXPECT(sharesAfter == f.sharesLender);
BEAST_EXPECT(availableAfter == availableBefore);
BEAST_EXPECT(totalAfter == totalBefore);
BEAST_EXPECT(lossAfter == lossBefore);
return;
}
// Post-fix exact-value derivation (fixture: sharesLender=5e9,
// totalBefore=6666.5, request=1000):
// sharesRedeemed = round(sharesLender * request / totalBefore)
// = round(750,018,750.469) = 750,018,750
// received = totalBefore * sharesRedeemed / sharesLender
// = 999.999999375 (slightly under 1,000 due to
// integer-share rounding)
constexpr std::uint64_t kExpectedSharesRedeemed = 750'018'750;
Number const expectedReceived =
totalBefore * Number(kExpectedSharesRedeemed) / Number(f.sharesLender);
BEAST_EXPECT(sharesAfter == f.sharesLender - kExpectedSharesRedeemed);
// LossUnrealized is unchanged: the loan-protocol side is untouched.
BEAST_EXPECT(lossAfter == lossBefore);
// The entire (total - available) gap is the impaired receivable,
// i.e. equal to lossUnrealized.
BEAST_EXPECT(totalAfter - availableAfter == lossAfter);
STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
Number const received{lenderBalanceAfter - lenderBalanceBefore};
BEAST_EXPECT(received == expectedReceived);
// Conservation: assets removed from the vault equal what the
// depositor received.
BEAST_EXPECT(totalBefore - totalAfter == received);
BEAST_EXPECT(availableBefore - availableAfter == received);
}
// Sole shareholder attempts to burn ALL outstanding shares via
// fixed-shares input while the vault still holds an impaired
// receivable. Pre-fix this fails with the zero-sized-vault invariant
// violation. Post-fix the full-price rate causes assetsWithdrawn to
// equal assetsTotal, which exceeds assetsAvailable, so the transaction
// is rejected with tecINSUFFICIENT_FUNDS.
void
testWithdrawSoleShareholderFullSharesRejected(FeatureBitset features)
{
using namespace test::jtx;
bool const withFix = features[fixCleanup3_2_0];
testcase(
std::string{"Vault withdraw: sole shareholder full-shares "
"burn is rejected while loss outstanding"} +
(withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
Env env(*this, features);
auto const f = setupStuckDepositor(env);
if (!f.vaultKeylet || f.sharesLender == 0)
{
BEAST_EXPECT(false);
return;
}
Keylet const& vaultKey = *f.vaultKeylet;
auto const vaultBefore = env.le(vaultKey);
if (!BEAST_EXPECT(vaultBefore))
return;
Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
Number const totalBefore = vaultBefore->at(sfAssetsTotal);
Number const lossBefore = vaultBefore->at(sfLossUnrealized);
// Fixed-shares input: ask for ALL outstanding shares.
STAmount const shareAmt{MPTIssue{f.shareAsset}, Number(f.sharesLender)};
Vault const v{env};
env(v.withdraw({
.depositor = f.lender,
.id = vaultKey.key,
.amount = shareAmt,
}),
Ter(withFix ? TER{tecINSUFFICIENT_FUNDS} : TER{tecINVARIANT_FAILED}));
env.close();
// Either way the transaction was rejected; vault state unchanged.
auto const vaultAfter = env.le(vaultKey);
if (!BEAST_EXPECT(vaultAfter))
return;
auto const issuanceAfter = env.le(keylet::mptIssuance(f.shareAsset));
if (!BEAST_EXPECT(issuanceAfter))
return;
BEAST_EXPECT(issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender);
BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == availableBefore);
BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
}
// Post-fix end-to-end resolution: after the sole-shareholder partial
// exit, the loan is repaid in full. With unrealized loss cleared and
// all assets back as cash, the depositor can burn all remaining
// shares and fully exit the vault. The final withdrawal hits the
// "force payout to assetsAvailable" branch in doApply.
void
testWithdrawSoleShareholderLoanRepaymentExit()
{
using namespace test::jtx;
using namespace loan;
testcase(
"Vault withdraw: sole shareholder fully exits after impaired "
"loan is repaid (fixCleanup3_2_0)");
Env env(*this, all_ | fixCleanup3_2_0);
auto const f = setupStuckDepositor(env);
if (!f.vaultKeylet || !f.asset || !f.loanKeylet || f.sharesLender == 0)
{
BEAST_EXPECT(false);
return;
}
Keylet const& vaultKey = *f.vaultKeylet;
Keylet const& loanKey = *f.loanKeylet;
PrettyAsset const& asset = *f.asset;
Vault const v{env};
// Sole-shareholder partial exit (see comment in
// testWithdrawSoleShareholderFixedAssetExit for why we request
// less than full AssetsAvailable).
{
STAmount const requestAssets = asset(1000).value();
env(v.withdraw({
.depositor = f.lender,
.id = vaultKey.key,
.amount = requestAssets,
}),
Ter(tesSUCCESS));
env.close();
}
// Confirm the "dormant-but-alive" state from the design doc. The
// partial exit burned exactly 750,018,750 shares (see derivation
// in testWithdrawSoleShareholderFixedAssetExit).
auto const tokenAfterExit = env.le(keylet::mptoken(f.shareAsset, f.lender.id()));
if (!BEAST_EXPECT(tokenAfterExit))
return;
std::uint64_t const retainedShares = tokenAfterExit->getFieldU64(sfMPTAmount);
BEAST_EXPECT(retainedShares == f.sharesLender - 750'018'750);
// Borrower repays the loan in full (pays more than the outstanding
// total; the loan transactor caps the receivable).
env(pay(f.borrower, loanKey.key, asset(kStuckPrincipal * 2)), Ter(tesSUCCESS));
env.close();
auto const vaultAfterRepay = env.le(vaultKey);
if (!BEAST_EXPECT(vaultAfterRepay))
return;
// Repayment converts the 3,333 receivable back to cash; assetsTotal
// is unchanged but assetsAvailable jumps by exactly the same amount,
// and lossUnrealized clears to zero.
BEAST_EXPECT(vaultAfterRepay->at(sfLossUnrealized) == beast::kZero);
BEAST_EXPECT(vaultAfterRepay->at(sfAssetsAvailable) == vaultAfterRepay->at(sfAssetsTotal));
STAmount const lenderBalanceBeforeFinal = env.balance(f.lender, asset);
Number const availableBeforeFinal = vaultAfterRepay->at(sfAssetsAvailable);
// Burn all remaining shares — the clean-state preconditions of
// the "final withdrawal" guard are now satisfied.
STAmount const allShares{MPTIssue{f.shareAsset}, Number(retainedShares)};
env(v.withdraw({
.depositor = f.lender,
.id = vaultKey.key,
.amount = allShares,
}),
Ter(tesSUCCESS));
env.close();
auto const vaultFinal = env.le(vaultKey);
if (!BEAST_EXPECT(vaultFinal))
return;
auto const issuanceFinal = env.le(keylet::mptIssuance(f.shareAsset));
if (!BEAST_EXPECT(issuanceFinal))
return;
// Zero-sized vault invariant satisfied: 0 shares, 0 assets.
BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
BEAST_EXPECT(vaultFinal->at(sfAssetsTotal) == beast::kZero);
BEAST_EXPECT(vaultFinal->at(sfAssetsAvailable) == beast::kZero);
BEAST_EXPECT(vaultFinal->at(sfLossUnrealized) == beast::kZero);
// The final payout equals exactly the AssetsAvailable that
// existed before the call (the "force payout" branch).
STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
Number const finalReceived{lenderBalanceAfter - lenderBalanceBeforeFinal};
BEAST_EXPECT(finalReceived == availableBeforeFinal);
}
// Clean-state regression: with no impaired loan, a sole shareholder
// burning all their shares fully empties the vault under both the
// pre-fix and post-fix code paths. Confirms the new logic doesn't
// break the existing happy-path close-out.
void
testWithdrawSoleShareholderCleanVaultUnaffected(FeatureBitset features)
{
using namespace test::jtx;
bool const withFix = features[fixCleanup3_2_0];
testcase(
std::string{"Vault withdraw: sole shareholder clean-state "
"close-out unchanged"} +
(withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
env.fund(XRP(kStuckFunding), issuer, lender);
env.close();
PrettyAsset const asset = issuer[iouCurrency_];
env(trust(lender, asset(10'000'000)));
env.close();
env(pay(issuer, lender, asset(kStuckDepositorIOU)));
env.close();
// Sole shareholder of a clean vault — no loan broker needed.
Vault const v{env};
auto [createTx, vaultKeylet] = v.create({.owner = lender, .asset = asset});
env(createTx);
env.close();
env(v.deposit({
.depositor = lender,
.id = vaultKeylet.key,
.amount = asset(kStuckDeposit),
}),
Ter(tesSUCCESS));
env.close();
auto const vaultBefore = env.le(vaultKeylet);
if (!BEAST_EXPECT(vaultBefore))
return;
auto const shareAsset = vaultBefore->at(sfShareMPTID);
auto const tokenLender = env.le(keylet::mptoken(shareAsset, lender.id()));
if (!BEAST_EXPECT(tokenLender))
return;
std::uint64_t const sharesLender = tokenLender->getFieldU64(sfMPTAmount);
// Sole shareholder, no loans, no loss. Burn everything.
STAmount const allShares{MPTIssue{shareAsset}, Number(sharesLender)};
env(v.withdraw({
.depositor = lender,
.id = vaultKeylet.key,
.amount = allShares,
}),
Ter(tesSUCCESS));
env.close();
auto const vaultFinal = env.le(vaultKeylet);
if (!BEAST_EXPECT(vaultFinal))
return;
auto const issuanceFinal = env.le(keylet::mptIssuance(shareAsset));
if (!BEAST_EXPECT(issuanceFinal))
return;
BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
BEAST_EXPECT(vaultFinal->at(sfAssetsTotal) == beast::kZero);
BEAST_EXPECT(vaultFinal->at(sfAssetsAvailable) == beast::kZero);
BEAST_EXPECT(vaultFinal->at(sfLossUnrealized) == beast::kZero);
// (Pre-fix path takes the regular code path; post-fix path enters
// the new final-withdrawal guard, which forces payout to exactly
// assetsAvailable. Either way the result is identical for a clean
// vault.)
(void)withFix;
}
// Sole shareholder in an impaired vault redeems a *partial* count of
// shares via fixed-shares input. Pre-fix the discounted formula is
// used; post-fix the full-price formula is used (waiveUnrealizedLoss
// = Yes). The relative payout therefore differs, and post-fix the
// depositor recovers proportionally more of the residual cash for
// the shares burned. In both cases the vault is left in a valid
// (non-empty) state.
void
testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice()
{
using namespace test::jtx;
testcase(
"Vault withdraw: sole-shareholder partial fixed-shares uses "
"full-price rate (fixCleanup3_2_0)");
Env env(*this, all_ | fixCleanup3_2_0);
auto const f = setupStuckDepositor(env);
if (!f.vaultKeylet || !f.asset || f.sharesLender == 0)
{
BEAST_EXPECT(false);
return;
}
Keylet const& vaultKey = *f.vaultKeylet;
PrettyAsset const& asset = *f.asset;
auto const vaultBefore = env.le(vaultKey);
if (!BEAST_EXPECT(vaultBefore))
return;
Number const totalBefore = vaultBefore->at(sfAssetsTotal);
Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
Number const lossBefore = vaultBefore->at(sfLossUnrealized);
// Burn exactly half of the outstanding shares.
std::uint64_t const halfShares = f.sharesLender / 2;
STAmount const halfAmt{MPTIssue{f.shareAsset}, Number(halfShares)};
STAmount const lenderBalanceBefore = env.balance(f.lender, asset);
Vault const v{env};
env(v.withdraw({
.depositor = f.lender,
.id = vaultKey.key,
.amount = halfAmt,
}),
Ter(tesSUCCESS));
env.close();
// Expected payout under the full-price formula:
// assets = totalBefore * halfShares / sharesLender
// which (with halfShares == sharesLender/2) is roughly
// totalBefore / 2.
STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
Number const received{lenderBalanceAfter - lenderBalanceBefore};
Number const expected = totalBefore * Number(halfShares) / Number(f.sharesLender);
BEAST_EXPECT(received == expected);
// The full-price payout exceeds the discounted formula by exactly
// lossBefore * halfShares / sharesLender — that's the whole point
// of the waive.
Number const discounted =
(totalBefore - lossBefore) * Number(halfShares) / Number(f.sharesLender);
Number const expectedDelta = lossBefore * Number(halfShares) / Number(f.sharesLender);
BEAST_EXPECT(received - discounted == expectedDelta);
auto const vaultAfter = env.le(vaultKey);
if (!BEAST_EXPECT(vaultAfter))
return;
auto const issuanceAfter = env.le(keylet::mptIssuance(f.shareAsset));
if (!BEAST_EXPECT(issuanceAfter))
return;
// Vault remains valid: half the shares remain, lossUnrealized
// is untouched, and the entire (total - available) gap is still
// the impaired receivable.
BEAST_EXPECT(
issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender - halfShares);
BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore - received);
BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
BEAST_EXPECT(
vaultAfter->at(sfAssetsTotal) - vaultAfter->at(sfAssetsAvailable) ==
vaultAfter->at(sfLossUnrealized));
// Conservation: vault delta matches the depositor's gain.
BEAST_EXPECT(totalBefore - vaultAfter->at(sfAssetsTotal) == received);
BEAST_EXPECT(availableBefore - vaultAfter->at(sfAssetsAvailable) == received);
}
// Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for the
// sfAssetsTotal and sfAssetsAvailable deltas, and visitEntry applies the
// same max() for the vault pseudo-account RippleState. When
@@ -7449,6 +8047,16 @@ public:
testAssetsMaximum();
testBug6LimitBypassWithShares();
testRemoveEmptyHoldingLockedAmount();
testWithdrawSoleShareholderFixedAssetExit(all_ - fixCleanup3_2_0);
testWithdrawSoleShareholderFixedAssetExit(all_);
testWithdrawSoleShareholderFullSharesRejected(all_ - fixCleanup3_2_0);
testWithdrawSoleShareholderFullSharesRejected(all_);
testWithdrawSoleShareholderCleanVaultUnaffected(all_ - fixCleanup3_2_0);
testWithdrawSoleShareholderCleanVaultUnaffected(all_);
testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice();
testWithdrawSoleShareholderLoanRepaymentExit();
testReferenceHolding();
testHoldingDeletionBlocked();
}