Merge branch 'ximinez/lending-XLS-66-ongoing' into ximinez/lending-number-simple

This commit is contained in:
Ed Hennis
2026-01-08 21:37:08 -04:00
committed by GitHub
9 changed files with 345 additions and 9 deletions

View File

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

View File

@@ -710,6 +710,8 @@ 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(
@@ -717,6 +719,7 @@ canWithdraw(
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.
@@ -730,12 +733,15 @@ 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(
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.
@@ -749,6 +755,8 @@ canWithdraw(
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
*/
[[nodiscard]] TER
canWithdraw(ReadView const& view, STTx const& tx);

View File

@@ -3,6 +3,7 @@
#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>
@@ -1341,12 +1342,58 @@ 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(
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))
@@ -1361,7 +1408,7 @@ canWithdraw(
return tecNO_PERMISSION;
}
return tesSUCCESS;
return withdrawToDestExceedsLimit(view, from, to, amount);
}
[[nodiscard]] TER
@@ -1369,11 +1416,12 @@ canWithdraw(
AccountID const& from,
ReadView const& view,
AccountID const& to,
STAmount const& amount,
bool hasDestinationTag)
{
auto const toSle = view.read(keylet::account(to));
return canWithdraw(from, view, to, toSle, hasDestinationTag);
return canWithdraw(from, view, to, toSle, amount, hasDestinationTag);
}
[[nodiscard]] TER
@@ -1382,7 +1430,8 @@ canWithdraw(ReadView const& view, STTx const& tx)
auto const from = tx[sfAccount];
auto const to = tx[~sfDestination].value_or(from);
return canWithdraw(from, view, to, tx.isFieldPresent(sfDestinationTag));
return canWithdraw(
from, view, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag));
}
TER

View File

@@ -1647,6 +1647,283 @@ class LoanBroker_test : public beast::unit_test::suite
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
@@ -1666,6 +1943,8 @@ public:
testRIPD4323();
testAMB06_VaultFreezeCheckMissing();
testRIPD4274();
// TODO: Write clawback failure tests with an issuer / MPT that doesn't
// have the right flags set.
}

View File

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

View File

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

View File

@@ -3,7 +3,6 @@
#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>
@@ -11,6 +10,7 @@
#include <xrpld/app/paths/detail/Steps.h>
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/Credit.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/XRPAmount.h>

View File

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