Compare commits

..

27 Commits

Author SHA1 Message Date
Bart
6780d42a21 Use std::bit_cast 2026-02-12 11:17:18 -05:00
Bart
360843f447 Merge branch 'develop' into bthomee/key 2026-02-12 09:48:45 -05:00
Pratik Mankawde
11e8d1f8a2 chore: Fix gcov lib coverage build failure on macOS (#6350)
For coverage builds, we try to link against the `gcov` library (specific to the environment). But as macOS doesn't have this library and thus doesn't have the coverage tools to generate reports, the coverage builds on that platform were failing on linking.

We actually don't need to explicitly force this linking, as the `CodeCoverage` file already has correct detection logic (currently on lines 177-193), which is invoked when the `--coverage` flag is provided:
* AppleClang: Uses `xcrun -f llvm-cov` to set `GCOV_TOOL="llvm-cov gcov"`.
* Clang: Finds `llvm-cov` to set `GCOV_TOOL="llvm-cov gcov"`.
* GCC: Finds `gcov` to set `GCOV_TOOL="gcov"`.
The `GCOV_TOOL` is then passed to `gcovr` on line 416, so the correct tool is used for processing coverage data.

This change therefore removes the `gcov` suffix from lines 473 and 475 in the `CodeCoverage.cmake` file.
2026-02-12 06:11:26 -05:00
Bart
578a9853f2 Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 10:13:22 -05:00
Bart
a3f5a05f95 Remove unnecessary newline 2026-02-11 09:16:42 -05:00
Bart
02db2943f3 Merge branch 'develop' into bthomee/key 2026-02-11 09:13:48 -05:00
Bart
803cd100f3 refactor: Use uint256 directly as key instead of void pointer 2026-02-02 11:39:53 -05:00
Bart
9639b79155 Copilot feedback 2026-02-02 10:59:45 -05:00
Bart
eabd485927 Merge branch 'develop' into bthomee/disable-cache 2026-01-29 09:28:48 +00:00
Bart
f3ea3e9646 Merge branch 'develop' into bthomee/disable-cache 2025-11-22 11:06:58 -05:00
Bart Thomee
4306d9ccc3 Restore freshening caches of tree node cache 2025-09-17 12:17:35 -04:00
Bart Thomee
1a4d9732ca Merge branch 'develop' into bthomee/disable-cache 2025-09-17 11:43:06 -04:00
Bart
aad6edb6b1 Merge branch 'develop' into bthomee/disable-cache 2025-08-13 08:03:40 -04:00
Bart
a4a1c4eecf Merge branch 'develop' into bthomee/disable-cache 2025-07-03 15:43:50 -04:00
Bart
fca6a8768f Merge branch 'develop' into bthomee/disable-cache 2025-06-02 12:02:43 -04:00
Bart
d96c4164b9 Merge branch 'develop' into bthomee/disable-cache 2025-05-22 09:18:07 -04:00
Bart Thomee
965fc75e8a Reserve vector size 2025-05-20 10:07:12 -04:00
Bart Thomee
2fa1c711d3 Removed unused config values 2025-05-20 09:50:13 -04:00
Bart Thomee
4650e7d2c6 Removed unused caches from SHAMapStoreImp 2025-05-20 09:49:55 -04:00
Bart Thomee
a213127852 Remove cache from SHAMapStoreImp 2025-05-19 16:59:43 -04:00
Bart Thomee
6e7537dada Remove cache from DatabaseNodeImp 2025-05-19 16:51:32 -04:00
Bart Thomee
0777f7c64b Merge branch 'develop' into bthomee/disable-cache 2025-05-19 16:37:11 -04:00
Bart Thomee
39bfcaf95c Merge branch 'develop' into bthomee/disable-cache 2025-05-17 18:26:07 -04:00
Bart Thomee
61c9a19868 Merge branch 'develop' into bthomee/disable-cache 2025-05-07 11:02:43 -04:00
Bart Thomee
d01851bc5a Only disable the database cache 2025-04-01 13:24:18 -04:00
Bart Thomee
d1703842e7 Fully disable cache 2025-04-01 11:41:20 -04:00
Bart Thomee
8d31b1739d TEST: Disable tagged cache to measure performance 2025-03-28 13:21:19 -04:00
29 changed files with 1494 additions and 3935 deletions

View File

@@ -466,11 +466,6 @@ function (add_code_coverage_to_target name scope)
target_compile_options(${name} ${scope} $<$<COMPILE_LANGUAGE:CXX>:${COVERAGE_CXX_COMPILER_FLAGS}>
$<$<COMPILE_LANGUAGE:C>:${COVERAGE_C_COMPILER_FLAGS}>)
target_link_libraries(
${name}
${scope}
$<$<LINK_LANGUAGE:CXX>:${COVERAGE_CXX_LINKER_FLAGS}
gcov>
$<$<LINK_LANGUAGE:C>:${COVERAGE_C_LINKER_FLAGS}
gcov>)
target_link_libraries(${name} ${scope} $<$<LINK_LANGUAGE:CXX>:${COVERAGE_CXX_LINKER_FLAGS}>
$<$<LINK_LANGUAGE:C>:${COVERAGE_C_LINKER_FLAGS}>)
endfunction () # add_code_coverage_to_target

View File

@@ -105,9 +105,6 @@ private:
template <class T>
concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::uint64_t>;
template <class STAmount, class Asset>
concept CanUseAsScale = requires(Asset a, Number n) { STAmount(a, n); } && requires(STAmount s) { s.exponent(); };
/** Number is a floating point type that can represent a wide range of values.
*
* It can represent all values that can be represented by an STAmount -
@@ -256,26 +253,6 @@ public:
constexpr int
exponent() const noexcept;
/** Get the scale of this Number for the given asset.
*
* "scale" is similar to "exponent", but from the perspective of STAmount,
* which has different rules for determining the exponent than Number.
*
* Because Number does not have access to STAmount or Asset, this function
* is implemented as a template, with the expectation that it will only be
* used by those types. Any types that fit the requirements will work,
* though, if there's a need.
*
* @tparam STAmount The STAmount type.
* @tparam Asset The Asset type.
* @param asset The asset to use for determining the scale.
* @return The scale of this Number for the given asset.
*/
template <class STAmount, class Asset>
int
scale(Asset const& asset) const
requires CanUseAsScale<STAmount, Asset>;
constexpr Number
operator+() const noexcept;
constexpr Number
@@ -597,14 +574,6 @@ Number::exponent() const noexcept
return e;
}
template <class STAmount, class Asset>
int
Number::scale(Asset const& asset) const
requires CanUseAsScale<STAmount, Asset>
{
return STAmount{asset, *this}.exponent();
}
inline constexpr Number
Number::operator+() const noexcept
{

View File

@@ -76,16 +76,16 @@ public:
If the object is not found or an error is encountered, the
result will indicate the condition.
@note This will be called concurrently.
@param key A pointer to the key data.
@param hash The hash of the object.
@param pObject [out] The created object if successful.
@return The result of the operation.
*/
virtual Status
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) = 0;
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pObject) = 0;
/** Fetch a batch synchronously. */
virtual std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
fetchBatch(std::vector<uint256 const*> const& hashes) = 0;
fetchBatch(std::vector<uint256> const& hashes) = 0;
/** Store a single object.
Depending on the implementation this may happen immediately

View File

@@ -42,8 +42,8 @@ private:
public:
using value_type = STAmount;
static int constexpr cMinOffset = -96;
static int constexpr 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;

View File

@@ -15,9 +15,6 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FIX (DefaultCoverLogicOptimization, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (LendingProtocolV1_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (BatchInnerSigs, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -29,7 +29,7 @@ DatabaseNodeImp::fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport
try
{
status = backend_->fetch(hash.data(), &nodeObject);
status = backend_->fetch(hash, &nodeObject);
}
catch (std::exception const& e)
{
@@ -62,18 +62,10 @@ DatabaseNodeImp::fetchBatch(std::vector<uint256> const& hashes)
using namespace std::chrono;
auto const before = steady_clock::now();
std::vector<uint256 const*> batch{};
batch.reserve(hashes.size());
for (size_t i = 0; i < hashes.size(); ++i)
{
auto const& hash = hashes[i];
batch.push_back(&hash);
}
// Get the node objects that match the hashes from the backend. To protect
// against the backends returning fewer or more results than expected, the
// container is resized to the number of hashes.
auto results = backend_->fetchBatch(batch).first;
auto results = backend_->fetchBatch(hashes).first;
XRPL_ASSERT(
results.size() == hashes.size() || results.empty(),
"number of output objects either matches number of input hashes or is empty");

View File

@@ -101,7 +101,7 @@ DatabaseRotatingImp::fetchNodeObject(uint256 const& hash, std::uint32_t, FetchRe
std::shared_ptr<NodeObject> nodeObject;
try
{
status = backend->fetch(hash.data(), &nodeObject);
status = backend->fetch(hash, &nodeObject);
}
catch (std::exception const& e)
{

View File

@@ -115,10 +115,9 @@ public:
//--------------------------------------------------------------------------
Status
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) override
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pObject) override
{
XRPL_ASSERT(db_, "xrpl::NodeStore::MemoryBackend::fetch : non-null database");
uint256 const hash(uint256::fromVoid(key));
std::lock_guard _(db_->mutex);
@@ -133,14 +132,14 @@ public:
}
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
fetchBatch(std::vector<uint256 const*> const& hashes) override
fetchBatch(std::vector<uint256> const& hashes) override
{
std::vector<std::shared_ptr<NodeObject>> results;
results.reserve(hashes.size());
for (auto const& h : hashes)
{
std::shared_ptr<NodeObject> nObj;
Status status = fetch(h->begin(), &nObj);
Status status = fetch(h, &nObj);
if (status != ok)
results.push_back({});
else

View File

@@ -177,17 +177,17 @@ public:
}
Status
fetch(void const* key, std::shared_ptr<NodeObject>* pno) override
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pno) override
{
Status status;
pno->reset();
nudb::error_code ec;
db_.fetch(
key,
[key, pno, &status](void const* data, std::size_t size) {
hash.data(),
[hash, pno, &status](void const* data, std::size_t size) {
nudb::detail::buffer bf;
auto const result = nodeobject_decompress(data, size, bf);
DecodedBlob decoded(key, result.first, result.second);
DecodedBlob decoded(hash.data(), result.first, result.second);
if (!decoded.wasOk())
{
status = dataCorrupt;
@@ -205,14 +205,14 @@ public:
}
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
fetchBatch(std::vector<uint256 const*> const& hashes) override
fetchBatch(std::vector<uint256> const& hashes) override
{
std::vector<std::shared_ptr<NodeObject>> results;
results.reserve(hashes.size());
for (auto const& h : hashes)
{
std::shared_ptr<NodeObject> nObj;
Status status = fetch(h->begin(), &nObj);
Status status = fetch(h, &nObj);
if (status != ok)
results.push_back({});
else

View File

@@ -36,13 +36,13 @@ public:
}
Status
fetch(void const*, std::shared_ptr<NodeObject>*) override
fetch(uint256 const&, std::shared_ptr<NodeObject>*) override
{
return notFound;
}
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
fetchBatch(std::vector<uint256 const*> const& hashes) override
fetchBatch(std::vector<uint256> const& hashes) override
{
return {};
}

View File

@@ -237,7 +237,7 @@ public:
//--------------------------------------------------------------------------
Status
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) override
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pObject) override
{
XRPL_ASSERT(m_db, "xrpl::NodeStore::RocksDBBackend::fetch : non-null database");
pObject->reset();
@@ -245,7 +245,7 @@ public:
Status status(ok);
rocksdb::ReadOptions const options;
rocksdb::Slice const slice(static_cast<char const*>(key), m_keyBytes);
rocksdb::Slice const slice(std::bit_cast<char const*>(hash.data()), m_keyBytes);
std::string string;
@@ -253,7 +253,7 @@ public:
if (getStatus.ok())
{
DecodedBlob decoded(key, string.data(), string.size());
DecodedBlob decoded(hash.data(), string.data(), string.size());
if (decoded.wasOk())
{
@@ -288,14 +288,14 @@ public:
}
std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
fetchBatch(std::vector<uint256 const*> const& hashes) override
fetchBatch(std::vector<uint256> const& hashes) override
{
std::vector<std::shared_ptr<NodeObject>> results;
results.reserve(hashes.size());
for (auto const& h : hashes)
{
std::shared_ptr<NodeObject> nObj;
Status status = fetch(h->begin(), &nObj);
Status status = fetch(h, &nObj);
if (status != ok)
results.push_back({});
else

View File

@@ -2581,6 +2581,11 @@ class Batch_test : public beast::unit_test::suite
{
testcase("loan");
bool const lendingBatchEnabled =
!std::any_of(Batch::disabledTxTypes.begin(), Batch::disabledTxTypes.end(), [](auto const& disabled) {
return disabled == ttLOAN_BROKER_SET;
});
using namespace test::jtx;
test::jtx::Env env{*this, features | featureSingleAssetVault | featureLendingProtocol | featureMPTokensV1};
@@ -2640,7 +2645,7 @@ class Batch_test : public beast::unit_test::suite
{
auto const [txIDs, batchID] = submitBatch(
env,
temBAD_SIGNATURE,
lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
@@ -2671,7 +2676,7 @@ class Batch_test : public beast::unit_test::suite
{
auto const [txIDs, batchID] = submitBatch(
env,
temBAD_SIGNER,
lendingBatchEnabled ? temBAD_SIGNER : temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
@@ -2691,7 +2696,7 @@ class Batch_test : public beast::unit_test::suite
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
TER(tesSUCCESS),
lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
@@ -2723,7 +2728,7 @@ class Batch_test : public beast::unit_test::suite
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
TER(tesSUCCESS),
lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
@@ -2738,7 +2743,8 @@ class Batch_test : public beast::unit_test::suite
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
if (auto const sleLoan = env.le(loanKeylet); BEAST_EXPECT(sleLoan))
if (auto const sleLoan = env.le(loanKeylet);
lendingBatchEnabled ? BEAST_EXPECT(sleLoan) : !BEAST_EXPECT(!sleLoan))
{
BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
}

View File

@@ -4,7 +4,6 @@
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/ApplyContext.h>
#include <xrpld/app/tx/detail/InvariantCheck.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
@@ -21,9 +20,6 @@
#include <boost/algorithm/string/predicate.hpp>
#include <initializer_list>
#include <string>
namespace xrpl {
namespace test {
@@ -3711,124 +3707,6 @@ class Invariants_test : public beast::unit_test::suite
precloseMpt);
}
void
testVaultComputeMinScale()
{
using namespace jtx;
Account const issuer{"issuer"};
PrettyAsset const vaultAsset = issuer["IOU"];
struct TestCase
{
std::string name;
std::int32_t expectedMinScale;
std::vector<ValidVault::DeltaInfo> values;
};
NumberMantissaScaleGuard g{MantissaRange::large};
auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
return {n, n.scale<STAmount>(vaultAsset.raw())};
};
auto const testCases = std::vector<TestCase>{
{
.name = "No values",
.expectedMinScale = 0,
.values = {},
},
{
.name = "Mixed integer and Number values",
.expectedMinScale = -15,
.values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
},
{
.name = "Mixed scales",
.expectedMinScale = -17,
.values = {makeDelta(Number{1, -2}), makeDelta(Number{5, -3}), makeDelta(Number{3, -2})},
},
{
.name = "Equal scales",
.expectedMinScale = -16,
.values = {makeDelta(Number{1, -1}), makeDelta(Number{5, -1}), makeDelta(Number{1, -1})},
},
{
.name = "Mixed mantissa sizes",
.expectedMinScale = -12,
.values =
{makeDelta(Number{1}),
makeDelta(Number{1234, -3}),
makeDelta(Number{12345, -6}),
makeDelta(Number{123, 1})},
},
};
for (auto const& tc : testCases)
{
testcase("vault computeMinScale: " + tc.name);
auto const actualScale = ValidVault::computeMinScale(vaultAsset, tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
"expected: " + std::to_string(tc.expectedMinScale) + ", actual: " + std::to_string(actualScale));
for (auto const& num : tc.values)
{
// None of these scales are far enough apart that rounding the
// values would lose information, so check that the rounded
// value matches the original.
auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale);
BEAST_EXPECTS(
actualRounded == num.delta,
"number " + to_string(num.delta) + " rounded to scale " + std::to_string(actualScale) + " is " +
to_string(actualRounded));
}
}
auto const testCases2 = std::vector<TestCase>{
{
.name = "False equivalence",
.expectedMinScale = -15,
.values =
{
makeDelta(Number{1234567890123456789, -18}),
makeDelta(Number{12345, -4}),
makeDelta(Number{1}),
},
},
};
// Unlike the first set of test cases, the values in these test could
// look equivalent if using the wrong scale.
for (auto const& tc : testCases2)
{
testcase("vault computeMinScale: " + tc.name);
auto const actualScale = ValidVault::computeMinScale(vaultAsset, tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
"expected: " + std::to_string(tc.expectedMinScale) + ", actual: " + std::to_string(actualScale));
std::optional<Number> first;
Number firstRounded;
for (auto const& num : tc.values)
{
if (!first)
{
first = num.delta;
firstRounded = roundToAsset(vaultAsset, num.delta, actualScale);
continue;
}
auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale);
BEAST_EXPECTS(
numRounded != firstRounded,
"at a scale of " + std::to_string(actualScale) + " " + to_string(num.delta) +
" == " + to_string(*first));
}
}
}
public:
void
run() override
@@ -3854,7 +3732,6 @@ public:
testValidPseudoAccounts();
testValidLoanBroker();
testVault();
testVaultComputeMinScale();
}
};

View File

@@ -3,11 +3,16 @@
#include <test/jtx.h>
#include <test/jtx/Account.h>
#include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/tx/detail/Batch.h>
#include <xrpld/app/tx/detail/LoanSet.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/SField.h>
#include <string>
#include <vector>

File diff suppressed because it is too large Load Diff

View File

@@ -1,728 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
//
#include <test/app/Loan/LoanBase.h>
#include <test/jtx.h>
#include <xrpl/protocol/SField.h>
#include <chrono>
namespace xrpl {
namespace test {
class LoanBatch_test : public LoanBase
{
protected:
void
testDisabled()
{
testcase("Disabled");
// Lending Protocol depends on Single Asset Vault (SAV). Test
// combinations of the two amendments.
// Single Asset Vault depends on MPTokensV1, but don't test every combo
// of that.
using namespace jtx;
auto failAll = [this](FeatureBitset features) {
using namespace loan;
using namespace std::chrono_literals;
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
auto const keylet = keylet::loanbroker(alice, env.seq(alice));
auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice));
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 4);
auto loanSet = set(alice, keylet.key, Number(10000));
loanSet[sfCounterparty] = bob.human();
auto batchTxn = env.jt(
batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
batch::inner(loanSet, aliceSeq),
batch::inner(del(alice, loanKeylet.key), aliceSeq + 1),
batch::inner(manage(alice, loanKeylet.key, tfLoanImpair), aliceSeq + 2),
batch::inner(pay(alice, loanKeylet.key, XRP(500)), aliceSeq + 3),
batch::sig(alice));
env(batchTxn, ter(temINVALID_INNER_BATCH));
};
failAll(all - featureMPTokensV1);
failAll(all - featureSingleAssetVault - featureLendingProtocol);
failAll(all - featureSingleAssetVault);
failAll(all - featureLendingProtocol);
}
void
testCreateAsset()
{
testcase("CreateAsset");
// Checks if a single asset vault can be created in a batch.
using namespace jtx;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
auto const IOU = issuer["IOU"];
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
env(trust(broker, IOU(20'000'000)));
env(pay(issuer, broker, IOU(10'000'000)));
env.close();
auto brokerSeq = env.seq(broker);
// The starting sequence should be brokerSeq + 1 because the batch
// outer transaction will consume the first sequence.
auto txns = createVaultAndBrokerTransactions(env, IOU, broker, BrokerParameters::defaults(), brokerSeq + 1);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto batchTxn = env.jt(
batch::outer(broker, brokerSeq, batchFee, tfAllOrNothing),
batch::inner(txns.vaultCreateTx, brokerSeq + 1),
batch::inner(txns.vaultDepositTx, brokerSeq + 2),
batch::inner(txns.brokerSetTx, brokerSeq + 3),
batch::inner(*txns.coverDepositTx, brokerSeq + 4));
env(batchTxn);
env.close();
checkVaultAndBroker(env, txns);
}
void
testLoanSetAndDelete()
{
testcase("LoanSetAndDelete");
// Checks if LoanSet works in a batch.
using namespace jtx;
using namespace jtx::loan;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
auto const IOU = issuer["IOU"];
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
env(trust(broker, IOU(20'000'000)));
env(pay(issuer, broker, IOU(10'000'000)));
env.close();
auto brokerInfo = createVaultAndBroker(env, IOU, broker);
LoanParameters const loanParams{
.account = broker,
.counter = borrower,
.principalRequest = Number{100},
.interest = TenthBips32{1922},
.payTotal = 5816,
.payInterval = 86400 * 6,
.gracePd = 86400 * 5,
};
auto loanSetTx = loanParams.getTransaction(env, brokerInfo);
// Get the loan keylet that will be created by the LoanSet
auto const brokerSeq = env.seq(broker);
// The loan keylet is based on the broker's LoanSequence
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Create the loan delete transaction
auto loanDelTx = del(broker, loanKeylet.key);
// Calculate batch fee: 1 signer (borrower) + 2 transactions
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Create the batch transaction with both LoanSet and LoanDelete
auto batchTxn = env.jt(
batch::outer(broker, brokerSeq, batchFee, tfAllOrNothing),
batch::inner(loanSetTx, brokerSeq + 1),
batch::inner(loanDelTx, brokerSeq + 2),
batch::sig(borrower));
env(batchTxn);
env.close();
// Verify the loan is not there.
BEAST_EXPECT(!env.le(loanKeylet));
}
void
testLoanSetAndImpair()
{
testcase("LoanSetAndManage");
// Creates a loan and impairs it in a single batch.
// When a loan is impaired, NextPaymentDueDate is set to currentTime.
using namespace jtx;
using namespace jtx::loan;
using namespace std::chrono_literals;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
auto const IOU = issuer["IOU"];
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
env(trust(broker, IOU(20'000'000)));
env(pay(issuer, broker, IOU(10'000'000)));
env.close();
auto brokerInfo = createVaultAndBroker(env, IOU, broker);
LoanParameters const loanParams{
.account = broker,
.counter = borrower,
.principalRequest = Number{100},
.interest = TenthBips32{1922},
.payTotal = 5816,
.payInterval = 86400 * 6, // 6 days
.gracePd = 86400 * 5, // 5 days grace period
};
auto loanSetTx = loanParams.getTransaction(env, brokerInfo);
// Get the loan keylet that will be created by the LoanSet
auto const brokerSeq = env.seq(broker);
// The loan keylet is based on the broker's LoanSequence
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Create the manage transaction to impair the loan
auto impairTx = manage(broker, loanKeylet.key, tfLoanImpair);
// Calculate batch fee: 1 signer (borrower) + 2 transactions
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Create the batch transaction with LoanSet and impair
auto batchTxn = env.jt(
batch::outer(broker, brokerSeq, batchFee, tfAllOrNothing),
batch::inner(loanSetTx, brokerSeq + 1),
batch::inner(impairTx, brokerSeq + 2),
batch::sig(borrower));
auto currentTime = env.now().time_since_epoch().count();
env(batchTxn);
env.close();
// Verify the loan was created and impaired
auto const finalLoanSle = env.le(loanKeylet);
BEAST_EXPECT(finalLoanSle);
BEAST_EXPECT(finalLoanSle->isFlag(lsfLoanImpaired));
// When impaired, NextPaymentDueDate should be set to current time
BEAST_EXPECT(finalLoanSle->at(sfNextPaymentDueDate) == currentTime);
// Verify Vault.LossUnrealized was increased
auto const finalBrokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
BEAST_EXPECT(finalBrokerSle);
if (finalBrokerSle)
{
auto const vaultKeylet = keylet::vault(finalBrokerSle->at(sfVaultID));
auto const vaultSle = env.le(vaultKeylet);
BEAST_EXPECT(vaultSle);
if (vaultSle)
{
// LossUnrealized = TotalValueOutstanding - ManagementFeeOutstanding
auto const expectedLoss =
finalLoanSle->at(sfTotalValueOutstanding) - finalLoanSle->at(sfManagementFeeOutstanding);
BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == expectedLoss);
}
}
}
void
testLoanDefaultWithdrawAndPay(FeatureBitset features)
{
testcase("LoanDefaultWithdrawAndPay");
// Creates a loan, advances time to make it defaultable, then in a batch:
// defaults the loan, withdraws the DefaultCovered amount, and makes a payment.
using namespace jtx;
using namespace jtx::loan;
using namespace std::chrono_literals;
Env env(*this, features);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
Account const recipient{"recipient"};
auto const IOU = issuer[iouCurrency];
env.fund(XRP(20'000), issuer, broker, borrower, recipient);
env.close();
env(trust(broker, IOU(20'000'000)));
env(trust(recipient, IOU(20'000'000)));
env(pay(issuer, broker, IOU(10'000'000)));
env.close();
// Create vault and broker with specific parameters to ensure DefaultCovered > vault available
BrokerParameters brokerParams = BrokerParameters::defaults();
brokerParams.vaultDeposit = 100; // Small vault deposit
brokerParams.coverDeposit = 1000; // Large cover deposit
auto brokerInfo = createVaultAndBroker(env, IOU, broker, brokerParams);
LoanParameters const loanParams{
.account = broker,
.counter = borrower,
.principalRequest = Number{100},
.interest = TenthBips32{1922},
.payTotal = 5816,
.payInterval = 86400 * 6, // 6 days
.gracePd = 86400 * 5, // 5 days grace period
};
// Create the loan first (not in batch)
env(loanParams(env, brokerInfo));
env.close();
// Get the loan keylet
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence) - 1;
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Verify loan exists
BEAST_EXPECT(env.le(loanKeylet));
// Advance time past the payment due date + grace period to make the loan defaultable
auto const loanSle = env.le(loanKeylet);
auto const nextPaymentDue = loanSle->at(sfNextPaymentDueDate);
auto const gracePeriod = loanSle->at(sfGracePeriod);
env.close(std::chrono::seconds{nextPaymentDue + gracePeriod + 60});
// Calculate DefaultCovered before defaulting
auto const totalValue = loanSle->at(sfTotalValueOutstanding);
auto const managementFee = loanSle->at(sfManagementFeeOutstanding);
auto const defaultAmount = totalValue - managementFee;
auto const debtTotal = brokerSle->at(sfDebtTotal);
auto const coverRateMin = TenthBips32{brokerSle->at(sfCoverRateMinimum)};
auto const coverRateLiq = TenthBips32{brokerSle->at(sfCoverRateLiquidation)};
auto const coverAvailable = brokerSle->at(sfCoverAvailable);
Number const defaultCovered = [&] {
// New formula: DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable)
if (env.enabled(fixDefaultCoverLogicOptimization))
{
return std::min(defaultAmount * coverRateMin.value() / tenthBipsPerUnity.value(), coverAvailable);
}
else
{
// MinimumCover = DebtTotal x CoverRateMinimum
Number const minimumCover = debtTotal * coverRateMin.value() / tenthBipsPerUnity.value();
// DefaultCovered = min(MinimumCover x CoverRateLiquidation, DefaultAmount, CoverAvailable)
return std::min(
{minimumCover * coverRateLiq.value() / tenthBipsPerUnity.value(), defaultAmount, coverAvailable});
}
}();
// Get vault available before default
auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
auto const vaultAvailableBefore = vaultSle->at(sfAssetsAvailable);
// Verify DefaultCovered will be greater than vault available
BEAST_EXPECT(defaultCovered > vaultAvailableBefore);
// Now batch: default, withdraw DefaultCovered, and make a payment
auto const brokerSeq = env.seq(broker);
auto defaultTx = manage(broker, loanKeylet.key, tfLoanDefault);
// Withdraw the DefaultCovered amount from vault
Vault vault{env};
auto withdrawTx =
vault.withdraw({.depositor = broker, .id = brokerInfo.vaultID, .amount = brokerInfo.asset(defaultCovered)});
// Make a payment to recipient
auto paymentTx = pay(broker, recipient, brokerInfo.asset(defaultCovered / 2));
// Calculate batch fee: 0 signers (broker signs outer) + 3 transactions
auto const batchFee = batch::calcBatchFee(env, 0, 3);
// Create the batch transaction
auto batchTxn = env.jt(
batch::outer(broker, brokerSeq, batchFee, tfAllOrNothing),
batch::inner(defaultTx, brokerSeq + 1),
batch::inner(withdrawTx, brokerSeq + 2),
batch::inner(paymentTx, brokerSeq + 3));
env(batchTxn);
env.close();
// Verify the loan is defaulted
auto const finalLoanSle = env.le(loanKeylet);
BEAST_EXPECT(finalLoanSle);
if (finalLoanSle)
{
BEAST_EXPECT(finalLoanSle->isFlag(lsfLoanDefault));
BEAST_EXPECT(finalLoanSle->at(sfPaymentRemaining) == 0);
BEAST_EXPECT(finalLoanSle->at(sfTotalValueOutstanding) == 0);
BEAST_EXPECT(finalLoanSle->at(sfPrincipalOutstanding) == 0);
BEAST_EXPECT(finalLoanSle->at(sfManagementFeeOutstanding) == 0);
BEAST_EXPECT(finalLoanSle->at(sfNextPaymentDueDate) == 0);
}
// Verify vault state after default and withdrawal
auto const finalVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
BEAST_EXPECT(finalVaultSle);
if (finalVaultSle)
{
// Vault should have received DefaultCovered, then withdrawn it
auto const expectedAvailable = vaultAvailableBefore + defaultCovered - defaultCovered;
BEAST_EXPECT(finalVaultSle->at(sfAssetsAvailable) == expectedAvailable);
}
// Verify recipient received payment
BEAST_EXPECT(env.balance(recipient, IOU) == brokerInfo.asset(defaultCovered / 2));
}
void
testRiskFreeArbitrage()
{
testcase("RiskFreeArbitrage");
// Demonstrates risk-free arbitrage using batch transactions:
// 1. Borrow funds from a loan
// 2. Buy XRP at one price
// 3. Sell XRP at a higher price
// 4. Repay the loan with interest
// All in a single atomic batch - if any step fails, everything reverts.
using namespace jtx;
using namespace jtx::loan;
using namespace std::chrono_literals;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
Account const marketMaker1{"mm1"}; // Sells XRP at $2.50
Account const marketMaker2{"mm2"}; // Buys XRP at $2.52
auto const IOU = issuer[iouCurrency];
env.fund(XRP(50'000), issuer, broker, borrower, marketMaker1, marketMaker2);
env.close();
// Set up trust lines
env(trust(broker, IOU(20'000'000)));
env(trust(borrower, IOU(20'000'000)));
env(trust(marketMaker1, IOU(20'000'000)));
env(trust(marketMaker2, IOU(20'000'000)));
env.close();
// Fund accounts
env(pay(issuer, broker, IOU(15'000'000)));
env(pay(issuer, marketMaker1, IOU(11'000'000)));
env(pay(issuer, marketMaker2, IOU(11'000'000)));
env.close();
// Create vault and broker
auto brokerInfo = createVaultAndBroker(env, IOU, broker);
// Get the loan keylet BEFORE creating the loan
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Market makers create offers:
// MM1: Sells 400 XRP for $1000 (price: $2.50 per XRP)
// MM2: Buys 400 XRP for $1008 (price: $2.52 per XRP)
env(offer(marketMaker1, IOU(1000), XRP(400)));
env(offer(marketMaker2, XRP(400), IOU(1008)));
env.close();
// Record vault state before batch
auto const initialVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
auto const initialVaultPseudoAccountID = initialVaultSle->at(sfAccount);
Account const initialVaultPseudoAccount("VaultPseudo", initialVaultPseudoAccountID);
auto const initialVaultBalance = env.balance(initialVaultPseudoAccount, IOU);
auto const initialAssetsAvailable = initialVaultSle->at(sfAssetsAvailable);
// Now create the arbitrage batch - ALL IN ONE ATOMIC TRANSACTION:
// 1. Create loan (borrow $1000)
// 2. Buy 400 XRP at $2.50 (cost: $1000)
// 3. Sell 400 XRP at $2.52 (revenue: $1008)
// 4. Repay loan principal + interest ($1000 + $1 = $1001)
// Profit: $1008 - $1001 = $7
auto const borrowerSeq = env.seq(borrower);
// Transaction 1: Create the loan (borrower receives $1000)
LoanParameters const loanParams{
.account = borrower,
.counter = broker,
.principalRequest = Number{1000},
.interest = TenthBips32{100}, // 0.1% interest
.payTotal = 1,
.payInterval = 86400, // 1 day
.gracePd = 86400, // 1 day grace period
};
auto loanSetTx = loanParams.getTransaction(env, brokerInfo);
// Transaction 2: Buy 400 XRP for $1000 from MM1
auto buyTx = offer(borrower, XRP(400), IOU(1000));
// Transaction 3: Sell 400 XRP for $1008 to MM2
auto sellTx = offer(borrower, IOU(1008), XRP(400));
// Transaction 4: Repay the loan
// We know the total will be $1001 (principal $1000 + interest $1)
auto payTx = pay(borrower, loanKeylet.key, IOU(1001));
// Calculate batch fee: 1 signer (broker as counterparty) + 4 transactions
auto const batchFee = batch::calcBatchFee(env, 1, 4);
// Create the batch transaction with tfAllOrNothing
auto batchTxn = env.jt(
batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
batch::inner(loanSetTx, borrowerSeq + 1),
batch::inner(buyTx, borrowerSeq + 2),
batch::inner(sellTx, borrowerSeq + 3),
batch::inner(payTx, borrowerSeq + 4),
batch::sig(broker));
env(batchTxn);
env.close();
// Verify the arbitrage was successful:
// 1. Borrower should have profit (~$7)
// 2. Loan should be fully paid
// 3. Market makers' offers should be consumed
// 4. Vault should have received the interest payment
auto const finalLoanSle = env.le(loanKeylet);
BEAST_EXPECT(finalLoanSle);
if (finalLoanSle)
{
// Loan should be fully paid (or nearly so, depending on rounding)
BEAST_EXPECT(finalLoanSle->at(sfTotalValueOutstanding) < IOU(1).value());
}
// Borrower should have profit:
// Started with $1000 from loan, spent $1000 on XRP, earned $1008 from selling XRP, paid $1001 to repay loan
// Net: $1000 - $1000 + $1008 - $1001 = $7 profit
auto const borrowerBalance = env.balance(borrower, IOU);
BEAST_EXPECT(borrowerBalance > IOU(6) && borrowerBalance < IOU(8));
// Verify offers were consumed
env.require(offers(marketMaker1, 0));
env.require(offers(marketMaker2, 0));
// Verify vault received the interest payment
// The vault should have received $1001 (principal $1000 + interest $1)
auto const finalVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
BEAST_EXPECT(finalVaultSle);
if (finalVaultSle)
{
auto const finalVaultPseudoAccountID = finalVaultSle->at(sfAccount);
BEAST_EXPECT(finalVaultPseudoAccountID == initialVaultPseudoAccountID);
// Check vault pseudo-account balance increased
auto const finalVaultBalance = env.balance(initialVaultPseudoAccount, IOU);
auto const vaultBalanceIncrease = finalVaultBalance - initialVaultBalance;
BEAST_EXPECT(vaultBalanceIncrease > IOU(0) && vaultBalanceIncrease < IOU(3));
// Check AssetsAvailable increased
auto const finalAssetsAvailable = finalVaultSle->at(sfAssetsAvailable);
auto const assetsAvailableIncrease = finalAssetsAvailable - initialAssetsAvailable;
BEAST_EXPECT(assetsAvailableIncrease > Number{0} && assetsAvailableIncrease < Number{2});
}
}
void
testRiskFreeArbitrageFails()
{
testcase("RiskFreeArbitrageFails");
// Demonstrates that batch transactions with tfAllOrNothing revert completely
// when any transaction fails. In this case, we make it impossible to sell
// the XRP at the higher price, which causes the entire batch to fail,
// including the loan creation.
using namespace jtx;
using namespace jtx::loan;
using namespace std::chrono_literals;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
Account const marketMaker1{"mm1"}; // Sells XRP at $2.50
auto const IOU = issuer[iouCurrency];
env.fund(XRP(50'000), issuer, broker, borrower, marketMaker1);
env.close();
// Set up trust lines
env(trust(broker, IOU(20'000'000)));
env(trust(borrower, IOU(20'000'000)));
env(trust(marketMaker1, IOU(20'000'000)));
env.close();
// Fund accounts
env(pay(issuer, broker, IOU(15'000'000)));
env(pay(issuer, marketMaker1, IOU(11'000'000)));
env.close();
// Create vault and broker
auto brokerInfo = createVaultAndBroker(env, IOU, broker);
// Get the loan keylet BEFORE creating the loan
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Market maker creates only ONE offer:
// MM1: Sells 400 XRP for $1000 (price: $2.50 per XRP)
// NOTE: No MM2 offer to buy XRP at higher price!
env(offer(marketMaker1, IOU(1000), XRP(400)));
env.close();
// Record initial state
auto const initialBorrowerBalance = env.balance(borrower, IOU);
auto const initialBrokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const initialLoanSequence = initialBrokerSle->at(sfLoanSequence);
// Record vault state before batch
auto const initialVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
auto const initialVaultPseudoAccountID = initialVaultSle->at(sfAccount);
Account const initialVaultPseudoAccount("VaultPseudo", initialVaultPseudoAccountID);
auto const initialVaultBalance = env.balance(initialVaultPseudoAccount, IOU);
auto const initialAssetsAvailable = initialVaultSle->at(sfAssetsAvailable);
// Try to create the arbitrage batch - this should FAIL:
// 1. Create loan (borrow $1000)
// 2. Buy 400 XRP at $2.50 (cost: $1000) - succeeds
// 3. Sell 400 XRP at $2.52 (revenue: $1008) - FAILS (no offer available)
// 4. Repay loan principal + interest ($1000 + $1 = $1001)
// Because of tfAllOrNothing, the entire batch should revert!
auto const borrowerSeq = env.seq(borrower);
// Transaction 1: Create the loan (borrower receives $1000)
LoanParameters const loanParams{
.account = borrower,
.counter = broker,
.principalRequest = Number{1000},
.interest = TenthBips32{100}, // 0.1% interest
.payTotal = 1,
.payInterval = 86400, // 1 day
.gracePd = 86400, // 1 day grace period
};
auto loanSetTx = loanParams.getTransaction(env, brokerInfo);
// Transaction 2: Buy 400 XRP for $1000 from MM1
auto buyTx = offer(borrower, XRP(400), IOU(1000));
buyTx[jss::Flags] = tfFillOrKill;
// Transaction 3: Try to sell 400 XRP for $1008 - this will FAIL
auto sellTx = offer(borrower, XRP(400), IOU(1008));
sellTx[jss::Flags] = tfFillOrKill;
// Transaction 4: Repay the loan
auto payTx = pay(borrower, loanKeylet.key, IOU(1001));
// Calculate batch fee: 1 signer (broker as counterparty) + 4 transactions
auto const batchFee = batch::calcBatchFee(env, 1, 4);
// Create the batch transaction with tfAllOrNothing
auto batchTxn = env.jt(
batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
batch::inner(loanSetTx, borrowerSeq + 1),
batch::inner(buyTx, borrowerSeq + 2),
batch::inner(sellTx, borrowerSeq + 3),
batch::inner(payTx, borrowerSeq + 4),
batch::sig(broker));
env(batchTxn);
env.close();
// Verify that EVERYTHING was reverted:
// 1. Loan was NOT created
// 2. Borrower balance unchanged (except for fee)
// 3. LoanSequence unchanged
// 4. Market maker's offer still exists
// 5. Vault received NOTHING
// Loan should NOT exist
auto const finalLoanSle = env.le(loanKeylet);
BEAST_EXPECT(!finalLoanSle);
// Borrower balance should be unchanged (except for batch fee)
auto const finalBorrowerBalance = env.balance(borrower, IOU);
BEAST_EXPECT(finalBorrowerBalance == initialBorrowerBalance);
// LoanSequence should be unchanged (loan was never created)
auto const finalBrokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const finalLoanSequence = finalBrokerSle->at(sfLoanSequence);
BEAST_EXPECT(finalLoanSequence == initialLoanSequence);
// Market maker's offer should still exist (not consumed)
env.require(offers(marketMaker1, 1));
// Verify vault received NOTHING (no loan was created, so no repayment)
auto const finalVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
BEAST_EXPECT(finalVaultSle);
if (finalVaultSle)
{
auto const finalVaultPseudoAccountID = finalVaultSle->at(sfAccount);
BEAST_EXPECT(finalVaultPseudoAccountID == initialVaultPseudoAccountID);
// Vault pseudo-account balance should be unchanged
auto const finalVaultBalance = env.balance(initialVaultPseudoAccount, IOU);
BEAST_EXPECT(finalVaultBalance == initialVaultBalance);
// AssetsAvailable should be unchanged
auto const finalAssetsAvailable = finalVaultSle->at(sfAssetsAvailable);
BEAST_EXPECT(finalAssetsAvailable == initialAssetsAvailable);
}
}
public:
void
run() override
{
testDisabled();
testCreateAsset();
testLoanSetAndDelete();
testLoanSetAndImpair();
testLoanDefaultWithdrawAndPay(all - fixDefaultCoverLogicOptimization);
testLoanDefaultWithdrawAndPay(all);
testRiskFreeArbitrage();
testRiskFreeArbitrageFails();
}
};
BEAST_DEFINE_TESTSUITE(LoanBatch, tx, xrpl);
} // namespace test
} // namespace xrpl

View File

@@ -1701,21 +1701,10 @@ class LoanBroker_test : public beast::unit_test::suite
testRIPD4274MPT();
}
void
testFixAmendmentEnabled()
{
using namespace jtx;
testcase("testFixAmendmentEnabled");
Env env{*this};
BEAST_EXPECT(env.enabled(fixLendingProtocolV1_1));
}
public:
void
run() override
{
testFixAmendmentEnabled();
testLoanBrokerSetDebtMaximum();
testLoanBrokerCoverDepositNullVault();

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,7 @@ namespace jtx {
std::tuple<Json::Value, Keylet>
Vault::create(CreateArgs const& args)
{
auto sequence = args.sequence ? *args.sequence : env.seq(args.owner);
auto keylet = keylet::vault(args.owner.id(), sequence);
auto keylet = keylet::vault(args.owner.id(), env.seq(args.owner));
Json::Value jv;
jv[jss::TransactionType] = jss::VaultCreate;
jv[jss::Account] = args.owner.human();

View File

@@ -26,7 +26,6 @@ struct Vault
Account owner;
Asset asset;
std::optional<std::uint32_t> flags{};
std::optional<uint32_t> sequence{};
};
/** Return a VaultCreate transaction and the Vault's expected keylet. */

View File

@@ -137,7 +137,7 @@ public:
{
std::shared_ptr<NodeObject> object;
Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object);
Status const status = backend.fetch(batch[i]->getHash(), &object);
BEAST_EXPECT(status == ok);
@@ -157,7 +157,7 @@ public:
{
std::shared_ptr<NodeObject> object;
Status const status = backend.fetch(batch[i]->getHash().cbegin(), &object);
Status const status = backend.fetch(batch[i]->getHash(), &object);
BEAST_EXPECT(status == notFound);
}

View File

@@ -313,7 +313,7 @@ public:
std::shared_ptr<NodeObject> obj;
std::shared_ptr<NodeObject> result;
obj = seq1_.obj(dist_(gen_));
backend_.fetch(obj->getHash().data(), &result);
backend_.fetch(obj->getHash(), &result);
suite_.expect(result && isSame(result, obj));
}
catch (std::exception const& e)
@@ -371,9 +371,9 @@ public:
{
try
{
auto const key = seq2_.key(i);
auto const hash = seq2_.key(i);
std::shared_ptr<NodeObject> result;
backend_.fetch(key.data(), &result);
backend_.fetch(hash, &result);
suite_.expect(!result);
}
catch (std::exception const& e)
@@ -438,9 +438,9 @@ public:
{
if (rand_(gen_) < missingNodePercent)
{
auto const key = seq2_.key(dist_(gen_));
auto const hash = seq2_.key(dist_(gen_));
std::shared_ptr<NodeObject> result;
backend_.fetch(key.data(), &result);
backend_.fetch(hash, &result);
suite_.expect(!result);
}
else
@@ -448,7 +448,7 @@ public:
std::shared_ptr<NodeObject> obj;
std::shared_ptr<NodeObject> result;
obj = seq1_.obj(dist_(gen_));
backend_.fetch(obj->getHash().data(), &result);
backend_.fetch(obj->getHash(), &result);
suite_.expect(result && isSame(result, obj));
}
}
@@ -524,8 +524,7 @@ public:
std::shared_ptr<NodeObject> result;
auto const j = older_(gen_);
obj = seq1_.obj(j);
std::shared_ptr<NodeObject> result1;
backend_.fetch(obj->getHash().data(), &result);
backend_.fetch(obj->getHash(), &result);
suite_.expect(result != nullptr);
suite_.expect(isSame(result, obj));
}
@@ -543,7 +542,7 @@ public:
std::shared_ptr<NodeObject> result;
auto const j = recent_(gen_);
obj = seq1_.obj(j);
backend_.fetch(obj->getHash().data(), &result);
backend_.fetch(obj->getHash(), &result);
suite_.expect(!result || isSame(result, obj));
break;
}

View File

@@ -167,7 +167,7 @@ getAssetsTotalScale(SLE::const_ref vaultSle)
{
if (!vaultSle)
return Number::minExponent - 1; // LCOV_EXCL_LINE
return vaultSle->at(sfAssetsTotal).scale<STAmount>(vaultSle->at(sfAsset));
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)}.exponent();
}
TER

View File

@@ -256,6 +256,13 @@ Batch::preflight(PreflightContext const& ctx)
return temINVALID;
}
if (std::any_of(disabledTxTypes.begin(), disabledTxTypes.end(), [txType](auto const& disabled) {
return txType == disabled;
}))
{
return temINVALID_INNER_BATCH;
}
if (!(stx.getFlags() & tfInnerBatchTxn))
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "

View File

@@ -34,6 +34,24 @@ public:
TER
doApply() override;
static constexpr auto disabledTxTypes = std::to_array<TxType>({
ttVAULT_CREATE,
ttVAULT_SET,
ttVAULT_DELETE,
ttVAULT_DEPOSIT,
ttVAULT_WITHDRAW,
ttVAULT_CLAWBACK,
ttLOAN_BROKER_SET,
ttLOAN_BROKER_DELETE,
ttLOAN_BROKER_COVER_DEPOSIT,
ttLOAN_BROKER_COVER_WITHDRAW,
ttLOAN_BROKER_COVER_CLAWBACK,
ttLOAN_SET,
ttLOAN_DELETE,
ttLOAN_MANAGE,
ttLOAN_PAY,
});
};
} // namespace xrpl

View File

@@ -19,11 +19,10 @@
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/nftPageMask.h>
#include <algorithm>
#include <cstddef>
#include <initializer_list>
#include <cstdint>
#include <optional>
namespace xrpl {
@@ -2467,13 +2466,8 @@ ValidVault::visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before,
// Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), so the
// invariants can validate that the change in account balances matches the
// balanceDelta captures the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), and
// preserves value scale (exponent) to round values to the same scale during
// validation. It is used to validate that the change in account
// balances matches the change in vault balances, stored to deltas_ at the
// end of this function.
DeltaInfo balanceDelta{numZero, std::nullopt};
// change in vault balances, stored to deltas_ at the end of this function.
Number balanceDelta{};
std::int8_t sign = 0;
if (before)
@@ -2487,33 +2481,18 @@ ValidVault::visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before,
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
beforeMPTs_.push_back(Shares::make(*before));
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
sign = 1;
break;
case ltMPTOKEN:
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
sign = -1;
break;
case ltACCOUNT_ROOT:
balanceDelta.delta = before->getFieldAmount(sfBalance);
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
case ltRIPPLE_STATE:
balanceDelta = before->getFieldAmount(sfBalance);
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = before->getFieldAmount(sfBalance);
balanceDelta.delta = amount;
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
@@ -2529,34 +2508,18 @@ ValidVault::visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before,
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
afterMPTs_.push_back(Shares::make(*after));
balanceDelta.delta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
sign = 1;
break;
case ltMPTOKEN:
balanceDelta.delta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
sign = -1;
break;
case ltACCOUNT_ROOT:
balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
case ltRIPPLE_STATE:
balanceDelta -= Number(after->getFieldAmount(sfBalance));
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = after->getFieldAmount(sfBalance);
balanceDelta.delta -= Number(amount);
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
if (amount.exponent() > balanceDelta.scale)
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
@@ -2568,11 +2531,7 @@ ValidVault::visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before,
// transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates.
if (sign != 0)
{
XRPL_ASSERT_PARTS(balanceDelta.scale, "xrpl::ValidVault::visitEntry", "scale initialized");
balanceDelta.delta *= sign;
deltas_[key] = balanceDelta;
}
deltas_[key] = balanceDelta * sign;
}
bool
@@ -2828,13 +2787,13 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
}
auto const& vaultAsset = afterVault.asset;
auto const deltaAssets = [&](AccountID const& id) -> std::optional<DeltaInfo> {
auto const deltaAssets = [&](AccountID const& id) -> std::optional<Number> {
auto const get = //
[&](auto const& it, std::int8_t sign = 1) -> std::optional<DeltaInfo> {
[&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
if (it == deltas_.end())
return std::nullopt;
return DeltaInfo{it->second.delta * sign, it->second.scale};
return it->second * sign;
};
return std::visit(
@@ -2852,7 +2811,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
},
vaultAsset.value());
};
auto const deltaAssetsTxAccount = [&]() -> std::optional<DeltaInfo> {
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
auto ret = deltaAssets(tx[sfAccount]);
// Nothing returned or not XRP transaction
if (!ret.has_value() || !vaultAsset.native())
@@ -2862,20 +2821,20 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
if (auto const delegate = tx[~sfDelegate]; delegate.has_value() && *delegate != tx[sfAccount])
return ret;
ret->delta += fee.drops();
if (ret->delta == zero)
*ret += fee.drops();
if (*ret == zero)
return std::nullopt;
return ret;
};
auto const deltaShares = [&](AccountID const& id) -> std::optional<DeltaInfo> {
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
auto const it = [&]() {
if (id == afterVault.pseudoId)
return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key);
return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
}();
return it != deltas_.end() ? std::optional<DeltaInfo>(it->second) : std::nullopt;
return it != deltas_.end() ? std::optional<Number>(it->second) : std::nullopt;
};
auto const vaultHoldsNoAssets = [&](Vault const& vault) {
@@ -2997,37 +2956,16 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
XRPL_ASSERT(!beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault");
auto const& beforeVault = beforeVault_[0];
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault balance";
return false; // That's all we can do
}
// Get the coarsest scale to round calculations to
DeltaInfo totalDelta{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
beforeVault.assetsTotal.scale<STAmount>(vaultAsset))};
DeltaInfo availableDelta{
afterVault.assetsAvailable - beforeVault.assetsAvailable,
std::max(
afterVault.assetsAvailable.scale<STAmount>(vaultAsset),
beforeVault.assetsAvailable.scale<STAmount>(vaultAsset))};
auto const minScale = computeMinScale(
vaultAsset,
{
*maybeVaultDeltaAssets,
totalDelta,
availableDelta,
});
auto const vaultDeltaAssets = roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
auto const txAmount = roundToAsset(vaultAsset, tx[sfAmount], minScale);
if (vaultDeltaAssets > txAmount)
if (*vaultDeltaAssets > tx[sfAmount])
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must not change vault "
@@ -3035,7 +2973,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
if (vaultDeltaAssets <= zero)
if (*vaultDeltaAssets <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase vault balance";
@@ -3052,22 +2990,16 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
if (!issuerDeposit)
{
auto const maybeAccDeltaAssets = deltaAssetsTxAccount();
if (!maybeAccDeltaAssets)
auto const accountDeltaAssets = deltaAssetsTxAccount();
if (!accountDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor "
"balance";
return false;
}
auto const localMinScale =
std::max(minScale, computeMinScale(vaultAsset, {*maybeAccDeltaAssets}));
auto const accountDeltaAssets =
roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
auto const localVaultDeltaAssets = roundToAsset(vaultAsset, vaultDeltaAssets, localMinScale);
if (accountDeltaAssets >= zero)
if (*accountDeltaAssets >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must decrease depositor "
@@ -3075,7 +3007,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
if (localVaultDeltaAssets * -1 != accountDeltaAssets)
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault and "
@@ -3092,17 +3024,16 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
if (!maybeAccDeltaShares)
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor "
"shares";
return false; // That's all we can do
}
// We don't need to round shares, they are integral MPT
auto const& accountDeltaShares = *maybeAccDeltaShares;
if (accountDeltaShares.delta <= zero)
if (*accountDeltaShares <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase depositor "
@@ -3110,17 +3041,15 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == zero)
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault shares";
return false; // That's all we can do
}
// We don't need to round shares, they are integral MPT
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
if (*vaultDeltaShares * -1 != *accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor and "
@@ -3128,18 +3057,13 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
auto const assetTotalDelta =
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
if (assetTotalDelta != vaultDeltaAssets)
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"outstanding must add up";
result = false;
}
auto const assetAvailableDelta =
roundToAsset(vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
if (assetAvailableDelta != vaultDeltaAssets)
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"available must add up";
@@ -3157,38 +3081,22 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
"vault");
auto const& beforeVault = beforeVault_[0];
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
if (!vaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"change vault balance";
return false; // That's all we can do
}
// Get the most coarse scale to round calculations to
auto const totalDelta = DeltaInfo{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
beforeVault.assetsTotal.scale<STAmount>(vaultAsset))};
auto const availableDelta = DeltaInfo{
afterVault.assetsAvailable - beforeVault.assetsAvailable,
std::max(
afterVault.assetsAvailable.scale<STAmount>(vaultAsset),
beforeVault.assetsAvailable.scale<STAmount>(vaultAsset))};
auto const minScale =
computeMinScale(vaultAsset, {*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultPseudoDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultPseudoDeltaAssets >= zero)
if (*vaultDeltaAssets >= zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"decrease vault balance";
result = false;
}
// Any payments (including withdrawal) going to the issuer
// do not change their balance, but destroy funds instead.
bool const issuerWithdrawal = [&]() -> bool {
@@ -3200,15 +3108,15 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
if (!issuerWithdrawal)
{
auto const maybeAccDelta = deltaAssetsTxAccount();
auto const maybeOtherAccDelta = [&]() -> std::optional<DeltaInfo> {
auto const accountDeltaAssets = deltaAssetsTxAccount();
auto const otherAccountDelta = [&]() -> std::optional<Number> {
if (auto const destination = tx[~sfDestination];
destination && *destination != tx[sfAccount])
return deltaAssets(*destination);
return std::nullopt;
}();
if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value())
if (accountDeltaAssets.has_value() == otherAccountDelta.has_value())
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change one "
@@ -3217,16 +3125,9 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
}
auto const destinationDelta = //
maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta;
// the scale of destinationDelta can be coarser than
// minScale, so we take that into account when rounding
auto const localMinScale = std::max(minScale, computeMinScale(vaultAsset, {destinationDelta}));
auto const roundedDestinationDelta =
roundToAsset(vaultAsset, destinationDelta.delta, localMinScale);
if (roundedDestinationDelta <= zero)
if (destinationDelta <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must increase "
@@ -3234,9 +3135,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
auto const localPseudoDeltaAssets =
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
if (localPseudoDeltaAssets * -1 != roundedDestinationDelta)
if (*vaultDeltaAssets * -1 != destinationDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault "
@@ -3244,7 +3143,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
}
// We don't need to round shares, they are integral MPT
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
@@ -3254,23 +3153,23 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
return false;
}
if (accountDeltaShares->delta >= zero)
if (*accountDeltaShares >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must decrease depositor "
"shares";
result = false;
}
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault shares";
return false; // That's all we can do
}
if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
if (*vaultDeltaShares * -1 != *accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change depositor "
@@ -3278,20 +3177,15 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
auto const assetTotalDelta =
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
// Note, vaultBalance is negative (see check above)
if (assetTotalDelta != vaultPseudoDeltaAssets)
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets outstanding must add up";
result = false;
}
auto const assetAvailableDelta =
roundToAsset(vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
if (assetAvailableDelta != vaultPseudoDeltaAssets)
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets available must add up";
@@ -3321,23 +3215,10 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
}
}
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (maybeVaultDeltaAssets)
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (vaultDeltaAssets)
{
auto const totalDelta = DeltaInfo{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
afterVault.assetsTotal.scale<STAmount>(vaultAsset),
beforeVault.assetsTotal.scale<STAmount>(vaultAsset))};
auto const availableDelta = DeltaInfo{
afterVault.assetsAvailable - beforeVault.assetsAvailable,
std::max(
afterVault.assetsAvailable.scale<STAmount>(vaultAsset),
beforeVault.assetsAvailable.scale<STAmount>(vaultAsset))};
auto const minScale =
computeMinScale(vaultAsset, {*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultDeltaAssets = roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets >= zero)
if (*vaultDeltaAssets >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease vault "
@@ -3345,9 +3226,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
auto const assetsTotalDelta =
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
if (assetsTotalDelta != vaultDeltaAssets)
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets outstanding "
@@ -3355,9 +3234,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
if (assetAvailableDelta != vaultDeltaAssets)
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets available "
@@ -3372,15 +3249,15 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
return false; // That's all we can do
}
// We don't need to round shares, they are integral MPT
auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
if (!maybeAccountDeltaShares)
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder shares";
return false; // That's all we can do
}
if (maybeAccountDeltaShares->delta >= zero)
if (*accountDeltaShares >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease holder "
@@ -3388,16 +3265,15 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
result = false;
}
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault shares";
return false; // That's all we can do
}
if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta)
if (*vaultDeltaShares * -1 != *accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder and "
@@ -3434,15 +3310,4 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
return true;
}
[[nodiscard]] std::int32_t
ValidVault::computeMinScale(Asset const& asset, std::vector<DeltaInfo> const& numbers)
{
if (numbers.size() == 0)
return 0;
auto const max = std::max_element(
numbers.begin(), numbers.end(), [](auto const& a, auto const& b) -> bool { return a.scale < b.scale; });
XRPL_ASSERT_PARTS(max->scale, "xrpl::ValidVault::computeMinScale", "scale set for destinationDelta");
return max->scale.value_or(STAmount::cMaxOffset);
}
} // namespace xrpl

View File

@@ -8,6 +8,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <cstdint>
#include <tuple>
#include <unordered_set>
@@ -663,19 +664,11 @@ class ValidVault
Shares static make(SLE const&);
};
public:
struct DeltaInfo final
{
Number delta = numZero;
std::optional<int> scale;
};
private:
std::vector<Vault> afterVault_ = {};
std::vector<Shares> afterMPTs_ = {};
std::vector<Vault> beforeVault_ = {};
std::vector<Shares> beforeMPTs_ = {};
std::unordered_map<uint256, DeltaInfo> deltas_ = {};
std::unordered_map<uint256, Number> deltas_ = {};
public:
void
@@ -683,10 +676,6 @@ public:
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
computeMinScale(Asset const& asset, std::vector<DeltaInfo> const& numbers);
};
// additional invariant checks can be declared above and then added to this

View File

@@ -122,7 +122,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
return roundToAsset(
vaultAsset,
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
currentDebtTotal.scale<STAmount>(vaultAsset));
currentDebtTotal.exponent());
}();
if (coverAvail < amount)
return tecINSUFFICIENT_FUNDS;

View File

@@ -141,37 +141,20 @@ LoanManage::defaultLoan(
// Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)};
auto const covered = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
if (view.rules().enabled(fixDefaultCoverLogicOptimization))
{
// New formula: DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable)
// Round the liquidation amount up
return roundToAsset(vaultAsset, tenthBipsOfValue(totalDefaultAmount, coverRateMinimum), loanScale);
}
else
{
auto const minimumCover = tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up
return roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
* Changes), specifically "if the `tfLoanDefault` flag is set" /
* "Apply the First-Loss Capital to the Default Amount"
*/
std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
loanScale);
}
}();
auto const defaultCovered = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
auto const minimumCover = tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up, too
auto const covered = roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
* Changes), specifically "if the `tfLoanDefault` flag is set" /
* "Apply the First-Loss Capital to the Default Amount"
*/
std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
loanScale);
auto const coverAvailable = *brokerSle->at(sfCoverAvailable);
return std::min(covered, coverAvailable);