mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-04 01:06:48 +00:00
Merge remote-tracking branch 'upstream/develop' into sponsor
This commit is contained in:
@@ -207,14 +207,14 @@ jobs:
|
||||
env:
|
||||
CONFIG_NAME: ${{ inputs.config_name }}
|
||||
run: |
|
||||
ASAN_OPTS="halt_on_error=0:use_sigaltstack=0:print_stacktrace=1:detect_container_overflow=0:detect_stack_use_after_return=0:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp"
|
||||
ASAN_OPTS="include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-asan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp"
|
||||
if [[ "${CONFIG_NAME}" == *gcc* ]]; then
|
||||
ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0"
|
||||
fi
|
||||
echo "ASAN_OPTIONS=${ASAN_OPTS}" >> ${GITHUB_ENV}
|
||||
echo "TSAN_OPTIONS=second_deadlock_stack=1:halt_on_error=0:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >> ${GITHUB_ENV}
|
||||
echo "UBSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >> ${GITHUB_ENV}
|
||||
echo "LSAN_OPTIONS=suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >> ${GITHUB_ENV}
|
||||
echo "TSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-tsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >> ${GITHUB_ENV}
|
||||
echo "UBSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >> ${GITHUB_ENV}
|
||||
echo "LSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-lsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >> ${GITHUB_ENV}
|
||||
|
||||
- name: Run the separate tests
|
||||
if: ${{ !inputs.build_only }}
|
||||
|
||||
@@ -112,6 +112,7 @@ words:
|
||||
- gpgcheck
|
||||
- gpgkey
|
||||
- hotwallet
|
||||
- hwaddress
|
||||
- hwrap
|
||||
- ifndef
|
||||
- inequation
|
||||
|
||||
8
docs/build/sanitizers.md
vendored
8
docs/build/sanitizers.md
vendored
@@ -89,8 +89,8 @@ cmake --build . --parallel 4
|
||||
**IMPORTANT**: ASAN with Boost produces many false positives. Use these options:
|
||||
|
||||
```bash
|
||||
export ASAN_OPTIONS="print_stacktrace=1:detect_container_overflow=0:suppressions=path/to/asan.supp:halt_on_error=0:log_path=asan.log"
|
||||
export LSAN_OPTIONS="suppressions=path/to/lsan.supp:halt_on_error=0:log_path=lsan.log"
|
||||
export ASAN_OPTIONS="include=sanitizers/suppressions/runtime-asan-options.txt:suppressions=sanitizers/suppressions/asan.supp"
|
||||
export LSAN_OPTIONS="include=sanitizers/suppressions/runtime-lsan-options.txt:suppressions=sanitizers/suppressions/lsan.supp"
|
||||
|
||||
# Run tests
|
||||
./xrpld --unittest --unittest-jobs=5
|
||||
@@ -108,7 +108,7 @@ export LSAN_OPTIONS="suppressions=path/to/lsan.supp:halt_on_error=0:log_path=lsa
|
||||
### ThreadSanitizer (TSan)
|
||||
|
||||
```bash
|
||||
export TSAN_OPTIONS="suppressions=path/to/tsan.supp halt_on_error=0 log_path=tsan.log"
|
||||
export TSAN_OPTIONS="include=sanitizers/suppressions/runtime-tsan-options.txt:suppressions=sanitizers/suppressions/tsan.supp"
|
||||
|
||||
# Run tests
|
||||
./xrpld --unittest --unittest-jobs=5
|
||||
@@ -129,7 +129,7 @@ More details [here](https://github.com/google/sanitizers/wiki/AddressSanitizerLe
|
||||
### UndefinedBehaviorSanitizer (UBSan)
|
||||
|
||||
```bash
|
||||
export UBSAN_OPTIONS="suppressions=path/to/ubsan.supp:print_stacktrace=1:halt_on_error=0:log_path=ubsan.log"
|
||||
export UBSAN_OPTIONS="include=sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=sanitizers/suppressions/ubsan.supp"
|
||||
|
||||
# Run tests
|
||||
./xrpld --unittest --unittest-jobs=5
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/sanitizers.h>
|
||||
#include <xrpl/beast/type_name.h>
|
||||
|
||||
#include <exception>
|
||||
@@ -23,16 +24,28 @@ LogThrow(std::string const& title);
|
||||
When called from within a catch block, it will pass
|
||||
control to the next matching exception handler, if any.
|
||||
Otherwise, std::terminate will be called.
|
||||
|
||||
ASAN can't handle sudden jumps in control flow very well. This
|
||||
function is marked as XRPL_NO_SANITIZE_ADDRESS to prevent it from
|
||||
triggering false positives, since it throws.
|
||||
*/
|
||||
[[noreturn]] inline void
|
||||
[[noreturn]] XRPL_NO_SANITIZE_ADDRESS inline void
|
||||
Rethrow()
|
||||
{
|
||||
LogThrow("Re-throwing exception");
|
||||
throw;
|
||||
}
|
||||
|
||||
/*
|
||||
Logs and throws an exception of type E.
|
||||
|
||||
ASAN can't handle sudden jumps in control flow very well. This
|
||||
function is marked as XRPL_NO_SANITIZE_ADDRESS to prevent it from
|
||||
triggering false positives, since it throws.
|
||||
*/
|
||||
|
||||
template <class E, class... Args>
|
||||
[[noreturn]] inline void
|
||||
[[noreturn]] XRPL_NO_SANITIZE_ADDRESS inline void
|
||||
Throw(Args&&... args)
|
||||
{
|
||||
static_assert(
|
||||
|
||||
13
include/xrpl/basics/sanitizers.h
Normal file
13
include/xrpl/basics/sanitizers.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
// Helper to disable ASan/HwASan for specific functions
|
||||
/*
|
||||
ASAN flags some false positives with sudden jumps in control flow, like
|
||||
exceptions, or when encountering coroutine stack switches. This macro can be used to disable ASAN
|
||||
intrumentation for specific functions.
|
||||
*/
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define XRPL_NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address", "hwaddress")))
|
||||
#else
|
||||
#define XRPL_NO_SANITIZE_ADDRESS
|
||||
#endif
|
||||
@@ -209,7 +209,7 @@ std::size_t constexpr maxDIDDocumentLength = 256;
|
||||
std::size_t constexpr maxDIDURILength = 256;
|
||||
|
||||
/** The maximum length of an Attestation inside a DID */
|
||||
std::size_t constexpr maxDIDAttestationLength = 256;
|
||||
std::size_t constexpr maxDIDDataLength = 256;
|
||||
|
||||
/** The maximum length of a domain */
|
||||
std::size_t constexpr maxDomainLength = 256;
|
||||
|
||||
@@ -83,6 +83,9 @@ public:
|
||||
std::uint32_t
|
||||
getSeqValue() const;
|
||||
|
||||
AccountID
|
||||
getFeePayer() const;
|
||||
|
||||
boost::container::flat_set<AccountID>
|
||||
getMentionedAccounts() const;
|
||||
|
||||
|
||||
@@ -129,8 +129,7 @@ protected:
|
||||
beast::Journal const j_;
|
||||
|
||||
AccountID const account_;
|
||||
XRPAmount mPriorBalance; // Balance before fees.
|
||||
XRPAmount mSourceBalance; // Balance after fees.
|
||||
XRPAmount preFeeBalance_; // Balance before fees.
|
||||
|
||||
virtual ~Transactor() = default;
|
||||
Transactor(Transactor const&) = delete;
|
||||
|
||||
@@ -2,15 +2,6 @@
|
||||
#
|
||||
# ASAN_OPTIONS="print_stacktrace=1:detect_container_overflow=0:suppressions=sanitizers/suppressions/asan.supp:halt_on_error=0"
|
||||
|
||||
# Leaks in Doctest tests: xrpl.test.*
|
||||
interceptor_name:src/libxrpl/net/HTTPClient.cpp
|
||||
interceptor_name:src/libxrpl/net/RegisterSSLCerts.cpp
|
||||
interceptor_name:src/tests/libxrpl/net/HTTPClient.cpp
|
||||
interceptor_name:xrpl/net/AutoSocket.h
|
||||
interceptor_name:xrpl/net/HTTPClient.h
|
||||
interceptor_name:xrpl/net/HTTPClientSSLContext.h
|
||||
interceptor_name:xrpl/net/RegisterSSLCerts.h
|
||||
|
||||
# Suppress false positive stack-buffer errors in thread stack allocation
|
||||
# Related to ASan's __asan_handle_no_return warnings (github.com/google/sanitizers/issues/189)
|
||||
# These occur during multi-threaded test initialization on macOS
|
||||
|
||||
8
sanitizers/suppressions/runtime-asan-options.txt
Normal file
8
sanitizers/suppressions/runtime-asan-options.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
detect_container_overflow=false
|
||||
detect_stack_use_after_return=false
|
||||
debug=true
|
||||
halt_on_error=false
|
||||
print_stats=true
|
||||
print_cmdline=true
|
||||
use_sigaltstack=0
|
||||
print_stacktrace=1
|
||||
1
sanitizers/suppressions/runtime-lsan-options.txt
Normal file
1
sanitizers/suppressions/runtime-lsan-options.txt
Normal file
@@ -0,0 +1 @@
|
||||
halt_on_error=false
|
||||
3
sanitizers/suppressions/runtime-tsan-options.txt
Normal file
3
sanitizers/suppressions/runtime-tsan-options.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
halt_on_error=false
|
||||
verbosity=1
|
||||
second_deadlock_stack=1
|
||||
1
sanitizers/suppressions/runtime-ubsan-options.txt
Normal file
1
sanitizers/suppressions/runtime-ubsan-options.txt
Normal file
@@ -0,0 +1 @@
|
||||
halt_on_error=false
|
||||
@@ -27,3 +27,11 @@ src:core/JobQueue.cpp
|
||||
src:libxrpl/beast/utility/beast_Journal.cpp
|
||||
src:test/beast/beast_PropertyStream_test.cpp
|
||||
src:src/test/app/Invariants_test.cpp
|
||||
|
||||
# ASan false positive: stack-use-after-scope in ErrorCodes.h inline functions.
|
||||
# When Clang inlines the StaticString overloads (e.g. invalid_field_error(StaticString)),
|
||||
# ASan scope-poisons the temporary std::string before the inlined callee finishes reading
|
||||
# through the const ref. This corrupts the coroutine stack and crashes the Simulate test.
|
||||
# See asan.supp comments for full explanation and planned fix.
|
||||
[address]
|
||||
src:*ErrorCodes.h
|
||||
|
||||
@@ -182,6 +182,17 @@ signed-integer-overflow:src/test/beast/LexicalCast_test.cpp
|
||||
# External library suppressions
|
||||
unsigned-integer-overflow:nudb/detail/xxhash.hpp
|
||||
|
||||
# Loan_test.cpp intentional underflow in test arithmetic
|
||||
unsigned-integer-overflow:src/test/app/Loan_test.cpp
|
||||
undefined:src/test/app/Loan_test.cpp
|
||||
|
||||
# Source tree restructured paths (libxrpl/tx/transactors/)
|
||||
# These duplicate the xrpld/app/tx/detail entries above for the new layout
|
||||
unsigned-integer-overflow:src/libxrpl/tx/transactors/oracle/SetOracle.cpp
|
||||
undefined:src/libxrpl/tx/transactors/oracle/SetOracle.cpp
|
||||
unsigned-integer-overflow:src/libxrpl/tx/transactors/nft/NFTokenMint.cpp
|
||||
undefined:src/libxrpl/tx/transactors/nft/NFTokenMint.cpp
|
||||
|
||||
# Protobuf intentional overflows in hash functions
|
||||
# Protobuf uses intentional unsigned overflow for hash computation (stringpiece.h:393)
|
||||
unsigned-integer-overflow:google/protobuf/stubs/stringpiece.h
|
||||
|
||||
@@ -258,7 +258,7 @@ Number::Guard::doRoundUp(
|
||||
}
|
||||
bringIntoRange(negative, mantissa, exponent, minMantissa);
|
||||
if (exponent > maxExponent)
|
||||
throw std::overflow_error(location);
|
||||
Throw<std::overflow_error>(std::string(location));
|
||||
}
|
||||
|
||||
template <UnsignedMantissa T>
|
||||
@@ -298,7 +298,7 @@ Number::Guard::doRound(rep& drops, std::string location)
|
||||
// or "(maxRep + 1) / 10", neither of which will round up when
|
||||
// converting to rep, though the latter might overflow _before_
|
||||
// rounding.
|
||||
throw std::overflow_error(location); // LCOV_EXCL_LINE
|
||||
Throw<std::overflow_error>(std::string(location)); // LCOV_EXCL_LINE
|
||||
}
|
||||
++drops;
|
||||
}
|
||||
|
||||
@@ -211,6 +211,20 @@ STTx::getSeqValue() const
|
||||
return getSeqProxy().value();
|
||||
}
|
||||
|
||||
AccountID
|
||||
STTx::getFeePayer() const
|
||||
{
|
||||
// If sfDelegate is present, the delegate account is the payer
|
||||
// note: if a delegate is specified, its authorization to act on behalf of the account is
|
||||
// enforced in `Transactor::checkPermission`
|
||||
// cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`)
|
||||
if (isFieldPresent(sfDelegate))
|
||||
return getAccountID(sfDelegate);
|
||||
|
||||
// Default payer
|
||||
return getAccountID(sfAccount);
|
||||
}
|
||||
|
||||
void
|
||||
STTx::sign(
|
||||
PublicKey const& publicKey,
|
||||
|
||||
@@ -484,7 +484,6 @@ Transactor::payFee()
|
||||
auto const feePaid = ctx_.tx[sfFee].xrp();
|
||||
|
||||
auto const payer = getFeePayer(view(), ctx_.tx);
|
||||
|
||||
auto const sle = view().peek(payer.entry);
|
||||
|
||||
JLOG(j_.trace()) << "Fee payer: " + to_string(payer.entry.key);
|
||||
@@ -502,13 +501,7 @@ Transactor::payFee()
|
||||
|
||||
view().update(sle);
|
||||
|
||||
if (payer.type == FeePayerType::Account)
|
||||
// Deduct the fee, so it's not available during the transaction.
|
||||
// Will only write the account back if the transaction succeeds.
|
||||
mSourceBalance -= feePaid;
|
||||
|
||||
// VFALCO Should we call view().rawDestroyXRP() here as well?
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -708,8 +701,7 @@ Transactor::apply()
|
||||
|
||||
if (sle)
|
||||
{
|
||||
mPriorBalance = STAmount{(*sle)[sfBalance]}.xrp();
|
||||
mSourceBalance = mPriorBalance;
|
||||
preFeeBalance_ = STAmount{(*sle)[sfBalance]}.xrp();
|
||||
|
||||
TER result = consumeSeqProxy(sle);
|
||||
if (result != tesSUCCESS)
|
||||
|
||||
@@ -219,7 +219,7 @@ SponsorshipSet::doApply()
|
||||
auto newSle = std::make_shared<SLE>(sponsorKeylet);
|
||||
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
ctx_.view(), ctx_.tx, sponsorAccSle, mPriorBalance, reserveSponsorAccSle, 1);
|
||||
ctx_.view(), ctx_.tx, sponsorAccSle, preFeeBalance_, reserveSponsorAccSle, 1);
|
||||
!isTesSuccess(ret))
|
||||
return tecUNFUNDED;
|
||||
|
||||
|
||||
@@ -400,10 +400,10 @@ DeleteAccount::doApply()
|
||||
// Use the current balance from the SLE, not mSourceBalance, because
|
||||
// the cleanup loop may have returned pre-funded sfFeeAmount from
|
||||
// ltSponsorship objects back to the account's sfBalance.
|
||||
auto const srcCurrentBalance = STAmount{(*src)[sfBalance]}.xrp();
|
||||
(*dst)[sfBalance] = (*dst)[sfBalance] + srcCurrentBalance;
|
||||
(*src)[sfBalance] = (*src)[sfBalance] - srcCurrentBalance;
|
||||
ctx_.deliver(srcCurrentBalance);
|
||||
auto const remainingBalance = src->getFieldAmount(sfBalance).xrp();
|
||||
(*dst)[sfBalance] = (*dst)[sfBalance] + remainingBalance;
|
||||
(*src)[sfBalance] = (*src)[sfBalance] - remainingBalance;
|
||||
ctx_.deliver(remainingBalance);
|
||||
|
||||
if (src->isFieldPresent(sfSponsor))
|
||||
{
|
||||
@@ -445,7 +445,7 @@ DeleteAccount::doApply()
|
||||
}
|
||||
|
||||
// Re-arm the password change fee if we can and need to.
|
||||
if (srcCurrentBalance > XRPAmount(0) && dst->isFlag(lsfPasswordSpent))
|
||||
if (remainingBalance > XRPAmount(0) && dst->isFlag(lsfPasswordSpent))
|
||||
dst->clearFlag(lsfPasswordSpent);
|
||||
|
||||
view().update(dst);
|
||||
|
||||
@@ -304,7 +304,7 @@ SetSignerList::replaceSignerList()
|
||||
// with CreateTicket.
|
||||
auto const sponsor = getTxReserveSponsor(ctx_.view(), ctx_.tx);
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
ctx_.view(), ctx_.tx, sle, mPriorBalance, sponsor, addedOwnerCount);
|
||||
ctx_.view(), ctx_.tx, sle, preFeeBalance_, sponsor, addedOwnerCount);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@ enum class DepositAuthPolicy { normal, dstCanBypass };
|
||||
struct TransferHelperSubmittingAccountInfo
|
||||
{
|
||||
AccountID account;
|
||||
STAmount preFeeBalance;
|
||||
STAmount preFeeBalance_;
|
||||
STAmount postFeeBalance;
|
||||
};
|
||||
|
||||
@@ -422,7 +422,7 @@ transferHelper(
|
||||
if (!submittingAccountInfo || submittingAccountInfo->account != src ||
|
||||
submittingAccountInfo->postFeeBalance != curBal)
|
||||
return curBal;
|
||||
return submittingAccountInfo->preFeeBalance;
|
||||
return submittingAccountInfo->preFeeBalance_;
|
||||
}();
|
||||
|
||||
if (availableBalance < amt + reserve)
|
||||
@@ -1858,7 +1858,8 @@ XChainCommit::doApply()
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
auto const bridgeSpec = ctx_.tx[sfXChainBridge];
|
||||
|
||||
if (!psb.read(keylet::account(account)))
|
||||
auto const sleAccount = psb.read(keylet::account(account));
|
||||
if (!sleAccount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sleBridge = readBridge(psb, bridgeSpec);
|
||||
@@ -1869,7 +1870,7 @@ XChainCommit::doApply()
|
||||
|
||||
// Support dipping into reserves to pay the fee
|
||||
TransferHelperSubmittingAccountInfo submittingAccountInfo{
|
||||
account_, mPriorBalance, mSourceBalance};
|
||||
account_, preFeeBalance_, (*sleAccount)[sfBalance]};
|
||||
|
||||
auto const thTer = transferHelper(
|
||||
psb,
|
||||
@@ -2141,7 +2142,7 @@ XChainCreateAccountCommit::doApply()
|
||||
|
||||
// Support dipping into reserves to pay the fee
|
||||
TransferHelperSubmittingAccountInfo submittingAccountInfo{
|
||||
account_, mPriorBalance, mSourceBalance};
|
||||
account_, preFeeBalance_, (*sle)[sfBalance]};
|
||||
STAmount const toTransfer = amount + reward;
|
||||
auto const thTer = transferHelper(
|
||||
psb,
|
||||
|
||||
@@ -316,7 +316,7 @@ CashCheck::doApply()
|
||||
|
||||
// Can the account cover the trust line's reserve?
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
psb, ctx_.tx, sleDst, mPriorBalance, sponsorSle, 1);
|
||||
psb, ctx_.tx, sleDst, preFeeBalance_, sponsorSle, 1);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
JLOG(j_.trace()) << "Trust line does not exist. "
|
||||
|
||||
@@ -138,7 +138,7 @@ CreateCheck::doApply()
|
||||
// check the starting balance because we want to allow dipping into the
|
||||
// reserve to pay fees.
|
||||
auto const sponsor = getTxReserveSponsor(view(), ctx_.tx);
|
||||
if (auto const ret = checkInsufficientReserve(view(), ctx_.tx, sle, mPriorBalance, sponsor, 1);
|
||||
if (auto const ret = checkInsufficientReserve(view(), ctx_.tx, sle, preFeeBalance_, sponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
// Note that we use the value from the sequence or ticket as the
|
||||
|
||||
@@ -84,7 +84,7 @@ CredentialAccept::doApply()
|
||||
|
||||
auto const newSponsor = getTxReserveSponsor(view(), ctx_.tx);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleSubject, mPriorBalance, newSponsor, 1);
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleSubject, preFeeBalance_, newSponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ CredentialCreate::doApply()
|
||||
|
||||
auto const sponsor = getTxReserveSponsor(view(), ctx_.tx);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleIssuer, mPriorBalance, sponsor, 1);
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleIssuer, preFeeBalance_, sponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ DelegateSet::doApply()
|
||||
|
||||
auto const sponsor = getTxReserveSponsor(view(), ctx_.tx);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleOwner, mPriorBalance, sponsor, 1);
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleOwner, preFeeBalance_, sponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ AMMClawback::applyGuts(Sandbox& sb)
|
||||
0,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
WithdrawAll::Yes,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
else
|
||||
std::tie(result, newLPTokenBalance, amountWithdraw, amount2Withdraw) =
|
||||
@@ -253,7 +253,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
WithdrawAll::Yes,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
|
||||
auto const& rules = sb.rules();
|
||||
@@ -285,7 +285,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
WithdrawAll::No,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
||||
0,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
WithdrawAll::No,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
}
|
||||
|
||||
|
||||
@@ -427,7 +427,7 @@ AMMWithdraw::withdraw(
|
||||
tfee,
|
||||
FreezeHandling::fhZERO_IF_FROZEN,
|
||||
isWithdrawAll(ctx_.tx),
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
j_);
|
||||
return {ter, newLPTokenBalance};
|
||||
}
|
||||
@@ -660,7 +660,7 @@ AMMWithdraw::equalWithdrawTokens(
|
||||
tfee,
|
||||
FreezeHandling::fhZERO_IF_FROZEN,
|
||||
isWithdrawAll(ctx_.tx),
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
ctx_.journal);
|
||||
return {ter, newLPTokenBalance};
|
||||
}
|
||||
|
||||
@@ -727,7 +727,7 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
{
|
||||
auto const sponsor = getTxReserveSponsor(sb, ctx_.tx);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(sb, ctx_.tx, sleCreator, mPriorBalance, sponsor, 1);
|
||||
checkInsufficientReserve(sb, ctx_.tx, sleCreator, preFeeBalance_, sponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
// If we are here, the signing account had an insufficient reserve
|
||||
|
||||
@@ -33,7 +33,7 @@ DIDDelete::deleteSLE(
|
||||
if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Unable to delete DID Token from owner.";
|
||||
JLOG(j.fatal()) << "Unable to delete DID from owner.";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@ DIDSet::preflight(PreflightContext const& ctx)
|
||||
};
|
||||
|
||||
if (isTooLong(sfURI, maxDIDURILength) || isTooLong(sfDIDDocument, maxDIDDocumentLength) ||
|
||||
isTooLong(sfData, maxDIDAttestationLength))
|
||||
isTooLong(sfData, maxDIDDataLength))
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
static TER
|
||||
addSLE(ApplyContext& ctx, std::shared_ptr<SLE> const& sle, AccountID const& owner)
|
||||
{
|
||||
auto const sleAccount = ctx.view().peek(keylet::account(owner));
|
||||
|
||||
@@ -165,7 +165,7 @@ EscrowCancel::doApply()
|
||||
ctx_.tx,
|
||||
parityRate,
|
||||
slep,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account, // sender and receiver are the same
|
||||
|
||||
@@ -392,9 +392,9 @@ EscrowCreate::doApply()
|
||||
// Check reserve and funds availability
|
||||
STAmount const amount{ctx_.tx[sfAmount]};
|
||||
|
||||
auto const balance = sle->getFieldAmount(sfBalance).xrp();
|
||||
auto const sponsor = getTxReserveSponsor(view(), ctx_.tx);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, mSourceBalance, sponsor, 1);
|
||||
if (auto const ret = checkInsufficientReserve(ctx_.view(), ctx_.tx, sle, balance, sponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -402,7 +402,7 @@ EscrowCreate::doApply()
|
||||
if (isXRP(amount))
|
||||
{
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
ctx_.view(), ctx_.tx, sle, mSourceBalance - STAmount(amount).xrp(), {}, 1);
|
||||
ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), {}, 1);
|
||||
!isTesSuccess(ret))
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ EscrowFinish::doApply()
|
||||
ctx_.tx,
|
||||
lockedRate,
|
||||
sled,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account,
|
||||
|
||||
@@ -167,7 +167,7 @@ LoanBrokerCoverWithdraw::doApply()
|
||||
|
||||
associateAsset(*broker, vaultAsset);
|
||||
|
||||
return doWithdraw(view(), tx, account_, dstAcct, brokerPseudoID, mPriorBalance, amount, j_);
|
||||
return doWithdraw(view(), tx, account_, dstAcct, brokerPseudoID, preFeeBalance_, amount, j_);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -219,14 +219,14 @@ LoanBrokerSet::doApply()
|
||||
auto const sponsor = getTxReserveSponsor(view, tx);
|
||||
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view, tx, owner, mPriorBalance, {}, sponsor ? 1 : 2);
|
||||
checkInsufficientReserve(view, tx, owner, preFeeBalance_, {}, sponsor ? 1 : 2);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (sponsor)
|
||||
{
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view, tx, owner, mPriorBalance, sponsor, 1);
|
||||
checkInsufficientReserve(view, tx, owner, preFeeBalance_, sponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ LoanBrokerSet::doApply()
|
||||
auto pseudoId = pseudo->at(sfAccount);
|
||||
|
||||
if (auto ter =
|
||||
addEmptyHolding(view, tx, pseudoId, mPriorBalance, sleVault->at(sfAsset), j_))
|
||||
addEmptyHolding(view, tx, pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_))
|
||||
return ter;
|
||||
|
||||
// Initialize data fields:
|
||||
|
||||
@@ -84,12 +84,11 @@ LoanSet::preflight(PreflightContext const& ctx)
|
||||
!validNumericMinimum(paymentInterval, LoanSet::minPaymentInterval))
|
||||
return temINVALID;
|
||||
// Grace period is between min default value and payment interval
|
||||
else if (
|
||||
auto const gracePeriod = tx[~sfGracePeriod]; //
|
||||
!validNumericRange(
|
||||
gracePeriod,
|
||||
paymentInterval.value_or(LoanSet::defaultPaymentInterval),
|
||||
defaultGracePeriod))
|
||||
else if (auto const gracePeriod = tx[~sfGracePeriod]; //
|
||||
!validNumericRange(
|
||||
gracePeriod,
|
||||
paymentInterval.value_or(LoanSet::defaultPaymentInterval),
|
||||
defaultGracePeriod))
|
||||
return temINVALID;
|
||||
|
||||
// Copied from preflight2
|
||||
@@ -473,7 +472,7 @@ LoanSet::doApply()
|
||||
auto const sponsorSle = getTxReserveSponsor(view, tx);
|
||||
{
|
||||
auto const balance =
|
||||
account_ == borrower ? mPriorBalance : borrowerSle->at(sfBalance).value().xrp();
|
||||
account_ == borrower ? preFeeBalance_ : borrowerSle->at(sfBalance).value().xrp();
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view, tx, borrowerSle, balance, sponsorSle, 1);
|
||||
!isTesSuccess(ret))
|
||||
|
||||
@@ -363,7 +363,7 @@ NFTokenAcceptOffer::transferNFToken(
|
||||
// NFTs free of reserve.
|
||||
if (view().rules().enabled(fixNFTokenReserve))
|
||||
{
|
||||
// To check if there is sufficient reserve, we cannot use mPriorBalance
|
||||
// To check if there is sufficient reserve, we cannot use preFeeBalance_
|
||||
// because NFT is sold for a price. So we must use the balance after
|
||||
// the deduction of the potential offer price. A small caveat here is
|
||||
// that the balance has already deducted the transaction fee, meaning
|
||||
|
||||
@@ -75,7 +75,7 @@ NFTokenCreateOffer::doApply()
|
||||
ctx_.tx[~sfExpiration],
|
||||
ctx_.tx.getSeqProxy(),
|
||||
ctx_.tx[sfNFTokenID],
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
j_,
|
||||
ctx_.tx.getFlags());
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ NFTokenMint::doApply()
|
||||
ctx_.tx[~sfExpiration],
|
||||
ctx_.tx.getSeqProxy(),
|
||||
nftokenID,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
@@ -315,7 +315,7 @@ NFTokenMint::doApply()
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
view().read(keylet::account(account_)),
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
sponsor,
|
||||
0);
|
||||
!isTesSuccess(ret))
|
||||
|
||||
@@ -145,7 +145,7 @@ DepositPreauth::doApply()
|
||||
// reserve to pay fees.
|
||||
auto const sponsor = getTxReserveSponsor(view(), ctx_.tx);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleOwner, mPriorBalance, sponsor, 1);
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleOwner, preFeeBalance_, sponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -191,7 +191,7 @@ DepositPreauth::doApply()
|
||||
// reserve to pay fees.
|
||||
auto const sponsor = getTxReserveSponsor(view(), ctx_.tx);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleOwner, mPriorBalance, sponsor, 1);
|
||||
checkInsufficientReserve(view(), ctx_.tx, sleOwner, preFeeBalance_, sponsor, 1);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -582,16 +582,16 @@ Payment::doApply()
|
||||
auto const reserve = calculateReserve(sleSrc, view().fees()) +
|
||||
((txFlags & tfSponsorCreatedAccount) ? view().fees().reserve : beast::zero);
|
||||
|
||||
// mPriorBalance is the balance on the sending account BEFORE the
|
||||
// preFeeBalance_ is the balance on the sending account BEFORE the
|
||||
// fees were charged. We want to make sure we have enough reserve
|
||||
// to send. Allow final spend to use reserve for fee.
|
||||
auto const mmm = std::max(reserve, ctx_.tx.getFieldAmount(sfFee).xrp());
|
||||
|
||||
if (mPriorBalance < dstAmount.xrp() + mmm)
|
||||
if (preFeeBalance_ < dstAmount.xrp() + mmm)
|
||||
{
|
||||
// Vote no. However the transaction might succeed, if applied in
|
||||
// a different order.
|
||||
JLOG(j_.trace()) << "Delay transaction: Insufficient funds: " << to_string(mPriorBalance)
|
||||
JLOG(j_.trace()) << "Delay transaction: Insufficient funds: " << to_string(preFeeBalance_)
|
||||
<< " / " << to_string(dstAmount.xrp() + mmm) << " (" << to_string(reserve)
|
||||
<< ")";
|
||||
|
||||
@@ -639,7 +639,7 @@ Payment::doApply()
|
||||
}
|
||||
|
||||
// Do the arithmetic for the transfer and make the ledger change.
|
||||
sleSrc->setFieldAmount(sfBalance, mSourceBalance - dstAmount);
|
||||
sleSrc->setFieldAmount(sfBalance, sleSrc->getFieldAmount(sfBalance) - dstAmount);
|
||||
sleDst->setFieldAmount(sfBalance, sleDst->getFieldAmount(sfBalance) + dstAmount);
|
||||
|
||||
// Re-arm the password change fee if we can and need to.
|
||||
|
||||
@@ -63,7 +63,7 @@ CreateTicket::doApply()
|
||||
std::uint32_t const ticketCount = ctx_.tx[sfTicketCount];
|
||||
auto const sponsor = getTxReserveSponsor(view(), ctx_.tx);
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
view(), ctx_.tx, sleAccountRoot, mPriorBalance, sponsor, ticketCount);
|
||||
view(), ctx_.tx, sleAccountRoot, preFeeBalance_, sponsor, ticketCount);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ MPTokenAuthorize::doApply()
|
||||
return authorizeMPToken(
|
||||
ctx_.view(),
|
||||
tx,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
tx[sfMPTokenIssuanceID],
|
||||
account_,
|
||||
ctx_.journal,
|
||||
|
||||
@@ -151,7 +151,7 @@ MPTokenIssuanceCreate::doApply()
|
||||
tx,
|
||||
j_,
|
||||
{
|
||||
.priorBalance = mPriorBalance,
|
||||
.priorBalance = preFeeBalance_,
|
||||
.account = account_,
|
||||
.sequence = tx.getSeqValue(),
|
||||
.flags = tx.getFlags(),
|
||||
|
||||
@@ -542,7 +542,7 @@ SetTrust::doApply()
|
||||
{
|
||||
// should be checked PreFunded Sponsor before adjustOwnerCount()
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
view(), ctx_.tx, sleLowAccount, mPriorBalance, txSponsorSle, 1);
|
||||
view(), ctx_.tx, sleLowAccount, preFeeBalance_, txSponsorSle, 1);
|
||||
isSponsoredAndPreFunded && !isTesSuccess(ret))
|
||||
return tecINSUF_RESERVE_LINE;
|
||||
|
||||
@@ -569,7 +569,7 @@ SetTrust::doApply()
|
||||
{
|
||||
// should be checked PreFunded Sponsor before adjustOwnerCount()
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
view(), ctx_.tx, sleHighAccount, mPriorBalance, txSponsorSle, 1);
|
||||
view(), ctx_.tx, sleHighAccount, preFeeBalance_, txSponsorSle, 1);
|
||||
isSponsoredAndPreFunded && !isTesSuccess(ret))
|
||||
return tecINSUF_RESERVE_LINE;
|
||||
|
||||
@@ -602,8 +602,8 @@ SetTrust::doApply()
|
||||
terResult = trustDelete(view(), sleRippleState, uLowAccountID, uHighAccountID, viewJ);
|
||||
}
|
||||
// Reserve is not scaled by load.
|
||||
else if (auto const ret =
|
||||
checkInsufficientReserve(view(), ctx_.tx, sle, mPriorBalance, txSponsorSle, 0);
|
||||
else if (auto const ret = checkInsufficientReserve(
|
||||
view(), ctx_.tx, sle, preFeeBalance_, txSponsorSle, 0);
|
||||
!freeTrustLine && bReserveIncrease && !isTesSuccess(ret))
|
||||
{
|
||||
JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to "
|
||||
@@ -636,7 +636,7 @@ SetTrust::doApply()
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
sle,
|
||||
mPriorBalance,
|
||||
preFeeBalance_,
|
||||
txSponsorSle,
|
||||
1);
|
||||
!freeTrustLine && !isTesSuccess(ret)) // Reserve is not scaled by load.
|
||||
|
||||
@@ -140,14 +140,16 @@ VaultCreate::doApply()
|
||||
{
|
||||
adjustOwnerCount(view(), owner, sponsor, 2, j_);
|
||||
addSponsorToLedgerEntry(vault, sponsor);
|
||||
if (auto const ret = checkInsufficientReserve(view(), tx, owner, mPriorBalance, sponsor, 0);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view(), tx, owner, preFeeBalance_, sponsor, 0);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
else
|
||||
{
|
||||
// after Sponsor Amendment, check insufficient reserve first
|
||||
if (auto const ret = checkInsufficientReserve(view(), tx, owner, mPriorBalance, sponsor, 2);
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(view(), tx, owner, preFeeBalance_, sponsor, 2);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
adjustOwnerCount(view(), owner, sponsor, 2, j_);
|
||||
@@ -161,7 +163,7 @@ VaultCreate::doApply()
|
||||
auto pseudoId = pseudo->at(sfAccount);
|
||||
auto asset = tx[sfAsset];
|
||||
|
||||
if (auto ter = addEmptyHolding(view(), tx, pseudoId, mPriorBalance, asset, j_);
|
||||
if (auto ter = addEmptyHolding(view(), tx, pseudoId, preFeeBalance_, asset, j_);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
@@ -222,7 +224,7 @@ VaultCreate::doApply()
|
||||
|
||||
// Explicitly create MPToken for the vault owner
|
||||
if (auto const err =
|
||||
authorizeMPToken(view(), tx, mPriorBalance, mptIssuanceID, account_, ctx_.journal);
|
||||
authorizeMPToken(view(), tx, preFeeBalance_, mptIssuanceID, account_, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
@@ -230,7 +232,7 @@ VaultCreate::doApply()
|
||||
if (txFlags & tfVaultPrivate)
|
||||
{
|
||||
if (auto const err = authorizeMPToken(
|
||||
view(), tx, mPriorBalance, mptIssuanceID, pseudoId, ctx_.journal, {}, account_);
|
||||
view(), tx, preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, account_);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ VaultDeposit::doApply()
|
||||
if (vault->isFlag(lsfVaultPrivate) && account_ != vault->at(sfOwner))
|
||||
{
|
||||
if (auto const err = enforceMPTokenAuthorization(
|
||||
ctx_.view(), ctx_.tx, mptIssuanceID, account_, mPriorBalance, j_);
|
||||
ctx_.view(), ctx_.tx, mptIssuanceID, account_, preFeeBalance_, j_);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
@@ -156,7 +156,12 @@ VaultDeposit::doApply()
|
||||
if (!view().exists(keylet::mptoken(mptIssuanceID, account_)))
|
||||
{
|
||||
if (auto const err = authorizeMPToken(
|
||||
view(), ctx_.tx, mPriorBalance, mptIssuanceID->value(), account_, ctx_.journal);
|
||||
view(),
|
||||
ctx_.tx,
|
||||
preFeeBalance_,
|
||||
mptIssuanceID->value(),
|
||||
account_,
|
||||
ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
@@ -170,7 +175,7 @@ VaultDeposit::doApply()
|
||||
if (auto const err = authorizeMPToken(
|
||||
view(),
|
||||
ctx_.tx,
|
||||
mPriorBalance, // priorBalance
|
||||
preFeeBalance_, // priorBalance
|
||||
mptIssuanceID->value(), // mptIssuanceID
|
||||
sleIssuance->at(sfIssuer), // account
|
||||
ctx_.journal,
|
||||
|
||||
@@ -233,7 +233,7 @@ VaultWithdraw::doApply()
|
||||
associateAsset(*vault, vaultAsset);
|
||||
|
||||
return doWithdraw(
|
||||
view(), ctx_.tx, account_, dstAcct, vaultAccount, mPriorBalance, assetsWithdrawn, j_);
|
||||
view(), ctx_.tx, account_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -189,7 +189,7 @@ struct DID_test : public beast::unit_test::suite
|
||||
Account const edna{"edna"};
|
||||
Account const francis{"francis"};
|
||||
Account const george{"george"};
|
||||
env.fund(XRP(5000), alice, bob, charlie, dave, edna, francis);
|
||||
env.fund(XRP(5000), alice, bob, charlie, dave, edna, francis, george);
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
@@ -355,12 +355,14 @@ struct DID_test : public beast::unit_test::suite
|
||||
testAccountReserve(all);
|
||||
testSetInvalid(all);
|
||||
testDeleteInvalid(all);
|
||||
testSetValidInitial(all);
|
||||
testSetModify(all);
|
||||
|
||||
testEnabled(all - emptyDID);
|
||||
testAccountReserve(all - emptyDID);
|
||||
testSetInvalid(all - emptyDID);
|
||||
testDeleteInvalid(all - emptyDID);
|
||||
testSetValidInitial(all - emptyDID);
|
||||
testSetModify(all - emptyDID);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
/** Sets the optional Attestation on a DIDSet. */
|
||||
/** Sets the optional Data on a DIDSet. */
|
||||
class data
|
||||
{
|
||||
private:
|
||||
|
||||
Reference in New Issue
Block a user