mirror of
https://github.com/XRPLF/rippled.git
synced 2026-01-13 19:25:24 +00:00
Compare commits
19 Commits
ximinez/le
...
ripple/was
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d83ec96848 | ||
|
|
419d53ec4c | ||
|
|
d4d70d5675 | ||
|
|
bbc28b3b1c | ||
|
|
5aab274b7a | ||
|
|
2c30e41191 | ||
|
|
8ea5106b0b | ||
|
|
1977df9c2e | ||
|
|
6c95548df5 | ||
|
|
90e0bbd0fc | ||
|
|
b57df290de | ||
|
|
8a403f1241 | ||
|
|
6d2640871d | ||
|
|
500bb68831 | ||
|
|
16087c9680 | ||
|
|
25c3060fef | ||
|
|
ce9f0b38a4 | ||
|
|
35f7cbf772 | ||
|
|
0db564d261 |
@@ -262,6 +262,7 @@ words:
|
||||
- venv
|
||||
- vfalco
|
||||
- vinnie
|
||||
- wasmi
|
||||
- wextra
|
||||
- wptr
|
||||
- writeme
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ==
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user