diff --git a/src/xrpld/app/paths/Credit.h b/include/xrpl/ledger/Credit.h similarity index 93% rename from src/xrpld/app/paths/Credit.h rename to include/xrpl/ledger/Credit.h index 5bdcd70e74..09b65b3dde 100644 --- a/src/xrpld/app/paths/Credit.h +++ b/include/xrpl/ledger/Credit.h @@ -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 #include diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 398209ab71..0902ddf36f 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -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); diff --git a/src/xrpld/app/paths/Credit.cpp b/src/libxrpl/ledger/Credit.cpp similarity index 100% rename from src/xrpld/app/paths/Credit.cpp rename to src/libxrpl/ledger/Credit.cpp diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index a86e695741..7886072b80 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -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( + [&](TIss const& issue) -> TER { + if constexpr (std::is_same_v) + { + 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 diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 49df9a2541..bdd323e6a0 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -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(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(MPTState); + + env.fund(XRP(1'000), issuer, broker, dest); + env.close(); + + auto const maybeToken = [&]() -> std::optional { + 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. } diff --git a/src/xrpld/app/paths/Flow.cpp b/src/xrpld/app/paths/Flow.cpp index a102e44854..b5088d15b3 100644 --- a/src/xrpld/app/paths/Flow.cpp +++ b/src/xrpld/app/paths/Flow.cpp @@ -1,11 +1,11 @@ #include -#include #include #include #include #include #include +#include #include #include diff --git a/src/xrpld/app/paths/detail/DirectStep.cpp b/src/xrpld/app/paths/detail/DirectStep.cpp index 4e701d348f..3d3a76f42d 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -1,8 +1,8 @@ -#include #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/paths/detail/StrandFlow.h b/src/xrpld/app/paths/detail/StrandFlow.h index fab92dca35..ca4b18f0a3 100644 --- a/src/xrpld/app/paths/detail/StrandFlow.h +++ b/src/xrpld/app/paths/detail/StrandFlow.h @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -11,6 +10,7 @@ #include #include +#include #include #include #include diff --git a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp index 83271321be..ed1866bf24 100644 --- a/src/xrpld/app/paths/detail/XRPEndpointStep.cpp +++ b/src/xrpld/app/paths/detail/XRPEndpointStep.cpp @@ -1,9 +1,9 @@ -#include #include #include #include #include +#include #include #include #include