mirror of
https://github.com/XRPLF/rippled.git
synced 2026-02-14 19:02:30 +00:00
Compare commits
24 Commits
develop
...
a1q123456/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
281dbd2873 | ||
|
|
93e98ac714 | ||
|
|
79b3b4ffea | ||
|
|
ba60306610 | ||
|
|
5040048e49 | ||
|
|
6674500896 | ||
|
|
7085aa3d55 | ||
|
|
c5d7ebe93d | ||
|
|
d0b5ca9dab | ||
|
|
5e51893e9b | ||
|
|
3422c11d02 | ||
|
|
f8d441bb6b | ||
|
|
5c87c4ffb0 | ||
|
|
0a9436def4 | ||
|
|
f76bf5340c | ||
|
|
1af0f4bd43 | ||
|
|
c6821ab842 | ||
|
|
aa12210fcd | ||
|
|
9235ec483a | ||
|
|
ffe0a3cc61 | ||
|
|
add9071b20 | ||
|
|
465e7b6d91 | ||
|
|
6223ebe05e | ||
|
|
4fe50c2d31 |
@@ -105,6 +105,9 @@ 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 -
|
||||
@@ -253,6 +256,26 @@ 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
|
||||
@@ -574,6 +597,14 @@ 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
|
||||
{
|
||||
|
||||
@@ -42,8 +42,8 @@ private:
|
||||
public:
|
||||
using value_type = STAmount;
|
||||
|
||||
static int const cMinOffset = -96;
|
||||
static int const cMaxOffset = 80;
|
||||
static int constexpr cMinOffset = -96;
|
||||
static int constexpr cMaxOffset = 80;
|
||||
|
||||
// Maximum native value supported by the code
|
||||
constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
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)
|
||||
|
||||
@@ -2581,11 +2581,6 @@ 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};
|
||||
@@ -2645,7 +2640,7 @@ class Batch_test : public beast::unit_test::suite
|
||||
{
|
||||
auto const [txIDs, batchID] = submitBatch(
|
||||
env,
|
||||
lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH,
|
||||
temBAD_SIGNATURE,
|
||||
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
|
||||
batch::inner(
|
||||
env.json(
|
||||
@@ -2676,7 +2671,7 @@ class Batch_test : public beast::unit_test::suite
|
||||
{
|
||||
auto const [txIDs, batchID] = submitBatch(
|
||||
env,
|
||||
lendingBatchEnabled ? temBAD_SIGNER : temINVALID_INNER_BATCH,
|
||||
temBAD_SIGNER,
|
||||
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
|
||||
batch::inner(
|
||||
env.json(
|
||||
@@ -2696,7 +2691,7 @@ class Batch_test : public beast::unit_test::suite
|
||||
auto const batchFee = batch::calcBatchFee(env, 1, 2);
|
||||
auto const [txIDs, batchID] = submitBatch(
|
||||
env,
|
||||
lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
|
||||
TER(tesSUCCESS),
|
||||
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
|
||||
batch::inner(
|
||||
env.json(
|
||||
@@ -2728,7 +2723,7 @@ class Batch_test : public beast::unit_test::suite
|
||||
auto const batchFee = batch::calcBatchFee(env, 1, 2);
|
||||
auto const [txIDs, batchID] = submitBatch(
|
||||
env,
|
||||
lendingBatchEnabled ? TER(tesSUCCESS) : TER(temINVALID_INNER_BATCH),
|
||||
TER(tesSUCCESS),
|
||||
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
|
||||
batch::inner(
|
||||
env.json(
|
||||
@@ -2743,8 +2738,7 @@ class Batch_test : public beast::unit_test::suite
|
||||
}
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(brokerKeylet));
|
||||
if (auto const sleLoan = env.le(loanKeylet);
|
||||
lendingBatchEnabled ? BEAST_EXPECT(sleLoan) : !BEAST_EXPECT(!sleLoan))
|
||||
if (auto const sleLoan = env.le(loanKeylet); BEAST_EXPECT(sleLoan))
|
||||
{
|
||||
BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#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>
|
||||
@@ -20,6 +21,9 @@
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
namespace test {
|
||||
|
||||
@@ -3707,6 +3711,124 @@ 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
|
||||
@@ -3732,6 +3854,7 @@ public:
|
||||
testValidPseudoAccounts();
|
||||
testValidLoanBroker();
|
||||
testVault();
|
||||
testVaultComputeMinScale();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,16 +3,11 @@
|
||||
#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>
|
||||
|
||||
|
||||
2513
src/test/app/Loan/LoanBase.h
Normal file
2513
src/test/app/Loan/LoanBase.h
Normal file
File diff suppressed because it is too large
Load Diff
717
src/test/app/Loan/LoanBatch_test.cpp
Normal file
717
src/test/app/Loan/LoanBatch_test.cpp
Normal file
@@ -0,0 +1,717 @@
|
||||
#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()
|
||||
{
|
||||
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, all);
|
||||
|
||||
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);
|
||||
|
||||
// MinimumCover = DebtTotal x CoverRateMinimum
|
||||
Number const minimumCover = debtTotal * coverRateMin.value() / tenthBipsPerUnity.value();
|
||||
// DefaultCovered = min(MinimumCover x CoverRateLiquidation, DefaultAmount, CoverAvailable)
|
||||
Number const defaultCovered =
|
||||
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();
|
||||
testRiskFreeArbitrage();
|
||||
testRiskFreeArbitrageFails();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LoanBatch, tx, xrpl);
|
||||
|
||||
} // namespace test
|
||||
} // namespace xrpl
|
||||
@@ -1701,10 +1701,21 @@ 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
@@ -14,7 +14,8 @@ namespace jtx {
|
||||
std::tuple<Json::Value, Keylet>
|
||||
Vault::create(CreateArgs const& args)
|
||||
{
|
||||
auto keylet = keylet::vault(args.owner.id(), env.seq(args.owner));
|
||||
auto sequence = args.sequence ? *args.sequence : env.seq(args.owner);
|
||||
auto keylet = keylet::vault(args.owner.id(), sequence);
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::VaultCreate;
|
||||
jv[jss::Account] = args.owner.human();
|
||||
|
||||
@@ -26,6 +26,7 @@ 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. */
|
||||
|
||||
@@ -167,7 +167,7 @@ getAssetsTotalScale(SLE::const_ref vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::minExponent - 1; // LCOV_EXCL_LINE
|
||||
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)}.exponent();
|
||||
return vaultSle->at(sfAssetsTotal).scale<STAmount>(vaultSle->at(sfAsset));
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -256,13 +256,6 @@ 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 << "]: "
|
||||
|
||||
@@ -34,24 +34,6 @@ 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
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
#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 <cstdint>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -2466,8 +2467,13 @@ 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
|
||||
// change in vault balances, stored to deltas_ at the end of this function.
|
||||
Number balanceDelta{};
|
||||
// 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};
|
||||
|
||||
std::int8_t sign = 0;
|
||||
if (before)
|
||||
@@ -2481,18 +2487,33 @@ 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 = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
|
||||
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = 1;
|
||||
break;
|
||||
case ltMPTOKEN:
|
||||
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
|
||||
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltACCOUNT_ROOT:
|
||||
case ltRIPPLE_STATE:
|
||||
balanceDelta = before->getFieldAmount(sfBalance);
|
||||
balanceDelta.delta = before->getFieldAmount(sfBalance);
|
||||
// Account balance is XRP, which is an int, so the scale is
|
||||
// always 0.
|
||||
balanceDelta.scale = 0;
|
||||
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:;
|
||||
}
|
||||
}
|
||||
@@ -2508,18 +2529,34 @@ 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 -= Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
|
||||
balanceDelta.delta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = 1;
|
||||
break;
|
||||
case ltMPTOKEN:
|
||||
balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
|
||||
balanceDelta.delta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
|
||||
// MPTs are ints, so the scale is always 0.
|
||||
balanceDelta.scale = 0;
|
||||
sign = -1;
|
||||
break;
|
||||
case ltACCOUNT_ROOT:
|
||||
case ltRIPPLE_STATE:
|
||||
balanceDelta -= Number(after->getFieldAmount(sfBalance));
|
||||
balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
|
||||
// Account balance is XRP, which is an int, so the scale is
|
||||
// always 0.
|
||||
balanceDelta.scale = 0;
|
||||
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:;
|
||||
}
|
||||
}
|
||||
@@ -2531,7 +2568,11 @@ 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)
|
||||
deltas_[key] = balanceDelta * sign;
|
||||
{
|
||||
XRPL_ASSERT_PARTS(balanceDelta.scale, "xrpl::ValidVault::visitEntry", "scale initialized");
|
||||
balanceDelta.delta *= sign;
|
||||
deltas_[key] = balanceDelta;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -2787,13 +2828,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<Number> {
|
||||
auto const deltaAssets = [&](AccountID const& id) -> std::optional<DeltaInfo> {
|
||||
auto const get = //
|
||||
[&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
|
||||
[&](auto const& it, std::int8_t sign = 1) -> std::optional<DeltaInfo> {
|
||||
if (it == deltas_.end())
|
||||
return std::nullopt;
|
||||
|
||||
return it->second * sign;
|
||||
return DeltaInfo{it->second.delta * sign, it->second.scale};
|
||||
};
|
||||
|
||||
return std::visit(
|
||||
@@ -2811,7 +2852,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
},
|
||||
vaultAsset.value());
|
||||
};
|
||||
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
|
||||
auto const deltaAssetsTxAccount = [&]() -> std::optional<DeltaInfo> {
|
||||
auto ret = deltaAssets(tx[sfAccount]);
|
||||
// Nothing returned or not XRP transaction
|
||||
if (!ret.has_value() || !vaultAsset.native())
|
||||
@@ -2821,20 +2862,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 += fee.drops();
|
||||
if (*ret == zero)
|
||||
ret->delta += fee.drops();
|
||||
if (ret->delta == zero)
|
||||
return std::nullopt;
|
||||
|
||||
return ret;
|
||||
};
|
||||
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
|
||||
auto const deltaShares = [&](AccountID const& id) -> std::optional<DeltaInfo> {
|
||||
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<Number>(it->second) : std::nullopt;
|
||||
return it != deltas_.end() ? std::optional<DeltaInfo>(it->second) : std::nullopt;
|
||||
};
|
||||
|
||||
auto const vaultHoldsNoAssets = [&](Vault const& vault) {
|
||||
@@ -2956,16 +2997,37 @@ 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 vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
|
||||
if (!vaultDeltaAssets)
|
||||
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets > tx[sfAmount])
|
||||
// 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)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must not change vault "
|
||||
@@ -2973,7 +3035,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";
|
||||
@@ -2990,16 +3052,22 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
|
||||
if (!issuerDeposit)
|
||||
{
|
||||
auto const accountDeltaAssets = deltaAssetsTxAccount();
|
||||
if (!accountDeltaAssets)
|
||||
auto const maybeAccDeltaAssets = deltaAssetsTxAccount();
|
||||
if (!maybeAccDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor "
|
||||
"balance";
|
||||
return false;
|
||||
}
|
||||
auto const localMinScale =
|
||||
std::max(minScale, computeMinScale(vaultAsset, {*maybeAccDeltaAssets}));
|
||||
|
||||
if (*accountDeltaAssets >= zero)
|
||||
auto const accountDeltaAssets =
|
||||
roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
|
||||
auto const localVaultDeltaAssets = roundToAsset(vaultAsset, vaultDeltaAssets, localMinScale);
|
||||
|
||||
if (accountDeltaAssets >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must decrease depositor "
|
||||
@@ -3007,7 +3075,7 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
|
||||
if (localVaultDeltaAssets * -1 != accountDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change vault and "
|
||||
@@ -3024,16 +3092,17 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!accountDeltaShares)
|
||||
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!maybeAccDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor "
|
||||
"shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*accountDeltaShares <= zero)
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const& accountDeltaShares = *maybeAccDeltaShares;
|
||||
if (accountDeltaShares.delta <= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must increase depositor "
|
||||
@@ -3041,15 +3110,17 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!vaultDeltaShares || *vaultDeltaShares == zero)
|
||||
auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
|
||||
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: deposit must change depositor and "
|
||||
@@ -3057,13 +3128,18 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
|
||||
auto const assetTotalDelta =
|
||||
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
if (assetTotalDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
|
||||
"outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
|
||||
|
||||
auto const assetAvailableDelta =
|
||||
roundToAsset(vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
if (assetAvailableDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
|
||||
"available must add up";
|
||||
@@ -3081,22 +3157,38 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
"vault");
|
||||
auto const& beforeVault = beforeVault_[0];
|
||||
|
||||
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
|
||||
if (!vaultDeltaAssets)
|
||||
if (!maybeVaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
|
||||
"change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets >= zero)
|
||||
// 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)
|
||||
{
|
||||
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 {
|
||||
@@ -3108,15 +3200,15 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
|
||||
if (!issuerWithdrawal)
|
||||
{
|
||||
auto const accountDeltaAssets = deltaAssetsTxAccount();
|
||||
auto const otherAccountDelta = [&]() -> std::optional<Number> {
|
||||
auto const maybeAccDelta = deltaAssetsTxAccount();
|
||||
auto const maybeOtherAccDelta = [&]() -> std::optional<DeltaInfo> {
|
||||
if (auto const destination = tx[~sfDestination];
|
||||
destination && *destination != tx[sfAccount])
|
||||
return deltaAssets(*destination);
|
||||
return std::nullopt;
|
||||
}();
|
||||
|
||||
if (accountDeltaAssets.has_value() == otherAccountDelta.has_value())
|
||||
if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value())
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change one "
|
||||
@@ -3125,9 +3217,16 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
}
|
||||
|
||||
auto const destinationDelta = //
|
||||
accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta;
|
||||
maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
|
||||
|
||||
if (destinationDelta <= zero)
|
||||
// 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)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must increase "
|
||||
@@ -3135,7 +3234,9 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets * -1 != destinationDelta)
|
||||
auto const localPseudoDeltaAssets =
|
||||
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
|
||||
if (localPseudoDeltaAssets * -1 != roundedDestinationDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change vault "
|
||||
@@ -3143,7 +3244,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)
|
||||
{
|
||||
@@ -3153,23 +3254,23 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*accountDeltaShares >= zero)
|
||||
if (accountDeltaShares->delta >= 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 == zero)
|
||||
if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: withdrawal must change depositor "
|
||||
@@ -3177,15 +3278,20 @@ 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 (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
|
||||
if (assetTotalDelta != vaultPseudoDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
|
||||
"assets outstanding must add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
|
||||
auto const assetAvailableDelta =
|
||||
roundToAsset(vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
|
||||
if (assetAvailableDelta != vaultPseudoDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
|
||||
"assets available must add up";
|
||||
@@ -3215,10 +3321,23 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
}
|
||||
}
|
||||
|
||||
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (vaultDeltaAssets)
|
||||
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (maybeVaultDeltaAssets)
|
||||
{
|
||||
if (*vaultDeltaAssets >= zero)
|
||||
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)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must decrease vault "
|
||||
@@ -3226,7 +3345,9 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
|
||||
auto const assetsTotalDelta =
|
||||
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
|
||||
if (assetsTotalDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets outstanding "
|
||||
@@ -3234,7 +3355,9 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
|
||||
auto const assetAvailableDelta = roundToAsset(
|
||||
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
|
||||
if (assetAvailableDelta != vaultDeltaAssets)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets available "
|
||||
@@ -3249,15 +3372,15 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
|
||||
if (!accountDeltaShares)
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
|
||||
if (!maybeAccountDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change holder shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*accountDeltaShares >= zero)
|
||||
if (maybeAccountDeltaShares->delta >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must decrease holder "
|
||||
@@ -3265,15 +3388,16 @@ 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 == zero)
|
||||
if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||
if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change holder and "
|
||||
@@ -3310,4 +3434,15 @@ 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
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <tuple>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -664,11 +663,19 @@ 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, Number> deltas_ = {};
|
||||
std::unordered_map<uint256, DeltaInfo> deltas_ = {};
|
||||
|
||||
public:
|
||||
void
|
||||
@@ -676,6 +683,10 @@ 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
|
||||
|
||||
@@ -122,7 +122,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
return roundToAsset(
|
||||
vaultAsset,
|
||||
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
|
||||
currentDebtTotal.exponent());
|
||||
currentDebtTotal.scale<STAmount>(vaultAsset));
|
||||
}();
|
||||
if (coverAvail < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
Reference in New Issue
Block a user