mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 23:30:54 +00:00
Compare commits
10 Commits
develop
...
tialymov/F
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
debdd961fc | ||
|
|
6434427bcb | ||
|
|
7668ee65d3 | ||
|
|
f96a96a880 | ||
|
|
feea4f7651 | ||
|
|
ecca139447 | ||
|
|
bea11fad42 | ||
|
|
a83944428f | ||
|
|
d5cd5d5727 | ||
|
|
b43df72c63 |
@@ -30,6 +30,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -584,9 +585,20 @@ 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.
|
||||
static std::set<SField const*> const kPseudoAccountFilter{&sfVaultID, &sfLoanBrokerID};
|
||||
if (view.rules().enabled(fixCleanup3_4_0) &&
|
||||
isPseudoAccount(view, account, kPseudoAccountFilter))
|
||||
return tesSUCCESS;
|
||||
|
||||
return TER{tecNO_AUTH};
|
||||
}
|
||||
return TER{tecNO_LINE};
|
||||
}
|
||||
|
||||
@@ -620,7 +620,9 @@ LoanPay::doApply()
|
||||
? STAmount{asset, 0}
|
||||
: conservationBalance(view, brokerPayee, asset, j_);
|
||||
|
||||
if (totalPaidToVaultRounded != beast::kZero)
|
||||
bool const skipPayeeAuth = view.rules().enabled(fixCleanup3_4_0);
|
||||
|
||||
if (!skipPayeeAuth && totalPaidToVaultRounded != beast::kZero)
|
||||
{
|
||||
if (auto const ter = requireAuth(view, asset, vaultPseudoAccount, AuthType::StrongAuth))
|
||||
return ter;
|
||||
@@ -644,8 +646,11 @@ LoanPay::doApply()
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
if (auto const ter = requireAuth(view, asset, brokerPayee, AuthType::StrongAuth))
|
||||
return ter;
|
||||
if (!skipPayeeAuth)
|
||||
{
|
||||
if (auto const ter = requireAuth(view, asset, brokerPayee, AuthType::StrongAuth))
|
||||
return ter;
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const ter = accountSendMulti(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/jtx_json.h>
|
||||
#include <test/jtx/noop.h>
|
||||
#include <test/jtx/pay.h>
|
||||
@@ -15,9 +16,11 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -730,6 +733,193 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Which pseudo-account is left holding an unauthorized trust line when the
|
||||
// repayment lands.
|
||||
enum class UnauthorizedPayee {
|
||||
// The vault's own line, as VaultCreate leaves it.
|
||||
Vault,
|
||||
// Same vault, but the issuer authorized the line by hand first.
|
||||
VaultAuthorized,
|
||||
// Vault line authorized, broker owner unable to take the fee, so the
|
||||
// fee goes to the loan broker's pseudo-account instead.
|
||||
Broker,
|
||||
};
|
||||
|
||||
// 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.
|
||||
//
|
||||
// The loan broker's pseudo-account has the same defect for the same reason,
|
||||
// and LoanPay reaches it whenever the broker owner cannot take the fee.
|
||||
//
|
||||
// The issuer can still repair either line by hand, because TrustSet accepts
|
||||
// a line that already exists even when its owner is a pseudo-account.
|
||||
void
|
||||
testRepayIntoUnauthorizedVault()
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
Account const lender{"lender"};
|
||||
Account const borrower{"borrower"};
|
||||
|
||||
auto runTestCases = [&](FeatureBitset features, UnauthorizedPayee payee) {
|
||||
bool const pseudoExempt = features[fixCleanup3_4_0];
|
||||
// With the vault's line repaired by the issuer, the only remaining
|
||||
// unauthorized payee is the broker's pseudo-account.
|
||||
bool const expectSuccess = pseudoExempt || payee == UnauthorizedPayee::VaultAuthorized;
|
||||
|
||||
auto const payeeLabel = [payee]() -> char const* {
|
||||
switch (payee)
|
||||
{
|
||||
case UnauthorizedPayee::Vault:
|
||||
return "vault";
|
||||
case UnauthorizedPayee::VaultAuthorized:
|
||||
return "vault authorized by the issuer";
|
||||
case UnauthorizedPayee::Broker:
|
||||
return "loan broker";
|
||||
}
|
||||
return ""; // LCOV_EXCL_LINE
|
||||
}();
|
||||
|
||||
testcase << "LoanPay crediting an unauthorized " << payeeLabel << ": 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());
|
||||
auto const brokerSle = env.le(broker.brokerKeylet());
|
||||
if (!BEAST_EXPECT(vaultSle && brokerSle))
|
||||
return;
|
||||
|
||||
Account const vaultPseudo{"vault pseudo-account", vaultSle->at(sfAccount)};
|
||||
Account const brokerPseudo{"broker pseudo-account", brokerSle->at(sfAccount)};
|
||||
|
||||
auto const lineIsAuthorized = [&](Account const& holder) -> bool {
|
||||
auto const line = env.le(keylet::trustLine(holder, asset.raw().get<Issue>()));
|
||||
if (!BEAST_EXPECT(line))
|
||||
return false;
|
||||
return line->isFlag(holder.id() > issuer.id() ? lsfLowAuth : lsfHighAuth);
|
||||
};
|
||||
|
||||
BEAST_EXPECT(!lineIsAuthorized(vaultPseudo));
|
||||
BEAST_EXPECT(!lineIsAuthorized(brokerPseudo));
|
||||
|
||||
if (payee != UnauthorizedPayee::Vault)
|
||||
{
|
||||
env(trust(issuer, asset(0), vaultPseudo, tfSetfAuth));
|
||||
env.close();
|
||||
BEAST_EXPECT(lineIsAuthorized(vaultPseudo));
|
||||
}
|
||||
|
||||
using namespace loan;
|
||||
|
||||
// The service fee guarantees the broker is owed something on the
|
||||
// first payment, so the broker leg of the transfer is exercised.
|
||||
Number const serviceFee = asset(2).value();
|
||||
auto const loanKeylet = nextLoanKeylet(env, broker);
|
||||
env(set(borrower, broker.brokerID, asset(1'000).value()),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
kLoanServiceFee(serviceFee),
|
||||
kInterestRate(percentageToTenthBips(12)),
|
||||
kPaymentTotal(12),
|
||||
kPaymentInterval(600),
|
||||
Fee(env.current()->fees().base * 2));
|
||||
env.close();
|
||||
|
||||
// Paying the principal out of the vault never needed authorization.
|
||||
BEAST_EXPECT(env.le(loanKeylet));
|
||||
|
||||
if (payee == UnauthorizedPayee::Broker)
|
||||
{
|
||||
// A deep-frozen owner cannot take the fee, so LoanPay pays it
|
||||
// into the broker's pseudo-account instead.
|
||||
env(trust(issuer, asset(0), lender, tfSetFreeze | tfSetDeepFreeze));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const state = getCurrentState(env, broker, loanKeylet);
|
||||
STAmount const payment{
|
||||
broker.asset,
|
||||
roundPeriodicPayment(
|
||||
broker.asset, state.periodicPayment + serviceFee, state.loanScale)};
|
||||
|
||||
// Repayment turns an outstanding loan back into cash the vault can
|
||||
// lend again, so AssetsAvailable is what moves. AssetsTotal already
|
||||
// counted the loan.
|
||||
auto const assetsAvailable = [&]() -> Number {
|
||||
auto const sle = env.le(broker.vaultKeylet());
|
||||
if (!BEAST_EXPECT(sle))
|
||||
return Number{};
|
||||
return sle->at(sfAssetsAvailable);
|
||||
};
|
||||
|
||||
auto const borrowerBefore = env.balance(borrower, asset).number();
|
||||
auto const vaultBefore = env.balance(vaultPseudo, asset).number();
|
||||
auto const brokerBefore = env.balance(brokerPseudo, asset).number();
|
||||
auto const assetsAvailableBefore = assetsAvailable();
|
||||
|
||||
env(pay(borrower, loanKeylet.key, payment),
|
||||
Ter(expectSuccess ? TER{tesSUCCESS} : TER{tecNO_AUTH}));
|
||||
env.close();
|
||||
|
||||
if (expectSuccess)
|
||||
{
|
||||
BEAST_EXPECT(env.balance(borrower, asset).number() < borrowerBefore);
|
||||
BEAST_EXPECT(env.balance(vaultPseudo, asset).number() > vaultBefore);
|
||||
BEAST_EXPECT(assetsAvailable() > assetsAvailableBefore);
|
||||
// Confirms the broker variant really did route the fee to the
|
||||
// pseudo-account rather than to the owner.
|
||||
BEAST_EXPECT(
|
||||
(env.balance(brokerPseudo, asset).number() > brokerBefore) ==
|
||||
(payee == UnauthorizedPayee::Broker));
|
||||
}
|
||||
else
|
||||
{
|
||||
// A rejected repayment must leave every balance untouched.
|
||||
BEAST_EXPECT(env.balance(borrower, asset).number() == borrowerBefore);
|
||||
BEAST_EXPECT(env.balance(vaultPseudo, asset).number() == vaultBefore);
|
||||
BEAST_EXPECT(env.balance(brokerPseudo, asset).number() == brokerBefore);
|
||||
BEAST_EXPECT(assetsAvailable() == assetsAvailableBefore);
|
||||
}
|
||||
};
|
||||
|
||||
for (auto const& features : {all_, all_ - fixCleanup3_4_0})
|
||||
{
|
||||
runTestCases(features, UnauthorizedPayee::Vault);
|
||||
runTestCases(features, UnauthorizedPayee::VaultAuthorized);
|
||||
runTestCases(features, UnauthorizedPayee::Broker);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanPayFundsConservedPayeeBelowReserve(FeatureBitset features)
|
||||
{
|
||||
@@ -838,6 +1028,7 @@ private:
|
||||
runAmendmentIndependent()
|
||||
{
|
||||
testLoanSetNearZeroInterestRateSucceeds();
|
||||
testRepayIntoUnauthorizedVault();
|
||||
}
|
||||
|
||||
// Tests run under each entry in amendmentCombinations().
|
||||
|
||||
Reference in New Issue
Block a user