mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-28 09:30:34 +00:00
Integrate Permissioned Domain & Credential Checks for Lending Protocol
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
This commit is contained in:
@@ -198,7 +198,10 @@ enum LedgerEntryType : std::uint16_t {
|
||||
LEDGER_OBJECT(Loan, \
|
||||
LSF_FLAG(lsfLoanDefault, 0x00010000) \
|
||||
LSF_FLAG(lsfLoanImpaired, 0x00020000) \
|
||||
LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */
|
||||
LSF_FLAG(lsfLoanOverpayment, 0x00040000)) /* True, loan allows overpayments */ \
|
||||
\
|
||||
LEDGER_OBJECT(LoanBroker, \
|
||||
LSF_FLAG(lsfLoanBrokerPrivate, 0x00010000))
|
||||
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -212,6 +212,10 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
TF_FLAG(tfLoanDefault, 0x00010000) \
|
||||
TF_FLAG(tfLoanImpair, 0x00020000) \
|
||||
TF_FLAG(tfLoanUnimpair, 0x00040000), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(LoanBrokerSet, \
|
||||
TF_FLAG(tfLoanBrokerPrivate, 0x00010000) , \
|
||||
MASK_ADJ(0))
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -515,6 +515,7 @@ LEDGER_ENTRY(ltLOAN_BROKER, 0x0088, LoanBroker, loan_broker, ({
|
||||
{sfCoverAvailable, soeDEFAULT},
|
||||
{sfCoverRateMinimum, soeDEFAULT},
|
||||
{sfCoverRateLiquidation, soeDEFAULT},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object representing a loan between a Borrower and a Loan Broker
|
||||
|
||||
@@ -943,6 +943,7 @@ TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet,
|
||||
{sfDebtMaximum, soeOPTIONAL},
|
||||
{sfCoverRateMinimum, soeOPTIONAL},
|
||||
{sfCoverRateLiquidation, soeOPTIONAL},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction deletes a Loan Broker */
|
||||
|
||||
@@ -22,6 +22,9 @@ public:
|
||||
static std::vector<OptionaledField<STNumber>> const&
|
||||
getValueFields();
|
||||
|
||||
static std::uint32_t
|
||||
getFlagsMask(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
|
||||
@@ -947,7 +947,8 @@ NoModifiedUnmodifiableFields::finalize(
|
||||
fieldChanged(before, after, sfOwner) ||
|
||||
fieldChanged(before, after, sfManagementFeeRate) ||
|
||||
fieldChanged(before, after, sfCoverRateMinimum) ||
|
||||
fieldChanged(before, after, sfCoverRateLiquidation);
|
||||
fieldChanged(before, after, sfCoverRateLiquidation) ||
|
||||
fieldChanged(before, after, sfFlags);
|
||||
break;
|
||||
case ltLOAN:
|
||||
/*
|
||||
|
||||
@@ -186,6 +186,12 @@ ValidLoanBroker::finalize(
|
||||
"is less than pseudo-account asset balance";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (after->at(~sfDomainID) && !after->isFlag(lsfLoanBrokerPrivate))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: DomainID is set on public Loan Broker";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpl/tx/transactors/lending/LoanBrokerSet.h>
|
||||
//
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/tx/transactors/lending/LendingHelpers.h>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -29,8 +30,11 @@ LoanBrokerSet::preflight(PreflightContext const& ctx)
|
||||
if (!validNumericRange(tx[~sfDebtMaximum], Number(maxMPTokenAmount), Number(0)))
|
||||
return temINVALID;
|
||||
|
||||
auto const domainID = tx.at(~sfDomainID);
|
||||
|
||||
if (tx.isFieldPresent(sfLoanBrokerID))
|
||||
{
|
||||
// We're modifying an existing LoanBroker.
|
||||
// Fixed fields can not be specified if we're modifying an existing
|
||||
// LoanBroker Object
|
||||
if (tx.isFieldPresent(sfManagementFeeRate) || tx.isFieldPresent(sfCoverRateMinimum) ||
|
||||
@@ -39,6 +43,43 @@ LoanBrokerSet::preflight(PreflightContext const& ctx)
|
||||
|
||||
if (tx[sfLoanBrokerID] == beast::zero)
|
||||
return temINVALID;
|
||||
|
||||
if (ctx.rules.enabled(fixLendingProtocolV1_1))
|
||||
{
|
||||
if (tx.isFlag(tfLoanBrokerPrivate))
|
||||
{
|
||||
// Cannot change private flag on existing broker
|
||||
return temINVALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We're creating a new LoanBroker.
|
||||
if (ctx.rules.enabled(fixLendingProtocolV1_1))
|
||||
{
|
||||
if (domainID)
|
||||
{
|
||||
if (*domainID == beast::zero)
|
||||
{
|
||||
// DomainID must not be zero if provided
|
||||
return temMALFORMED;
|
||||
}
|
||||
if (!tx.isFlag(tfLoanBrokerPrivate))
|
||||
{
|
||||
// Public brokers cannot have a DomainID
|
||||
return temINVALID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.rules.enabled(fixLendingProtocolV1_1))
|
||||
{
|
||||
if (tx.isFlag(tfLoanBrokerPrivate) || tx.isFieldPresent(sfDomainID))
|
||||
{
|
||||
return temDISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const vaultID = tx.at(~sfVaultID))
|
||||
@@ -68,6 +109,16 @@ LoanBrokerSet::getValueFields()
|
||||
return valueFields;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
LoanBrokerSet::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.rules.enabled(fixLendingProtocolV1_1))
|
||||
{
|
||||
return tfLoanSetMask;
|
||||
}
|
||||
return tfUniversalMask;
|
||||
}
|
||||
|
||||
TER
|
||||
LoanBrokerSet::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -90,6 +141,17 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
auto const domainID = tx[~sfDomainID];
|
||||
if (domainID && *domainID != beast::zero)
|
||||
{
|
||||
auto const sleDomain = ctx.view.read(keylet::permissionedDomain(*domainID));
|
||||
if (!sleDomain)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Domain does not exist.";
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const brokerID = tx[~sfLoanBrokerID])
|
||||
{
|
||||
// Updating an existing Broker
|
||||
@@ -121,6 +183,11 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
}
|
||||
|
||||
if (domainID && *domainID != beast::zero && !sleBroker->isFlag(lsfLoanBrokerPrivate))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -179,6 +246,14 @@ LoanBrokerSet::doApply()
|
||||
if (auto const debtMax = tx[~sfDebtMaximum])
|
||||
broker->at(sfDebtMaximum) = *debtMax;
|
||||
|
||||
auto domainID = tx[~sfDomainID];
|
||||
if (domainID && *domainID == beast::zero)
|
||||
{
|
||||
domainID = std::nullopt;
|
||||
}
|
||||
|
||||
broker->at(~sfDomainID) = domainID;
|
||||
|
||||
view.update(broker);
|
||||
|
||||
associateAsset(*broker, vaultAsset);
|
||||
@@ -250,6 +325,14 @@ LoanBrokerSet::doApply()
|
||||
if (auto const coverLiq = tx[~sfCoverRateLiquidation])
|
||||
broker->at(sfCoverRateLiquidation) = *coverLiq;
|
||||
|
||||
if (tx.isFlag(tfLoanBrokerPrivate))
|
||||
{
|
||||
broker->setFlag(lsfLoanBrokerPrivate);
|
||||
}
|
||||
|
||||
auto const domainID = tx[~sfDomainID];
|
||||
broker->at(~sfDomainID) = domainID;
|
||||
|
||||
view.insert(broker);
|
||||
|
||||
associateAsset(*broker, vaultAsset);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <xrpl/tx/transactors/lending/LoanSet.h>
|
||||
//
|
||||
#include <xrpl/ledger/CredentialHelpers.h>
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/tx/transactors/lending/LendingHelpers.h>
|
||||
@@ -329,6 +330,27 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (brokerSle->isFlag(lsfLoanBrokerPrivate))
|
||||
{
|
||||
auto const domainID = brokerSle->at(~sfDomainID);
|
||||
if (!domainID)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Private LoanBroker must have a DomainID.";
|
||||
return tecNO_AUTH;
|
||||
}
|
||||
|
||||
auto const sleDomain = ctx.view.read(keylet::permissionedDomain(*domainID));
|
||||
if (!sleDomain)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Domain does not exist."; // LCOV_EXCL_LINE
|
||||
return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (auto const ter = credentials::validDomain(ctx.view, *domainID, borrower);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/permissioned_domains.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h>
|
||||
@@ -1797,6 +1799,338 @@ class LoanBroker_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(env.enabled(fixLendingProtocolV1_1));
|
||||
}
|
||||
|
||||
void
|
||||
testPrivateLoanBroker()
|
||||
{
|
||||
testcase("Private Loan Broker with Permissioned Domain");
|
||||
using namespace jtx;
|
||||
using namespace loanBroker;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const alice{"alice"};
|
||||
Account const credIssuer{"credIssuer"};
|
||||
std::string const credType = "LoanCredential";
|
||||
|
||||
Env env{*this};
|
||||
Vault vault{env};
|
||||
|
||||
env.fund(XRP(100'000), issuer, alice, credIssuer);
|
||||
env.close();
|
||||
|
||||
// Create an IOU asset and vault
|
||||
env(trust(alice, issuer["IOU"](1'000'000)));
|
||||
env.close();
|
||||
PrettyAsset const asset{issuer["IOU"]};
|
||||
env(pay(issuer, alice, asset(100'000)));
|
||||
env.close();
|
||||
|
||||
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Create a permissioned domain
|
||||
pdomain::Credentials const credentials{{credIssuer, credType}};
|
||||
env(pdomain::setTx(credIssuer, credentials));
|
||||
env.close();
|
||||
auto const domainId = pdomain::getNewDomain(env.meta());
|
||||
BEAST_EXPECT(domainId != beast::zero);
|
||||
|
||||
// Test 1: Cannot set tfLoanBrokerPrivate without fixLendingProtocolV1_1
|
||||
{
|
||||
Env envNoFix{*this};
|
||||
envNoFix.disableFeature(fixLendingProtocolV1_1);
|
||||
Vault vault2{envNoFix};
|
||||
|
||||
envNoFix.fund(XRP(100'000), issuer, alice);
|
||||
envNoFix.close();
|
||||
envNoFix(trust(alice, issuer["IOU"](1'000'000)));
|
||||
envNoFix.close();
|
||||
envNoFix(pay(issuer, alice, asset(100'000)));
|
||||
envNoFix.close();
|
||||
|
||||
auto [tx2, vaultKeylet2] = vault2.create({.owner = alice, .asset = asset});
|
||||
envNoFix(tx2);
|
||||
envNoFix.close();
|
||||
|
||||
// tfLoanBrokerPrivate should be disabled
|
||||
envNoFix(set(alice, vaultKeylet2.key, tfLoanBrokerPrivate), ter(temINVALID_FLAG));
|
||||
}
|
||||
|
||||
// Test 2: Create a private loan broker with DomainID
|
||||
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate), domainID(domainId));
|
||||
env.close();
|
||||
|
||||
// Verify the broker was created with the correct flags and DomainID
|
||||
{
|
||||
auto const sleBroker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(sleBroker);
|
||||
if (sleBroker)
|
||||
{
|
||||
BEAST_EXPECT(sleBroker->isFlag(lsfLoanBrokerPrivate));
|
||||
BEAST_EXPECT(sleBroker->at(~sfDomainID) == domainId);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Cannot create public broker with DomainID
|
||||
{
|
||||
// Public broker (no tfLoanBrokerPrivate) with DomainID should fail
|
||||
env(set(alice, vaultKeylet.key), domainID(domainId), ter(temINVALID));
|
||||
}
|
||||
|
||||
// Test 4: Cannot create broker with zero DomainID
|
||||
{
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate),
|
||||
domainID(beast::zero),
|
||||
ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// Test 5: Cannot create broker with non-existent DomainID
|
||||
{
|
||||
uint256 fakeDomainId;
|
||||
BEAST_EXPECT(fakeDomainId.parseHex(
|
||||
"1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF"));
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate),
|
||||
domainID(fakeDomainId),
|
||||
ter(tecOBJECT_NOT_FOUND));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPrivateLoanBrokerModify()
|
||||
{
|
||||
testcase("Modify Private Loan Broker DomainID");
|
||||
using namespace jtx;
|
||||
using namespace loanBroker;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const alice{"alice"};
|
||||
Account const credIssuer{"credIssuer"};
|
||||
std::string const credType = "LoanCredential";
|
||||
|
||||
Env env{*this};
|
||||
Vault vault{env};
|
||||
|
||||
env.fund(XRP(100'000), issuer, alice, credIssuer);
|
||||
env.close();
|
||||
|
||||
// Create an IOU asset and vault
|
||||
env(trust(alice, issuer["IOU"](1'000'000)));
|
||||
env.close();
|
||||
PrettyAsset const asset{issuer["IOU"]};
|
||||
env(pay(issuer, alice, asset(100'000)));
|
||||
env.close();
|
||||
|
||||
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Create two permissioned domains
|
||||
pdomain::Credentials const credentials{{credIssuer, credType}};
|
||||
env(pdomain::setTx(credIssuer, credentials));
|
||||
env.close();
|
||||
auto const domainId1 = pdomain::getNewDomain(env.meta());
|
||||
|
||||
env(pdomain::setTx(credIssuer, credentials));
|
||||
env.close();
|
||||
auto const domainId2 = pdomain::getNewDomain(env.meta());
|
||||
|
||||
// Create a private loan broker with DomainID
|
||||
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate), domainID(domainId1));
|
||||
env.close();
|
||||
|
||||
// Test 1: Modify DomainID to a different domain
|
||||
env(set(alice, vaultKeylet.key), loanBrokerID(brokerKeylet.key), domainID(domainId2));
|
||||
env.close();
|
||||
{
|
||||
auto const sleBroker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(sleBroker);
|
||||
if (sleBroker)
|
||||
{
|
||||
BEAST_EXPECT(sleBroker->at(~sfDomainID) == domainId2);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Clear DomainID by setting to zero
|
||||
env(set(alice, vaultKeylet.key), loanBrokerID(brokerKeylet.key), domainID(beast::zero));
|
||||
env.close();
|
||||
{
|
||||
auto const sleBroker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(sleBroker);
|
||||
if (sleBroker)
|
||||
{
|
||||
BEAST_EXPECT(!sleBroker->at(~sfDomainID));
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Set DomainID back
|
||||
env(set(alice, vaultKeylet.key), loanBrokerID(brokerKeylet.key), domainID(domainId1));
|
||||
env.close();
|
||||
{
|
||||
auto const sleBroker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(sleBroker);
|
||||
if (sleBroker)
|
||||
{
|
||||
BEAST_EXPECT(sleBroker->at(~sfDomainID) == domainId1);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4: Cannot set tfLoanBrokerPrivate when modifying (flag is immutable)
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate),
|
||||
loanBrokerID(brokerKeylet.key),
|
||||
ter(temINVALID));
|
||||
|
||||
// Test 5: Create a public broker and try to set DomainID on it
|
||||
auto const publicBrokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(set(alice, vaultKeylet.key)); // No tfLoanBrokerPrivate = public
|
||||
env.close();
|
||||
{
|
||||
auto const sleBroker = env.le(publicBrokerKeylet);
|
||||
BEAST_EXPECT(sleBroker);
|
||||
if (sleBroker)
|
||||
{
|
||||
BEAST_EXPECT(!sleBroker->isFlag(lsfLoanBrokerPrivate));
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot set non-zero DomainID on public broker
|
||||
env(set(alice, vaultKeylet.key),
|
||||
loanBrokerID(publicBrokerKeylet.key),
|
||||
domainID(domainId1),
|
||||
ter(tecNO_PERMISSION));
|
||||
|
||||
// But zero DomainID on public broker is allowed (no-op)
|
||||
env(set(alice, vaultKeylet.key),
|
||||
loanBrokerID(publicBrokerKeylet.key),
|
||||
domainID(beast::zero));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testPrivateBrokerUnsetDomainBlocksLoans()
|
||||
{
|
||||
testcase("Unsetting DomainID on private broker blocks new loans");
|
||||
using namespace jtx;
|
||||
using namespace loanBroker;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const alice{"alice"}; // Broker owner
|
||||
Account const bob{"bob"}; // Borrower
|
||||
Account const credIssuer{"credIssuer"};
|
||||
std::string const credType = "LoanCredential";
|
||||
|
||||
Env env{*this};
|
||||
Vault vault{env};
|
||||
|
||||
env.fund(XRP(100'000), issuer, alice, bob, credIssuer);
|
||||
env.close();
|
||||
|
||||
// Create an IOU asset and vault
|
||||
env(trust(alice, issuer["IOU"](1'000'000)));
|
||||
env(trust(bob, issuer["IOU"](1'000'000)));
|
||||
env.close();
|
||||
PrettyAsset const asset{issuer["IOU"]};
|
||||
env(pay(issuer, alice, asset(100'000)));
|
||||
env(pay(issuer, bob, asset(10'000)));
|
||||
env.close();
|
||||
|
||||
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Deposit into vault
|
||||
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50'000)}));
|
||||
env.close();
|
||||
|
||||
// Create a permissioned domain
|
||||
pdomain::Credentials const credentials{{credIssuer, credType}};
|
||||
env(pdomain::setTx(credIssuer, credentials));
|
||||
env.close();
|
||||
auto const domainId = pdomain::getNewDomain(env.meta());
|
||||
|
||||
// Create credential for bob and accept it
|
||||
env(credentials::create(bob, credIssuer, credType));
|
||||
env.close();
|
||||
env(credentials::accept(bob, credIssuer, credType));
|
||||
env.close();
|
||||
|
||||
// Create a private loan broker with DomainID
|
||||
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate), domainID(domainId));
|
||||
env.close();
|
||||
|
||||
// Deposit cover into broker
|
||||
env(coverDeposit(alice, brokerKeylet.key, asset(1000)));
|
||||
env.close();
|
||||
|
||||
// Bob can create a loan (has credentials, domain is set)
|
||||
{
|
||||
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
|
||||
auto setTx = env.jt(loan::set(bob, brokerKeylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(loanKeylet));
|
||||
}
|
||||
|
||||
// Now unset the DomainID by setting it to zero
|
||||
env(set(alice, vaultKeylet.key), loanBrokerID(brokerKeylet.key), domainID(beast::zero));
|
||||
env.close();
|
||||
|
||||
// Verify DomainID is unset
|
||||
{
|
||||
auto const sleBroker = env.le(brokerKeylet);
|
||||
BEAST_EXPECT(sleBroker);
|
||||
if (sleBroker)
|
||||
{
|
||||
BEAST_EXPECT(!sleBroker->at(~sfDomainID));
|
||||
BEAST_EXPECT(sleBroker->isFlag(lsfLoanBrokerPrivate));
|
||||
}
|
||||
}
|
||||
|
||||
// Bob cannot create a new loan (private broker has no domain configured)
|
||||
{
|
||||
auto const loanKeylet2 = keylet::loan(brokerKeylet.key, 2);
|
||||
auto setTx = env.jt(loan::set(bob, brokerKeylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx, ter(tecNO_AUTH));
|
||||
|
||||
BEAST_EXPECT(!env.le(loanKeylet2));
|
||||
}
|
||||
|
||||
// Set the DomainID back
|
||||
env(set(alice, vaultKeylet.key), loanBrokerID(brokerKeylet.key), domainID(domainId));
|
||||
env.close();
|
||||
|
||||
// Now bob can create a loan again
|
||||
{
|
||||
auto const loanKeylet3 = keylet::loan(brokerKeylet.key, 2);
|
||||
auto setTx = env.jt(loan::set(bob, brokerKeylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(loanKeylet3));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -1819,6 +2153,10 @@ public:
|
||||
|
||||
testRIPD4274();
|
||||
|
||||
testPrivateLoanBroker();
|
||||
testPrivateLoanBrokerModify();
|
||||
testPrivateBrokerUnsetDomainBlocksLoans();
|
||||
|
||||
// TODO: Write clawback failure tests with an issuer / MPT that doesn't
|
||||
// have the right flags set.
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
//
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/permissioned_domains.h>
|
||||
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -7097,6 +7099,245 @@ protected:
|
||||
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
|
||||
}
|
||||
|
||||
void
|
||||
testPrivateBrokerCredentials()
|
||||
{
|
||||
testcase("Private Broker Credential Validation");
|
||||
using namespace jtx;
|
||||
using namespace loanBroker;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const alice{"alice"}; // Broker owner
|
||||
Account const bob{"bob"}; // Borrower with credentials
|
||||
Account const carol{"carol"}; // Borrower without credentials
|
||||
Account const credIssuer{"credIssuer"};
|
||||
std::string const credType = "LoanCredential";
|
||||
|
||||
Env env{*this};
|
||||
Vault vault{env};
|
||||
|
||||
env.fund(XRP(100'000), issuer, alice, bob, carol, credIssuer);
|
||||
env.close();
|
||||
|
||||
// Create an IOU asset and vault
|
||||
env(trust(alice, issuer["IOU"](1'000'000)));
|
||||
env(trust(bob, issuer["IOU"](1'000'000)));
|
||||
env(trust(carol, issuer["IOU"](1'000'000)));
|
||||
env.close();
|
||||
PrettyAsset const asset{issuer["IOU"]};
|
||||
env(pay(issuer, alice, asset(100'000)));
|
||||
env(pay(issuer, bob, asset(10'000)));
|
||||
env(pay(issuer, carol, asset(10'000)));
|
||||
env.close();
|
||||
|
||||
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Deposit into vault
|
||||
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50'000)}));
|
||||
env.close();
|
||||
|
||||
// Create a permissioned domain
|
||||
pdomain::Credentials const credentials{{credIssuer, credType}};
|
||||
env(pdomain::setTx(credIssuer, credentials));
|
||||
env.close();
|
||||
auto const domainId = pdomain::getNewDomain(env.meta());
|
||||
|
||||
// Create a private loan broker with DomainID
|
||||
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate), domainID(domainId));
|
||||
env.close();
|
||||
|
||||
// Deposit cover into broker
|
||||
env(coverDeposit(alice, brokerKeylet.key, asset(1000)));
|
||||
env.close();
|
||||
|
||||
// Test 1: Borrower without credentials cannot create loan
|
||||
{
|
||||
auto setTx = env.jt(loan::set(carol, brokerKeylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx, ter(tecNO_AUTH));
|
||||
}
|
||||
|
||||
// Create credential for bob and accept it
|
||||
env(credentials::create(bob, credIssuer, credType));
|
||||
env.close();
|
||||
env(credentials::accept(bob, credIssuer, credType));
|
||||
env.close();
|
||||
|
||||
// Test 2: Borrower with valid credentials can create loan
|
||||
{
|
||||
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
|
||||
auto setTx = env.jt(loan::set(bob, brokerKeylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(loanKeylet));
|
||||
}
|
||||
|
||||
// Test 3: Private broker without DomainID configured rejects loans
|
||||
{
|
||||
// Create another private broker without DomainID
|
||||
auto const broker2Keylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate)); // No domainID
|
||||
env.close();
|
||||
|
||||
env(coverDeposit(alice, broker2Keylet.key, asset(1000)));
|
||||
env.close();
|
||||
|
||||
// Even bob with credentials should fail - broker has no domain configured
|
||||
auto setTx = env.jt(loan::set(bob, broker2Keylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx, ter(tecNO_AUTH));
|
||||
}
|
||||
|
||||
// Test 4: Public broker allows anyone to create loan
|
||||
{
|
||||
auto const publicBrokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(set(alice, vaultKeylet.key)); // No tfLoanBrokerPrivate = public
|
||||
env.close();
|
||||
|
||||
env(coverDeposit(alice, publicBrokerKeylet.key, asset(1000)));
|
||||
env.close();
|
||||
|
||||
// Carol without credentials can create loan on public broker
|
||||
auto const loanKeylet = keylet::loan(publicBrokerKeylet.key, 1);
|
||||
auto setTx = env.jt(loan::set(carol, publicBrokerKeylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(loanKeylet));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPrivateBrokerLoanPayAfterCredentialRevoked()
|
||||
{
|
||||
testcase("LoanPay works after borrower credential is revoked");
|
||||
using namespace jtx;
|
||||
using namespace loanBroker;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const alice{"alice"}; // Broker owner
|
||||
Account const bob{"bob"}; // Borrower
|
||||
Account const credIssuer{"credIssuer"};
|
||||
std::string const credType = "LoanCredential";
|
||||
|
||||
Env env{*this};
|
||||
Vault vault{env};
|
||||
|
||||
env.fund(XRP(100'000), issuer, alice, bob, credIssuer);
|
||||
env.close();
|
||||
|
||||
// Create an IOU asset and vault
|
||||
env(trust(alice, issuer["IOU"](1'000'000)));
|
||||
env(trust(bob, issuer["IOU"](1'000'000)));
|
||||
env.close();
|
||||
PrettyAsset const asset{issuer["IOU"]};
|
||||
env(pay(issuer, alice, asset(100'000)));
|
||||
env(pay(issuer, bob, asset(10'000)));
|
||||
env.close();
|
||||
|
||||
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Deposit into vault
|
||||
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50'000)}));
|
||||
env.close();
|
||||
|
||||
// Create a permissioned domain
|
||||
pdomain::Credentials const credentials{{credIssuer, credType}};
|
||||
env(pdomain::setTx(credIssuer, credentials));
|
||||
env.close();
|
||||
auto const domainId = pdomain::getNewDomain(env.meta());
|
||||
|
||||
// Create credential for bob and accept it
|
||||
env(credentials::create(bob, credIssuer, credType));
|
||||
env.close();
|
||||
env(credentials::accept(bob, credIssuer, credType));
|
||||
env.close();
|
||||
|
||||
// Create a private loan broker with DomainID
|
||||
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(set(alice, vaultKeylet.key, tfLoanBrokerPrivate), domainID(domainId));
|
||||
env.close();
|
||||
|
||||
// Deposit cover into broker
|
||||
env(coverDeposit(alice, brokerKeylet.key, asset(1000)));
|
||||
env.close();
|
||||
|
||||
// Create a loan for bob
|
||||
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
|
||||
{
|
||||
auto setTx = env.jt(loan::set(bob, brokerKeylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(loanKeylet));
|
||||
}
|
||||
|
||||
// Now delete bob's credential
|
||||
env(credentials::deleteCred(bob, bob, credIssuer, credType));
|
||||
env.close();
|
||||
|
||||
// Verify credential is gone
|
||||
auto const credKeylet = credentials::keylet(bob, credIssuer, credType);
|
||||
BEAST_EXPECT(!env.le(credKeylet));
|
||||
|
||||
// Bob should still be able to make a loan payment even without credentials
|
||||
env(loan::pay(bob, loanKeylet.key, asset(51)));
|
||||
env.close();
|
||||
|
||||
// Verify the loan still exists and payment was processed
|
||||
auto const sleLoan = env.le(loanKeylet);
|
||||
BEAST_EXPECT(sleLoan);
|
||||
|
||||
// Bob should NOT be able to create a NEW loan (no credentials)
|
||||
{
|
||||
auto const newLoanKeylet = keylet::loan(brokerKeylet.key, 2);
|
||||
auto setTx = env.jt(loan::set(bob, brokerKeylet.key, asset(100).number()));
|
||||
sig(sfCounterpartySignature, alice)(env, setTx);
|
||||
fee{env.current()->fees().base * 2}(env, setTx);
|
||||
loan::counterparty(alice)(env, setTx);
|
||||
loan::interestRate(TenthBips32{1000})(env, setTx);
|
||||
loan::paymentTotal(2)(env, setTx);
|
||||
loan::paymentInterval(100)(env, setTx);
|
||||
env(setTx, ter(tecNO_AUTH));
|
||||
|
||||
BEAST_EXPECT(!env.le(newLoanKeylet));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -7151,6 +7392,9 @@ public:
|
||||
testLoanPayBrokerOwnerNoPermissionedDomainMPT();
|
||||
testLoanSetBrokerOwnerNoPermissionedDomainMPT();
|
||||
testSequentialFLCDepletion();
|
||||
|
||||
testPrivateBrokerCredentials();
|
||||
testPrivateBrokerLoanPayAfterCredentialRevoked();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -717,6 +717,8 @@ auto const coverRateLiquidation =
|
||||
|
||||
auto const destination = JTxFieldWrapper<accountIDField>(sfDestination);
|
||||
|
||||
auto const domainID = JTxFieldWrapper<uint256Field>(sfDomainID);
|
||||
|
||||
} // namespace loanBroker
|
||||
|
||||
/* Loan */
|
||||
|
||||
Reference in New Issue
Block a user