mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-04 17:27:00 +00:00
Compare commits
8 Commits
pratik/Uni
...
tapanito/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5abb72ec0a | ||
|
|
42b7e858a4 | ||
|
|
3daf40c408 | ||
|
|
43fc095b1a | ||
|
|
45fa34de4b | ||
|
|
250b0d2c25 | ||
|
|
488e1be1dc | ||
|
|
ad111bcc22 |
24
docker/check-tool-versions.sh
Executable file
24
docker/check-tool-versions.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Verify that every tool expected in the Nix CI env is present and runnable.
|
||||
set -euo pipefail
|
||||
|
||||
ccache --version
|
||||
clang --version
|
||||
clang++ --version
|
||||
clang-format --version
|
||||
cmake --version
|
||||
conan --version
|
||||
g++ --version
|
||||
gcc --version
|
||||
gcovr --version
|
||||
git --version
|
||||
less --version
|
||||
make --version
|
||||
mold --version
|
||||
ninja --version
|
||||
perl --version
|
||||
pkg-config --version
|
||||
pre-commit --version
|
||||
python3 --version
|
||||
run-clang-tidy --help
|
||||
vim --version
|
||||
89
docker/install-sanitizer-libs.sh
Executable file
89
docker/install-sanitizer-libs.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
# Install sanitizer runtime libraries required to run binaries compiled with:
|
||||
# -fsanitize=address → libasan.so.8
|
||||
# -fsanitize=thread → libtsan.so.2
|
||||
# -fsanitize=undefined → libubsan.so.1
|
||||
#
|
||||
# The exact SONAMEs required depend on the compiler toolchain used to build the
|
||||
# test binaries (see nix/ci-env.nix). If the toolchain is bumped and SONAMEs
|
||||
# change, update the list below (or detect them from the binaries).
|
||||
#
|
||||
# Supported base images:
|
||||
# debian:bookworm
|
||||
# ubuntu:20.04
|
||||
# rhel:9
|
||||
# nixos/nix — tests are skipped; this script is not called
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ ! -f /etc/os-release ]; then
|
||||
echo "ERROR: /etc/os-release not found; cannot detect OS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. /etc/os-release
|
||||
|
||||
echo "Detected OS: ${ID} ${VERSION_ID:-}"
|
||||
|
||||
case "${ID}" in
|
||||
debian)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
libasan8 \
|
||||
libtsan2 \
|
||||
libubsan1
|
||||
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
;;
|
||||
|
||||
ubuntu)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
gnupg \
|
||||
software-properties-common
|
||||
add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
libasan8 \
|
||||
libtsan2 \
|
||||
libubsan1
|
||||
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
;;
|
||||
|
||||
rhel | centos | rocky | almalinux)
|
||||
dnf install -y \
|
||||
libasan8 \
|
||||
libtsan2 \
|
||||
libubsan
|
||||
|
||||
dnf clean -y all
|
||||
rm -rf /var/cache/dnf/*
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "ERROR: unsupported OS '${ID}'. Supported: debian, ubuntu, rhel-family" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verify that every expected library is now resolvable by the dynamic linker.
|
||||
missing=0
|
||||
for lib in libasan.so.8 libtsan.so.2 libubsan.so.1; do
|
||||
if ldconfig -p | grep -q "${lib}"; then
|
||||
echo "OK: ${lib} found"
|
||||
else
|
||||
echo "ERROR: ${lib} not found after installation" >&2
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${missing}" -ne 0 ]; then
|
||||
echo "ERROR: ${missing} library/libraries missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All sanitizer runtime libraries installed successfully."
|
||||
@@ -32,7 +32,7 @@ FROM ${BASE_IMAGE} AS final
|
||||
ARG BASE_IMAGE
|
||||
|
||||
# bash is not located at /bin/bash in nixos/nix, so we need to create a symlink to it.
|
||||
RUN if [ -d /nix ]; then \
|
||||
RUN if echo "${BASE_IMAGE}" | grep -qiE 'nixos'; then \
|
||||
ln -s /root/.nix-profile/bin/bash /bin/bash; \
|
||||
fi
|
||||
|
||||
@@ -65,38 +65,44 @@ if [ ! -e "${target}" ]; then
|
||||
fi
|
||||
EOF
|
||||
|
||||
RUN <<EOF
|
||||
ccache --version
|
||||
clang --version
|
||||
clang++ --version
|
||||
clang-format --version
|
||||
cmake --version
|
||||
conan --version
|
||||
g++ --version
|
||||
gcc --version
|
||||
gcovr --version
|
||||
git --version
|
||||
make --version
|
||||
mold --version
|
||||
ninja --version
|
||||
perl --version
|
||||
pkg-config --version
|
||||
pre-commit --version
|
||||
python3 --version
|
||||
run-clang-tidy --help
|
||||
vim --version
|
||||
EOF
|
||||
COPY docker/check-tool-versions.sh /tmp/check-tool-versions.sh
|
||||
RUN /tmp/check-tool-versions.sh
|
||||
|
||||
# Sanity-check that the sanitizer runtimes shipped with g++/clang++ are able to build binaries
|
||||
# Sanity-check that the g++/clang++ are able to build binaries, including sanitizer-instrumented ones.
|
||||
COPY docker/test_files/cpp_sources/ /tmp/cpp_sources/
|
||||
COPY docker/test_files/compile-cpp-sources.sh /tmp/compile-cpp-sources.sh
|
||||
RUN /tmp/compile-cpp-sources.sh /tmp/cpp_sources /tmp/bins
|
||||
|
||||
# Sanity-check that the built binaries are able to run.
|
||||
# We only support running the test binaries on Ubuntu and NixOS right now (will be fixed in the future)
|
||||
#
|
||||
# When build and test images will be separate, we will be to run on vanilla images.
|
||||
COPY docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh
|
||||
RUN if echo "${BASE_IMAGE}" | grep -qiE '(ubuntu|nixos)'; then \
|
||||
/tmp/run-test-binaries.sh /tmp/bins; \
|
||||
# Tester: start from a clean BASE_IMAGE, install sanitizer runtime libraries,
|
||||
# and run the compiled test binaries to verify they execute correctly.
|
||||
FROM ${BASE_IMAGE} AS tester
|
||||
|
||||
ARG BASE_IMAGE
|
||||
|
||||
# bash is not located at /bin/bash in nixos/nix, so we need to create a symlink to it.
|
||||
RUN if echo "${BASE_IMAGE}" | grep -qiE 'nixos'; then \
|
||||
ln -s /root/.nix-profile/bin/bash /bin/bash; \
|
||||
fi
|
||||
|
||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
|
||||
|
||||
# Sanity-check that the built binaries run correctly in the vanilla base image, with the necessary sanitizer runtime libraries installed.
|
||||
COPY docker/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh
|
||||
COPY docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh
|
||||
COPY --from=final /tmp/bins /tmp/bins
|
||||
|
||||
RUN <<EOF
|
||||
if echo "${BASE_IMAGE}" | grep -qiE 'nixos'; then
|
||||
echo "Skipping runnning binaries on NixOS."
|
||||
else
|
||||
/tmp/install-sanitizer-libs.sh
|
||||
/tmp/run-test-binaries.sh /tmp/bins
|
||||
fi
|
||||
touch /tmp/tests-passed
|
||||
EOF
|
||||
|
||||
# Output: the final image, gated on a successful test run in the tester stage.
|
||||
# Copying the sentinel from tester creates a hard build dependency: if the test
|
||||
# run above failed, this stage — and the overall build — fails too.
|
||||
FROM final
|
||||
COPY --from=tester /tmp/tests-passed /tmp/tests-passed
|
||||
|
||||
@@ -20,14 +20,21 @@ function compile() {
|
||||
local src="${src_dir}/${name}.cpp"
|
||||
local binary="${dst_dir}/${name}-${compiler}"
|
||||
|
||||
echo "=== Compile ${name} with ${compiler} ==="
|
||||
cmd="${compiler} -std=c++23 -O1 -g \
|
||||
echo "=== Compiling ${name} with ${compiler} ==="
|
||||
# Always statically link libstdc++ so the test binary does not depend on
|
||||
# the host's libstdc++.so.6 version.
|
||||
local compile_cmd="${compiler} -std=c++23 -O1 -g \
|
||||
-pthread \
|
||||
-Wl,--dynamic-linker=${loader} \
|
||||
-static-libstdc++ \
|
||||
${san_flag} \
|
||||
${src} -o ${binary}"
|
||||
echo "Command: ${cmd}"
|
||||
eval "${cmd}"
|
||||
echo "Compile cmd: ${compile_cmd}"
|
||||
eval "${compile_cmd}"
|
||||
|
||||
echo "=== Patching ${binary} to use ${loader} as PT_INTERP ==="
|
||||
local patch_cmd="patchelf --set-interpreter ${loader} --remove-rpath ${binary}"
|
||||
echo "Patch cmd: ${patch_cmd}"
|
||||
eval "${patch_cmd}"
|
||||
}
|
||||
|
||||
declare -A sanitize=(
|
||||
|
||||
@@ -7,6 +7,8 @@ set -eo pipefail
|
||||
|
||||
bins_dir="${1:?usage: $0 <bins_dir>}"
|
||||
|
||||
failed_binaries=()
|
||||
|
||||
# Run a binary and verify its exit code and output.
|
||||
# Usage: run <binary> <expected_output> <expected_rc>
|
||||
function run() {
|
||||
@@ -18,27 +20,34 @@ function run() {
|
||||
out_file="$(mktemp)"
|
||||
|
||||
echo "=== Run ${binary} ==="
|
||||
local rc=0
|
||||
"${binary}" >"${out_file}" 2>&1 || rc=$?
|
||||
set +e
|
||||
"${binary}" >"${out_file}" 2>&1
|
||||
local rc=$?
|
||||
set -e
|
||||
|
||||
cat "${out_file}"
|
||||
|
||||
local failed=0
|
||||
if [ "${expected_rc}" = "nonzero" ]; then
|
||||
if [ "${rc}" -eq 0 ]; then
|
||||
echo "ERROR: expected non-zero exit code from ${binary}, got ${rc}" >&2
|
||||
exit 1
|
||||
failed=1
|
||||
fi
|
||||
elif [ "${rc}" -ne "${expected_rc}" ]; then
|
||||
echo "ERROR: expected exit code ${expected_rc} from ${binary}, got ${rc}" >&2
|
||||
exit 1
|
||||
failed=1
|
||||
fi
|
||||
|
||||
grep -q "${expected_output}" "${out_file}" ||
|
||||
{
|
||||
echo "ERROR: expected '${expected_output}' from ${binary}" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "OK: '${expected_output}' detected"
|
||||
if ! grep -q "${expected_output}" "${out_file}"; then
|
||||
echo "ERROR: expected '${expected_output}' from ${binary}" >&2
|
||||
failed=1
|
||||
fi
|
||||
|
||||
if [ "${failed}" -eq 0 ]; then
|
||||
echo "OK: '${expected_output}' detected"
|
||||
else
|
||||
failed_binaries+=("${binary}")
|
||||
fi
|
||||
}
|
||||
|
||||
declare -A expect=(
|
||||
@@ -52,6 +61,15 @@ declare -A expect=(
|
||||
for compiler in g++ clang++; do
|
||||
for name in regular asan tsan ubsan; do
|
||||
binary="${bins_dir}/${name}-${compiler}"
|
||||
|
||||
if [ "${name}" = "tsan" ] && [ "${compiler}" = "g++" ] &&
|
||||
grep -qi 'debian' /etc/os-release 2>/dev/null &&
|
||||
[ "$(uname -m)" = "aarch64" ]; then
|
||||
echo "=== Skipping ${binary} (tsan-g++ unsupported on Debian ARM64) ==="
|
||||
echo " NOTE: to enable it, add --security-opt seccomp=unconfined to your docker run command"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "${name}" = "regular" ]; then
|
||||
expected_rc=0
|
||||
else
|
||||
@@ -60,3 +78,9 @@ for compiler in g++ clang++; do
|
||||
run "${binary}" "${expect[$name]}" "${expected_rc}"
|
||||
done
|
||||
done
|
||||
|
||||
if [ "${#failed_binaries[@]}" -gt 0 ]; then
|
||||
echo "ERROR: the following binaries failed:" >&2
|
||||
printf ' %s\n' "${failed_binaries[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -15,6 +15,7 @@ in
|
||||
git
|
||||
gnumake
|
||||
llvmPackages_22.clang-tools
|
||||
less # needed for git diff
|
||||
mold
|
||||
ninja
|
||||
patchelf
|
||||
|
||||
@@ -78,12 +78,13 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
auto const& vaultAccount = vault->at(sfAccount);
|
||||
auto const& account = ctx.tx[sfAccount];
|
||||
auto const& dstAcct = ctx.tx[~sfDestination].value_or(account);
|
||||
|
||||
auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
|
||||
// Post-fixCleanup3_2_0: withdraw is a recovery path that bypasses the
|
||||
// lsfMPTCanTransfer flag check, so an issuer cannot trap depositor funds.
|
||||
// Other transferability checks (IOU NoRipple, freeze, requireAuth) still
|
||||
// apply.
|
||||
auto const waive = ctx.view.rules().enabled(fixCleanup3_2_0) ? WaiveMPTCanTransfer::Yes
|
||||
: WaiveMPTCanTransfer::No;
|
||||
auto const waive = fix320Enabled ? WaiveMPTCanTransfer::Yes : WaiveMPTCanTransfer::No;
|
||||
if (auto ter = canTransfer(ctx.view, vaultAsset, vaultAccount, dstAcct, waive);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
@@ -160,14 +161,36 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Cannot withdraw from a Vault an Asset frozen for the destination account
|
||||
if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset))
|
||||
return ret;
|
||||
// Pre-fixCleanup3_2_0: any freeze on the destination blocked withdrawal
|
||||
// even though frozen accounts can still receive. Post-amendment:
|
||||
// - Frozen pseudo-account (vault) cannot send assets.
|
||||
// - Only deep-frozen destinations (which cannot receive at all) are blocked.
|
||||
if (fix320Enabled)
|
||||
{
|
||||
if (dstAcct != vaultAsset.getIssuer())
|
||||
{
|
||||
if (auto const ret = checkFrozen(ctx.view, vaultAccount, vaultAsset))
|
||||
return ret;
|
||||
|
||||
// Cannot return shares to the vault, if the underlying asset was frozen for
|
||||
// the submitter
|
||||
if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
|
||||
return ret;
|
||||
if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset))
|
||||
return ret;
|
||||
|
||||
// Cannot return shares to the vault, if the underlying asset was frozen for
|
||||
// the submitter
|
||||
if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset))
|
||||
return ret;
|
||||
|
||||
// Cannot return shares to the vault, if the underlying asset was frozen for
|
||||
// the submitter
|
||||
if (auto const ret = checkFrozen(ctx.view, account, Asset{vaultShare}))
|
||||
return ret;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -254,8 +277,14 @@ VaultWithdraw::doApply()
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
|
||||
if (accountHolds(
|
||||
view(), accountID_, share, FreezeHandling::ZeroIfFrozen, AuthHandling::IgnoreAuth, j_) <
|
||||
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
|
||||
bool const redeemingToIssuer =
|
||||
view().rules().enabled(fixCleanup3_2_0) && dstAcct == vaultAsset.getIssuer();
|
||||
auto const freezeHandling =
|
||||
redeemingToIssuer ? FreezeHandling::IgnoreFreeze : FreezeHandling::ZeroIfFrozen;
|
||||
|
||||
// Share freeze checks are transitive. We skip them when withdrawing to the issuer alltogether.
|
||||
if (accountHolds(view(), accountID_, share, freezeHandling, AuthHandling::IgnoreAuth, j_) <
|
||||
sharesRedeemed)
|
||||
{
|
||||
JLOG(j_.debug()) << "VaultWithdraw: account doesn't hold enough shares";
|
||||
@@ -358,8 +387,6 @@ VaultWithdraw::doApply()
|
||||
// else quietly ignore, account balance is not zero
|
||||
}
|
||||
|
||||
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
|
||||
|
||||
associateAsset(*vault, vaultAsset);
|
||||
|
||||
return doWithdraw(
|
||||
|
||||
@@ -1701,16 +1701,21 @@ class Vault_test : public beast::unit_test::Suite
|
||||
tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
|
||||
env(tx, Ter(tecLOCKED));
|
||||
|
||||
tx[sfDestination] = issuer.human();
|
||||
env(tx, Ter(tecLOCKED));
|
||||
|
||||
// Clawback is still permitted, even with global lock
|
||||
// Clawback is still permitted, even with global lock.
|
||||
tx = vault.clawback(
|
||||
{.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
|
||||
{.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(50)});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Clawback removed shares MPToken
|
||||
// Redemption to the issuer bypasses freeze checks end-to-end:
|
||||
// preclaim's issuer guard skips all three checks, and doApply uses
|
||||
// FreezeHandling::IgnoreFreeze for the accountHolds balance check.
|
||||
tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
|
||||
tx[sfDestination] = issuer.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Withdrawal burned remaining depositor shares — MPToken is removed.
|
||||
auto const mptSle = env.le(keylet::mptoken(share, depositor.id()));
|
||||
BEAST_EXPECT(mptSle == nullptr);
|
||||
|
||||
@@ -2791,13 +2796,16 @@ class Vault_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
{
|
||||
// Post-fixCleanup3_2_0: the frozen pseudo-account is checked
|
||||
// directly as a sender, so the error is tecFROZEN rather than
|
||||
// the transitive tecLOCKED from the share freeze check.
|
||||
auto tx =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(100)});
|
||||
env(tx, Ter{tecLOCKED});
|
||||
env(tx, Ter{tecFROZEN});
|
||||
|
||||
// also when trying to withdraw to a 3rd party
|
||||
tx[sfDestination] = charlie.human();
|
||||
env(tx, Ter{tecLOCKED});
|
||||
env(tx, Ter{tecFROZEN});
|
||||
env.close();
|
||||
}
|
||||
|
||||
@@ -2821,6 +2829,153 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
});
|
||||
|
||||
{
|
||||
testcase("IOU frozen trust line to vault account: pre-fixCleanup3_2_0");
|
||||
|
||||
// Pre-amendment: there is no direct pseudo-account sender check.
|
||||
// The frozen trust line is caught transitively via the share freeze
|
||||
// (isVaultPseudoAccountFrozen), so withdrawals fail with tecLOCKED
|
||||
// rather than tecFROZEN.
|
||||
Env env{*this, testableAmendments() - fixCleanup3_2_0};
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const charlie{"charlie"};
|
||||
Vault const vault{env};
|
||||
env.fund(XRP(1000), issuer, owner, charlie);
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const asset = issuer["IOU"];
|
||||
env.trust(asset(1000), owner);
|
||||
env(pay(issuer, owner, asset(200)));
|
||||
env.trust(asset(1000), charlie);
|
||||
env.close();
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
|
||||
// Freeze the trust line to the vault pseudo-account
|
||||
auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
|
||||
auto trustSet = [&]() {
|
||||
json::Value jv;
|
||||
jv[jss::Account] = issuer.human();
|
||||
{
|
||||
auto& ja = jv[jss::LimitAmount] =
|
||||
asset(0).value().getJson(JsonOptions::Values::None);
|
||||
ja[jss::issuer] = toBase58(vaultAcct.id());
|
||||
}
|
||||
jv[jss::TransactionType] = jss::TrustSet;
|
||||
jv[jss::Flags] = tfSetFreeze;
|
||||
return jv;
|
||||
}();
|
||||
env(trustSet);
|
||||
env.close();
|
||||
|
||||
{
|
||||
auto t =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(50)});
|
||||
// Pre-amendment: caught transitively via share freeze.
|
||||
env(t, Ter{tecLOCKED});
|
||||
|
||||
t[sfDestination] = charlie.human();
|
||||
env(t, Ter{tecLOCKED});
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Clear freeze and clean up
|
||||
trustSet[jss::Flags] = tfClearFreeze;
|
||||
env(trustSet);
|
||||
env.close();
|
||||
|
||||
env(vault.clawback(
|
||||
{.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}));
|
||||
env.close();
|
||||
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
}
|
||||
|
||||
testCase([&, this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const& charlie,
|
||||
auto vaultAccount,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto issuanceId) {
|
||||
testcase("IOU frozen vault trust line, withdrawal to issuer is exempt");
|
||||
|
||||
// When the vault's trust line is frozen, withdrawals to any
|
||||
// non-issuer destination are blocked by checkFrozen(vaultAccount).
|
||||
// Withdrawals whose destination IS the IOU issuer bypass that
|
||||
// check entirely (redemption path).
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
|
||||
Asset const share = Asset(issuanceId(keylet));
|
||||
|
||||
// Freeze the trust line to the vault pseudo-account
|
||||
auto trustSet = [&, account = vaultAccount(keylet)]() {
|
||||
json::Value jv;
|
||||
jv[jss::Account] = issuer.human();
|
||||
{
|
||||
auto& ja = jv[jss::LimitAmount] =
|
||||
asset(0).value().getJson(JsonOptions::Values::None);
|
||||
ja[jss::issuer] = toBase58(account);
|
||||
}
|
||||
jv[jss::TransactionType] = jss::TrustSet;
|
||||
jv[jss::Flags] = tfSetFreeze;
|
||||
return jv;
|
||||
}();
|
||||
env(trustSet);
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Non-issuer destinations are blocked
|
||||
auto t =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
||||
env(t, Ter{tecFROZEN});
|
||||
|
||||
t[sfDestination] = charlie.human();
|
||||
env(t, Ter{tecFROZEN});
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Withdrawal to the IOU issuer succeeds end-to-end: the issuer
|
||||
// guard skips all preclaim checks, and doApply uses
|
||||
// FreezeHandling::IgnoreFreeze so accountHolds returns the
|
||||
// actual balance rather than zero.
|
||||
auto t =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(50)});
|
||||
t[sfDestination] = issuer.human();
|
||||
env(t);
|
||||
env.close();
|
||||
}
|
||||
|
||||
// vault now has 50 assets / owner holds 50'000'000 shares
|
||||
// Clear freeze and drain what remains.
|
||||
trustSet[jss::Flags] = tfClearFreeze;
|
||||
env(trustSet);
|
||||
env.close();
|
||||
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner, .id = keylet.key, .amount = share(50'000'000)}));
|
||||
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&, this](
|
||||
Env& env,
|
||||
@@ -2913,10 +3068,13 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env(trust(issuer, asset(0), owner, tfSetFreeze));
|
||||
env.close();
|
||||
|
||||
// Cannot withdraw
|
||||
// Post-fixCleanup3_2_0: destination check uses checkDeepFrozen; a
|
||||
// regularly-frozen owner can still receive, so withdrawal is not
|
||||
// blocked by the destination check. The share lock (transitive
|
||||
// from the frozen underlying IOU) still prevents withdrawal.
|
||||
auto const withdraw =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
||||
env(withdraw, Ter{tecFROZEN});
|
||||
env(withdraw, Ter{tecLOCKED});
|
||||
|
||||
// Cannot withdraw to 3rd party
|
||||
env(withdrawToCharlie, Ter{tecLOCKED});
|
||||
@@ -3260,20 +3418,33 @@ class Vault_test : public beast::unit_test::Suite
|
||||
}(keylet);
|
||||
env(withdrawToCharlie);
|
||||
|
||||
// Freeze the 3rd party
|
||||
// Freeze the 3rd party (regular freeze only)
|
||||
env(trust(issuer, asset(0), charlie, tfSetFreeze));
|
||||
env.close();
|
||||
|
||||
// Can withdraw
|
||||
// Can withdraw to self
|
||||
auto const withdraw =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
||||
env(withdraw);
|
||||
env.close();
|
||||
|
||||
// Cannot withdraw to 3rd party
|
||||
// Post-fixCleanup3_2_0: a regularly-frozen destination can still
|
||||
// receive, so withdrawal to charlie is not blocked.
|
||||
env(withdrawToCharlie);
|
||||
env.close();
|
||||
|
||||
// Deep-freeze charlie: the destination can neither send nor receive.
|
||||
env(trust(issuer, asset(0), charlie, tfSetDeepFreeze));
|
||||
env.close();
|
||||
|
||||
// Cannot withdraw to deep-frozen 3rd party.
|
||||
env(withdrawToCharlie, Ter{tecFROZEN});
|
||||
env.close();
|
||||
|
||||
// Clear the freeze so the vault can be cleaned up.
|
||||
env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze));
|
||||
env.close();
|
||||
|
||||
env(vault.clawback(
|
||||
{.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}));
|
||||
env.close();
|
||||
@@ -3282,6 +3453,56 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
});
|
||||
|
||||
{
|
||||
testcase("IOU frozen trust line to 3rd party: pre-fixCleanup3_2_0");
|
||||
|
||||
// Pre-amendment: checkFrozen was used for the destination; a
|
||||
// regularly-frozen 3rd party could not receive vault assets.
|
||||
Env env{*this, testableAmendments() - fixCleanup3_2_0};
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const charlie{"charlie"};
|
||||
Vault vault{env};
|
||||
env.fund(XRP(1000), issuer, owner, charlie);
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const asset = issuer["IOU"];
|
||||
env.trust(asset(1000), owner);
|
||||
env(pay(issuer, owner, asset(200)));
|
||||
env.trust(asset(1000), charlie);
|
||||
env.close();
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
|
||||
auto const withdrawToCharlie = [&]() {
|
||||
auto t =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
||||
t[sfDestination] = charlie.human();
|
||||
return t;
|
||||
}();
|
||||
env(withdrawToCharlie);
|
||||
|
||||
// Freeze the 3rd party
|
||||
env(trust(issuer, asset(0), charlie, tfSetFreeze));
|
||||
env.close();
|
||||
|
||||
// Pre-amendment: regular freeze on the destination blocks withdrawal.
|
||||
env(withdrawToCharlie, Ter{tecFROZEN});
|
||||
|
||||
env(vault.clawback(
|
||||
{.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}));
|
||||
env.close();
|
||||
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
}
|
||||
|
||||
testCase([&, this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
@@ -3304,7 +3525,10 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Cannot withdraw
|
||||
// Post-fixCleanup3_2_0: checkFrozen(vaultAccount, vaultAsset)
|
||||
// fires first. isFrozen checks lsfGlobalFreeze on the issuer
|
||||
// unconditionally, so the vault pseudo-account (sender) is
|
||||
// blocked before any destination or share check is reached.
|
||||
auto tx =
|
||||
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
||||
env(tx, Ter{tecFROZEN});
|
||||
@@ -3314,6 +3538,11 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env(tx, Ter{tecFROZEN});
|
||||
env.close();
|
||||
|
||||
// Withdrawal to the IOU issuer succeeds (redemption path)
|
||||
tx[sfDestination] = issuer.human();
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Cannot deposit some more
|
||||
tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user