Compare commits

..

19 Commits

Author SHA1 Message Date
Olek
d83ec96848 Switch to wasmi v1.0.6 (#6204) 2026-01-12 13:36:02 -05:00
Mayukha Vadari
419d53ec4c Merge branch 'develop' into ripple/wasmi 2026-01-12 13:10:58 -05:00
Mayukha Vadari
d4d70d5675 Merge branch 'develop' into ripple/wasmi 2026-01-12 12:27:48 -05:00
Mayukha Vadari
bbc28b3b1c Merge branch 'develop' into ripple/wasmi 2026-01-08 11:42:28 -05:00
Mayukha Vadari
5aab274b7a Merge branch 'develop' into ripple/wasmi 2026-01-07 16:52:10 -05:00
Mayukha Vadari
2c30e41191 use the develop hashes 2026-01-07 16:50:45 -05:00
Mayukha Vadari
8ea5106b0b Merge branch 'develop' into ripple/wasmi 2026-01-07 14:34:49 -05:00
Mayukha Vadari
1977df9c2e Merge remote-tracking branch 'upstream/develop' into ripple/wasmi 2026-01-05 18:43:49 -05:00
Mayukha Vadari
6c95548df5 Merge remote-tracking branch 'upstream/develop' into ripple/wasmi 2025-12-22 15:51:19 -08:00
Mayukha Vadari
90e0bbd0fc Merge branch 'develop' into ripple/wasmi 2025-12-08 14:28:41 -05:00
Olek
b57df290de Use conan repo for wasmi lib (#6109)
* Use conan repo for wasmi lib
* Generate lockfile
2025-12-08 13:02:01 -05:00
Mayukha Vadari
8a403f1241 Merge branch 'develop' into ripple/wasmi 2025-12-05 14:32:48 -05:00
Mayukha Vadari
6d2640871d Merge branch 'develop' into ripple/wasmi 2025-12-02 18:40:54 -05:00
Olek
500bb68831 Fix win build (#6076) 2025-11-24 16:56:23 -05:00
Mayukha Vadari
16087c9680 fix merge issue 2025-11-25 02:57:47 +05:30
Mayukha Vadari
25c3060fef remove conan.lock (temporary) 2025-11-25 02:40:57 +05:30
Mayukha Vadari
ce9f0b38a4 Merge branch 'develop' into ripple/wasmi 2025-11-25 02:33:47 +05:30
Mayukha Vadari
35f7cbf772 update 2025-11-25 02:31:51 +05:30
Mayukha Vadari
0db564d261 WASMI data 2025-11-04 15:57:07 -05:00
35 changed files with 901 additions and 3493 deletions

View File

@@ -262,6 +262,7 @@ words:
- venv
- vfalco
- vinnie
- wasmi
- wextra
- wptr
- writeme

View File

@@ -116,6 +116,7 @@ find_package(date REQUIRED)
find_package(ed25519 REQUIRED)
find_package(nudb REQUIRED)
find_package(secp256k1 REQUIRED)
find_package(wasmi REQUIRED)
find_package(xxHash REQUIRED)
target_link_libraries(xrpl_libs INTERFACE

View File

@@ -63,6 +63,7 @@ target_link_libraries(xrpl.imports.main
Xrpl::opts
Xrpl::syslibs
secp256k1::secp256k1
wasmi::wasmi
xrpl.libpb
xxHash::xxhash
$<$<BOOL:${voidstar}>:antithesis-sdk-cpp>

View File

@@ -3,6 +3,7 @@
"requires": [
"zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1756234269.497",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1756234289.683",
"wasmi/1.0.6#407c9db14601a8af1c7dd3b388f3e4cd%1768164779.349",
"sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1756234266.869",
"soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1756234262.318",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246",

View File

@@ -35,6 +35,7 @@ class Xrpl(ConanFile):
"openssl/3.5.4",
"secp256k1/0.7.0",
"soci/4.0.3",
"wasmi/1.0.6",
"zlib/1.3.1",
]
@@ -210,6 +211,7 @@ class Xrpl(ConanFile):
"soci::soci",
"secp256k1::secp256k1",
"sqlite3::sqlite",
"wasmi::wasmi",
"xxhash::xxhash",
"zlib::zlib",
]

View File

@@ -61,9 +61,6 @@ enum FreezeHandling { fhIGNORE_FREEZE, fhZERO_IF_FROZEN };
/** Controls the treatment of unauthorized MPT balances */
enum AuthHandling { ahIGNORE_AUTH, ahZERO_IF_UNAUTHORIZED };
/** Controls whether to include the account's full spendable balance */
enum SpendableHandling { shSIMPLE_BALANCE, shFULL_BALANCE };
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, AccountID const& issuer);
@@ -308,17 +305,7 @@ isLPTokenFrozen(
Issue const& asset,
Issue const& asset2);
// Returns the amount an account can spend.
//
// If shSIMPLE_BALANCE is specified, this is the amount the account can spend
// without going into debt.
//
// If shFULL_BALANCE is specified, this is the amount the account can spend
// total. Specifically:
// * The account can go into debt if using a trust line, and the other side has
// a non-zero limit.
// * If the account is the asset issuer the limit is defined by the asset /
// issuance.
// Returns the amount an account can spend without going into debt.
//
// <-- saAmount: amount of currency held by account. May be negative.
[[nodiscard]] STAmount
@@ -328,8 +315,7 @@ accountHolds(
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j,
SpendableHandling includeFullBalance = shSIMPLE_BALANCE);
beast::Journal j);
[[nodiscard]] STAmount
accountHolds(
@@ -337,8 +323,7 @@ accountHolds(
AccountID const& account,
Issue const& issue,
FreezeHandling zeroIfFrozen,
beast::Journal j,
SpendableHandling includeFullBalance = shSIMPLE_BALANCE);
beast::Journal j);
[[nodiscard]] STAmount
accountHolds(
@@ -347,8 +332,7 @@ accountHolds(
MPTIssue const& mptIssue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j,
SpendableHandling includeFullBalance = shSIMPLE_BALANCE);
beast::Journal j);
[[nodiscard]] STAmount
accountHolds(
@@ -357,8 +341,50 @@ accountHolds(
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j,
SpendableHandling includeFullBalance = shSIMPLE_BALANCE);
beast::Journal j);
// Returns the amount an account can spend total.
//
// These functions use accountHolds, but unlike accountHolds:
// * The account can go into debt.
// * If the account is the asset issuer the only limit is defined by the asset /
// issuance.
//
// <-- saAmount: amount of currency held by account. May be negative.
[[nodiscard]] STAmount
accountSpendable(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j);
[[nodiscard]] STAmount
accountSpendable(
ReadView const& view,
AccountID const& account,
Issue const& issue,
FreezeHandling zeroIfFrozen,
beast::Journal j);
[[nodiscard]] STAmount
accountSpendable(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptIssue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j);
[[nodiscard]] STAmount
accountSpendable(
ReadView const& view,
AccountID const& account,
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j);
// Returns the amount an account can spend of the currency type saDefault, or
// returns saDefault if this account is the issuer of the currency in
@@ -629,7 +655,7 @@ createPseudoAccount(
uint256 const& pseudoOwnerKey,
SField const& ownerField);
// Returns true if and only if sleAcct is a pseudo-account or specific
// Returns true iff sleAcct is a pseudo-account or specific
// pseudo-accounts in pseudoFieldFilter.
//
// Returns false if sleAcct is
@@ -684,16 +710,13 @@ checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag);
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
*/
[[nodiscard]] TER
canWithdraw(
ReadView const& view,
AccountID const& from,
ReadView const& view,
AccountID const& to,
SLE::const_ref toSle,
STAmount const& amount,
bool hasDestinationTag);
/** Checks that can withdraw funds from an object to itself or a destination.
@@ -707,15 +730,12 @@ canWithdraw(
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
*/
[[nodiscard]] TER
canWithdraw(
ReadView const& view,
AccountID const& from,
ReadView const& view,
AccountID const& to,
STAmount const& amount,
bool hasDestinationTag);
/** Checks that can withdraw funds from an object to itself or a destination.
@@ -729,8 +749,6 @@ canWithdraw(
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
*/
[[nodiscard]] TER
canWithdraw(ReadView const& view, STTx const& tx);

View File

@@ -541,7 +541,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
{sfStartDate, soeREQUIRED},
{sfPaymentInterval, soeREQUIRED},
{sfGracePeriod, soeDEFAULT},
{sfPreviousPaymentDueDate, soeDEFAULT},
{sfPreviousPaymentDate, soeDEFAULT},
{sfNextPaymentDueDate, soeDEFAULT},
// The loan object tracks these values:
//

View File

@@ -102,7 +102,7 @@ TYPED_SFIELD(sfMutableFlags, UINT32, 53)
TYPED_SFIELD(sfStartDate, UINT32, 54)
TYPED_SFIELD(sfPaymentInterval, UINT32, 55)
TYPED_SFIELD(sfGracePeriod, UINT32, 56)
TYPED_SFIELD(sfPreviousPaymentDueDate, UINT32, 57)
TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 57)
TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 58)
TYPED_SFIELD(sfPaymentRemaining, UINT32, 59)
TYPED_SFIELD(sfPaymentTotal, UINT32, 60)

View File

@@ -3,7 +3,6 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/CredentialHelpers.h>
#include <xrpl/ledger/Credit.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/Feature.h>
@@ -465,8 +464,7 @@ accountHolds(
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j,
SpendableHandling includeFullBalance)
beast::Journal j)
{
STAmount amount;
if (isXRP(currency))
@@ -474,19 +472,11 @@ accountHolds(
return {xrpLiquid(view, account, 0, j)};
}
bool const returnSpendable = (includeFullBalance == shFULL_BALANCE);
if (returnSpendable && account == issuer)
// If the account is the issuer, then their limit is effectively
// infinite
return STAmount{
Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset};
// IOU: Return balance on trust line modulo freeze
SLE::const_pointer const sle =
getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j);
return getTrustLineBalance(
view, sle, account, currency, issuer, returnSpendable, j);
return getTrustLineBalance(view, sle, account, currency, issuer, false, j);
}
STAmount
@@ -495,17 +485,10 @@ accountHolds(
AccountID const& account,
Issue const& issue,
FreezeHandling zeroIfFrozen,
beast::Journal j,
SpendableHandling includeFullBalance)
beast::Journal j)
{
return accountHolds(
view,
account,
issue.currency,
issue.account,
zeroIfFrozen,
j,
includeFullBalance);
view, account, issue.currency, issue.account, zeroIfFrozen, j);
}
STAmount
@@ -515,28 +498,8 @@ accountHolds(
MPTIssue const& mptIssue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j,
SpendableHandling includeFullBalance)
beast::Journal j)
{
bool const returnSpendable = (includeFullBalance == shFULL_BALANCE);
if (returnSpendable && account == mptIssue.getIssuer())
{
// if the account is the issuer, and the issuance exists, their limit is
// the issuance limit minus the outstanding value
auto const issuance =
view.read(keylet::mptIssuance(mptIssue.getMptID()));
if (!issuance)
{
return STAmount{mptIssue};
}
return STAmount{
mptIssue,
issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) -
issuance->at(sfOutstandingAmount)};
}
STAmount amount;
auto const sleMpt =
@@ -584,27 +547,108 @@ accountHolds(
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j,
SpendableHandling includeFullBalance)
beast::Journal j)
{
return std::visit(
[&]<ValidIssueType TIss>(TIss const& value) {
if constexpr (std::is_same_v<TIss, Issue>)
[&](auto const& value) {
if constexpr (std::is_same_v<
std::remove_cvref_t<decltype(value)>,
Issue>)
{
return accountHolds(
view, account, value, zeroIfFrozen, j, includeFullBalance);
return accountHolds(view, account, value, zeroIfFrozen, j);
}
else if constexpr (std::is_same_v<TIss, MPTIssue>)
return accountHolds(
view, account, value, zeroIfFrozen, zeroIfUnauthorized, j);
},
asset.value());
}
STAmount
accountSpendable(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
if (isXRP(currency))
return accountHolds(view, account, currency, issuer, zeroIfFrozen, j);
if (account == issuer)
// If the account is the issuer, then their limit is effectively
// infinite
return STAmount{
Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset};
// IOU: Return balance on trust line modulo freeze
SLE::const_pointer const sle =
getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j);
return getTrustLineBalance(view, sle, account, currency, issuer, true, j);
}
STAmount
accountSpendable(
ReadView const& view,
AccountID const& account,
Issue const& issue,
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
return accountSpendable(
view, account, issue.currency, issue.account, zeroIfFrozen, j);
}
STAmount
accountSpendable(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptIssue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j)
{
if (account == mptIssue.getIssuer())
{
// if the account is the issuer, and the issuance exists, their limit is
// the issuance limit minus the outstanding value
auto const issuance =
view.read(keylet::mptIssuance(mptIssue.getMptID()));
if (!issuance)
{
return STAmount{mptIssue};
}
return STAmount{
mptIssue,
issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) -
issuance->at(sfOutstandingAmount)};
}
return accountHolds(
view, account, mptIssue, zeroIfFrozen, zeroIfUnauthorized, j);
}
[[nodiscard]] STAmount
accountSpendable(
ReadView const& view,
AccountID const& account,
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j)
{
return std::visit(
[&](auto const& value) {
if constexpr (std::is_same_v<
std::remove_cvref_t<decltype(value)>,
Issue>)
{
return accountHolds(
view,
account,
value,
zeroIfFrozen,
zeroIfUnauthorized,
j,
includeFullBalance);
return accountSpendable(view, account, value, zeroIfFrozen, j);
}
return accountSpendable(
view, account, value, zeroIfFrozen, zeroIfUnauthorized, j);
},
asset.value());
}
@@ -1161,7 +1205,8 @@ getPseudoAccountFields()
// LCOV_EXCL_START
LogicError(
"xrpl::getPseudoAccountFields : unable to find account root "
"ledger format");
"ledger "
"format");
// LCOV_EXCL_STOP
}
auto const& soTemplate = ar->getSOTemplate();
@@ -1297,58 +1342,12 @@ checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag)
return tesSUCCESS;
}
/*
* Checks if a withdrawal amount into the destination account exceeds
* any applicable receiving limit.
* Called by VaultWithdraw and LoanBrokerCoverWithdraw.
*
* IOU : Performs the trustline check against the destination account's
* credit limit to ensure the account's trust maximum is not exceeded.
*
* MPT: The limit check is effectively skipped (returns true). This is
* because MPT MaximumAmount relates to token supply, and withdrawal does not
* involve minting new tokens that could exceed the global cap.
* On withdrawal, tokens are simply transferred from the vault's pseudo-account
* to the destination account. Since no new MPT tokens are minted during this
* transfer, the withdrawal cannot violate the MPT MaximumAmount/supply cap
* even if `from` is the issuer.
*/
static TER
withdrawToDestExceedsLimit(
ReadView const& view,
AccountID const& from,
AccountID const& to,
STAmount const& amount)
{
auto const& issuer = amount.getIssuer();
if (from == to || to == issuer || isXRP(issuer))
return tesSUCCESS;
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) -> TER {
if constexpr (std::is_same_v<TIss, Issue>)
{
auto const& currency = issue.currency;
auto const owed = creditBalance(view, to, issuer, currency);
if (owed <= beast::zero)
{
auto const limit = creditLimit(view, to, issuer, currency);
if (-owed >= limit || amount > (limit + owed))
return tecNO_LINE;
}
}
return tesSUCCESS;
},
amount.asset().value());
}
[[nodiscard]] TER
canWithdraw(
ReadView const& view,
AccountID const& from,
ReadView const& view,
AccountID const& to,
SLE::const_ref toSle,
STAmount const& amount,
bool hasDestinationTag)
{
if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag))
@@ -1363,20 +1362,19 @@ canWithdraw(
return tecNO_PERMISSION;
}
return withdrawToDestExceedsLimit(view, from, to, amount);
return tesSUCCESS;
}
[[nodiscard]] TER
canWithdraw(
ReadView const& view,
AccountID const& from,
ReadView const& view,
AccountID const& to,
STAmount const& amount,
bool hasDestinationTag)
{
auto const toSle = view.read(keylet::account(to));
return canWithdraw(view, from, to, toSle, amount, hasDestinationTag);
return canWithdraw(from, view, to, toSle, hasDestinationTag);
}
[[nodiscard]] TER
@@ -1385,8 +1383,7 @@ canWithdraw(ReadView const& view, STTx const& tx)
auto const from = tx[sfAccount];
auto const to = tx[~sfDestination].value_or(from);
return canWithdraw(
view, from, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag));
return canWithdraw(from, view, to, tx.isFieldPresent(sfDestinationTag));
}
TER
@@ -2169,105 +2166,6 @@ rippleSendIOU(
return terResult;
}
template <class TAsset>
static TER
doSendMulti(
std::string const& name,
ApplyView& view,
AccountID const& senderID,
TAsset const& issue,
MultiplePaymentDestinations const& receivers,
STAmount& actual,
beast::Journal j,
WaiveTransferFee waiveFee,
// Don't pass back parameters that the caller already has
std::function<
TER(AccountID const& senderID,
AccountID const& receiverID,
STAmount const& amount,
bool checkIssuer)> doCredit,
std::function<
TER(AccountID const& issuer,
STAmount const& takeFromSender,
STAmount const& amount)> preMint = {})
{
// Use the same pattern for all the SendMulti functions to help avoid
// divergence and copy/paste errors.
auto const& issuer = issue.getIssuer();
// These values may not stay in sync
STAmount takeFromSender{issue};
actual = takeFromSender;
// Failures return immediately.
for (auto const& r : receivers)
{
auto const& receiverID = r.first;
STAmount amount{issue, r.second};
if (amount < beast::zero)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
*/
if (!amount || (senderID == receiverID))
continue;
using namespace std::string_literals;
XRPL_ASSERT(
!isXRP(receiverID),
("xrpl::"s + name + " : receiver is not XRP").c_str());
if (senderID == issuer || receiverID == issuer || issuer == noAccount())
{
if (preMint)
{
if (auto const ter = preMint(issuer, takeFromSender, amount))
return ter;
}
// Direct send: redeeming IOUs and/or sending own IOUs.
if (auto const ter = doCredit(senderID, receiverID, amount, false))
return ter;
actual += amount;
// Do not add amount to takeFromSender, because doCredit took
// it.
continue;
}
// Sending 3rd party: transit.
// Calculate the amount to transfer accounting
// for any transfer fees if the fee is not waived:
STAmount actualSend =
(waiveFee == WaiveTransferFee::Yes || issue.native())
? amount
: multiply(amount, transferRate(view, amount));
actual += actualSend;
takeFromSender += actualSend;
JLOG(j.debug()) << name << "> " << to_string(senderID) << " - > "
<< to_string(receiverID)
<< " : deliver=" << amount.getFullText()
<< " cost=" << actualSend.getFullText();
if (TER const terResult = doCredit(issuer, receiverID, amount, true))
return terResult;
}
if (senderID != issuer && takeFromSender)
{
if (TER const terResult =
doCredit(senderID, issuer, takeFromSender, true))
return terResult;
}
return tesSUCCESS;
}
// Send regardless of limits.
// --> receivers: Amount/currency/issuer to deliver to receivers.
// <-- saActual: Amount actually cost to sender. Sender pays fees.
@@ -2281,28 +2179,72 @@ rippleSendMultiIOU(
beast::Journal j,
WaiveTransferFee waiveFee)
{
auto const& issuer = issue.getIssuer();
XRPL_ASSERT(
!isXRP(senderID), "xrpl::rippleSendMultiIOU : sender is not XRP");
auto doCredit = [&view, j](
AccountID const& senderID,
AccountID const& receiverID,
STAmount const& amount,
bool checkIssuer) {
return rippleCreditIOU(
view, senderID, receiverID, amount, checkIssuer, j);
};
// These may diverge
STAmount takeFromSender{issue};
actual = takeFromSender;
return doSendMulti(
"rippleSendMultiIOU",
view,
senderID,
issue,
receivers,
actual,
j,
waiveFee,
doCredit);
// Failures return immediately.
for (auto const& r : receivers)
{
auto const& receiverID = r.first;
STAmount amount{issue, r.second};
/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
*/
if (!amount || (senderID == receiverID))
continue;
XRPL_ASSERT(
!isXRP(receiverID),
"xrpl::rippleSendMultiIOU : receiver is not XRP");
if (senderID == issuer || receiverID == issuer || issuer == noAccount())
{
// Direct send: redeeming IOUs and/or sending own IOUs.
if (auto const ter = rippleCreditIOU(
view, senderID, receiverID, amount, false, j))
return ter;
actual += amount;
// Do not add amount to takeFromSender, because rippleCreditIOU took
// it.
continue;
}
// Sending 3rd party IOUs: transit.
// Calculate the amount to transfer accounting
// for any transfer fees if the fee is not waived:
STAmount actualSend = (waiveFee == WaiveTransferFee::Yes)
? amount
: multiply(amount, transferRate(view, issuer));
actual += actualSend;
takeFromSender += actualSend;
JLOG(j.debug()) << "rippleSendMultiIOU> " << to_string(senderID)
<< " - > " << to_string(receiverID)
<< " : deliver=" << amount.getFullText()
<< " cost=" << actual.getFullText();
if (TER const terResult =
rippleCreditIOU(view, issuer, receiverID, amount, true, j))
return terResult;
}
if (senderID != issuer && takeFromSender)
{
if (TER const terResult = rippleCreditIOU(
view, senderID, issuer, takeFromSender, true, j))
return terResult;
}
return tesSUCCESS;
}
static TER
@@ -2443,9 +2385,9 @@ accountSendMultiIOU(
"xrpl::accountSendMultiIOU",
"multiple recipients provided");
STAmount actual;
if (!issue.native())
{
STAmount actual;
JLOG(j.trace()) << "accountSendMultiIOU: " << to_string(senderID)
<< " sending " << receivers.size() << " IOUs";
@@ -2474,97 +2416,6 @@ accountSendMultiIOU(
<< sender_bal << ") -> " << receivers.size() << " receivers.";
}
auto doCredit = [&view, &sender, &receivers, j](
AccountID const& senderID,
AccountID const& receiverID,
STAmount const& amount,
bool /*checkIssuer*/) -> TER {
if (!senderID)
{
SLE::pointer receiver = receiverID != beast::zero
? view.peek(keylet::account(receiverID))
: SLE::pointer();
if (auto stream = j.trace())
{
std::string receiver_bal("-");
if (receiver)
receiver_bal =
receiver->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU> " << to_string(senderID)
<< " -> " << to_string(receiverID) << " ("
<< receiver_bal << ") : " << amount.getFullText();
}
if (receiver)
{
// Increment XRP balance.
auto const rcvBal = receiver->getFieldAmount(sfBalance);
receiver->setFieldAmount(sfBalance, rcvBal + amount);
view.creditHook(xrpAccount(), receiverID, amount, -rcvBal);
view.update(receiver);
}
if (auto stream = j.trace())
{
std::string receiver_bal("-");
if (receiver)
receiver_bal =
receiver->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU< " << to_string(senderID)
<< " -> " << to_string(receiverID) << " ("
<< receiver_bal << ") : " << amount.getFullText();
}
return tesSUCCESS;
}
// Sender
if (sender)
{
if (sender->getFieldAmount(sfBalance) < amount)
{
return TER{tecFAILED_PROCESSING};
}
else
{
auto const sndBal = sender->getFieldAmount(sfBalance);
view.creditHook(senderID, xrpAccount(), amount, sndBal);
// Decrement XRP balance.
sender->setFieldAmount(sfBalance, sndBal - amount);
view.update(sender);
}
}
if (auto stream = j.trace())
{
std::string sender_bal("-");
if (sender)
sender_bal = sender->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU< " << to_string(senderID) << " ("
<< sender_bal << ") -> " << receivers.size()
<< " receivers.";
}
return tesSUCCESS;
};
return doSendMulti(
"accountSendMultiIOU",
view,
senderID,
issue,
receivers,
actual,
j,
waiveFee,
doCredit);
// Failures return immediately.
STAmount takeFromSender{issue};
for (auto const& r : receivers)
@@ -2796,51 +2647,90 @@ rippleSendMultiMPT(
beast::Journal j,
WaiveTransferFee waiveFee)
{
// Safe to get MPT since rippleSendMultiMPT is only called by
// accountSendMultiMPT
auto const& issuer = mptIssue.getIssuer();
auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()));
if (!sle)
return tecOBJECT_NOT_FOUND;
auto preMint = [&](AccountID const& issuer,
STAmount const& takeFromSender,
STAmount const& amount) -> TER {
// if sender is issuer, check that the new OutstandingAmount will
// not exceed MaximumAmount
if (senderID == issuer)
// These may diverge
STAmount takeFromSender{mptIssue};
actual = takeFromSender;
for (auto const& r : receivers)
{
auto const& receiverID = r.first;
STAmount amount{mptIssue, r.second};
if (amount < beast::zero)
{
XRPL_ASSERT_PARTS(
takeFromSender == beast::zero,
"rippler::rippleSendMultiMPT",
"sender == issuer, takeFromSender == zero");
auto const sendAmount = amount.mpt().value();
auto const maximumAmount =
sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
if (sendAmount > maximumAmount ||
sle->getFieldU64(sfOutstandingAmount) >
maximumAmount - sendAmount)
return tecPATH_DRY;
return tecINTERNAL; // LCOV_EXCL_LINE
}
return tesSUCCESS;
};
auto doCredit = [&view, j](
AccountID const& senderID,
AccountID const& receiverID,
STAmount const& amount,
bool) {
return rippleCreditMPT(view, senderID, receiverID, amount, j);
};
/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
*/
if (!amount || (senderID == receiverID))
continue;
return doSendMulti(
"rippleSendMultiMPT",
view,
senderID,
mptIssue,
receivers,
actual,
j,
waiveFee,
doCredit,
preMint);
if (senderID == issuer || receiverID == issuer)
{
// if sender is issuer, check that the new OutstandingAmount will
// not exceed MaximumAmount
if (senderID == issuer)
{
XRPL_ASSERT_PARTS(
takeFromSender == beast::zero,
"rippler::rippleSendMultiMPT",
"sender == issuer, takeFromSender == zero");
auto const sendAmount = amount.mpt().value();
auto const maximumAmount =
sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
if (sendAmount > maximumAmount ||
sle->getFieldU64(sfOutstandingAmount) >
maximumAmount - sendAmount)
return tecPATH_DRY;
}
// Direct send: redeeming MPTs and/or sending own MPTs.
if (auto const ter =
rippleCreditMPT(view, senderID, receiverID, amount, j))
return ter;
actual += amount;
// Do not add amount to takeFromSender, because rippleCreditMPT took
// it
continue;
}
// Sending 3rd party MPTs: transit.
STAmount actualSend = (waiveFee == WaiveTransferFee::Yes)
? amount
: multiply(
amount,
transferRate(view, amount.get<MPTIssue>().getMptID()));
actual += actualSend;
takeFromSender += actualSend;
JLOG(j.debug()) << "rippleSendMultiMPT> " << to_string(senderID)
<< " - > " << to_string(receiverID)
<< " : deliver=" << amount.getFullText()
<< " cost=" << actualSend.getFullText();
if (auto const terResult =
rippleCreditMPT(view, issuer, receiverID, amount, j))
return terResult;
}
if (senderID != issuer && takeFromSender)
{
if (TER const terResult =
rippleCreditMPT(view, senderID, issuer, takeFromSender, j))
return terResult;
}
return tesSUCCESS;
}
static TER

File diff suppressed because it is too large Load Diff

View File

@@ -1024,12 +1024,6 @@ class LoanBroker_test : public beast::unit_test::suite
destination(dest),
ter(tecFROZEN),
THISLINE);
// preclaim: tecPSEUDO_ACCOUNT
env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
destination(vaultInfo.pseudoAccount),
ter(tecPSEUDO_ACCOUNT),
THISLINE);
}
if (brokerTest == CoverClawback)
@@ -1442,467 +1436,10 @@ class LoanBroker_test : public beast::unit_test::suite
});
}
void
testLoanBrokerSetDebtMaximum()
{
testcase("testLoanBrokerSetDebtMaximum");
using namespace jtx;
using namespace loanBroker;
Account const issuer{"issuer"};
Account const alice{"alice"};
Env env(*this);
Vault vault{env};
env.fund(XRP(100'000), issuer, alice);
env.close();
PrettyAsset const asset = [&]() {
env(trust(alice, issuer["IOU"](1'000'000)), THISLINE);
env.close();
return PrettyAsset(issuer["IOU"]);
}();
env(pay(issuer, alice, asset(100'000)), THISLINE);
env.close();
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
env(tx, THISLINE);
env.close();
auto const le = env.le(vaultKeylet);
VaultInfo vaultInfo = [&]() {
if (BEAST_EXPECT(le))
return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
return VaultInfo{asset, {}, {}};
}();
if (vaultInfo.vaultID == uint256{})
return;
env(vault.deposit(
{.depositor = alice,
.id = vaultKeylet.key,
.amount = asset(50)}),
THISLINE);
env.close();
auto const brokerKeylet =
keylet::loanbroker(alice.id(), env.seq(alice));
env(set(alice, vaultInfo.vaultID), THISLINE);
env.close();
Account const borrower{"borrower"};
env.fund(XRP(1'000), borrower);
env(loan::set(borrower, brokerKeylet.key, asset(50).value()),
sig(sfCounterpartySignature, alice),
fee(env.current()->fees().base * 2),
THISLINE);
auto const broker = env.le(brokerKeylet);
if (!BEAST_EXPECT(broker))
return;
BEAST_EXPECT(broker->at(sfDebtTotal) == 50);
auto debtTotal = broker->at(sfDebtTotal);
auto tx2 = set(alice, vaultInfo.vaultID);
tx2[sfLoanBrokerID] = to_string(brokerKeylet.key);
tx2[sfDebtMaximum] = debtTotal - 1;
env(tx2, ter(tecLIMIT_EXCEEDED), THISLINE);
tx2[sfDebtMaximum] = debtTotal + 1;
env(tx2, ter(tesSUCCESS), THISLINE);
tx2[sfDebtMaximum] = 0;
env(tx2, ter(tesSUCCESS), THISLINE);
}
void
testRIPD4323()
{
testcase << "RIPD-4323";
using namespace jtx;
Account const issuer("issuer");
Account const holder("holder");
Account const& broker = issuer;
auto test = [&](auto&& getToken) {
Env env(*this);
env.fund(XRP(1'000), issuer, holder);
env.close();
auto const [token, deposit, err] = getToken(env);
Vault vault(env);
auto const [tx, keylet] =
vault.create({.owner = broker, .asset = token.asset()});
env(tx);
env.close();
env(vault.deposit(
{.depositor = broker, .id = keylet.key, .amount = deposit}),
ter(err));
env.close();
auto const brokerKeylet =
keylet::loanbroker(broker, env.seq(broker));
env(loanBroker::set(broker, keylet.key));
env.close();
env(loanBroker::coverDeposit(broker, brokerKeylet.key, deposit),
ter(err));
env.close();
};
test([&](Env&) {
// issuer can issue any amount
auto const token = issuer["IOU"];
return std::make_tuple(token, token(1'000), tesSUCCESS);
});
std::vector<std::tuple<
std::uint64_t, // pay to holder
std::optional<std::uint64_t>, // max amount
std::uint64_t, // deposit amount
TER>> // expected error
mptTests = {
// issuer can issue up to 2'000 tokens
{2'000, 4'000, 1'000, tesSUCCESS},
// issuer can issue 500 tokens (250 VaultDeposit +
// 250 LoanBrokerCoverDeposit)
{2'000, 2'500, 250, tesSUCCESS},
// issuer can issue 500 tokens (250 VaultDeposit +
// 250 LoanBrokerCoverDeposit). MaximumAmount is default.
{maxMPTokenAmount - 500, std::nullopt, 250, tesSUCCESS},
// issuer can issue 500, and fails on depositing 1'000
{2'000, 2'500, 1'000, tecINSUFFICIENT_FUNDS},
// issuer has already issued MaximumAmount
{2'000, 2'000, 1'000, tecINSUFFICIENT_FUNDS},
// issuer has already issued MaximumAmount. MaximumAmount is
// default.
{maxMPTokenAmount, std::nullopt, 250, tecINSUFFICIENT_FUNDS},
};
for (auto const& [pay, max, deposit, err] : mptTests)
{
test([&](Env& env) -> std::tuple<MPT, PrettyAmount, TER> {
MPT const token = MPTTester(
{.env = env,
.issuer = issuer,
.holders = {holder},
.pay = pay,
.flags = MPTDEXFlags,
.maxAmt = max});
return std::make_tuple(token, token(deposit), err);
});
}
}
void
testAMB06_VaultFreezeCheckMissing()
{
testcase << "RIPD-4466 - LoanBrokerSet disallows frozen vaults";
using namespace jtx;
Env env(*this);
Account const issuer{"issuer"}, lender{"lender"}, borrower{"borrower"};
env.fund(XRP(20'000), issuer, lender, borrower);
auto const IOU = issuer["IOU"];
Vault vault{env};
auto [tx, vaultKeylet] =
vault.create({.owner = lender, .asset = IOU.asset()});
env(tx);
env.close();
// Get vault pseudo-account and FREEZE it
auto const vaultSle = env.le(vaultKeylet);
auto const vaultPseudo = vaultSle->at(sfAccount);
auto const vaultPseudoAcct = Account("VaultPseudo", vaultPseudo);
env(trust(issuer, vaultPseudoAcct["IOU"](0), tfSetFreeze));
env(loanBroker::set(lender, vaultKeylet.key), ter(tecFROZEN));
}
void
testRIPD4274IOU()
{
using namespace jtx;
Account issuer("broker");
Account broker("issuer");
Account dest("destination");
auto const token = issuer["IOU"];
enum TrustState {
RequireAuth,
ZeroLimit,
ReachedLimit,
NearLimit,
NoTrustLine,
};
auto test = [&](TrustState trustState) {
Env env(*this);
testcase << "RIPD-4274 IOU with state: "
<< static_cast<int>(trustState);
auto setTrustLine = [&](Account const& acct, TrustState state) {
switch (state)
{
case RequireAuth:
env(trust(issuer, token(0), acct, tfSetfAuth));
break;
case ZeroLimit: {
auto jv = trust(acct, token(0));
// set QualityIn so that the trustline is not
// auto-deleted
jv[sfQualityIn] = 10'000'000;
env(jv);
}
break;
case ReachedLimit: {
env(trust(acct, token(1'000)));
env(pay(issuer, acct, token(1'000)));
env.close();
}
break;
case NearLimit: {
env(trust(acct, token(1'000)));
env(pay(issuer, acct, token(950)));
env.close();
}
break;
case NoTrustLine:
// don't create a trustline
break;
default:
BEAST_EXPECT(false);
}
env.close();
};
env.fund(XRP(1'000), issuer, broker, dest);
env.close();
if (trustState == RequireAuth)
{
env(fset(issuer, asfRequireAuth));
env.close();
setTrustLine(broker, RequireAuth);
}
setTrustLine(dest, trustState);
env(trust(broker, token(2'000), 0));
env(pay(issuer, broker, token(2'000)));
env.close();
Vault vault(env);
auto const [tx, keylet] =
vault.create({.owner = broker, .asset = token.asset()});
env(tx);
env.close();
// Test Vault withdraw
env(vault.deposit(
{.depositor = broker,
.id = keylet.key,
.amount = token(1'000)}));
env.close();
env(vault.withdraw(
{.depositor = broker,
.id = keylet.key,
.amount = token(1'000)}),
loanBroker::destination(dest),
ter(std::ignore));
BEAST_EXPECT(env.ter() == tecNO_LINE);
env.close();
env(vault.withdraw(
{.depositor = broker,
.id = keylet.key,
.amount = token(1'000)}));
// Test LoanBroker withdraw
auto const brokerKeylet =
keylet::loanbroker(broker, env.seq(broker));
env(loanBroker::set(broker, keylet.key));
env.close();
env(loanBroker::coverDeposit(
broker, brokerKeylet.key, token(1'000)));
env.close();
env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)),
loanBroker::destination(dest),
ter(std::ignore));
BEAST_EXPECT(env.ter() == tecNO_LINE);
env.close();
// Clearing RequireAuth shouldn't change the result
if (trustState == RequireAuth)
{
env(fclear(issuer, asfRequireAuth));
env.close();
env(loanBroker::coverWithdraw(
broker, brokerKeylet.key, token(100)),
loanBroker::destination(dest),
ter(std::ignore));
BEAST_EXPECT(env.ter() == tecNO_LINE);
env.close();
}
};
test(RequireAuth);
test(ZeroLimit);
test(ReachedLimit);
test(NearLimit);
test(NoTrustLine);
}
void
testRIPD4274MPT()
{
using namespace jtx;
Account issuer("broker");
Account broker("issuer");
Account dest("destination");
enum MPTState {
RequireAuth,
ReachedMAX,
NoMPT,
};
auto test = [&](MPTState MPTState) {
Env env(*this);
testcase << "RIPD-4274 MPT with state: "
<< static_cast<int>(MPTState);
env.fund(XRP(1'000), issuer, broker, dest);
env.close();
auto const maybeToken = [&]() -> std::optional<MPT> {
switch (MPTState)
{
case RequireAuth: {
auto tester = MPTTester(
{.env = env,
.issuer = issuer,
.holders = {broker, dest},
.pay = 2'000,
.flags = MPTDEXFlags | tfMPTRequireAuth,
.authHolder = true,
.maxAmt = 5'000});
// unauthorize dest
tester.authorize(
{.account = issuer,
.holder = dest,
.flags = tfMPTUnauthorize});
return tester;
}
case ReachedMAX: {
auto tester = MPTTester(
{.env = env,
.issuer = issuer,
.holders = {broker, dest},
.pay = 2'000,
.flags = MPTDEXFlags,
.maxAmt = 4'000});
BEAST_EXPECT(
env.balance(issuer, tester) == tester(-4'000));
return tester;
}
case NoMPT: {
return MPTTester(
{.env = env,
.issuer = issuer,
.holders = {broker},
.pay = 2'000,
.flags = MPTDEXFlags,
.maxAmt = 4'000});
}
default:
return std::nullopt;
}
}();
if (!BEAST_EXPECT(maybeToken))
return;
auto const& token = *maybeToken;
Vault vault(env);
auto const [tx, keylet] =
vault.create({.owner = broker, .asset = token.asset()});
env(tx);
env.close();
// Test Vault withdraw
env(vault.deposit(
{.depositor = broker,
.id = keylet.key,
.amount = token(1'000)}));
env.close();
env(vault.withdraw(
{.depositor = broker,
.id = keylet.key,
.amount = token(1'000)}),
loanBroker::destination(dest),
ter(std::ignore));
// Shouldn't fail if at MaximumAmount since no new tokens are issued
TER const err =
MPTState == ReachedMAX ? TER(tesSUCCESS) : tecNO_AUTH;
BEAST_EXPECT(env.ter() == err);
env.close();
if (err != tesSUCCESS)
{
env(vault.withdraw(
{.depositor = broker,
.id = keylet.key,
.amount = token(1'000)}));
}
// Test LoanBroker withdraw
auto const brokerKeylet =
keylet::loanbroker(broker, env.seq(broker));
env(loanBroker::set(broker, keylet.key));
env.close();
env(loanBroker::coverDeposit(
broker, brokerKeylet.key, token(1'000)));
env.close();
env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)),
loanBroker::destination(dest),
ter(std::ignore));
BEAST_EXPECT(env.ter() == err);
env.close();
};
test(RequireAuth);
test(ReachedMAX);
test(NoMPT);
}
void
testRIPD4274()
{
testRIPD4274IOU();
testRIPD4274MPT();
}
public:
void
run() override
{
testLoanBrokerSetDebtMaximum();
testLoanBrokerCoverDepositNullVault();
testDisabled();
@@ -1914,11 +1451,6 @@ public:
testInvalidLoanBrokerSet();
testRequireAuth();
testRIPD4323();
testAMB06_VaultFreezeCheckMissing();
testRIPD4274();
// TODO: Write clawback failure tests with an issuer / MPT that doesn't
// have the right flags set.
}

File diff suppressed because it is too large Load Diff

View File

@@ -5324,7 +5324,7 @@ class Vault_test : public beast::unit_test::suite
// Create a simple Loan for the full amount of Vault assets
env(set(depositor, brokerKeylet.key, asset(100).value()),
loan::interestRate(TenthBips32(0)),
gracePeriod(60),
gracePeriod(10),
paymentInterval(120),
paymentTotal(10),
sig(sfCounterpartySignature, owner),
@@ -5344,7 +5344,7 @@ class Vault_test : public beast::unit_test::suite
THISLINE);
env.close();
env.close(std::chrono::seconds{120 + 60});
env.close(std::chrono::seconds{120 + 10});
env(manage(owner, loanKeylet.key, tfLoanDefault),
ter(tesSUCCESS),

View File

@@ -644,7 +644,7 @@ MPTTester::operator[](std::string const& name) const
}
PrettyAmount
MPTTester::operator()(std::int64_t amount) const
MPTTester::operator()(std::uint64_t amount) const
{
return MPT("", issuanceID())(amount);
}

View File

@@ -272,7 +272,7 @@ public:
operator[](std::string const& name) const;
PrettyAmount
operator()(std::int64_t amount) const;
operator()(std::uint64_t amount) const;
operator Asset() const;

View File

@@ -84,10 +84,50 @@ struct LoanPaymentParts
operator==(LoanPaymentParts const& other) const;
};
/* Describes the initial computed properties of a loan.
*
* This structure contains the fundamental calculated values that define a
* loan's payment structure and amortization schedule. These properties are
* computed:
* - At loan creation (LoanSet transaction)
* - When loan terms change (e.g., after an overpayment that reduces the loan
* balance)
*/
struct LoanProperties
{
// The unrounded amount to be paid at each regular payment period.
// Calculated using the standard amortization formula based on principal,
// interest rate, and number of payments.
// The actual amount paid in the LoanPay transaction must be rounded up to
// the precision of the asset and loan.
Number periodicPayment;
// The total amount the borrower will pay over the life of the loan.
// Equal to periodicPayment * paymentsRemaining.
// This includes principal, interest, and management fees.
Number totalValueOutstanding;
// The total management fee that will be paid to the broker over the
// loan's lifetime. This is a percentage of the total interest (gross)
// as specified by the broker's management fee rate.
Number managementFeeOwedToBroker;
// The scale (decimal places) used for rounding all loan amounts.
// This is the maximum of:
// - The asset's native scale
// - A minimum scale required to represent the periodic payment accurately
// All loan state values (principal, interest, fees) are rounded to this
// scale.
std::int32_t loanScale;
// The principal portion of the first payment.
Number firstPaymentPrincipal;
};
/** This structure captures the parts of a loan state.
*
* Whether the values are theoretical (unrounded) or rounded will depend on how
* it was computed.
* Whether the values are raw (unrounded) or rounded will depend on how it was
* computed.
*
* Many of the fields can be derived from each other, but they're all provided
* here to reduce code duplication and possible mistakes.
@@ -121,39 +161,6 @@ struct LoanState
}
};
/* Describes the initial computed properties of a loan.
*
* This structure contains the fundamental calculated values that define a
* loan's payment structure and amortization schedule. These properties are
* computed:
* - At loan creation (LoanSet transaction)
* - When loan terms change (e.g., after an overpayment that reduces the loan
* balance)
*/
struct LoanProperties
{
// The unrounded amount to be paid at each regular payment period.
// Calculated using the standard amortization formula based on principal,
// interest rate, and number of payments.
// The actual amount paid in the LoanPay transaction must be rounded up to
// the precision of the asset and loan.
Number periodicPayment;
// The loan's current state, with all values rounded to the loan's scale.
LoanState loanState;
// The scale (decimal places) used for rounding all loan amounts.
// This is the maximum of:
// - The asset's native scale
// - A minimum scale required to represent the periodic payment accurately
// All loan state values (principal, interest, fees) are rounded to this
// scale.
std::int32_t loanScale;
// The principal portion of the first payment.
Number firstPaymentPrincipal;
};
// Some values get re-rounded to the vault scale any time they are adjusted. In
// addition, they are prevented from ever going below zero. This helps avoid
// accumulated rounding errors and leftover dust amounts.
@@ -172,12 +179,11 @@ adjustImpreciseNumber(
}
inline int
getAssetsTotalScale(SLE::const_ref vaultSle)
getVaultScale(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).exponent();
}
TER
@@ -190,12 +196,20 @@ checkLoanGuards(
beast::Journal j);
LoanState
computeTheoreticalLoanState(
computeRawLoanState(
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate);
LoanState
computeRawLoanState(
Number const& periodicPayment,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate);
// Constructs a valid LoanState object from arbitrary inputs
LoanState
constructLoanState(
@@ -217,7 +231,7 @@ computeManagementFee(
Number
computeFullPaymentInterest(
Number const& theoreticalPrincipalOutstanding,
Number const& rawPrincipalOutstanding,
Number const& periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t paymentInterval,
@@ -225,6 +239,17 @@ computeFullPaymentInterest(
std::uint32_t startDate,
TenthBips32 closeInterestRate);
Number
computeFullPaymentInterest(
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t paymentRemaining,
NetClock::time_point parentCloseTime,
std::uint32_t paymentInterval,
std::uint32_t prevPaymentDate,
std::uint32_t startDate,
TenthBips32 closeInterestRate);
namespace detail {
// These classes and functions should only be accessed by LendingHelper
// functions and unit tests
@@ -362,70 +387,6 @@ struct LoanStateDeltas
nonNegative();
};
Expected<std::pair<LoanPaymentParts, LoanProperties>, TER>
tryOverpayment(
Asset const& asset,
std::int32_t loanScale,
ExtendedPaymentComponents const& overpaymentComponents,
LoanState const& roundedLoanState,
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t paymentRemaining,
TenthBips16 const managementFeeRate,
beast::Journal j);
Number
computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining);
Number
computePaymentFactor(
Number const& periodicRate,
std::uint32_t paymentsRemaining);
std::pair<Number, Number>
computeInterestAndFeeParts(
Asset const& asset,
Number const& interest,
TenthBips16 managementFeeRate,
std::int32_t loanScale);
Number
loanPeriodicPayment(
Number const& principalOutstanding,
Number const& periodicRate,
std::uint32_t paymentsRemaining);
Number
loanPrincipalFromPeriodicPayment(
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t paymentsRemaining);
Number
loanLatePaymentInterest(
Number const& principalOutstanding,
TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime,
std::uint32_t nextPaymentDueDate);
Number
loanAccruedInterest(
Number const& principalOutstanding,
Number const& periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate,
std::uint32_t paymentInterval);
ExtendedPaymentComponents
computeOverpaymentComponents(
Asset const& asset,
int32_t const loanScale,
Number const& overpayment,
TenthBips32 const overpaymentInterestRate,
TenthBips32 const overpaymentFeeRate,
TenthBips16 const managementFeeRate);
PaymentComponents
computePaymentComponents(
Asset const& asset,
@@ -452,22 +413,13 @@ operator+(LoanState const& lhs, detail::LoanStateDeltas const& rhs);
LoanProperties
computeLoanProperties(
Asset const& asset,
Number const& principalOutstanding,
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate,
std::int32_t minimumScale);
LoanProperties
computeLoanProperties(
Asset const& asset,
Number const& principalOutstanding,
Number const& periodicRate,
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate,
std::int32_t minimumScale);
bool
isRounded(Asset const& asset, Number const& value, std::int32_t scale);

View File

@@ -100,9 +100,6 @@ computePaymentFactor(
Number const& periodicRate,
std::uint32_t paymentsRemaining)
{
if (paymentsRemaining == 0)
return numZero;
// For zero interest, payment factor is simply 1/paymentsRemaining
if (periodicRate == beast::zero)
return Number{1} / paymentsRemaining;
@@ -135,6 +132,27 @@ loanPeriodicPayment(
computePaymentFactor(periodicRate, paymentsRemaining);
}
/* Calculates the periodic payment amount from annualized interest rate.
* Converts the annual rate to periodic rate before computing payment.
*
* Equation (7) from XLS-66 spec, Section A-2 Equation Glossary
*/
Number
loanPeriodicPayment(
Number const& principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
if (principalOutstanding == 0 || paymentsRemaining == 0)
return 0;
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
return loanPeriodicPayment(
principalOutstanding, periodicRate, paymentsRemaining);
}
/* Reverse-calculates principal from periodic payment amount.
* Used to determine theoretical principal at any point in the schedule.
*
@@ -146,9 +164,6 @@ loanPrincipalFromPeriodicPayment(
Number const& periodicRate,
std::uint32_t paymentsRemaining)
{
if (paymentsRemaining == 0)
return numZero;
if (periodicRate == 0)
return periodicPayment * paymentsRemaining;
@@ -156,6 +171,21 @@ loanPrincipalFromPeriodicPayment(
computePaymentFactor(periodicRate, paymentsRemaining);
}
/* Splits gross interest into net interest (to vault) and management fee (to
* broker). Returns pair of (net interest, management fee).
*
* Equation (33) from XLS-66 spec, Section A-2 Equation Glossary
*/
std::pair<Number, Number>
computeInterestAndFeeParts(
Number const& interest,
TenthBips16 managementFeeRate)
{
auto const fee = tenthBipsOfValue(interest, managementFeeRate);
return std::make_pair(interest - fee, fee);
}
/*
* Computes the interest and management fee parts from interest amount.
*
@@ -186,12 +216,6 @@ loanLatePaymentInterest(
NetClock::time_point parentCloseTime,
std::uint32_t nextPaymentDueDate)
{
if (principalOutstanding == beast::zero)
return numZero;
if (lateInterestRate == TenthBips32{0})
return numZero;
auto const now = parentCloseTime.time_since_epoch().count();
// If the payment is not late by any amount of time, then there's no late
@@ -224,9 +248,6 @@ loanAccruedInterest(
if (periodicRate == beast::zero)
return numZero;
if (paymentInterval == 0)
return numZero;
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
auto const now = parentCloseTime.time_since_epoch().count();
@@ -380,33 +401,42 @@ doPayment(
* The function preserves accumulated rounding errors across the re-amortization
* to ensure the loan state remains consistent with its payment history.
*/
Expected<std::pair<LoanPaymentParts, LoanProperties>, TER>
Expected<LoanPaymentParts, TER>
tryOverpayment(
Asset const& asset,
std::int32_t loanScale,
ExtendedPaymentComponents const& overpaymentComponents,
LoanState const& roundedOldState,
Number const& periodicPayment,
Number& totalValueOutstanding,
Number& principalOutstanding,
Number& managementFeeOutstanding,
Number& periodicPayment,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
Number const& periodicRate,
std::uint32_t paymentRemaining,
std::uint32_t prevPaymentDate,
std::optional<std::uint32_t> nextDueDate,
TenthBips16 const managementFeeRate,
beast::Journal j)
{
// Calculate what the loan state SHOULD be theoretically (at full precision)
auto const theoreticalState = computeTheoreticalLoanState(
auto const raw = computeRawLoanState(
periodicPayment, periodicRate, paymentRemaining, managementFeeRate);
// Get the actual loan state (with accumulated rounding from past payments)
auto const rounded = constructLoanState(
totalValueOutstanding, principalOutstanding, managementFeeOutstanding);
// Calculate the accumulated rounding errors. These need to be preserved
// across the re-amortization to maintain consistency with the loan's
// payment history. Without preserving these errors, the loan could end
// up with a different total value than what the borrower has actually paid.
auto const errors = roundedOldState - theoreticalState;
auto const errors = rounded - raw;
// Compute the new principal by applying the overpayment to the theoretical
// principal. Use max with 0 to ensure we never go negative.
auto const newTheoreticalPrincipal = std::max(
theoreticalState.principalOutstanding -
overpaymentComponents.trackedPrincipalDelta,
// Compute the new principal by applying the overpayment to the raw
// (theoretical) principal. Use max with 0 to ensure we never go negative.
auto const newRawPrincipal = std::max(
raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta,
Number{0});
// Compute new loan properties based on the reduced principal. This
@@ -414,8 +444,9 @@ tryOverpayment(
// for the remaining payment schedule.
auto newLoanProperties = computeLoanProperties(
asset,
newTheoreticalPrincipal,
periodicRate,
newRawPrincipal,
interestRate,
paymentInterval,
paymentRemaining,
managementFeeRate,
loanScale);
@@ -423,60 +454,56 @@ tryOverpayment(
JLOG(j.debug()) << "new periodic payment: "
<< newLoanProperties.periodicPayment
<< ", new total value: "
<< newLoanProperties.loanState.valueOutstanding
<< newLoanProperties.totalValueOutstanding
<< ", first payment principal: "
<< newLoanProperties.firstPaymentPrincipal;
// Calculate what the new loan state should be with the new periodic payment
// including rounding errors
auto const newTheoreticalState = computeTheoreticalLoanState(
newLoanProperties.periodicPayment,
periodicRate,
paymentRemaining,
managementFeeRate) +
auto const newRaw = computeRawLoanState(
newLoanProperties.periodicPayment,
periodicRate,
paymentRemaining,
managementFeeRate) +
errors;
JLOG(j.debug()) << "new theoretical value: "
<< newTheoreticalState.valueOutstanding << ", principal: "
<< newTheoreticalState.principalOutstanding
<< ", interest gross: "
<< newTheoreticalState.interestOutstanding();
// Update the loan state variables with the new values that include the
// preserved rounding errors. This ensures the loan's tracked state remains
JLOG(j.debug()) << "new raw value: " << newRaw.valueOutstanding
<< ", principal: " << newRaw.principalOutstanding
<< ", interest gross: " << newRaw.interestOutstanding();
// Update the loan state variables with the new values PLUS the preserved
// rounding errors. This ensures the loan's tracked state remains
// consistent with its payment history.
auto const principalOutstanding = std::clamp(
roundToAsset(
asset,
newTheoreticalState.principalOutstanding,
loanScale,
Number::upward),
numZero,
roundedOldState.principalOutstanding);
auto const totalValueOutstanding = std::clamp(
roundToAsset(
asset,
principalOutstanding + newTheoreticalState.interestOutstanding(),
loanScale,
Number::upward),
numZero,
roundedOldState.valueOutstanding);
auto const managementFeeOutstanding = std::clamp(
roundToAsset(asset, newTheoreticalState.managementFeeDue, loanScale),
numZero,
roundedOldState.managementFeeDue);
auto const roundedNewState = constructLoanState(
principalOutstanding = std::clamp(
roundToAsset(
asset, newRaw.principalOutstanding, loanScale, Number::upward),
numZero,
rounded.principalOutstanding);
totalValueOutstanding = std::clamp(
roundToAsset(
asset,
principalOutstanding + newRaw.interestOutstanding(),
loanScale,
Number::upward),
numZero,
rounded.valueOutstanding);
managementFeeOutstanding = std::clamp(
roundToAsset(asset, newRaw.managementFeeDue, loanScale),
numZero,
rounded.managementFeeDue);
auto const newRounded = constructLoanState(
totalValueOutstanding, principalOutstanding, managementFeeOutstanding);
// Update newLoanProperties so that checkLoanGuards can make an accurate
// evaluation.
newLoanProperties.loanState = roundedNewState;
newLoanProperties.totalValueOutstanding = newRounded.valueOutstanding;
JLOG(j.debug()) << "new rounded value: " << roundedNewState.valueOutstanding
<< ", principal: " << roundedNewState.principalOutstanding
<< ", interest gross: "
<< roundedNewState.interestOutstanding();
JLOG(j.debug()) << "new rounded value: " << newRounded.valueOutstanding
<< ", principal: " << newRounded.principalOutstanding
<< ", interest gross: " << newRounded.interestOutstanding();
// Update the periodic payment to reflect the re-amortized schedule
periodicPayment = newLoanProperties.periodicPayment;
// check that the loan is still valid
if (auto const ter = checkLoanGuards(
@@ -486,7 +513,7 @@ tryOverpayment(
// small interest amounts, that may have already been paid
// off. Check what's still outstanding. This should
// guarantee that the interest checks pass.
roundedNewState.interestOutstanding() != beast::zero,
newRounded.interestOutstanding() != beast::zero,
paymentRemaining,
newLoanProperties,
j))
@@ -500,40 +527,32 @@ tryOverpayment(
// Validate that all computed properties are reasonable. These checks should
// never fail under normal circumstances, but we validate defensively.
if (newLoanProperties.periodicPayment <= 0 ||
newLoanProperties.loanState.valueOutstanding <= 0 ||
newLoanProperties.loanState.managementFeeDue < 0)
newLoanProperties.totalValueOutstanding <= 0 ||
newLoanProperties.managementFeeOwedToBroker < 0)
{
// LCOV_EXCL_START
JLOG(j.warn()) << "Overpayment not allowed: Computed loan "
"properties are invalid. Does "
"not compute. TotalValueOutstanding: "
<< newLoanProperties.loanState.valueOutstanding
<< newLoanProperties.totalValueOutstanding
<< ", PeriodicPayment : "
<< newLoanProperties.periodicPayment
<< ", ManagementFeeOwedToBroker: "
<< newLoanProperties.loanState.managementFeeDue;
<< newLoanProperties.managementFeeOwedToBroker;
return Unexpected(tesSUCCESS);
// LCOV_EXCL_STOP
}
auto const deltas = roundedOldState - roundedNewState;
auto const deltas = rounded - newRounded;
// The change in loan management fee is equal to the change between the old
// and the new outstanding management fees
XRPL_ASSERT_PARTS(
deltas.managementFee ==
roundedOldState.managementFeeDue - managementFeeOutstanding,
"xrpl::detail::tryOverpayment",
"no fee change");
auto const hypotheticalValueOutstanding =
rounded.valueOutstanding - deltas.principal;
// Calculate how the loan's value changed due to the overpayment.
// This should be negative (value decreased) or zero. A principal
// overpayment should never increase the loan's value.
// The value change is derived from the reduction in interest due to
// the lower principal.
// We do not consider the change in management fee here, since
// management fees are excluded from the valueOutstanding.
auto const valueChange = -deltas.interest;
auto const valueChange =
newRounded.valueOutstanding - hypotheticalValueOutstanding;
if (valueChange > 0)
{
JLOG(j.warn()) << "Principal overpayment would increase the value of "
@@ -541,23 +560,21 @@ tryOverpayment(
return Unexpected(tesSUCCESS);
}
return std::make_pair(
LoanPaymentParts{
// Principal paid is the reduction in principal outstanding
.principalPaid = deltas.principal,
// Interest paid is the reduction in interest due
.interestPaid = overpaymentComponents.untrackedInterest,
// Value change includes both the reduction from paying down
// principal (negative) and any untracked interest penalties
// (positive, e.g., if the overpayment itself incurs a fee)
.valueChange =
valueChange + overpaymentComponents.untrackedInterest,
// Fee paid includes both the reduction in tracked management fees
// and any untracked fees on the overpayment itself
.feePaid = overpaymentComponents.untrackedManagementFee +
overpaymentComponents.trackedManagementFeeDelta,
},
newLoanProperties);
return LoanPaymentParts{
// Principal paid is the reduction in principal outstanding
.principalPaid = deltas.principal,
// Interest paid is the reduction in interest due
.interestPaid =
deltas.interest + overpaymentComponents.untrackedInterest,
// Value change includes both the reduction from paying down principal
// (negative) and any untracked interest penalties (positive, e.g., if
// the overpayment itself incurs a fee)
.valueChange =
valueChange + overpaymentComponents.trackedInterestPart(),
// Fee paid includes both the reduction in tracked management fees and
// any untracked fees on the overpayment itself
.feePaid = deltas.managementFee +
overpaymentComponents.untrackedManagementFee};
}
/* Validates and applies an overpayment to the loan state.
@@ -581,16 +598,23 @@ doOverpayment(
NumberProxy& principalOutstandingProxy,
NumberProxy& managementFeeOutstandingProxy,
NumberProxy& periodicPaymentProxy,
TenthBips32 const interestRate,
std::uint32_t const paymentInterval,
Number const& periodicRate,
std::uint32_t const paymentRemaining,
std::uint32_t const prevPaymentDate,
std::optional<std::uint32_t> const nextDueDate,
TenthBips16 const managementFeeRate,
beast::Journal j)
{
auto const loanState = constructLoanState(
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy);
auto const periodicPayment = periodicPaymentProxy;
// Create temporary copies of the loan state that can be safely modified
// and discarded if the overpayment doesn't work out. This prevents
// corrupting the actual ledger data if validation fails.
Number totalValueOutstanding = totalValueOutstandingProxy;
Number principalOutstanding = principalOutstandingProxy;
Number managementFeeOutstanding = managementFeeOutstandingProxy;
Number periodicPayment = periodicPaymentProxy;
JLOG(j.debug())
<< "overpayment components:"
<< ", totalValue before: " << *totalValueOutstandingProxy
@@ -609,28 +633,33 @@ doOverpayment(
asset,
loanScale,
overpaymentComponents,
loanState,
totalValueOutstanding,
principalOutstanding,
managementFeeOutstanding,
periodicPayment,
interestRate,
paymentInterval,
periodicRate,
paymentRemaining,
prevPaymentDate,
nextDueDate,
managementFeeRate,
j);
if (!ret)
return Unexpected(ret.error());
auto const& [loanPaymentParts, newLoanProperties] = *ret;
auto const newRoundedLoanState = newLoanProperties.loanState;
auto const& loanPaymentParts = *ret;
// Safety check: the principal must have decreased. If it didn't (or
// increased!), something went wrong in the calculation and we should
// reject the overpayment.
if (principalOutstandingProxy <= newRoundedLoanState.principalOutstanding)
if (principalOutstandingProxy <= principalOutstanding)
{
// LCOV_EXCL_START
JLOG(j.warn()) << "Overpayment not allowed: principal "
<< "outstanding did not decrease. Before: "
<< *principalOutstandingProxy << ". After: "
<< newRoundedLoanState.principalOutstanding;
<< *principalOutstandingProxy
<< ". After: " << principalOutstanding;
return Unexpected(tesSUCCESS);
// LCOV_EXCL_STOP
}
@@ -641,29 +670,34 @@ doOverpayment(
XRPL_ASSERT_PARTS(
overpaymentComponents.trackedPrincipalDelta ==
principalOutstandingProxy -
newRoundedLoanState.principalOutstanding,
principalOutstandingProxy - principalOutstanding,
"xrpl::detail::doOverpayment",
"principal change agrees");
XRPL_ASSERT_PARTS(
overpaymentComponents.trackedManagementFeeDelta ==
managementFeeOutstandingProxy - managementFeeOutstanding,
"xrpl::detail::doOverpayment",
"no fee change");
// I'm not 100% sure the following asserts are correct. If in doubt, and
// everything else works, remove any that cause trouble.
JLOG(j.debug())
<< "valueChange: " << loanPaymentParts.valueChange
<< ", totalValue before: " << *totalValueOutstandingProxy
<< ", totalValue after: " << newRoundedLoanState.valueOutstanding
<< ", totalValue delta: "
<< (totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding)
<< ", principalDelta: " << overpaymentComponents.trackedPrincipalDelta
<< ", principalPaid: " << loanPaymentParts.principalPaid
<< ", Computed difference: "
<< overpaymentComponents.trackedPrincipalDelta -
(totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding);
JLOG(j.debug()) << "valueChange: " << loanPaymentParts.valueChange
<< ", totalValue before: " << *totalValueOutstandingProxy
<< ", totalValue after: " << totalValueOutstanding
<< ", totalValue delta: "
<< (totalValueOutstandingProxy - totalValueOutstanding)
<< ", principalDelta: "
<< overpaymentComponents.trackedPrincipalDelta
<< ", principalPaid: " << loanPaymentParts.principalPaid
<< ", Computed difference: "
<< overpaymentComponents.trackedPrincipalDelta -
(totalValueOutstandingProxy - totalValueOutstanding);
XRPL_ASSERT_PARTS(
loanPaymentParts.valueChange ==
newRoundedLoanState.valueOutstanding -
totalValueOutstanding -
(totalValueOutstandingProxy -
overpaymentComponents.trackedPrincipalDelta) +
overpaymentComponents.trackedInterestPart(),
@@ -676,12 +710,19 @@ doOverpayment(
"xrpl::detail::doOverpayment",
"principal payment matches");
XRPL_ASSERT_PARTS(
loanPaymentParts.feePaid ==
overpaymentComponents.untrackedManagementFee +
overpaymentComponents.trackedManagementFeeDelta,
"xrpl::detail::doOverpayment",
"fee payment matches");
// All validations passed, so update the proxy objects (which will
// modify the actual Loan ledger object)
totalValueOutstandingProxy = newRoundedLoanState.valueOutstanding;
principalOutstandingProxy = newRoundedLoanState.principalOutstanding;
managementFeeOutstandingProxy = newRoundedLoanState.managementFeeDue;
periodicPaymentProxy = newLoanProperties.periodicPayment;
totalValueOutstandingProxy = totalValueOutstanding;
principalOutstandingProxy = principalOutstanding;
managementFeeOutstandingProxy = managementFeeOutstanding;
periodicPaymentProxy = periodicPayment;
return loanPaymentParts;
}
@@ -748,21 +789,25 @@ computeLatePayment(
// this to keep the logic clear. This preserves all the other fields without
// having to enumerate them.
ExtendedPaymentComponents const late{
periodic,
// Untracked management fee includes:
// 1. Regular service fee (from periodic.untrackedManagementFee)
// 2. Late payment fee (fixed penalty)
// 3. Management fee portion of late interest
periodic.untrackedManagementFee + latePaymentFee +
roundedLateManagementFee,
ExtendedPaymentComponents const late = [&]() {
auto inner = periodic;
// Untracked interest includes:
// 1. Any untracked interest from the regular payment (usually 0)
// 2. Late penalty interest (increases loan value)
// This positive value indicates the loan's value increased due
// to the late payment.
periodic.untrackedInterest + roundedLateInterest};
return ExtendedPaymentComponents{
inner,
// Untracked management fee includes:
// 1. Regular service fee (from periodic.untrackedManagementFee)
// 2. Late payment fee (fixed penalty)
// 3. Management fee portion of late interest
periodic.untrackedManagementFee + latePaymentFee +
roundedLateManagementFee,
// Untracked interest includes:
// 1. Any untracked interest from the regular payment (usually 0)
// 2. Late penalty interest (increases loan value)
// This positive value indicates the loan's value increased due
// to the late payment.
periodic.untrackedInterest + roundedLateInterest};
}();
XRPL_ASSERT_PARTS(
isRounded(asset, late.totalDue, loanScale),
@@ -830,16 +875,15 @@ computeFullPayment(
}
// Calculate the theoretical principal based on the payment schedule.
// This theoretical (unrounded) value is used to compute interest and
// penalties accurately.
Number const theoreticalPrincipalOutstanding =
loanPrincipalFromPeriodicPayment(
periodicPayment, periodicRate, paymentRemaining);
// This raw (unrounded) value is used to compute interest and penalties
// accurately.
Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
periodicPayment, periodicRate, paymentRemaining);
// Full payment interest includes both accrued interest (time since last
// payment) and prepayment penalty (for closing early).
auto const fullPaymentInterest = computeFullPaymentInterest(
theoreticalPrincipalOutstanding,
rawPrincipalOutstanding,
periodicRate,
view.parentCloseTime(),
paymentInterval,
@@ -852,8 +896,9 @@ computeFullPayment(
auto const [roundedFullInterest, roundedFullManagementFee] = [&]() {
auto const interest = roundToAsset(
asset, fullPaymentInterest, loanScale, Number::downward);
return computeInterestAndFeeParts(
auto const parts = computeInterestAndFeeParts(
asset, interest, managementFeeRate, loanScale);
return std::make_tuple(parts.first, parts.second);
}();
ExtendedPaymentComponents const full{
@@ -898,8 +943,7 @@ computeFullPayment(
JLOG(j.trace()) << "computeFullPayment result: periodicPayment: "
<< periodicPayment << ", periodicRate: " << periodicRate
<< ", paymentRemaining: " << paymentRemaining
<< ", theoreticalPrincipalOutstanding: "
<< theoreticalPrincipalOutstanding
<< ", rawPrincipalOutstanding: " << rawPrincipalOutstanding
<< ", fullPaymentInterest: " << fullPaymentInterest
<< ", roundedFullInterest: " << roundedFullInterest
<< ", roundedFullManagementFee: "
@@ -936,8 +980,6 @@ PaymentComponents::trackedInterestPart() const
*
* Special handling for the final payment: all remaining balances are paid off
* regardless of the periodic payment amount.
*
* Implements the pseudo-code function `compute_payment_due()`.
*/
PaymentComponents
computePaymentComponents(
@@ -981,7 +1023,7 @@ computePaymentComponents(
// Calculate what the loan state SHOULD be after this payment (the target).
// This is computed at full precision using the theoretical amortization.
LoanState const trueTarget = computeTheoreticalLoanState(
LoanState const trueTarget = computeRawLoanState(
periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate);
// Round the target to the loan's scale to match how actual loan values
@@ -1187,12 +1229,17 @@ computeOverpaymentComponents(
// This interest doesn't follow the normal amortization schedule - it's
// a one-time charge for paying early.
// Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary
auto const [rawOverpaymentInterest, _] = [&]() {
Number const interest =
tenthBipsOfValue(overpayment, overpaymentInterestRate);
return detail::computeInterestAndFeeParts(interest, managementFeeRate);
}();
// Round the penalty interest components to the loan scale
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] =
[&]() {
auto const interest = roundToAsset(
asset,
tenthBipsOfValue(overpayment, overpaymentInterestRate),
loanScale);
Number const interest =
roundToAsset(asset, rawOverpaymentInterest, loanScale);
return detail::computeInterestAndFeeParts(
asset, interest, managementFeeRate, loanScale);
}();
@@ -1209,11 +1256,12 @@ computeOverpaymentComponents(
.specialCase = detail::PaymentSpecialCase::extra},
// Untracked management fee is the fixed overpayment fee
overpaymentFee,
// Untracked interest is the penalty interest charged for overpaying.
// This is positive, representing a one-time cost, but it's typically
// much smaller than the interest savings from reducing principal.
// It is equal to the paymentComponents.trackedInterestPart()
// but is kept separate for clarity.
// Untracked interest is the penalty interest charged for
// overpaying.
// This is positive, representing a one-time cost, but it's
// typically
// much smaller than the interest savings from reducing
// principal.
roundedOverpaymentInterest};
XRPL_ASSERT_PARTS(
result.trackedInterestPart() == roundedOverpaymentInterest,
@@ -1272,7 +1320,7 @@ checkLoanGuards(
beast::Journal j)
{
auto const totalInterestOutstanding =
properties.loanState.valueOutstanding - principalRequested;
properties.totalValueOutstanding - principalRequested;
// Guard 1: if there is no computed total interest over the life of the
// loan for a non-zero interest rate, we cannot properly amortize the
// loan
@@ -1327,13 +1375,13 @@ checkLoanGuards(
NumberRoundModeGuard mg(Number::upward);
if (std::int64_t const computedPayments{
properties.loanState.valueOutstanding / roundedPayment};
properties.totalValueOutstanding / roundedPayment};
computedPayments != paymentTotal)
{
JLOG(j.warn()) << "Loan Periodic payment ("
<< properties.periodicPayment << ") rounding ("
<< roundedPayment << ") on a total value of "
<< properties.loanState.valueOutstanding
<< properties.totalValueOutstanding
<< " can not complete the loan in the specified "
"number of payments ("
<< computedPayments << " != " << paymentTotal << ")";
@@ -1351,7 +1399,7 @@ checkLoanGuards(
*/
Number
computeFullPaymentInterest(
Number const& theoreticalPrincipalOutstanding,
Number const& rawPrincipalOutstanding,
Number const& periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t paymentInterval,
@@ -1360,7 +1408,7 @@ computeFullPaymentInterest(
TenthBips32 closeInterestRate)
{
auto const accruedInterest = detail::loanAccruedInterest(
theoreticalPrincipalOutstanding,
rawPrincipalOutstanding,
periodicRate,
parentCloseTime,
startDate,
@@ -1374,7 +1422,7 @@ computeFullPaymentInterest(
// Equation (28) from XLS-66 spec, Section A-2 Equation Glossary
auto const prepaymentPenalty = closeInterestRate == beast::zero
? Number{}
: tenthBipsOfValue(theoreticalPrincipalOutstanding, closeInterestRate);
: tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate);
XRPL_ASSERT(
prepaymentPenalty >= 0,
@@ -1385,17 +1433,42 @@ computeFullPaymentInterest(
return accruedInterest + prepaymentPenalty;
}
Number
computeFullPaymentInterest(
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t paymentRemaining,
NetClock::time_point parentCloseTime,
std::uint32_t paymentInterval,
std::uint32_t prevPaymentDate,
std::uint32_t startDate,
TenthBips32 closeInterestRate)
{
Number const rawPrincipalOutstanding =
detail::loanPrincipalFromPeriodicPayment(
periodicPayment, periodicRate, paymentRemaining);
return computeFullPaymentInterest(
rawPrincipalOutstanding,
periodicRate,
parentCloseTime,
paymentInterval,
prevPaymentDate,
startDate,
closeInterestRate);
}
/* Calculates the theoretical loan state at maximum precision for a given point
* in the amortization schedule.
*
* This function computes what the loan's outstanding balances should be based
* on the periodic payment amount and number of payments remaining,
* without considering any rounding that may have been applied to the actual
* Loan object's state. This "theoretical" (unrounded) state is used as a target
* for computing payment components and validating that the loan's tracked state
* Loan object's state. This "raw" (unrounded) state is used as a target for
* computing payment components and validating that the loan's tracked state
* hasn't drifted too far from the theoretical values.
*
* The theoretical state serves several purposes:
* The raw state serves several purposes:
* 1. Computing the expected payment breakdown (principal, interest, fees)
* 2. Detecting and correcting rounding errors that accumulate over time
* 3. Validating that overpayments are calculated correctly
@@ -1403,12 +1476,9 @@ computeFullPaymentInterest(
*
* If paymentRemaining is 0, returns a fully zeroed-out LoanState,
* representing a completely paid-off loan.
*
* Implements the `calculate_true_loan_state` function from the XLS-66 spec
* section 3.2.4.4 Transaction Pseudo-code
*/
LoanState
computeTheoreticalLoanState(
computeRawLoanState(
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t const paymentRemaining,
@@ -1424,42 +1494,55 @@ computeTheoreticalLoanState(
}
// Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
Number const totalValueOutstanding = periodicPayment * paymentRemaining;
Number const rawTotalValueOutstanding = periodicPayment * paymentRemaining;
Number const principalOutstanding =
Number const rawPrincipalOutstanding =
detail::loanPrincipalFromPeriodicPayment(
periodicPayment, periodicRate, paymentRemaining);
// Equation (31) from XLS-66 spec, Section A-2 Equation Glossary
Number const interestOutstandingGross =
totalValueOutstanding - principalOutstanding;
Number const rawInterestOutstandingGross =
rawTotalValueOutstanding - rawPrincipalOutstanding;
// Equation (32) from XLS-66 spec, Section A-2 Equation Glossary
Number const managementFeeOutstanding =
tenthBipsOfValue(interestOutstandingGross, managementFeeRate);
Number const rawManagementFeeOutstanding =
tenthBipsOfValue(rawInterestOutstandingGross, managementFeeRate);
// Equation (33) from XLS-66 spec, Section A-2 Equation Glossary
Number const interestOutstandingNet =
interestOutstandingGross - managementFeeOutstanding;
Number const rawInterestOutstandingNet =
rawInterestOutstandingGross - rawManagementFeeOutstanding;
return LoanState{
.valueOutstanding = totalValueOutstanding,
.principalOutstanding = principalOutstanding,
.interestDue = interestOutstandingNet,
.managementFeeDue = managementFeeOutstanding,
};
.valueOutstanding = rawTotalValueOutstanding,
.principalOutstanding = rawPrincipalOutstanding,
.interestDue = rawInterestOutstandingNet,
.managementFeeDue = rawManagementFeeOutstanding};
};
LoanState
computeRawLoanState(
Number const& periodicPayment,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate)
{
return computeRawLoanState(
periodicPayment,
loanPeriodicRate(interestRate, paymentInterval),
paymentRemaining,
managementFeeRate);
}
/* Constructs a LoanState from rounded Loan ledger object values.
*
* This function creates a LoanState structure from the three tracked values
* stored in a Loan ledger object. Unlike calculateTheoreticalLoanState(), which
* stored in a Loan ledger object. Unlike calculateRawLoanState(), which
* computes theoretical unrounded values, this function works with values
* that have already been rounded to the loan's scale.
*
* The key difference from calculateTheoreticalLoanState():
* - calculateTheoreticalLoanState: Computes theoretical values at full
* precision
* The key difference from calculateRawLoanState():
* - calculateRawLoanState: Computes theoretical values at full precision
* - constructRoundedLoanState: Builds state from actual rounded ledger values
*
* The interestDue field is derived from the other three values rather than
@@ -1517,16 +1600,11 @@ computeManagementFee(
/*
* Given the loan parameters, compute the derived properties of the loan.
*
* Pulls together several formulas from the XLS-66 spec, which are noted at each
* step, plus the concepts from 3.2.4.3 Conceptual Loan Value. They are used for
* to check some of the conditions in 3.2.1.5 Failure Conditions for the LoanSet
* transaction.
*/
LoanProperties
computeLoanProperties(
Asset const& asset,
Number const& principalOutstanding,
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
@@ -1537,39 +1615,12 @@ computeLoanProperties(
XRPL_ASSERT(
interestRate == 0 || periodicRate > 0,
"xrpl::computeLoanProperties : valid rate");
return computeLoanProperties(
asset,
principalOutstanding,
periodicRate,
paymentsRemaining,
managementFeeRate,
minimumScale);
}
/*
* Given the loan parameters, compute the derived properties of the loan.
*
* Pulls together several formulas from the XLS-66 spec, which are noted at each
* step, plus the concepts from 3.2.4.3 Conceptual Loan Value. They are used for
* to check some of the conditions in 3.2.1.5 Failure Conditions for the LoanSet
* transaction.
*/
LoanProperties
computeLoanProperties(
Asset const& asset,
Number const& principalOutstanding,
Number const& periodicRate,
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate,
std::int32_t minimumScale)
{
auto const periodicPayment = detail::loanPeriodicPayment(
principalOutstanding, periodicRate, paymentsRemaining);
auto const [totalValueOutstanding, loanScale] = [&]() {
// only round up if there should be interest
NumberRoundModeGuard mg(
periodicRate == 0 ? Number::to_nearest : Number::upward);
NumberRoundModeGuard mg(Number::to_nearest);
// Use STAmount's internal rounding instead of roundToAsset, because
// we're going to use this result to determine the scale for all the
// other rounding.
@@ -1590,7 +1641,7 @@ computeLoanProperties(
// We may need to truncate the total value because of the minimum
// scale
amount = roundToAsset(asset, amount, loanScale);
amount = roundToAsset(asset, amount, loanScale, Number::to_nearest);
return std::make_pair(amount, loanScale);
}();
@@ -1598,12 +1649,12 @@ computeLoanProperties(
// Since we just figured out the loan scale, we haven't been able to
// validate that the principal fits in it, so to allow this function to
// succeed, round it here, and let the caller do the validation.
auto const roundedPrincipalOutstanding = roundToAsset(
principalOutstanding = roundToAsset(
asset, principalOutstanding, loanScale, Number::to_nearest);
// Equation (31) from XLS-66 spec, Section A-2 Equation Glossary
auto const totalInterestOutstanding =
totalValueOutstanding - roundedPrincipalOutstanding;
totalValueOutstanding - principalOutstanding;
auto const feeOwedToBroker = computeManagementFee(
asset, totalInterestOutstanding, managementFeeRate, loanScale);
@@ -1613,13 +1664,13 @@ computeLoanProperties(
auto const firstPaymentPrincipal = [&]() {
// Compute the parts for the first payment. Ensure that the
// principal payment will actually change the principal.
auto const startingState = computeTheoreticalLoanState(
auto const startingState = computeRawLoanState(
periodicPayment,
periodicRate,
paymentsRemaining,
managementFeeRate);
auto const firstPaymentState = computeTheoreticalLoanState(
auto const firstPaymentState = computeRawLoanState(
periodicPayment,
periodicRate,
paymentsRemaining - 1,
@@ -1633,13 +1684,10 @@ computeLoanProperties(
return LoanProperties{
.periodicPayment = periodicPayment,
.loanState = constructLoanState(
totalValueOutstanding,
roundedPrincipalOutstanding,
feeOwedToBroker),
.totalValueOutstanding = totalValueOutstanding,
.managementFeeOwedToBroker = feeOwedToBroker,
.loanScale = loanScale,
.firstPaymentPrincipal = firstPaymentPrincipal,
};
.firstPaymentPrincipal = firstPaymentPrincipal};
}
/*
@@ -1692,7 +1740,7 @@ loanMakePayment(
Number const periodicPayment = loan->at(sfPeriodicPayment);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDueDate);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate);
std::uint32_t const startDate = loan->at(sfStartDate);
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
@@ -1968,8 +2016,12 @@ loanMakePayment(
principalOutstandingProxy,
managementFeeOutstandingProxy,
periodicPaymentProxy,
interestRate,
paymentInterval,
periodicRate,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
managementFeeRate,
j))
totalParts += *overResult;

View File

@@ -1,5 +1,5 @@
#ifndef XRPL_LEDGER_CREDIT_H_INCLUDED
#define XRPL_LEDGER_CREDIT_H_INCLUDED
#ifndef XRPL_APP_PATHS_CREDIT_H_INCLUDED
#define XRPL_APP_PATHS_CREDIT_H_INCLUDED
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/IOUAmount.h>

View File

@@ -1,11 +1,11 @@
#include <xrpld/app/paths/AMMContext.h>
#include <xrpld/app/paths/Credit.h>
#include <xrpld/app/paths/Flow.h>
#include <xrpld/app/paths/detail/AmountSpec.h>
#include <xrpld/app/paths/detail/Steps.h>
#include <xrpld/app/paths/detail/StrandFlow.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/Credit.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/XRPAmount.h>

View File

@@ -1,8 +1,8 @@
#include <xrpld/app/paths/Credit.h>
#include <xrpld/app/paths/detail/StepChecks.h>
#include <xrpld/app/paths/detail/Steps.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/Credit.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>

View File

@@ -3,6 +3,7 @@
#include <xrpld/app/misc/AMMHelpers.h>
#include <xrpld/app/paths/AMMContext.h>
#include <xrpld/app/paths/Credit.h>
#include <xrpld/app/paths/Flow.h>
#include <xrpld/app/paths/detail/AmountSpec.h>
#include <xrpld/app/paths/detail/FlatSets.h>
@@ -10,7 +11,6 @@
#include <xrpld/app/paths/detail/Steps.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/Credit.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/XRPAmount.h>

View File

@@ -1,9 +1,9 @@
#include <xrpld/app/paths/Credit.h>
#include <xrpld/app/paths/detail/AmountSpec.h>
#include <xrpld/app/paths/detail/StepChecks.h>
#include <xrpld/app/paths/detail/Steps.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/Credit.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>

View File

@@ -270,7 +270,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
return findClawAmount.error();
}
STAmount const& clawAmount = *findClawAmount;
STAmount const clawAmount = *findClawAmount;
// Explicitly check the balance of the trust line / MPT to make sure the
// balance is actually there. It should always match `sfCoverAvailable`, so
@@ -287,14 +287,6 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
// Check if the vault asset issuer has the correct flags
auto const sleIssuer =
ctx.view.read(keylet::account(vaultAsset.getIssuer()));
if (!sleIssuer)
{
// LCOV_EXCL_START
JLOG(ctx.j.fatal()) << "Issuer account does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
return std::visit(
[&]<typename T>(T const&) {
return preclaimHelper<T>(ctx, *sleIssuer, clawAmount);
@@ -329,7 +321,7 @@ LoanBrokerCoverClawback::doApply()
determineClawAmount(*sleBroker, vaultAsset, amount);
if (!findClawAmount)
return tecINTERNAL; // LCOV_EXCL_LINE
STAmount const& clawAmount = *findClawAmount;
STAmount const clawAmount = *findClawAmount;
// Just for paranoia's sake
if (clawAmount.native())
return tecINTERNAL; // LCOV_EXCL_LINE

View File

@@ -81,8 +81,7 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
vaultAsset,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j,
SpendableHandling::shFULL_BALANCE) < amount)
ctx.j) < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;

View File

@@ -48,11 +48,6 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
auto const dstAcct = tx[~sfDestination].value_or(account);
if (isPseudoAccount(ctx.view, dstAcct))
{
JLOG(ctx.j.warn()) << "Trying to withdraw into a pseudo-account.";
return tecPSEUDO_ACCOUNT;
}
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
if (!sleBroker)
{

View File

@@ -46,6 +46,30 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Owner count is " << ownerCount;
return tecHAS_OBLIGATIONS;
}
if (auto const debtTotal = sleBroker->at(sfDebtTotal);
debtTotal != beast::zero)
{
// Any remaining debt should have been wiped out by the last Loan
// Delete. This check is purely defensive.
auto const vault =
ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
if (!vault)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const asset = vault->at(sfAsset);
auto const scale = getVaultScale(vault);
auto const rounded =
roundToAsset(asset, debtTotal, scale, Number::towards_zero);
if (rounded != beast::zero)
{
// LCOV_EXCL_START
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is "
<< debtTotal << ", which rounds to " << rounded;
return tecHAS_OBLIGATIONS;
// LCOV_EXCL_START
}
}
auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
if (!vault)
@@ -58,26 +82,6 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
Asset const asset = vault->at(sfAsset);
if (auto const debtTotal = sleBroker->at(sfDebtTotal);
debtTotal != beast::zero)
{
// Any remaining debt should have been wiped out by the last Loan
// Delete. This check is purely defensive.
auto const scale = getAssetsTotalScale(vault);
auto const rounded =
roundToAsset(asset, debtTotal, scale, Number::towards_zero);
if (rounded != beast::zero)
{
// LCOV_EXCL_START
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is "
<< debtTotal << ", which rounds to " << rounded;
return tecHAS_OBLIGATIONS;
// LCOV_EXCL_STOP
}
}
auto const coverAvailable =
STAmount{asset, sleBroker->at(sfCoverAvailable)};
// If there are assets in the cover, broker will receive them on deletion.

View File

@@ -89,18 +89,6 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
if (auto const debtMax = tx[~sfDebtMaximum])
{
// Can't reduce the debt maximum below the current total debt
auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
if (*debtMax != 0 && *debtMax < currentDebtTotal)
{
JLOG(ctx.j.warn())
<< "Cannot reduce DebtMaximum below current DebtTotal.";
return tecLIMIT_EXCEEDED;
}
}
}
else
{
@@ -117,13 +105,6 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
}
if (auto const ter = canAddHolding(ctx.view, sleVault->at(sfAsset)))
return ter;
if (auto const ter = checkFrozen(
ctx.view, sleVault->at(sfAccount), sleVault->at(sfAsset)))
{
JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen.";
return ter;
}
}
return tesSUCCESS;
}

View File

@@ -115,7 +115,7 @@ LoanDelete::doApply()
roundToAsset(
vaultSle->at(sfAsset),
debtTotalProxy,
getAssetsTotalScale(vaultSle),
getVaultScale(vaultSle),
Number::towards_zero) == beast::zero,
"xrpl::LoanDelete::doApply",
"last loan, remaining debt rounds to zero");

View File

@@ -106,7 +106,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
if (loanBrokerSle->at(sfOwner) != account)
{
JLOG(ctx.j.warn())
<< "LoanBroker for Loan does not belong to the account. LoanManage "
<< "LoanBroker for Loan does not belong to the account. LoanModify "
"can only be submitted by the Loan Broker.";
return tecNO_PERMISSION;
}
@@ -158,7 +158,7 @@ LoanManage::defaultLoan(
auto const minimumCover =
tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up, too
auto const covered = roundToAsset(
return roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
@@ -169,9 +169,6 @@ LoanManage::defaultLoan(
tenthBipsOfValue(minimumCover, coverRateLiquidation),
totalDefaultAmount),
loanScale);
auto const coverAvailable = *brokerSle->at(sfCoverAvailable);
return std::min(covered, coverAvailable);
}();
auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
@@ -181,7 +178,7 @@ LoanManage::defaultLoan(
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the accounting by rounding some of the values to that
// scale.
auto const vaultScale = getAssetsTotalScale(vaultSle);
auto const vaultScale = getVaultScale(vaultSle);
{
// Decrease the Total Value of the Vault:
@@ -226,13 +223,11 @@ LoanManage::defaultLoan(
}
if (*vaultAvailableProxy > *vaultTotalProxy)
{
// LCOV_EXCL_START
JLOG(j.fatal())
<< "Vault assets available must not be greater "
"than assets outstanding. Available: "
<< *vaultAvailableProxy << ", Total: " << *vaultTotalProxy;
return tecINTERNAL;
// LCOV_EXCL_STOP
JLOG(j.warn()) << "Vault assets available must not be greater "
"than assets outstanding. Available: "
<< *vaultAvailableProxy
<< ", Total: " << *vaultTotalProxy;
return tecLIMIT_EXCEEDED;
}
// The loss has been realized
@@ -247,11 +242,7 @@ LoanManage::defaultLoan(
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
adjustImpreciseNumber(
vaultLossUnrealizedProxy,
-totalDefaultAmount,
vaultAsset,
vaultScale);
vaultLossUnrealizedProxy -= totalDefaultAmount;
}
view.update(vaultSle);
}
@@ -259,9 +250,11 @@ LoanManage::defaultLoan(
// Update the LoanBroker object:
{
auto const asset = *vaultSle->at(sfAsset);
// Decrease the Debt of the LoanBroker:
adjustImpreciseNumber(
brokerDebtTotalProxy, -totalDefaultAmount, vaultAsset, vaultScale);
brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale);
// Decrease the First-Loss Capital Cover Available:
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
if (coverAvailableProxy < defaultCovered)
@@ -304,20 +297,13 @@ LoanManage::impairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j)
{
Number const lossUnrealized = owedToVault(loanSle);
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the accounting by rounding some of the values to that
// scale.
auto const vaultScale = getAssetsTotalScale(vaultSle);
// Update the Vault object(set "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
adjustImpreciseNumber(
vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
vaultLossUnrealizedProxy += lossUnrealized;
if (vaultLossUnrealizedProxy >
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
{
@@ -343,19 +329,13 @@ LoanManage::impairLoan(
return tesSUCCESS;
}
[[nodiscard]] TER
TER
LoanManage::unimpairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j)
{
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the accounting by rounding some of the values to that
// scale.
auto const vaultScale = getAssetsTotalScale(vaultSle);
// Update the Vault object(clear "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
Number const lossReversed = owedToVault(loanSle);
@@ -367,18 +347,14 @@ LoanManage::unimpairLoan(
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Reverse the "paper loss"
adjustImpreciseNumber(
vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
vaultLossUnrealizedProxy -= lossReversed;
view.update(vaultSle);
// Update the Loan object
loanSle->clearFlag(lsfLoanImpaired);
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const normalPaymentDueDate =
std::max(
loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) +
std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) +
paymentInterval;
if (!hasExpired(view, normalPaymentDueDate))
{
@@ -420,12 +396,22 @@ LoanManage::doApply()
// Valid flag combinations are checked in preflight. No flags is valid -
// just a noop.
if (tx.isFlag(tfLoanDefault))
return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
if (tx.isFlag(tfLoanImpair))
return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
if (tx.isFlag(tfLoanUnimpair))
return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_);
// Noop, as described above.
{
if (auto const ter =
defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_))
return ter;
}
else if (tx.isFlag(tfLoanImpair))
{
if (auto const ter = impairLoan(view, loanSle, vaultSle, j_))
return ter;
}
else if (tx.isFlag(tfLoanUnimpair))
{
if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_))
return ter;
}
return tesSUCCESS;
}

View File

@@ -44,17 +44,15 @@ public:
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
/** Helper function that might be needed by other transactors
*/
[[nodiscard]] static TER
static TER
unimpairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
TER

View File

@@ -152,7 +152,9 @@ LoanPay::preclaim(PreclaimContext const& ctx)
}
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
auto const paymentRemaining = loanSle->at(sfPaymentRemaining);
TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)};
if (paymentRemaining == 0 || principalOutstanding == 0)
{
@@ -209,14 +211,13 @@ LoanPay::preclaim(PreclaimContext const& ctx)
// Do not support "partial payments" - if the transaction says to pay X,
// then the account must have X available, even if the loan payment takes
// less.
if (auto const balance = accountHolds(
if (auto const balance = accountSpendable(
ctx.view,
account,
asset,
fhZERO_IF_FROZEN,
ahZERO_IF_UNAUTHORIZED,
ctx.j,
SpendableHandling::shFULL_BALANCE);
ctx.j);
balance < amount)
{
JLOG(ctx.j.warn()) << "Payment amount too large. Amount: "
@@ -261,12 +262,11 @@ LoanPay::doApply()
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
// Send the broker fee to the owner if they have sufficient cover available,
// _and_ if the owner can receive funds
// _and_ if the broker is authorized to hold funds. If not, so as not to
// block the payment, add it to the cover balance (send it to the broker
// pseudo account).
// _and_ if the owner can receive funds. If not, so as not to block the
// payment, add it to the cover balance (send it to the broker pseudo
// account).
//
// Normally freeze status is checked in preclaim, but we do it here to
// Normally freeze status is checked in preflight, but we do it here to
// avoid duplicating the check. It'll claim a fee either way.
bool const sendBrokerFeeToOwner = [&]() {
// Round the minimum required cover up to be conservative. This ensures
@@ -278,8 +278,7 @@ LoanPay::doApply()
asset,
tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum),
loanScale) &&
!isDeepFrozen(view, brokerOwner, asset) &&
!requireAuth(view, asset, brokerOwner, AuthType::StrongAuth);
!isDeepFrozen(view, brokerOwner, asset);
}();
auto const brokerPayee =
@@ -306,12 +305,7 @@ LoanPay::doApply()
// change will be discarded.
if (loanSle->isFlag(lsfLoanImpaired))
{
if (auto const ret =
LoanManage::unimpairLoan(view, loanSle, vaultSle, asset, j_))
{
JLOG(j_.fatal()) << "Failed to unimpair loan before payment.";
return ret; // LCOV_EXCL_LINE
}
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
}
LoanPaymentType const paymentType = [&tx]() {
@@ -383,7 +377,7 @@ LoanPay::doApply()
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the payment by rounding some of the values to that scale.
auto const vaultScale = getAssetsTotalScale(vaultSle);
auto const vaultScale = assetsTotalProxy.value().exponent();
auto const totalPaidToVaultRaw =
paymentParts->principalPaid + paymentParts->interestPaid;
@@ -427,41 +421,35 @@ LoanPay::doApply()
// Vault object state changes
view.update(vaultSle);
#if !NDEBUG
{
Number const assetsAvailableBefore = *assetsAvailableProxy;
Number const pseudoAccountBalanceBefore = accountHolds(
view,
vaultPseudoAccount,
asset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
Number const assetsAvailableBefore = *assetsAvailableProxy;
Number const pseudoAccountBalanceBefore = accountHolds(
view,
vaultPseudoAccount,
asset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
{
XRPL_ASSERT_PARTS(
assetsAvailableBefore == pseudoAccountBalanceBefore,
"xrpl::LoanPay::doApply",
"vault pseudo balance agrees before");
}
#endif
assetsAvailableProxy += totalPaidToVaultRounded;
assetsTotalProxy += paymentParts->valueChange;
assetsAvailableProxy += totalPaidToVaultRounded;
assetsTotalProxy += paymentParts->valueChange;
XRPL_ASSERT_PARTS(
*assetsAvailableProxy <= *assetsTotalProxy,
"xrpl::LoanPay::doApply",
"assets available must not be greater than assets outstanding");
XRPL_ASSERT_PARTS(
*assetsAvailableProxy <= *assetsTotalProxy,
"xrpl::LoanPay::doApply",
"assets available must not be greater than assets outstanding");
if (*assetsAvailableProxy > *assetsTotalProxy)
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Vault assets available must not be greater "
"than assets outstanding. Available: "
<< *assetsAvailableProxy
<< ", Total: " << *assetsTotalProxy;
return tecINTERNAL;
// LCOV_EXCL_STOP
if (*assetsAvailableProxy > *assetsTotalProxy)
{
// LCOV_EXCL_START
return tecINTERNAL;
// LCOV_EXCL_STOP
}
}
JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw
@@ -486,34 +474,21 @@ LoanPay::doApply()
}
#if !NDEBUG
auto const accountBalanceBefore = accountHolds(
view,
account_,
asset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
auto const accountBalanceBefore = accountSpendable(
view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const vaultBalanceBefore = account_ == vaultPseudoAccount
? STAmount{asset, 0}
: accountHolds(
: accountSpendable(
view,
vaultPseudoAccount,
asset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
j_);
auto const brokerBalanceBefore = account_ == brokerPayee
? STAmount{asset, 0}
: accountHolds(
view,
brokerPayee,
asset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
: accountSpendable(
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
#endif
if (totalPaidToVaultRounded != beast::zero)
@@ -554,7 +529,6 @@ LoanPay::doApply()
WaiveTransferFee::Yes))
return ter;
#if !NDEBUG
Number const assetsAvailableAfter = *assetsAvailableProxy;
Number const pseudoAccountBalanceAfter = accountHolds(
view,
@@ -568,34 +542,22 @@ LoanPay::doApply()
"xrpl::LoanPay::doApply",
"vault pseudo balance agrees after");
auto const accountBalanceAfter = accountHolds(
view,
account_,
asset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
#if !NDEBUG
auto const accountBalanceAfter = accountSpendable(
view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const vaultBalanceAfter = account_ == vaultPseudoAccount
? STAmount{asset, 0}
: accountHolds(
: accountSpendable(
view,
vaultPseudoAccount,
asset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
j_);
auto const brokerBalanceAfter = account_ == brokerPayee
? STAmount{asset, 0}
: accountHolds(
view,
brokerPayee,
asset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_,
SpendableHandling::shFULL_BALANCE);
: accountSpendable(
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
XRPL_ASSERT_PARTS(
accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore ==

View File

@@ -88,12 +88,10 @@ LoanSet::preflight(PreflightContext const& ctx)
if (auto const paymentInterval = tx[~sfPaymentInterval];
!validNumericMinimum(paymentInterval, LoanSet::minPaymentInterval))
return temINVALID;
// Grace period is between min default value and payment interval
else if (auto const gracePeriod = tx[~sfGracePeriod]; //
!validNumericRange(
gracePeriod,
paymentInterval.value_or(LoanSet::defaultPaymentInterval),
defaultGracePeriod))
else if (!validNumericRange(
tx[~sfGracePeriod],
paymentInterval.value_or(LoanSet::defaultPaymentInterval)))
return temINVALID;
// Copied from preflight2
@@ -284,15 +282,6 @@ LoanSet::preclaim(PreclaimContext const& ctx)
if (!vault)
// Should be impossible
return tefBAD_LEDGER; // LCOV_EXCL_LINE
if (vault->at(sfAssetsMaximum) != 0 &&
vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum))
{
JLOG(ctx.j.warn())
<< "Vault at maximum assets limit. Can't add another loan.";
return tecLIMIT_EXCEEDED;
}
Asset const asset = vault->at(sfAsset);
auto const vaultPseudo = vault->at(sfAccount);
@@ -394,7 +383,7 @@ LoanSet::doApply()
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
auto const vaultScale = getAssetsTotalScale(vaultSle);
auto const vaultScale = getVaultScale(vaultSle);
if (vaultAvailableProxy < principalRequested)
{
JLOG(j_.warn())
@@ -417,21 +406,6 @@ LoanSet::doApply()
TenthBips16{brokerSle->at(sfManagementFeeRate)},
vaultScale);
LoanState const state = constructLoanState(
properties.loanState.valueOutstanding,
principalRequested,
properties.loanState.managementFeeDue);
auto const vaultMaximum = *vaultSle->at(sfAssetsMaximum);
XRPL_ASSERT_PARTS(
vaultMaximum == 0 || vaultMaximum > *vaultTotalProxy,
"xrpl::LoanSet::doApply",
"Vault is below maximum limit");
if (vaultMaximum != 0 && state.interestDue > vaultMaximum - vaultTotalProxy)
{
JLOG(j_.warn()) << "Loan would exceed the maximum assets of the vault";
return tecLIMIT_EXCEEDED;
}
// Check that relevant values won't lose precision. This is mostly only
// relevant for IOU assets.
{
@@ -443,8 +417,8 @@ LoanSet::doApply()
JLOG(j_.warn())
<< field.f->getName() << " (" << *value
<< ") has too much precision. Total loan value is "
<< properties.loanState.valueOutstanding
<< " with a scale of " << properties.loanScale;
<< properties.totalValueOutstanding << " with a scale of "
<< properties.loanScale;
return tecPRECISION_LOSS;
}
}
@@ -460,20 +434,22 @@ LoanSet::doApply()
return ret;
// Check that the other computed values are valid
if (properties.loanState.managementFeeDue < 0 ||
properties.loanState.valueOutstanding <= 0 ||
if (properties.managementFeeOwedToBroker < 0 ||
properties.totalValueOutstanding <= 0 ||
properties.periodicPayment <= 0)
{
// LCOV_EXCL_START
JLOG(j_.warn())
<< "Computed loan properties are invalid. Does not compute."
<< " Management fee: " << properties.loanState.managementFeeDue
<< ". Total Value: " << properties.loanState.valueOutstanding
<< ". PeriodicPayment: " << properties.periodicPayment;
<< "Computed loan properties are invalid. Does not compute.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
LoanState const state = constructLoanState(
properties.totalValueOutstanding,
principalRequested,
properties.managementFeeOwedToBroker);
auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
auto const loanAssetsToBorrower = principalRequested - originationFee;
@@ -558,11 +534,11 @@ LoanSet::doApply()
// ignore tecDUPLICATE. That means the holding already exists,
// and is fine here
return ter;
}
if (auto const ter =
requireAuth(view, vaultAsset, brokerOwner, AuthType::StrongAuth))
return ter;
if (auto const ter = requireAuth(
view, vaultAsset, brokerOwner, AuthType::StrongAuth))
return ter;
}
if (auto const ter = accountSendMulti(
view,
@@ -612,10 +588,9 @@ LoanSet::doApply()
// Set dynamic / computed fields to their initial values
loan->at(sfPrincipalOutstanding) = principalRequested;
loan->at(sfPeriodicPayment) = properties.periodicPayment;
loan->at(sfTotalValueOutstanding) = properties.loanState.valueOutstanding;
loan->at(sfManagementFeeOutstanding) =
properties.loanState.managementFeeDue;
loan->at(sfPreviousPaymentDueDate) = 0;
loan->at(sfTotalValueOutstanding) = properties.totalValueOutstanding;
loan->at(sfManagementFeeOutstanding) = properties.managementFeeOwedToBroker;
loan->at(sfPreviousPaymentDate) = 0;
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
loan->at(sfPaymentRemaining) = paymentTotal;
view.insert(loan);

View File

@@ -115,14 +115,16 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
!isTesSuccess(ter))
return ter;
if (accountHolds(
// Asset issuer does not have any balance, they can just create funds by
// depositing in the vault.
if ((vaultAsset.native() || vaultAsset.getIssuer() != account) &&
accountHolds(
ctx.view,
account,
vaultAsset,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j,
SpendableHandling::shFULL_BALANCE) < assets)
ctx.j) < assets)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;

View File

@@ -425,7 +425,7 @@ parseLoan(Json::Value const& params, Json::StaticString const fieldName)
}
auto const id = LedgerEntryHelpers::requiredUInt256(
params, jss::loan_broker_id, "malformedBroker");
params, jss::loan_broker_id, "malformedLoanBrokerID");
if (!id)
return Unexpected(id.error());
auto const seq = LedgerEntryHelpers::requiredUInt32(