diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index 868c9fb26d..12bd913a29 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace xrpl { @@ -584,9 +585,19 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account, { if (trustLine) { - return trustLine->isFlag((account > issue.account) ? lsfLowAuth : lsfHighAuth) - ? tesSUCCESS - : TER{tecNO_AUTH}; + if (trustLine->isFlag((account > issue.account) ? lsfLowAuth : lsfHighAuth)) + return tesSUCCESS; + + // A Vault or LoanBroker holds the asset on behalf of its + // participants, and its pseudo-account has no signing key, so it + // can never authorize its own line and no transaction offers the + // issuer a chance to do it either. Treat a line it already owns as + // authorized, the same way MPT does. + if (view.rules().enabled(fixCleanup3_4_0) && + isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID})) + return tesSUCCESS; + + return TER{tecNO_AUTH}; } return TER{tecNO_LINE}; } diff --git a/src/test/app/lending/LoanPay_test.cpp b/src/test/app/lending/LoanPay_test.cpp index 9d840fe1bf..28623a3411 100644 --- a/src/test/app/lending/LoanPay_test.cpp +++ b/src/test/app/lending/LoanPay_test.cpp @@ -13,9 +13,12 @@ #include #include #include +#include +#include #include #include #include +#include #include #include #include @@ -728,10 +731,94 @@ private: } } + // A vault holding an IOU whose issuer requires authorization ends up with + // its own trust line unauthorized: VaultCreate opens the line without the + // auth flag, and the pseudo-account has no key to sign a TrustSet for + // itself. Neither deposits nor loan origination look at that line, so the + // vault appears to work right up to the first repayment, which is the only + // step that has to credit the vault back. + void + testRepayIntoUnauthorizedVault() + { + using namespace jtx; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + auto runTestCases = [&](FeatureBitset features) { + bool const pseudoExempt = features[fixCleanup3_4_0]; + + testcase << "LoanPay into a vault whose own trust line is unauthorized: pseudo-account " + << (pseudoExempt ? "exempt" : "not exempt"); + + Env env{*this, features}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + env(fset(issuer, asfRequireAuth)); + env.close(); + + PrettyAsset const asset = issuer[iouCurrency_]; + env(trust(lender, asset(100'000'000))); + env(trust(borrower, asset(100'000'000))); + env.close(); + + // Authorize the two participants. Nothing asks the issuer to also + // authorize the vault, which is the whole point of this test. + env(trust(issuer, asset(0), lender, tfSetfAuth)); + env(trust(issuer, asset(0), borrower, tfSetfAuth)); + env.close(); + + env(pay(issuer, lender, asset(10'000'000))); + env(pay(issuer, borrower, asset(10'000))); + env.close(); + + // Creating the vault and funding it with deposits succeeds even + // though the vault cannot be authorized to hold the asset. + BrokerInfo const broker{createVaultAndBroker(env, asset, lender)}; + + auto const vaultSle = env.le(broker.vaultKeylet()); + if (!BEAST_EXPECT(vaultSle)) + return; + + AccountID const vaultPseudo = vaultSle->at(sfAccount); + auto const vaultLine = env.le(keylet::trustLine(vaultPseudo, asset.raw().get())); + if (!BEAST_EXPECT(vaultLine)) + return; + BEAST_EXPECT(!vaultLine->isFlag(vaultPseudo > issuer.id() ? lsfLowAuth : lsfHighAuth)); + + using namespace loan; + + auto const loanKeylet = nextLoanKeylet(env, broker); + env(set(borrower, broker.brokerID, asset(1'000).value()), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2)); + env.close(); + + // Paying the principal out of the vault never needed authorization. + BEAST_EXPECT(env.le(loanKeylet)); + + auto const state = getCurrentState(env, broker, loanKeylet); + STAmount const payment{ + broker.asset, + roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; + + env(pay(borrower, loanKeylet.key, payment), + Ter(pseudoExempt ? TER{tesSUCCESS} : TER{tecNO_AUTH})); + env.close(); + }; + + runTestCases(all_); + runTestCases(all_ - fixCleanup3_4_0); + } + void runAmendmentIndependent() { testLoanSetNearZeroInterestRateSucceeds(); + testRepayIntoUnauthorizedVault(); } // Tests run under each entry in amendmentCombinations().