mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-18 09:52:31 +00:00
Compare commits
7 Commits
ximinez/lo
...
ximinez/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b62296a1d2 | ||
|
|
62f9f99197 | ||
|
|
48bc20eb0c | ||
|
|
7cc286f9e0 | ||
|
|
bff5865eab | ||
|
|
9076cbdd1f | ||
|
|
e003a57f07 |
16
.github/workflows/reusable-build-test-config.yml
vendored
16
.github/workflows/reusable-build-test-config.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
name: ${{ inputs.config_name }}
|
||||
runs-on: ${{ fromJSON(inputs.runs_on) }}
|
||||
container: ${{ inputs.image != '' && inputs.image || null }}
|
||||
timeout-minutes: ${{ inputs.sanitizers != '' && 360 || 60 }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
# Use a namespace to keep the objects separate for each configuration.
|
||||
CCACHE_NAMESPACE: ${{ inputs.config_name }}
|
||||
@@ -230,17 +230,11 @@ jobs:
|
||||
|
||||
- name: Set sanitizer options
|
||||
if: ${{ !inputs.build_only && env.SANITIZERS_ENABLED == 'true' }}
|
||||
env:
|
||||
CONFIG_NAME: ${{ inputs.config_name }}
|
||||
run: |
|
||||
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=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}
|
||||
echo "ASAN_OPTIONS=print_stacktrace=1:detect_container_overflow=0:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp" >> ${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}
|
||||
|
||||
- name: Run the separate tests
|
||||
if: ${{ !inputs.build_only }}
|
||||
|
||||
@@ -23,7 +23,7 @@ target_compile_definitions(
|
||||
BOOST_FILESYSTEM_NO_DEPRECATED
|
||||
>
|
||||
$<$<NOT:$<BOOL:${boost_show_deprecated}>>:
|
||||
BOOST_COROUTINES2_NO_DEPRECATION_WARNING
|
||||
BOOST_COROUTINES_NO_DEPRECATION_WARNING
|
||||
BOOST_BEAST_ALLOW_DEPRECATED
|
||||
BOOST_FILESYSTEM_DEPRECATED
|
||||
>
|
||||
|
||||
@@ -7,7 +7,7 @@ find_package(
|
||||
COMPONENTS
|
||||
chrono
|
||||
container
|
||||
context
|
||||
coroutine
|
||||
date_time
|
||||
filesystem
|
||||
json
|
||||
@@ -26,7 +26,7 @@ target_link_libraries(
|
||||
Boost::headers
|
||||
Boost::chrono
|
||||
Boost::container
|
||||
Boost::context
|
||||
Boost::coroutine
|
||||
Boost::date_time
|
||||
Boost::filesystem
|
||||
Boost::json
|
||||
@@ -38,26 +38,23 @@ target_link_libraries(
|
||||
if(Boost_COMPILER)
|
||||
target_link_libraries(xrpl_boost INTERFACE Boost::disable_autolinking)
|
||||
endif()
|
||||
|
||||
# GCC 14+ has a false positive -Wuninitialized warning in Boost.Coroutine2's
|
||||
# state.hpp when compiled with -O3. This is due to GCC's intentional behavior
|
||||
# change (Bug #98871, #119388) where warnings from inlined system header code
|
||||
# are no longer suppressed by -isystem. The warning occurs in operator|= in
|
||||
# boost/coroutine2/detail/state.hpp when inlined from push_control_block::destroy().
|
||||
# See: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=119388
|
||||
if(is_gcc AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 14)
|
||||
target_compile_options(xrpl_boost INTERFACE -Wno-uninitialized)
|
||||
endif()
|
||||
|
||||
# Boost.Context's ucontext backend has ASAN fiber-switching annotations
|
||||
# (start/finish_switch_fiber) that are compiled in when BOOST_USE_ASAN is defined.
|
||||
# This tells ASAN about coroutine stack switches, preventing false positive
|
||||
# stack-use-after-scope errors. BOOST_USE_UCONTEXT ensures the ucontext backend
|
||||
# is selected (fcontext does not support ASAN annotations).
|
||||
# These defines must match what Boost was compiled with (see conan/profiles/sanitizers).
|
||||
if(enable_asan)
|
||||
target_compile_definitions(
|
||||
xrpl_boost
|
||||
INTERFACE BOOST_USE_ASAN BOOST_USE_UCONTEXT
|
||||
if(SANITIZERS_ENABLED AND is_clang)
|
||||
# TODO: gcc does not support -fsanitize-blacklist...can we do something else for gcc ?
|
||||
if(NOT Boost_INCLUDE_DIRS AND TARGET Boost::headers)
|
||||
get_target_property(
|
||||
Boost_INCLUDE_DIRS
|
||||
Boost::headers
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
)
|
||||
endif()
|
||||
message(STATUS "Adding [${Boost_INCLUDE_DIRS}] to sanitizer blacklist")
|
||||
file(
|
||||
WRITE ${CMAKE_CURRENT_BINARY_DIR}/san_bl.txt
|
||||
"src:${Boost_INCLUDE_DIRS}/*"
|
||||
)
|
||||
target_compile_options(
|
||||
opts
|
||||
INTERFACE # ignore boost headers for sanitizing
|
||||
-fsanitize-blacklist=${CMAKE_CURRENT_BINARY_DIR}/san_bl.txt
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -7,21 +7,16 @@ include(default)
|
||||
{% if compiler == "gcc" %}
|
||||
{% if "address" in sanitizers or "thread" in sanitizers or "undefinedbehavior" in sanitizers %}
|
||||
{% set sanitizer_list = [] %}
|
||||
{% set defines = [] %}
|
||||
{% set model_code = "" %}
|
||||
{% set extra_cxxflags = ["-fno-omit-frame-pointer", "-O1", "-Wno-stringop-overflow"] %}
|
||||
|
||||
{% if "address" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("address") %}
|
||||
{% set model_code = "-mcmodel=large" %}
|
||||
{% set _ = defines.append("BOOST_USE_ASAN")%}
|
||||
{% set _ = defines.append("BOOST_USE_UCONTEXT")%}
|
||||
{% elif "thread" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("thread") %}
|
||||
{% set model_code = "-mcmodel=medium" %}
|
||||
{% set _ = extra_cxxflags.append("-Wno-tsan") %}
|
||||
{% set _ = defines.append("BOOST_USE_TSAN")%}
|
||||
{% set _ = defines.append("BOOST_USE_UCONTEXT")%}
|
||||
{% endif %}
|
||||
|
||||
{% if "undefinedbehavior" in sanitizers %}
|
||||
@@ -34,22 +29,16 @@ include(default)
|
||||
tools.build:cxxflags+=['{{sanitizer_flags}} {{" ".join(extra_cxxflags)}}']
|
||||
tools.build:sharedlinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:exelinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:defines+={{defines}}
|
||||
{% endif %}
|
||||
{% elif compiler == "apple-clang" or compiler == "clang" %}
|
||||
{% if "address" in sanitizers or "thread" in sanitizers or "undefinedbehavior" in sanitizers %}
|
||||
{% set sanitizer_list = [] %}
|
||||
{% set defines = [] %}
|
||||
{% set extra_cxxflags = ["-fno-omit-frame-pointer", "-O1"] %}
|
||||
|
||||
{% if "address" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("address") %}
|
||||
{% set _ = defines.append("BOOST_USE_ASAN")%}
|
||||
{% set _ = defines.append("BOOST_USE_UCONTEXT")%}
|
||||
{% elif "thread" in sanitizers %}
|
||||
{% set _ = sanitizer_list.append("thread") %}
|
||||
{% set _ = defines.append("BOOST_USE_TSAN")%}
|
||||
{% set _ = defines.append("BOOST_USE_UCONTEXT")%}
|
||||
{% endif %}
|
||||
|
||||
{% if "undefinedbehavior" in sanitizers %}
|
||||
@@ -63,24 +52,8 @@ include(default)
|
||||
tools.build:cxxflags+=['{{sanitizer_flags}} {{" ".join(extra_cxxflags)}}']
|
||||
tools.build:sharedlinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:exelinkflags+=['{{sanitizer_flags}}']
|
||||
tools.build:defines+={{defines}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags", "tools.build:defines"]
|
||||
|
||||
[options]
|
||||
{% if sanitizers %}
|
||||
{% if "address" in sanitizers %}
|
||||
# Build Boost.Context with ucontext backend (not fcontext) so that
|
||||
# ASAN fiber-switching annotations (__sanitizer_start/finish_switch_fiber)
|
||||
# are compiled into the library. fcontext (assembly) has no ASAN support.
|
||||
# define=BOOST_USE_ASAN=1 is critical: it must be defined when building
|
||||
# Boost.Context itself so the ucontext backend compiles in the ASAN annotations.
|
||||
boost/*:extra_b2_flags=context-impl=ucontext address-sanitizer=on define=BOOST_USE_ASAN=1
|
||||
boost/*:without_context=False
|
||||
# Boost stacktrace fails to build with some sanitizers
|
||||
boost/*:without_stacktrace=True
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
tools.info.package_id:confs+=["tools.build:cxxflags", "tools.build:exelinkflags", "tools.build:sharedlinkflags"]
|
||||
|
||||
12
conanfile.py
12
conanfile.py
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
|
||||
@@ -57,9 +56,6 @@ class Xrpl(ConanFile):
|
||||
"tests": False,
|
||||
"unity": False,
|
||||
"xrpld": False,
|
||||
"boost/*:without_context": False,
|
||||
"boost/*:without_coroutine": True,
|
||||
"boost/*:without_coroutine2": False,
|
||||
"date/*:header_only": True,
|
||||
"ed25519/*:shared": False,
|
||||
"grpc/*:shared": False,
|
||||
@@ -129,12 +125,6 @@ class Xrpl(ConanFile):
|
||||
if self.settings.compiler in ["clang", "gcc"]:
|
||||
self.options["boost"].without_cobalt = True
|
||||
|
||||
# Check if environment variable exists
|
||||
if "SANITIZERS" in os.environ:
|
||||
sanitizers = os.environ["SANITIZERS"]
|
||||
if "address" in sanitizers.lower():
|
||||
self.default_options["fPIC"] = False
|
||||
|
||||
def requirements(self):
|
||||
self.requires("boost/1.90.0", force=True, transitive_headers=True)
|
||||
self.requires("date/3.0.4", transitive_headers=True)
|
||||
@@ -201,7 +191,7 @@ class Xrpl(ConanFile):
|
||||
"boost::headers",
|
||||
"boost::chrono",
|
||||
"boost::container",
|
||||
"boost::context",
|
||||
"boost::coroutine",
|
||||
"boost::date_time",
|
||||
"boost::filesystem",
|
||||
"boost::json",
|
||||
|
||||
@@ -104,7 +104,6 @@ words:
|
||||
- endmacro
|
||||
- exceptioned
|
||||
- Falco
|
||||
- fcontext
|
||||
- finalizers
|
||||
- firewalled
|
||||
- fmtdur
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
template <class F>
|
||||
@@ -8,18 +10,16 @@ JobQueue::Coro::Coro(Coro_create_t, JobQueue& jq, JobType type, std::string cons
|
||||
, type_(type)
|
||||
, name_(name)
|
||||
, coro_(
|
||||
// Stack size of 1MB wasn't sufficient for deep calls. ASAN tests flagged the issue. Hence
|
||||
// increasing the size to 1.5MB.
|
||||
boost::context::protected_fixedsize_stack(1536 * 1024),
|
||||
[this, fn = std::forward<F>(f)](
|
||||
boost::coroutines2::asymmetric_coroutine<void>::push_type& do_yield) {
|
||||
boost::coroutines::asymmetric_coroutine<void>::push_type& do_yield) {
|
||||
yield_ = &do_yield;
|
||||
yield();
|
||||
fn(shared_from_this());
|
||||
#ifndef NDEBUG
|
||||
finished_ = true;
|
||||
#endif
|
||||
})
|
||||
},
|
||||
boost::coroutines::attributes(megabytes(1)))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
#include <xrpl/core/detail/Workers.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <boost/context/protected_fixedsize_stack.hpp>
|
||||
#include <boost/coroutine2/all.hpp>
|
||||
#include <boost/coroutine/all.hpp>
|
||||
|
||||
#include <set>
|
||||
|
||||
@@ -49,8 +48,8 @@ public:
|
||||
std::mutex mutex_;
|
||||
std::mutex mutex_run_;
|
||||
std::condition_variable cv_;
|
||||
boost::coroutines2::coroutine<void>::pull_type coro_;
|
||||
boost::coroutines2::coroutine<void>::push_type* yield_;
|
||||
boost::coroutines::asymmetric_coroutine<void>::pull_type coro_;
|
||||
boost::coroutines::asymmetric_coroutine<void>::push_type* yield_;
|
||||
#ifndef NDEBUG
|
||||
bool finished_ = false;
|
||||
#endif
|
||||
|
||||
@@ -42,8 +42,8 @@ private:
|
||||
public:
|
||||
using value_type = STAmount;
|
||||
|
||||
static constexpr int cMinOffset = -96;
|
||||
static constexpr int cMaxOffset = 80;
|
||||
static int const cMinOffset = -96;
|
||||
static int const cMaxOffset = 80;
|
||||
|
||||
// Maximum native value supported by the code
|
||||
constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull;
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
# The idea is to empty this file gradually by fixing the underlying issues and removing suppressions.
|
||||
#
|
||||
# ASAN_OPTIONS="print_stacktrace=1:detect_container_overflow=0:suppressions=sanitizers/suppressions/asan.supp:halt_on_error=0"
|
||||
#
|
||||
# The detect_container_overflow=0 option disables false positives from:
|
||||
# - Boost intrusive containers (slist_iterator.hpp, hashtable.hpp, aged_unordered_container.h)
|
||||
# - Boost context/coroutine stack switching (Workers.cpp, thread.h)
|
||||
#
|
||||
# See: https://github.com/google/sanitizers/wiki/AddressSanitizerContainerOverflow
|
||||
|
||||
# Boost
|
||||
interceptor_name:boost/asio
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -1847,18 +1847,8 @@ loanMakePayment(
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// overpayment handling
|
||||
//
|
||||
// If the "fixSecurity3_1_3" amendment is enabled, truncate "amount",
|
||||
// at the loan scale. If the raw value is used, the overpayment
|
||||
// amount could be meaningless dust. Trying to process such a small
|
||||
// amount will, at best, waste time when all the result values round
|
||||
// to zero. At worst, it can cause logical errors with tiny amounts
|
||||
// of interest that don't add up correctly.
|
||||
auto const roundedAmount = view.rules().enabled(fixSecurity3_1_3)
|
||||
? roundToAsset(asset, amount, loanScale, Number::towards_zero)
|
||||
: amount;
|
||||
if (paymentType == LoanPaymentType::overpayment && loan->isFlag(lsfLoanOverpayment) &&
|
||||
paymentRemainingProxy > 0 && totalPaid < roundedAmount &&
|
||||
paymentRemainingProxy > 0 && totalPaid < amount &&
|
||||
numPayments < loanMaximumPaymentsPerTransaction)
|
||||
{
|
||||
TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
|
||||
@@ -1867,7 +1857,7 @@ loanMakePayment(
|
||||
// It shouldn't be possible for the overpayment to be greater than
|
||||
// totalValueOutstanding, because that would have been processed as
|
||||
// another normal payment. But cap it just in case.
|
||||
Number const overpayment = std::min(roundedAmount - totalPaid, *totalValueOutstandingProxy);
|
||||
Number const overpayment = std::min(amount - totalPaid, *totalValueOutstandingProxy);
|
||||
|
||||
detail::ExtendedPaymentComponents const overpaymentComponents =
|
||||
detail::computeOverpaymentComponents(
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -438,10 +437,9 @@ LoanPay::doApply()
|
||||
// Vault object state changes
|
||||
view.update(vaultSle);
|
||||
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const assetsTotalBefore = *assetsTotalProxy;
|
||||
#if !NDEBUG
|
||||
{
|
||||
Number const assetsAvailableBefore = *assetsAvailableProxy;
|
||||
Number const pseudoAccountBalanceBefore = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
@@ -465,6 +463,16 @@ LoanPay::doApply()
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
|
||||
if (*assetsAvailableProxy > *assetsTotalProxy)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.fatal()) << "Vault assets available must not be greater "
|
||||
"than assets outstanding. Available: "
|
||||
<< *assetsAvailableProxy << ", Total: " << *assetsTotalProxy;
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
|
||||
<< ", total paid to vault rounded: " << totalPaidToVaultRounded
|
||||
<< ", total paid to broker: " << totalPaidToBroker
|
||||
@@ -490,68 +498,12 @@ LoanPay::doApply()
|
||||
associateAsset(*vaultSle, asset);
|
||||
|
||||
// Duplicate some checks after rounding
|
||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||
Number const assetsTotalAfter = *assetsTotalProxy;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter <= assetsTotalAfter,
|
||||
*assetsAvailableProxy <= *assetsTotalProxy,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"assets available must not be greater than assets outstanding");
|
||||
if (assetsAvailableAfter == assetsAvailableBefore)
|
||||
{
|
||||
// An unchanged assetsAvailable indicates that the amount paid to the
|
||||
// vault was zero, or rounded to zero. That should be impossible, but I
|
||||
// can't rule it out for extreme edge cases, so fail gracefully if it
|
||||
// happens.
|
||||
//
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.warn()) << "LoanPay: Vault assets available unchanged after rounding: " //
|
||||
<< "Before: " << assetsAvailableBefore //
|
||||
<< ", After: " << assetsAvailableAfter;
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (paymentParts->valueChange != beast::zero && assetsTotalAfter == assetsTotalBefore)
|
||||
{
|
||||
// Non-zero valueChange with an unchanged assetsTotal indicates that the
|
||||
// actual value change rounded to zero. That should be impossible, but I
|
||||
// can't rule it out for extreme edge cases, so fail gracefully if it
|
||||
// happens.
|
||||
//
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.warn())
|
||||
<< "LoanPay: Vault assets expected change, but unchanged after rounding: " //
|
||||
<< "Before: " << assetsTotalBefore //
|
||||
<< ", After: " << assetsTotalAfter //
|
||||
<< ", ValueChange: " << paymentParts->valueChange;
|
||||
return tecPRECISION_LOSS;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (paymentParts->valueChange == beast::zero && assetsTotalAfter != assetsTotalBefore)
|
||||
{
|
||||
// A change in assetsTotal when there was no valueChange indicates that
|
||||
// something really weird happened. That should be flat out impossible.
|
||||
//
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.fatal()) << "LoanPay: Vault assets changed unexpectedly after rounding: " //
|
||||
<< "Before: " << assetsTotalBefore //
|
||||
<< ", After: " << assetsTotalAfter //
|
||||
<< ", ValueChange: " << paymentParts->valueChange;
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
if (assetsAvailableAfter > assetsTotalAfter)
|
||||
{
|
||||
// Assets available are not allowed to be larger than assets total.
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.fatal()) << "LoanPay: Vault assets available must not be greater "
|
||||
"than assets outstanding. Available: "
|
||||
<< assetsAvailableAfter << ", Total: " << assetsTotalAfter;
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// These three values are used to check that funds are conserved after the transfers
|
||||
#if !NDEBUG
|
||||
auto const accountBalanceBefore = accountHolds(
|
||||
view,
|
||||
account_,
|
||||
@@ -580,6 +532,7 @@ LoanPay::doApply()
|
||||
ahIGNORE_AUTH,
|
||||
j_,
|
||||
SpendableHandling::shFULL_BALANCE);
|
||||
#endif
|
||||
|
||||
if (totalPaidToVaultRounded != beast::zero)
|
||||
{
|
||||
@@ -615,22 +568,19 @@ LoanPay::doApply()
|
||||
return ter;
|
||||
|
||||
#if !NDEBUG
|
||||
{
|
||||
Number const pseudoAccountBalanceAfter = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
AuthHandling::ahIGNORE_AUTH,
|
||||
j_);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter == pseudoAccountBalanceAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees after");
|
||||
}
|
||||
#endif
|
||||
Number const assetsAvailableAfter = *assetsAvailableProxy;
|
||||
Number const pseudoAccountBalanceAfter = accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
AuthHandling::ahIGNORE_AUTH,
|
||||
j_);
|
||||
XRPL_ASSERT_PARTS(
|
||||
assetsAvailableAfter == pseudoAccountBalanceAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault pseudo balance agrees after");
|
||||
|
||||
// Check that funds are conserved
|
||||
auto const accountBalanceAfter = accountHolds(
|
||||
view,
|
||||
account_,
|
||||
@@ -659,114 +609,14 @@ LoanPay::doApply()
|
||||
ahIGNORE_AUTH,
|
||||
j_,
|
||||
SpendableHandling::shFULL_BALANCE);
|
||||
auto const balanceScale = [&]() {
|
||||
// Find a reasonable scale to use for the balance comparisons.
|
||||
//
|
||||
// First find the minimum and maximum exponent of all the non-zero balances, before and
|
||||
// after. If min and max are equal, use that value. If they are not, use "max + 1" to reduce
|
||||
// rounding discrepancies without making the result meaningless. Cap the scale at
|
||||
// STAmount::cMaxOffset, just in case the numbers are all very large.
|
||||
std::vector<int> exponents;
|
||||
|
||||
for (auto const& a : {
|
||||
accountBalanceBefore,
|
||||
vaultBalanceBefore,
|
||||
brokerBalanceBefore,
|
||||
accountBalanceAfter,
|
||||
vaultBalanceAfter,
|
||||
brokerBalanceAfter,
|
||||
})
|
||||
{
|
||||
// Exclude zeroes
|
||||
if (a != beast::zero)
|
||||
exponents.push_back(a.exponent());
|
||||
}
|
||||
if (exponents.empty())
|
||||
{
|
||||
UNREACHABLE("xrpl::LoanPay::doApply : all zeroes");
|
||||
return 0;
|
||||
}
|
||||
auto const [minItr, maxItr] = std::minmax_element(exponents.begin(), exponents.end());
|
||||
auto const min = *minItr;
|
||||
auto const max = *maxItr;
|
||||
JLOG(j_.trace()) << "Min scale: " << min << ", max scale: " << max;
|
||||
// IOU rounding can be interesting. We want all the balance checks to agree, but don't want
|
||||
// to round to such an extreme that it becomes meaningless. e.g. Everything rounds to one
|
||||
// digit. So add 1 to the max (reducing the number of digits after the decimal point by 1)
|
||||
// if the scales are not already all the same.
|
||||
return std::min(min == max ? max : max + 1, STAmount::cMaxOffset);
|
||||
}();
|
||||
|
||||
auto const accountBalanceBeforeRounded = roundToScale(accountBalanceBefore, balanceScale);
|
||||
auto const vaultBalanceBeforeRounded = roundToScale(vaultBalanceBefore, balanceScale);
|
||||
auto const brokerBalanceBeforeRounded = roundToScale(brokerBalanceBefore, balanceScale);
|
||||
|
||||
auto const totalBalanceBefore = accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore;
|
||||
auto const totalBalanceBeforeRounded = roundToScale(totalBalanceBefore, balanceScale);
|
||||
|
||||
JLOG(j_.trace()) << "Before: " //
|
||||
<< "account " << Number(accountBalanceBeforeRounded) << " ("
|
||||
<< Number(accountBalanceBefore) << ")"
|
||||
<< ", vault " << Number(vaultBalanceBeforeRounded) << " ("
|
||||
<< Number(vaultBalanceBefore) << ")"
|
||||
<< ", broker " << Number(brokerBalanceBeforeRounded) << " ("
|
||||
<< Number(brokerBalanceBefore) << ")"
|
||||
<< ", total " << Number(totalBalanceBeforeRounded) << " ("
|
||||
<< Number(totalBalanceBefore) << ")";
|
||||
|
||||
auto const accountBalanceAfterRounded = roundToScale(accountBalanceAfter, balanceScale);
|
||||
auto const vaultBalanceAfterRounded = roundToScale(vaultBalanceAfter, balanceScale);
|
||||
auto const brokerBalanceAfterRounded = roundToScale(brokerBalanceAfter, balanceScale);
|
||||
|
||||
auto const totalBalanceAfter = accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter;
|
||||
auto const totalBalanceAfterRounded = roundToScale(totalBalanceAfter, balanceScale);
|
||||
|
||||
JLOG(j_.trace()) << "After: " //
|
||||
<< "account " << Number(accountBalanceAfterRounded) << " ("
|
||||
<< Number(accountBalanceAfter) << ")"
|
||||
<< ", vault " << Number(vaultBalanceAfterRounded) << " ("
|
||||
<< Number(vaultBalanceAfter) << ")"
|
||||
<< ", broker " << Number(brokerBalanceAfterRounded) << " ("
|
||||
<< Number(brokerBalanceAfter) << ")"
|
||||
<< ", total " << Number(totalBalanceAfterRounded) << " ("
|
||||
<< Number(totalBalanceAfter) << ")";
|
||||
|
||||
auto const accountBalanceChange = accountBalanceAfter - accountBalanceBefore;
|
||||
auto const vaultBalanceChange = vaultBalanceAfter - vaultBalanceBefore;
|
||||
auto const brokerBalanceChange = brokerBalanceAfter - brokerBalanceBefore;
|
||||
|
||||
auto const totalBalanceChange = accountBalanceChange + vaultBalanceChange + brokerBalanceChange;
|
||||
auto const totalBalanceChangeRounded = roundToScale(totalBalanceChange, balanceScale);
|
||||
|
||||
JLOG(j_.trace()) << "Changes: " //
|
||||
<< "account " << to_string(accountBalanceChange) //
|
||||
<< ", vault " << to_string(vaultBalanceChange) //
|
||||
<< ", broker " << to_string(brokerBalanceChange) //
|
||||
<< ", total " << to_string(totalBalanceChangeRounded) << " ("
|
||||
<< Number(totalBalanceChange) << ")";
|
||||
|
||||
if (totalBalanceBeforeRounded != totalBalanceAfterRounded)
|
||||
{
|
||||
JLOG(j_.warn()) << "Total rounded balances don't match"
|
||||
<< (totalBalanceChangeRounded == beast::zero ? ", but total changes do"
|
||||
: "");
|
||||
}
|
||||
if (totalBalanceChangeRounded != beast::zero)
|
||||
{
|
||||
JLOG(j_.warn()) << "Total balance changes don't match"
|
||||
<< (totalBalanceBeforeRounded == totalBalanceAfterRounded
|
||||
? ", but total balances do"
|
||||
: "");
|
||||
}
|
||||
|
||||
// Rounding for IOUs can be weird, so check a few different ways to show
|
||||
// that funds are conserved.
|
||||
XRPL_ASSERT_PARTS(
|
||||
totalBalanceBeforeRounded == totalBalanceAfterRounded ||
|
||||
totalBalanceChangeRounded == beast::zero,
|
||||
accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore ==
|
||||
accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"funds are conserved (with rounding)");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
accountBalanceAfter >= beast::zero, "xrpl::LoanPay::doApply", "positive account balance");
|
||||
XRPL_ASSERT_PARTS(
|
||||
accountBalanceAfter < accountBalanceBefore || account_ == asset.getIssuer(),
|
||||
"xrpl::LoanPay::doApply",
|
||||
@@ -787,6 +637,7 @@ LoanPay::doApply()
|
||||
vaultBalanceAfter > vaultBalanceBefore || brokerBalanceAfter > brokerBalanceBefore,
|
||||
"xrpl::LoanPay::doApply",
|
||||
"vault and/or broker balance increased");
|
||||
#endif
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -368,11 +368,16 @@ protected:
|
||||
env.balance(vaultPseudo, broker.asset).number());
|
||||
if (ownerCount == 0)
|
||||
{
|
||||
// The Vault must be perfectly balanced if there
|
||||
// are no loans outstanding
|
||||
// Allow some slop for rounding IOUs
|
||||
|
||||
// TODO: This needs to be an exact match once all the
|
||||
// other rounding issues are worked out.
|
||||
auto const total = vaultSle->at(sfAssetsTotal);
|
||||
auto const available = vaultSle->at(sfAssetsAvailable);
|
||||
env.test.BEAST_EXPECT(total == available);
|
||||
env.test.BEAST_EXPECT(
|
||||
total == available ||
|
||||
(!broker.asset.integral() && available != 0 &&
|
||||
((total - available) / available < Number(1, -6))));
|
||||
env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
|
||||
}
|
||||
}
|
||||
@@ -7062,129 +7067,6 @@ protected:
|
||||
BEAST_EXPECT(afterSecondCoverAvailable == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testYieldTheftRounding(std::uint32_t flags)
|
||||
{
|
||||
testcase("Yield Theft via Rounding Manipulation");
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
|
||||
// 1. Setup Environment
|
||||
Env env(*this, all);
|
||||
Account const issuer{"issuer"};
|
||||
Account const lender{"lender"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
env.fund(XRP(1000), issuer, lender, borrower);
|
||||
env.close();
|
||||
|
||||
// 2. Asset Selection
|
||||
PrettyAsset const iou = issuer["USD"];
|
||||
env(trust(lender, iou(100'000'000)));
|
||||
env(trust(borrower, iou(100'000'000)));
|
||||
env(pay(issuer, lender, iou(100'000'000)));
|
||||
env(pay(issuer, borrower, iou(100'000'000)));
|
||||
env.close();
|
||||
|
||||
// 3. Create Vault and Broker with High Debt Limit (100M)
|
||||
auto const brokerInfo = createVaultAndBroker(
|
||||
env,
|
||||
iou,
|
||||
lender,
|
||||
{
|
||||
.vaultDeposit = 5'000'000,
|
||||
.debtMax = Number{100'000'000},
|
||||
.coverDeposit = 500'000,
|
||||
});
|
||||
auto const [currentSeq, vaultId, vaultKeylet] = [&]() {
|
||||
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
|
||||
auto const currentSeq = brokerSle->at(sfLoanSequence);
|
||||
auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID));
|
||||
auto const vaultId = brokerSle->at(sfVaultID);
|
||||
return std::make_tuple(currentSeq, vaultId, vaultKeylet);
|
||||
}();
|
||||
|
||||
// 4. Loan Parameters (Attack Vector)
|
||||
Number const principal = 1'000'000;
|
||||
TenthBips32 const interestRate = TenthBips32{1}; // 0.001%
|
||||
std::uint32_t const paymentInterval = 86400;
|
||||
std::uint32_t const paymentTotal = 3650;
|
||||
|
||||
auto const loanSetFee = fee(env.current()->fees().base * 2);
|
||||
env(set(borrower, brokerInfo.brokerID, iou(principal).value(), flags),
|
||||
sig(sfCounterpartySignature, lender),
|
||||
loan::interestRate(interestRate),
|
||||
loan::paymentInterval(paymentInterval),
|
||||
loan::paymentTotal(paymentTotal),
|
||||
fee(loanSetFee));
|
||||
env.close();
|
||||
|
||||
// --- RETRIEVE OBJECTS & SETUP ATTACK ---
|
||||
|
||||
auto borrowerBalance = [&]() { return env.balance(borrower, iou); };
|
||||
auto const borrowerScale = static_cast<STAmount const&>(borrowerBalance()).exponent();
|
||||
|
||||
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq);
|
||||
auto const [periodicPayment, loanScale] = [&]() {
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
// Construct Payment
|
||||
return std::make_tuple(
|
||||
STAmount{iou, loanSle->at(sfPeriodicPayment)}, loanSle->at(sfLoanScale));
|
||||
}();
|
||||
auto const roundedPayment = roundToScale(periodicPayment, borrowerScale, Number::upward);
|
||||
|
||||
// ATTACK: Add dust buffer (1e-9) to force 'excess' logic execution
|
||||
STAmount const paymentBuffer{iou, Number(1, -9)};
|
||||
STAmount const attackPayment = periodicPayment + paymentBuffer;
|
||||
|
||||
auto const initialVaultAssets = env.le(vaultKeylet)->at(sfAssetsTotal);
|
||||
|
||||
// 5. Execution Loop
|
||||
int yieldTheftCount = 0;
|
||||
auto previousAssetsTotal = initialVaultAssets;
|
||||
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
auto const balanceBefore = borrowerBalance();
|
||||
env(pay(borrower, loanKeylet.key, attackPayment, flags));
|
||||
env.close();
|
||||
auto const borrowerDelta = balanceBefore - borrowerBalance();
|
||||
BEAST_EXPECT(borrowerDelta.signum() == roundedPayment.signum());
|
||||
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
if (!BEAST_EXPECT(loanSle))
|
||||
break;
|
||||
auto const updatedPayment = STAmount{iou, loanSle->at(sfPeriodicPayment)};
|
||||
BEAST_EXPECT(
|
||||
(roundToScale(updatedPayment, borrowerScale, Number::upward) == roundedPayment));
|
||||
BEAST_EXPECT(
|
||||
(updatedPayment == periodicPayment) ||
|
||||
(flags == tfLoanOverpayment && i >= 2 && updatedPayment < periodicPayment));
|
||||
|
||||
auto const currentVaultSle = env.le(vaultKeylet);
|
||||
if (!BEAST_EXPECT(currentVaultSle))
|
||||
break;
|
||||
|
||||
auto const currentAssetsTotal = currentVaultSle->at(sfAssetsTotal);
|
||||
auto const delta = currentAssetsTotal - previousAssetsTotal;
|
||||
|
||||
BEAST_EXPECT(
|
||||
(delta == beast::zero && borrowerDelta <= roundedPayment) ||
|
||||
(delta > beast::zero && borrowerDelta > roundedPayment));
|
||||
|
||||
// If tx succeeded but Assets Total didn't change, interest was
|
||||
// stolen.
|
||||
if (delta == beast::zero && borrowerDelta > roundedPayment)
|
||||
{
|
||||
yieldTheftCount++;
|
||||
}
|
||||
|
||||
previousAssetsTotal = currentAssetsTotal;
|
||||
}
|
||||
|
||||
BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -7193,11 +7075,6 @@ public:
|
||||
testLoanPayLateFullPaymentBypassesPenalties();
|
||||
testLoanCoverMinimumRoundingExploit();
|
||||
#endif
|
||||
for (auto const flags : {0u, tfLoanOverpayment})
|
||||
{
|
||||
testYieldTheftRounding(flags);
|
||||
}
|
||||
|
||||
testInvalidLoanSet();
|
||||
|
||||
auto const all = jtx::testable_amendments();
|
||||
|
||||
Reference in New Issue
Block a user