xrpld
Loading...
Searching...
No Matches
LoanBroker_test.cpp
1
2#include <test/jtx/Account.h>
3#include <test/jtx/CaptureLogs.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/JTx.h>
6#include <test/jtx/TestHelpers.h>
7#include <test/jtx/amount.h>
8#include <test/jtx/balance.h>
9#include <test/jtx/credentials.h>
10#include <test/jtx/deposit.h>
11#include <test/jtx/fee.h>
12#include <test/jtx/flags.h>
13#include <test/jtx/mpt.h>
14#include <test/jtx/pay.h>
15#include <test/jtx/seq.h>
16#include <test/jtx/sig.h>
17#include <test/jtx/tag.h>
18#include <test/jtx/tags.h>
19#include <test/jtx/ter.h>
20#include <test/jtx/trust.h>
21#include <test/jtx/txflags.h>
22#include <test/jtx/vault.h>
23#include <test/unit_test/SuiteJournal.h>
24
25#include <xrpl/basics/Number.h>
26#include <xrpl/basics/base_uint.h>
27#include <xrpl/basics/strHex.h>
28#include <xrpl/beast/unit_test/suite.h>
29#include <xrpl/beast/utility/Journal.h>
30#include <xrpl/beast/utility/Zero.h>
31#include <xrpl/core/ServiceRegistry.h>
32#include <xrpl/ledger/ApplyView.h>
33#include <xrpl/ledger/OpenView.h>
34#include <xrpl/ledger/helpers/AccountRootHelpers.h>
35#include <xrpl/protocol/AccountID.h>
36#include <xrpl/protocol/Feature.h>
37#include <xrpl/protocol/Indexes.h>
38#include <xrpl/protocol/Issue.h>
39#include <xrpl/protocol/LedgerFormats.h>
40#include <xrpl/protocol/MPTIssue.h>
41#include <xrpl/protocol/Protocol.h>
42#include <xrpl/protocol/SField.h>
43#include <xrpl/protocol/STAmount.h>
44#include <xrpl/protocol/STObject.h>
45#include <xrpl/protocol/STTx.h>
46#include <xrpl/protocol/SeqProxy.h>
47#include <xrpl/protocol/Serializer.h>
48#include <xrpl/protocol/TER.h>
49#include <xrpl/protocol/TxFlags.h>
50#include <xrpl/protocol/TxFormats.h>
51#include <xrpl/protocol/Units.h>
52#include <xrpl/protocol/jss.h>
53#include <xrpl/tx/ApplyContext.h>
54#include <xrpl/tx/Transactor.h>
55#include <xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h>
56
57#include <array>
58#include <cstdint>
59#include <exception>
60#include <functional>
61#include <memory>
62#include <optional>
63#include <string>
64#include <string_view>
65#include <tuple>
66#include <utility>
67#include <vector>
68
69namespace xrpl::test {
70
72{
73 // Ensure that all the features needed for Lending Protocol are included,
74 // even if they are set to unsupported.
75 //
76 // featureLendingProtocolV1_1 is excluded from the default set: it adds
77 // the closed-ended vault gate on LoanBrokerSet::preclaim (see
78 // LoanBrokerSet.cpp), but this suite exercises loan-broker mechanics on
79 // plain open-ended vaults. Tests that specifically exercise the
80 // amendment opt it back in explicitly and use closed-ended vaults.
81 FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1};
82
83 void
85 {
86 testcase("Disabled");
87 // Lending Protocol depends on Single Asset Vault (SAV). Test
88 // combinations of the two amendments.
89 // Single Asset Vault depends on MPTokensV1, but don't test every combo
90 // of that.
91 using namespace jtx;
92 auto failAll = [this](FeatureBitset features, bool goodVault = false) {
93 Env env(*this, features);
94
95 Account const alice{"alice"};
96 env.fund(XRP(10000), alice);
97
98 // Try to create a vault
99 PrettyAsset const asset{xrpIssue(), 1'000'000};
100 Vault const vault{env};
101 auto const [tx, keylet] = vault.create({.owner = alice, .asset = asset});
102 env(tx, Ter(goodVault ? Ter(tesSUCCESS) : Ter(temDISABLED)));
103 env.close();
104 BEAST_EXPECT(static_cast<bool>(env.le(keylet)) == goodVault);
105
106 using namespace loan_broker;
107 // Can't create a loan broker regardless of whether the vault exists
108 env(set(alice, keylet.key), Ter(temDISABLED));
109 auto const brokerKeylet =
110 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
111 // Other LoanBroker transactions are disabled, too.
112 // 1. LoanBrokerCoverDeposit
113 env(coverDeposit(alice, brokerKeylet.key, asset(1000)), Ter(temDISABLED));
114 // 2. LoanBrokerCoverWithdraw
115 env(coverWithdraw(alice, brokerKeylet.key, asset(1000)), Ter(temDISABLED));
116 // 3. LoanBrokerCoverClawback
117 env(coverClawback(alice), Ter(temDISABLED));
118 env(coverClawback(alice), kLoanBrokerId(brokerKeylet.key), Ter(temDISABLED));
119 env(coverClawback(alice), kAmount(asset(0)), Ter(temDISABLED));
120 env(coverClawback(alice),
121 kLoanBrokerId(brokerKeylet.key),
122 kAmount(asset(1000)),
124 // 4. LoanBrokerDelete
125 env(del(alice, brokerKeylet.key), Ter(temDISABLED));
126 };
127 failAll(all_ - featureMPTokensV1);
128 failAll(all_ - featureSingleAssetVault - featureLendingProtocol);
129 failAll(all_ - featureSingleAssetVault);
130 failAll(all_ - featureLendingProtocol, true);
131 }
132
134 {
138 VaultInfo(jtx::PrettyAsset const& asset, UInt256 const& vaultId, AccountID const& pseudo)
139 : asset(asset), vaultID(vaultId), pseudoAccount("vault", pseudo)
140 {
141 }
142 };
143
144 void
146 char const* label,
147 jtx::Env& env,
148 jtx::Account const& issuer,
149 jtx::Account const& alice,
150 jtx::Account const& evan,
151 jtx::Account const& bystander,
152 VaultInfo const& vault,
153 VaultInfo const& badVault,
154 std::function<jtx::JTx(jtx::JTx const&)> modifyJTx,
155 std::function<void(SLE::ConstRef)> checkBroker,
156 std::function<void(SLE::ConstRef)> changeBroker,
157 std::function<void(SLE::ConstRef)> checkChangedBroker)
158 {
159 {
160 auto const& asset = vault.asset.raw();
161 std::string_view assetLabel;
162 if (asset.native())
163 {
164 assetLabel = "XRP ";
165 }
166 else if (asset.holds<Issue>())
167 {
168 assetLabel = "IOU ";
169 }
170 else if (asset.holds<MPTIssue>())
171 {
172 assetLabel = "MPT ";
173 }
174 else
175 {
176 assetLabel = "Unknown ";
177 }
178 testcase << "Lifecycle: " << assetLabel << label;
179 }
180
181 using namespace jtx;
182 using namespace loan_broker;
183
184 // Bogus assets to use in test cases
185 static PrettyAsset const kBadMptAsset = [&]() {
186 MPTTester badMptt{env, evan, kMptInitNoFund};
187 badMptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
188 env.close();
189 return badMptt["BAD"];
190 }();
191 static PrettyAsset const kBadIouAsset = evan["BAD"];
192 static Account const kNonExistent{"NonExistent"};
193 static PrettyAsset const kGhostIouAsset = kNonExistent["GST"];
194 PrettyAsset const vaultPseudoIouAsset = vault.pseudoAccount["PSD"];
195
196 auto const badKeylet =
197 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
198 env(set(alice, badVault.vaultID));
199 env.close();
200 auto const badBrokerPseudo = [&]() {
201 if (auto const le = env.le(badKeylet); BEAST_EXPECT(le))
202 {
203 return Account{"Bad Broker pseudo-account", le->at(sfAccount)};
204 }
205 // Just to make the build work
206 return vault.pseudoAccount;
207 }();
208 PrettyAsset const badBrokerPseudoIouAsset = badBrokerPseudo["WAT"];
209
210 auto const keylet = keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
211 {
212 // Start with default values
213 auto jtx = env.jt(set(alice, vault.vaultID));
214 // Modify as desired
215 if (modifyJTx)
216 jtx = modifyJTx(jtx);
217 // Successfully create a Loan Broker
218 env(jtx);
219 }
220
221 env.close();
222 if (auto broker = env.le(keylet); BEAST_EXPECT(broker))
223 {
224 // log << "Broker after create: " << to_string(broker->getJson())
225 // << std::endl;
226 BEAST_EXPECT(broker->at(sfVaultID) == vault.vaultID);
227 BEAST_EXPECT(broker->at(sfAccount) != alice.id());
228 BEAST_EXPECT(broker->at(sfOwner) == alice.id());
229 BEAST_EXPECT(broker->at(sfFlags) == 0);
230 BEAST_EXPECT(broker->at(sfSequence) == env.seq(alice) - 1);
231 BEAST_EXPECT(broker->at(sfOwnerCount) == 0);
232 BEAST_EXPECT(broker->at(sfLoanSequence) == 1);
233 BEAST_EXPECT(broker->at(sfDebtTotal) == 0);
234 BEAST_EXPECT(broker->at(sfCoverAvailable) == 0);
235 if (checkBroker)
236 checkBroker(broker);
237
238 // if (auto const vaultSLE = env.le(keylet::vault(vault.vaultID)))
239 //{
240 // log << "Vault: " << to_string(vaultSLE->getJson()) <<
241 // std::endl;
242 // }
243 // Load the pseudo-account
244 Account const pseudoAccount{"Broker pseudo-account", broker->at(sfAccount)};
245
246 auto const pseudoKeylet = keylet::account(pseudoAccount);
247 if (auto const pseudo = env.le(pseudoKeylet); BEAST_EXPECT(pseudo))
248 {
249 // log << "Pseudo-account after create: "
250 // << to_string(pseudo->getJson()) << std::endl
251 // << std::endl;
252 BEAST_EXPECT(
253 pseudo->at(sfFlags) == (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
254 BEAST_EXPECT(pseudo->at(sfSequence) == 0);
255 BEAST_EXPECT(pseudo->at(sfBalance) == beast::kZero);
256 BEAST_EXPECT(pseudo->at(sfOwnerCount) == (vault.asset.raw().native() ? 0 : 1));
257 BEAST_EXPECT(!pseudo->isFieldPresent(sfAccountTxnID));
258 BEAST_EXPECT(!pseudo->isFieldPresent(sfRegularKey));
259 BEAST_EXPECT(!pseudo->isFieldPresent(sfEmailHash));
260 BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletLocator));
261 BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletSize));
262 BEAST_EXPECT(!pseudo->isFieldPresent(sfMessageKey));
263 BEAST_EXPECT(!pseudo->isFieldPresent(sfTransferRate));
264 BEAST_EXPECT(!pseudo->isFieldPresent(sfDomain));
265 BEAST_EXPECT(!pseudo->isFieldPresent(sfTickSize));
266 BEAST_EXPECT(!pseudo->isFieldPresent(sfTicketCount));
267 BEAST_EXPECT(!pseudo->isFieldPresent(sfNFTokenMinter));
268 BEAST_EXPECT(!pseudo->isFieldPresent(sfMintedNFTokens));
269 BEAST_EXPECT(!pseudo->isFieldPresent(sfBurnedNFTokens));
270 BEAST_EXPECT(!pseudo->isFieldPresent(sfFirstNFTokenSequence));
271 BEAST_EXPECT(!pseudo->isFieldPresent(sfAMMID));
272 BEAST_EXPECT(!pseudo->isFieldPresent(sfVaultID));
273 BEAST_EXPECT(pseudo->at(sfLoanBrokerID) == keylet.key);
274 }
275
276 {
277 // Get the AccountInfo RPC result for the broker pseudo-account
278 std::string const pseudoStr = to_string(pseudoAccount.id());
279 auto const accountInfo = env.rpc("account_info", pseudoStr);
280 if (BEAST_EXPECT(accountInfo.isObject()))
281 {
282 auto const& accountData = accountInfo[jss::result][jss::account_data];
283 if (BEAST_EXPECT(accountData.isObject()))
284 {
285 BEAST_EXPECT(accountData[jss::Account] == pseudoStr);
286 BEAST_EXPECT(accountData[sfLoanBrokerID] == to_string(keylet.key));
287 }
288 auto const& pseudoInfo = accountInfo[jss::result][jss::pseudo_account];
289 if (BEAST_EXPECT(pseudoInfo.isObject()))
290 {
291 BEAST_EXPECT(pseudoInfo[jss::type] == "LoanBroker");
292 }
293 }
294 }
295
296 auto verifyCoverAmount =
297 [&env, &vault, &pseudoAccount, &broker, &keylet, this](auto n) {
298 using namespace jtx;
299
300 broker = env.le(keylet);
301 if (BEAST_EXPECT(broker))
302 {
303 auto const amount = vault.asset(n);
304 BEAST_EXPECT(broker->at(sfCoverAvailable) == amount.number());
305 env.require(jtx::Balance(pseudoAccount, amount));
306 }
307 };
308
309 // Test Cover funding before allowing alterations
310 env(coverDeposit(alice, UInt256(0), vault.asset(10)), Ter(temINVALID));
311 env(coverDeposit(evan, keylet.key, vault.asset(10)), Ter(tecNO_PERMISSION));
312 env(coverDeposit(evan, keylet.key, vault.asset(0)), Ter(temBAD_AMOUNT));
313 env(coverDeposit(evan, keylet.key, vault.asset(-10)), Ter(temBAD_AMOUNT));
314 env(coverDeposit(alice, vault.vaultID, vault.asset(10)), Ter(tecNO_ENTRY));
315
316 verifyCoverAmount(0);
317
318 // Test cover clawback failure cases BEFORE depositing any cover
319 // Need one of brokerID or amount
320 env(coverClawback(alice), Ter(temINVALID));
321 env(coverClawback(alice), kLoanBrokerId(UInt256(0)), Ter(temINVALID));
322 env(coverClawback(alice), kAmount(XRP(1000)), Ter(temBAD_AMOUNT));
323 env(coverClawback(alice), kAmount(vault.asset(-10)), Ter(temBAD_AMOUNT));
324 // Clawbacks with an MPT need to specify the broker ID
325 env(coverClawback(alice), kAmount(kBadMptAsset(1)), Ter(temINVALID));
326 env(coverClawback(evan), kLoanBrokerId(vault.vaultID), Ter(tecNO_ENTRY));
327 // Only the issuer can clawback
328 env(coverClawback(alice), kLoanBrokerId(keylet.key), Ter(tecNO_PERMISSION));
329 if (vault.asset.raw().native())
330 {
331 // Can not clawback XRP under any circumstances
332 env(coverClawback(issuer), kLoanBrokerId(keylet.key), Ter(tecNO_PERMISSION));
333 }
334 else
335 {
336 if (vault.asset.raw().holds<Issue>())
337 {
338 // Clawbacks without a kLoanBrokerId need to specify an IOU
339 // with the broker's pseudo-account as the issuer
340 env(coverClawback(alice), kAmount(kGhostIouAsset(1)), Ter(tecNO_ENTRY));
341 env(coverClawback(alice), kAmount(kBadIouAsset(1)), Ter(tecOBJECT_NOT_FOUND));
342 // Pseudo-account is not for a broker
343 env(coverClawback(alice),
344 kAmount(vaultPseudoIouAsset(1)),
346 // If we specify a pseudo-account as the IOU amount, it
347 // needs to match the loan broker
348 env(coverClawback(issuer),
349 kLoanBrokerId(keylet.key),
350 kAmount(badBrokerPseudoIouAsset(10)),
352 PrettyAsset const brokerWrongCurrencyAsset = pseudoAccount["WAT"];
353 env(coverClawback(issuer),
354 kLoanBrokerId(keylet.key),
355 kAmount(brokerWrongCurrencyAsset(10)),
357 }
358 else
359 {
360 // Clawbacks with an MPT need to specify the broker ID, even
361 // if the asset is valid
362 BEAST_EXPECT(vault.asset.raw().holds<MPTIssue>());
363 env(coverClawback(alice), kAmount(vault.asset(10)), Ter(temINVALID));
364 }
365 // Since no cover has been deposited, there's nothing to claw
366 // back
367 env(coverClawback(issuer),
368 kLoanBrokerId(keylet.key),
369 kAmount(vault.asset(10)),
371 }
372 env.close();
373
374 // Fund the cover deposit
375 env(coverDeposit(alice, keylet.key, vault.asset(10)));
376 env.close();
377 verifyCoverAmount(10);
378
379 // Test withdrawal failure cases
380 env(coverWithdraw(alice, UInt256(0), vault.asset(10)), Ter(temINVALID));
381 env(coverWithdraw(evan, keylet.key, vault.asset(10)), Ter(tecNO_PERMISSION));
382 env(coverWithdraw(evan, keylet.key, vault.asset(0)), Ter(temBAD_AMOUNT));
383 env(coverWithdraw(evan, keylet.key, vault.asset(-10)), Ter(temBAD_AMOUNT));
384 env(coverWithdraw(alice, vault.vaultID, vault.asset(10)), Ter(tecNO_ENTRY));
385 env(coverWithdraw(alice, keylet.key, vault.asset(900)), Ter(tecINSUFFICIENT_FUNDS));
386
387 // Skip this test for XRP, because that can always be sent
388 if (!vault.asset.raw().native())
389 {
390 TER const expected = vault.asset.raw().holds<MPTIssue>() ? tecNO_AUTH : tecNO_LINE;
391 env(coverWithdraw(alice, keylet.key, vault.asset(1)),
392 kDestination(bystander),
393 Ter(expected));
394 }
395
396 // Can not withdraw to the zero address
397 env(coverWithdraw(alice, keylet.key, vault.asset(1)),
398 kDestination(AccountID{}),
400
401 // Withdraw some of the cover amount
402 env(coverWithdraw(alice, keylet.key, vault.asset(7)));
403 env.close();
404 verifyCoverAmount(3);
405
406 // Add some more cover
407 env(coverDeposit(alice, keylet.key, vault.asset(5)));
408 env.close();
409 verifyCoverAmount(8);
410
411 // Withdraw some more. Send it to Evan. Very generous, considering
412 // how much trouble he's been.
413 env(coverWithdraw(alice, keylet.key, vault.asset(1)), kDestination(evan));
414 env.close();
415 verifyCoverAmount(7);
416
417 // Withdraw some more. Send it to Evan. Very generous, considering
418 // how much trouble he's been.
419 env(coverWithdraw(alice, keylet.key, vault.asset(1)), kDestination(evan), Dtag(3));
420 env.close();
421 verifyCoverAmount(6);
422
423 if (!vault.asset.raw().native())
424 {
425 // Issuer claws back some of the cover
426 env(coverClawback(issuer), kLoanBrokerId(keylet.key), kAmount(vault.asset(2)));
427 env.close();
428 verifyCoverAmount(4);
429
430 // Deposit some back
431 env(coverDeposit(alice, keylet.key, vault.asset(5)));
432 env.close();
433 verifyCoverAmount(9);
434
435 // Issuer claws it all back in various different ways
436 for (auto const& tx : {
437 // defer autofills until submission time
438 env.json(
439 coverClawback(issuer),
440 kLoanBrokerId(keylet.key),
441 Fee(kNone),
442 Seq(kNone),
443 Sig(kNone)),
444 env.json(
445 coverClawback(issuer),
446 kLoanBrokerId(keylet.key),
447 kAmount(vault.asset(0)),
448 Fee(kNone),
449 Seq(kNone),
450 Sig(kNone)),
451 env.json(
452 coverClawback(issuer),
453 kLoanBrokerId(keylet.key),
454 kAmount(vault.asset(6)),
455 Fee(kNone),
456 Seq(kNone),
457 Sig(kNone)),
458 // amount will be truncated to what's available
459 env.json(
460 coverClawback(issuer),
461 kLoanBrokerId(keylet.key),
462 kAmount(vault.asset(100)),
463 Fee(kNone),
464 Seq(kNone),
465 Sig(kNone)),
466 })
467 {
468 // Issuer claws it all back
469 env(tx);
470 env.close();
471 verifyCoverAmount(0);
472
473 // Deposit some back
474 env(coverDeposit(alice, keylet.key, vault.asset(6)));
475 env.close();
476 verifyCoverAmount(6);
477 }
478 }
479
480 // no-op
481 env(set(alice, vault.vaultID), kLoanBrokerId(keylet.key));
482 env.close();
483
484 // Make modifications to the broker
485 if (changeBroker)
486 changeBroker(broker);
487
488 env.close();
489
490 // Check the results of modifications
491 broker = env.le(keylet);
492 if (BEAST_EXPECT(broker) && checkChangedBroker)
493 checkChangedBroker(broker);
494
495 // Verify that fields get removed when set to default values
496 // Debt maximum: explicit 0
497 // Data: explicit empty
498 env(set(alice, vault.vaultID),
499 kLoanBrokerId(broker->key()),
500 kDebtMaximum(Number(0)),
501 kData(""));
502 env.close();
503
504 // Check the updated fields
505 broker = env.le(keylet);
506 if (BEAST_EXPECT(broker))
507 {
508 BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
509 BEAST_EXPECT(!broker->isFieldPresent(sfData));
510 }
511
513 // try to delete the wrong broker object
514 env(del(alice, vault.vaultID), Ter(tecNO_ENTRY));
515 // evan tries to delete the broker
516 env(del(evan, keylet.key), Ter(tecNO_PERMISSION));
517
518 // Get the "bad" broker out of the way
519 env(del(alice, badKeylet.key));
520 env.close();
521
522 // Note alice's balance of the asset and the broker account's cover
523 // funds
524 auto const aliceBalance = env.balance(alice, vault.asset);
525 auto const coverFunds = env.balance(pseudoAccount, vault.asset);
526 BEAST_EXPECT(coverFunds.number() == broker->at(sfCoverAvailable));
527 BEAST_EXPECT(coverFunds != beast::kZero);
528 verifyCoverAmount(6);
529
530 // delete the broker
531 // log << "Broker before delete: " << to_string(broker->getJson())
532 // << std::endl;
533 // if (auto const pseudo = env.le(pseudoKeylet);
534 // BEAST_EXPECT(pseudo))
535 //{
536 // log << "Pseudo-account before delete: "
537 // << to_string(pseudo->getJson()) << std::endl
538 // << std::endl;
539 //}
540
541 env(del(alice, keylet.key));
542 env.close();
543 {
544 broker = env.le(keylet);
545 BEAST_EXPECT(!broker);
546 auto pseudo = env.le(pseudoKeylet);
547 BEAST_EXPECT(!pseudo);
548 }
549 auto const expectedBalance = aliceBalance + coverFunds -
550 (aliceBalance.value().native() ? STAmount(env.current()->fees().base.value())
551 : vault.asset(0));
552 env.require(jtx::Balance(alice, expectedBalance));
553 env.require(jtx::Balance(pseudoAccount, vault.asset(kNone)));
554 }
555 }
556
557 void
559 {
560 testcase("Lifecycle");
561 using namespace jtx;
562
563 // Create 3 loan brokers: one for XRP, one for an IOU, and one for an
564 // MPT. That'll require three corresponding SAVs.
565 Env env(*this, all_);
566
567 Account const issuer{"issuer"};
568 // For simplicity, alice will be the sole actor for the vault & brokers.
569 Account const alice{"alice"};
570 // Evan will attempt to be naughty
571 Account const evan{"evan"};
572 // Bystander doesn't have anything to do with the SAV or Broker, or any
573 // of the relevant tokens
574 Account const bystander{"bystander"};
575 Vault vault{env};
576
577 // Fund the accounts and trust lines with the same amount so that tests
578 // can use the same values regardless of the asset.
579 env.fund(XRP(100'000), issuer, noripple(alice, evan, bystander));
580 env.close();
581
582 env(fset(issuer, asfAllowTrustLineClawback));
583 env.close();
584
585 // Create assets
586 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
587 PrettyAsset const iouAsset = issuer["IOU"];
588 env(trust(alice, iouAsset(1'000'000)));
589 env(trust(evan, iouAsset(1'000'000)));
590 env.close();
591 env(pay(issuer, evan, iouAsset(100'000)));
592 env(pay(issuer, alice, iouAsset(100'000)));
593 env.close();
594
595 MPTTester mptt{env, issuer, kMptInitNoFund};
596 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
597 env.close();
598 PrettyAsset const mptAsset = mptt["MPT"];
599 mptt.authorize({.account = alice});
600 mptt.authorize({.account = evan});
601 env.close();
602 env(pay(issuer, alice, mptAsset(100'000)));
603 env(pay(issuer, evan, mptAsset(100'000)));
604 env.close();
605
606 std::array const assets{xrpAsset, iouAsset, mptAsset};
607
608 // Create vaults
610 for (auto const& asset : assets)
611 {
612 auto [tx, keylet] = vault.create({.owner = alice, .asset = asset});
613 env(tx);
614 env.close();
615 if (auto const le = env.le(keylet); BEAST_EXPECT(env.le(keylet)))
616 {
617 vaults.emplace_back(asset, keylet.key, le->at(sfAccount));
618 }
619
620 env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(50)}));
621 env.close();
622 }
623 VaultInfo const badVault = [&]() -> VaultInfo {
624 auto [tx, keylet] = vault.create({.owner = alice, .asset = iouAsset});
625 env(tx);
626 env.close();
627 if (auto const le = env.le(keylet); BEAST_EXPECT(env.le(keylet)))
628 {
629 return {iouAsset, keylet.key, le->at(sfAccount)};
630 }
631 // This should never happen
632 return {iouAsset, keylet.key, evan.id()};
633 }();
634
635 auto const aliceOriginalCount = env.ownerCount(alice);
636
637 // Create and update Loan Brokers
638 for (auto const& vault : vaults)
639 {
640 {
641 // Get the AccountInfo RPC result for the vault pseudo-account
642 std::string const pseudoStr = to_string(vault.pseudoAccount.id());
643 auto const accountInfo = env.rpc("account_info", pseudoStr);
644 if (BEAST_EXPECT(accountInfo.isObject()))
645 {
646 auto const& accountData = accountInfo[jss::result][jss::account_data];
647 if (BEAST_EXPECT(accountData.isObject()))
648 {
649 BEAST_EXPECT(accountData[jss::Account] == pseudoStr);
650 BEAST_EXPECT(accountData[sfVaultID] == to_string(vault.vaultID));
651 }
652 auto const& pseudoInfo = accountInfo[jss::result][jss::pseudo_account];
653 if (BEAST_EXPECT(pseudoInfo.isObject()))
654 {
655 BEAST_EXPECT(pseudoInfo[jss::type] == "Vault");
656 }
657 }
658 }
659
660 using namespace loan_broker;
661 using namespace xrpl::lending;
662
663 TenthBips32 const tenthBipsZero{0};
664
665 auto badKeylet = keylet::vault(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
666 // Try some failure cases
667 // not the vault owner
668 env(set(evan, vault.vaultID), Ter(tecNO_PERMISSION));
669 // not a vault
670 env(set(alice, badKeylet.key), Ter(tecNO_ENTRY));
671 // flags are checked first
672 env(set(evan, vault.vaultID, ~tfUniversal), Ter(temINVALID_FLAG));
673 // field length validation
674 // sfData: good length, bad account
675 env(set(evan, vault.vaultID),
678 // sfData: too long
679 env(set(evan, vault.vaultID),
681 Ter(temINVALID));
682 // sfManagementFeeRate: good value, bad account
683 env(set(evan, vault.vaultID),
684 kManagementFeeRate(kMaxManagementFeeRate),
686 // sfManagementFeeRate: too big
687 env(set(evan, vault.vaultID),
688 kManagementFeeRate(kMaxManagementFeeRate + TenthBips16(10)),
689 Ter(temINVALID));
690 // sfCoverRateMinimum and sfCoverRateLiquidation are linked
691 // Cover: good value, bad account
692 env(set(evan, vault.vaultID),
693 kCoverRateMinimum(kMaxCoverRate),
694 kCoverRateLiquidation(kMaxCoverRate),
696 // CoverMinimum: too big
697 env(set(evan, vault.vaultID),
698 kCoverRateMinimum(kMaxCoverRate + 1),
699 kCoverRateLiquidation(kMaxCoverRate + 1),
700 Ter(temINVALID));
701 // CoverLiquidation: too big
702 env(set(evan, vault.vaultID),
703 kCoverRateMinimum(kMaxCoverRate / 2),
704 kCoverRateLiquidation(kMaxCoverRate + 1),
705 Ter(temINVALID));
706 // Cover: zero min, non-zero liquidation - implicit and
707 // explicit zero values.
708 env(set(evan, vault.vaultID), kCoverRateLiquidation(kMaxCoverRate), Ter(temINVALID));
709 env(set(evan, vault.vaultID),
710 kCoverRateMinimum(tenthBipsZero),
711 kCoverRateLiquidation(kMaxCoverRate),
712 Ter(temINVALID));
713 // Cover: non-zero min, zero liquidation - implicit and
714 // explicit zero values.
715 env(set(evan, vault.vaultID), kCoverRateMinimum(kMaxCoverRate), Ter(temINVALID));
716 env(set(evan, vault.vaultID),
717 kCoverRateMinimum(kMaxCoverRate),
718 kCoverRateLiquidation(tenthBipsZero),
719 Ter(temINVALID));
720 // sfDebtMaximum: good value, bad account
721 env(set(evan, vault.vaultID), kDebtMaximum(Number(0)), Ter(tecNO_PERMISSION));
722 // sfDebtMaximum: overflow
723 env(set(evan, vault.vaultID), kDebtMaximum(Number(1, 100)), Ter(temINVALID));
724 // sfDebtMaximum: negative
725 env(set(evan, vault.vaultID), kDebtMaximum(Number(-1)), Ter(temINVALID));
726
727 std::string testData;
728 lifecycle(
729 "default fields",
730 env,
731 issuer,
732 alice,
733 evan,
734 bystander,
735 vault,
736 badVault,
737 // No modifications
738 {},
739 [&](SLE::ConstRef broker) {
740 // Extra checks
741 BEAST_EXPECT(!broker->isFieldPresent(sfManagementFeeRate));
742 BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateMinimum));
743 BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateLiquidation));
744 BEAST_EXPECT(!broker->isFieldPresent(sfData));
745 BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
746 BEAST_EXPECT(broker->at(sfDebtMaximum) == 0);
747 BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0);
748 BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0);
749
750 BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount + 4);
751 },
752 [&](SLE::ConstRef broker) {
753 // Modifications
754
755 // Update the fields
756 auto const nextKeylet =
757 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
758
759 // fields that can't be changed
760 // LoanBrokerID
761 env(set(alice, vault.vaultID), kLoanBrokerId(nextKeylet.key), Ter(tecNO_ENTRY));
762 // VaultID
763 env(set(alice, nextKeylet.key), kLoanBrokerId(broker->key()), Ter(tecNO_ENTRY));
764 // Owner
765 env(set(evan, vault.vaultID),
766 kLoanBrokerId(broker->key()),
768 // ManagementFeeRate
769 env(set(alice, vault.vaultID),
770 kLoanBrokerId(broker->key()),
771 kManagementFeeRate(kMaxManagementFeeRate),
772 Ter(temINVALID));
773 // CoverRateMinimum
774 env(set(alice, vault.vaultID),
775 kLoanBrokerId(broker->key()),
776 kCoverRateMinimum(kMaxManagementFeeRate),
777 Ter(temINVALID));
778 // CoverRateLiquidation
779 env(set(alice, vault.vaultID),
780 kLoanBrokerId(broker->key()),
781 kCoverRateLiquidation(kMaxManagementFeeRate),
782 Ter(temINVALID));
783
784 // fields that can be changed
785 testData = "Test Data 1234";
786 // Bad data: too long
787 env(set(alice, vault.vaultID),
788 kLoanBrokerId(broker->key()),
790 Ter(temINVALID));
791
792 // Bad debt maximum
793 env(set(alice, vault.vaultID),
794 kLoanBrokerId(broker->key()),
795 kDebtMaximum(Number(-175, -1)),
796 Ter(temINVALID));
797 Number debtMax{175, -1};
798 if (vault.asset.integral())
799 {
800 env(set(alice, vault.vaultID),
801 kLoanBrokerId(broker->key()),
802 kData(testData),
803 kDebtMaximum(debtMax),
805 roundToAsset(vault.asset, debtMax);
806 }
807 // Data & Debt maximum
808 env(set(alice, vault.vaultID),
809 kLoanBrokerId(broker->key()),
810 kData(testData),
811 kDebtMaximum(debtMax));
812 },
813 [&](SLE::ConstRef broker) {
814 // Check the updated fields
815 BEAST_EXPECT(checkVL(broker->at(sfData), testData));
816 Number const expected = STAmount{vault.asset, Number(175, -1)};
817 auto const actual = broker->at(sfDebtMaximum);
818 BEAST_EXPECTS(
819 actual == expected,
820 "Expected: " + to_string(expected) + ", Actual: " + to_string(actual));
821 });
822
823 lifecycle(
824 "non-default fields",
825 env,
826 issuer,
827 alice,
828 evan,
829 bystander,
830 vault,
831 badVault,
832 [&](jtx::JTx const& jv) {
833 testData = "spam spam spam spam";
834 // Finally, create another Loan Broker with kNone of the
835 // values at default
836 return env.jt(
837 jv,
838 kData(testData),
839 kManagementFeeRate(TenthBips16(123)),
840 kDebtMaximum(Number(9)),
841 kCoverRateMinimum(TenthBips32(100)),
842 kCoverRateLiquidation(TenthBips32(200)));
843 },
844 [&](SLE::ConstRef broker) {
845 // Extra checks
846 BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123);
847 BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100);
848 BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 200);
849 BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9));
850 BEAST_EXPECT(checkVL(broker->at(sfData), testData));
851 },
852 [&](SLE::ConstRef broker) {
853 // Reset Data & Debt maximum to default values
854 env(set(alice, vault.vaultID),
855 kLoanBrokerId(broker->key()),
856 kData(""),
857 kDebtMaximum(Number(0)));
858 },
859 [&](SLE::ConstRef broker) {
860 // Check the updated fields
861 BEAST_EXPECT(!broker->isFieldPresent(sfData));
862 BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
863 });
864 }
865
866 BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount);
867 }
868
870
871 void
874 getAsset,
875 LoanBrokerTest brokerTest)
876 {
877 using namespace jtx;
878 using namespace loan_broker;
879 Account const issuer{"issuer"};
880 Account const alice{"alice"};
881 Env env(*this, all_);
882 Vault const vault{env};
883
884 env.fund(XRP(100'000), issuer, alice);
885 env.close();
886
887 PrettyAsset const asset = [&]() {
888 if (getAsset)
889 return getAsset(env, issuer, alice);
890 env(trust(alice, issuer["IOU"](1'000'000)));
891 env.close();
892 return PrettyAsset(issuer["IOU"]);
893 }();
894
895 env(pay(issuer, alice, asset(100'000)));
896 env.close();
897
898 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
899 env(tx);
900 env.close();
901 auto const le = env.le(vaultKeylet);
902 VaultInfo vaultInfo = [&]() {
903 if (BEAST_EXPECT(le))
904 return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
905 return VaultInfo{asset, {}, {}};
906 }();
907 if (vaultInfo.vaultID == UInt256{})
908 return;
909
910 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
911 env.close();
912
913 auto const brokerKeylet =
914 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
915 env(set(alice, vaultInfo.vaultID));
916 env.close();
917
918 auto broker = env.le(brokerKeylet);
919 if (!BEAST_EXPECT(broker))
920 return;
921
922 auto testZeroBrokerID = [&](auto&& getTxJv) {
923 auto jv = getTxJv();
924 // empty broker ID
925 jv[sfLoanBrokerID] = "";
926 env(jv, Ter(temINVALID));
927 // zero broker ID
928 jv[sfLoanBrokerID] = to_string(UInt256{});
929 // needs a flag to distinguish the parsed STTx from the prior
930 // test
932 };
933 auto testZeroVaultID = [&](auto&& getTxJv) {
934 auto jv = getTxJv();
935 // empty broker ID
936 jv[sfVaultID] = "";
937 env(jv, Ter(temINVALID));
938 // zero broker ID
939 jv[sfVaultID] = to_string(UInt256{});
940 // needs a flag to distinguish the parsed STTx from the prior
941 // test
943 };
944
945 if (brokerTest == LoanBrokerTest::CoverDeposit)
946 {
947 // preflight: temINVALID (empty/zero broker id)
948 testZeroBrokerID([&]() { return coverDeposit(alice, brokerKeylet.key, asset(10)); });
949
950 // preclaim: tecWRONG_ASSET
951 env(coverDeposit(alice, brokerKeylet.key, issuer["BAD"](10)), Ter(tecWRONG_ASSET));
952
953 // preclaim: tecINSUFFICIENT_FUNDS
954 env(pay(alice, issuer, asset(100'000 - 50)));
955 env.close();
956 env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)),
958 // Freeze/lock tests are in testCoverDepositFreezes/testCoverWithdrawFreezes
959 }
960 else
961 {
962 // Fund the cover deposit
963 env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)));
964 }
965 env.close();
966
967 if (brokerTest == LoanBrokerTest::CoverWithdraw)
968 {
969 // preflight: temINVALID (empty/zero broker id)
970 testZeroBrokerID([&]() { return coverWithdraw(alice, brokerKeylet.key, asset(10)); });
971
972 // preclaim: tecWRONG_ASSET
973 env(coverWithdraw(alice, brokerKeylet.key, issuer["BAD"](10)), Ter(tecWRONG_ASSET));
974
975 // preclaim: tecNO_DST
976 Account const bogus{"bogus"};
977 env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
978 kDestination(bogus),
979 Ter(tecNO_DST));
980
981 // preclaim: tecDST_TAG_NEEDED
982 Account const dest{"dest"};
983 env.fund(XRP(1'000), dest);
984
985 env(fset(dest, asfRequireDest));
986 env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
987 kDestination(dest),
989 env(fclear(dest, asfRequireDest));
990
991 // preclaim: tecNO_PERMISSION
992 env(fset(dest, asfDepositAuth));
993 env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
994 kDestination(dest),
996 env(fclear(dest, asfDepositAuth));
997 // Freeze/lock tests are in testCoverDepositFreezes/testCoverWithdrawFreezes
998
999 // preclaim: tecPSEUDO_ACCOUNT
1000 env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
1001 kDestination(vaultInfo.pseudoAccount),
1003 }
1004
1005 if (brokerTest == LoanBrokerTest::CoverClawback)
1006 {
1007 // preflight: temINVALID (empty/zero broker id)
1008 testZeroBrokerID([&]() {
1009 return env.json(
1010 coverClawback(alice),
1011 kLoanBrokerId(brokerKeylet.key),
1012 kAmount(vaultInfo.asset(2)));
1013 });
1014
1015 if (asset.holds<Issue>())
1016 {
1017 // preclaim: AllowTrustLineClawback is not set
1018 env(coverClawback(issuer),
1019 kLoanBrokerId(brokerKeylet.key),
1020 kAmount(vaultInfo.asset(2)),
1022
1023 // preclaim: NoFreeze is set
1024 env(fset(issuer, asfAllowTrustLineClawback | asfNoFreeze));
1025 env.close();
1026 env(coverClawback(issuer),
1027 kLoanBrokerId(brokerKeylet.key),
1028 kAmount(vaultInfo.asset(2)),
1030 }
1031 else
1032 {
1033 // preclaim: MPTCanClawback is not set or MPTCanLock is not set
1034 env(coverClawback(issuer),
1035 kLoanBrokerId(brokerKeylet.key),
1036 kAmount(vaultInfo.asset(2)),
1038 }
1039 env.close();
1040 }
1041
1042 if (brokerTest == LoanBrokerTest::Delete)
1043 {
1044 Account const borrower{"borrower"};
1045 env.fund(XRP(1'000), borrower);
1046 env(loan::set(borrower, brokerKeylet.key, asset(50).value()),
1047 Sig(sfCounterpartySignature, alice),
1048 Fee(env.current()->fees().base * 2));
1049
1050 // preflight: temINVALID (empty/zero broker id)
1051 testZeroBrokerID([&]() { return del(alice, brokerKeylet.key); });
1052
1053 // preclaim: tecHAS_OBLIGATIONS
1054 env(del(alice, brokerKeylet.key), Ter(tecHAS_OBLIGATIONS));
1055
1056 // Repay and delete the loan
1057 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
1058 env(loan::pay(borrower, loanKeylet.key, asset(50).value()));
1059 env(loan::del(alice, loanKeylet.key));
1060
1061 env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze));
1062 // preclaim: tecFROZEN (deep frozen)
1063 env(del(alice, brokerKeylet.key), Ter(tecFROZEN));
1064 env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze));
1065
1066 // successful delete the loan broker object
1067 env(del(alice, brokerKeylet.key), Ter(tesSUCCESS));
1068 }
1069 else
1070 {
1071 env(del(alice, brokerKeylet.key));
1072 }
1073
1074 if (brokerTest == LoanBrokerTest::Set)
1075 {
1076 // preflight: temINVALID (empty/zero broker id)
1077 testZeroBrokerID([&]() {
1078 return env.json(set(alice, vaultInfo.vaultID), kLoanBrokerId(brokerKeylet.key));
1079 });
1080 // preflight: temINVALID (empty/zero vault id)
1081 testZeroVaultID([&]() {
1082 return env.json(set(alice, vaultInfo.vaultID), kLoanBrokerId(brokerKeylet.key));
1083 });
1084
1085 if (asset.holds<Issue>())
1086 {
1087 env(fclear(issuer, asfDefaultRipple));
1088 env.close();
1089 // preclaim: DefaultRipple is not set
1090 env(set(alice, vaultInfo.vaultID), Ter(terNO_RIPPLE));
1091
1092 env(fset(issuer, asfDefaultRipple));
1093 env.close();
1094 }
1095
1096 auto const amt =
1097 env.balance(alice) - accountReserve(*env.current(), alice.id(), env.journal);
1098 env(pay(alice, issuer, amt));
1099
1100 // preclaim:: tecINSUFFICIENT_RESERVE
1101 env(set(alice, vaultInfo.vaultID), Ter(tecINSUFFICIENT_RESERVE));
1102 }
1103 }
1104
1105 void
1107 {
1108 testcase("Invalid LoanBrokerCoverClawback");
1109 using namespace jtx;
1110 using namespace loan_broker;
1111
1112 // preflight
1113 {
1114 Account const alice{"alice"};
1115 Account const issuer{"issuer"};
1116 auto const usd = alice["USD"];
1117 Env env(*this, all_);
1118 env.fund(XRP(100'000), alice);
1119 env.close();
1120
1121 auto jtx = env.jt(coverClawback(alice), kAmount(usd(100)));
1122
1123 // holder == account
1124 env(jtx, Ter(temINVALID));
1125
1126 // holder == beast::kZero
1127 STAmount const bad(Issue{usd.currency, beast::kZero}, 100);
1128 jtx.jv[sfAmount] = bad.getJson();
1129 jtx.stx = env.ust(jtx);
1130 Serializer s;
1131 jtx.stx->add(s);
1132 auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result];
1133 // fails in doSubmit() on STTx construction
1134 BEAST_EXPECT(jrr[jss::error] == "invalidTransaction");
1135 BEAST_EXPECT(jrr[jss::error_exception] == "invalid native account");
1136 }
1137
1138 // preclaim
1139
1140 // Issue:
1141 // AllowTrustLineClawback is not set or NoFreeze is set
1143
1144 // MPTIssue:
1145 // MPTCanClawback is not set
1147 [&](Env& env, Account const& issuer, Account const& alice) -> MPT {
1148 MPTTester const mpt({.env = env, .issuer = issuer, .holders = {alice}});
1149 return mpt;
1150 },
1152 }
1153
1154 void
1156 {
1157 testcase("Invalid LoanBrokerCoverDeposit");
1158 using namespace jtx;
1159
1160 // preclaim:
1161 // tecWRONG_ASSET, tecINSUFFICIENT_FUNDS, frozen asset
1163 }
1164
1165 void
1167 {
1168 testcase("Invalid LoanBrokerCoverWithdraw");
1169 using namespace jtx;
1170
1171 /*
1172 preflight: illegal net
1173 isLegalNet() check is probably redundant. STAmount parsing
1174 should throw an exception on deserialize
1175
1176 preclaim: tecWRONG_ASSET, tecNO_DST, tecDST_TAG_NEEDED,
1177 tecNO_PERMISSION, checkFrozen failure, checkDeepFrozenFailure,
1178 second+third tecINSUFFICIENT_FUNDS (can this happen)?
1179 doApply: tecPATH_DRY (can it happen, funds already checked?)
1180 */
1182 }
1183
1184 void
1186 {
1187 using namespace jtx;
1188 testcase("Invalid LoanBrokerDelete");
1189 /*
1190 preclaim: tecHAS_OBLIGATIONS
1191 doApply:
1192 accountSend failure, removeEmptyHolding failure,
1193 all tecHAS_OBLIGATIONS (can any of these happen?)
1194 */
1196 }
1197
1198 void
1200 {
1201 using namespace jtx;
1202 testcase("Invalid LoanBrokerSet");
1203
1204 /*preclaim: canAddHolding failure (can it happen with MPT?
1205 can't create Vault if CanTransfer is not enabled.)
1206 doApply:
1207 first+second dirLink failure, createPseudoAccount failure,
1208 addEmptyHolding failure
1209 can any of these happen?
1210 */
1212 }
1213
1214 void
1216 {
1217 // This test is lifted directly from
1218 // https://bugs.immunefi.com/dashboard/submission/57808
1219 using namespace jtx;
1220 Env env(*this, all_);
1221
1222 Account const alice{"alice"};
1223 env.fund(XRP(10000), alice);
1224 env.close();
1225
1226 // Create a Vault owned by alice with an XRP asset
1227 PrettyAsset const asset{xrpIssue(), 1};
1228 Vault const vault{env};
1229 auto const [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
1230 env(createTx);
1231 env.close();
1232
1233 // Predict LoanBroker key using alice's current sequence BEFORE submit
1234 auto const brokerKeylet =
1235 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
1236
1237 // Create LoanBroker pointing to the vault
1238 env(loan_broker::set(alice, vaultKeylet.key));
1239 env.close();
1240
1241 // Build the CoverDeposit STTx directly
1242 STTx tx{ttLOAN_BROKER_COVER_DEPOSIT, [](STObject&) {}};
1243 tx.setAccountID(sfAccount, alice.id());
1244 tx.setFieldH256(sfLoanBrokerID, brokerKeylet.key);
1245 tx.setFieldAmount(sfAmount, asset(1));
1246
1247 // Create a writable view cloned from the current ledger and remove the
1248 // vault SLE
1249 OpenView ov{*env.current()};
1251 beast::Journal const jlog{sink};
1252 ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1253
1254 if (auto sleBroker = ac.view().peek(keylet::loanBroker(brokerKeylet.key)))
1255 {
1256 auto const vaultID = (*sleBroker)[sfVaultID];
1257 if (auto sleVault = ac.view().peek(keylet::vault(vaultID)))
1258 {
1259 ac.view().erase(sleVault);
1260 }
1261 }
1262
1263 // Invoke preclaim against the mutated (ApplyView) view; triggers
1264 // nullptr deref
1265 PreclaimContext const pctx{env.app(), ac.view(), tesSUCCESS, tx, TapNone, jlog};
1267 }
1268
1269 void
1271 {
1272 testcase("Require Auth - Implicit Pseudo-account authorization");
1273 using namespace jtx;
1274 using namespace loan_broker;
1275
1276 Account const issuer{"issuer"};
1277 Account const alice{"alice"};
1278 Env env(*this, all_);
1279 Vault vault{env};
1280
1281 env.fund(XRP(100'000), issuer, alice);
1282 env.close();
1283
1284 auto asset = MPTTester({
1285 .env = env,
1286 .issuer = issuer,
1287 .holders = {alice},
1288 .flags = kMptDexFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock,
1289 .authHolder = true,
1290 });
1291
1292 env(pay(issuer, alice, asset(100'000)));
1293 env.close();
1294
1295 // Alice is not authorized, can still create the vault
1296 asset.authorize({.account = issuer, .holder = alice, .flags = tfMPTUnauthorize});
1297 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
1298 env(tx);
1299 env.close();
1300
1301 auto const le = env.le(vaultKeylet);
1302 VaultInfo vaultInfo = [&]() {
1303 if (BEAST_EXPECT(le))
1304 return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
1305 return VaultInfo{asset, {}, {}};
1306 }();
1307 if (vaultInfo.vaultID == UInt256{})
1308 return;
1309
1310 // Can't unauthorize Vault pseudo-account
1311 asset.authorize(
1312 {.account = issuer,
1313 .holder = vaultInfo.pseudoAccount,
1314 .flags = tfMPTUnauthorize,
1315 .err = tecNO_PERMISSION});
1316
1317 auto forUnauthAuth = [&](auto&& doTx) {
1318 for (auto const flag : {tfMPTUnauthorize, 0u})
1319 {
1320 asset.authorize({.account = issuer, .holder = alice, .flags = flag});
1321 env.close();
1322 doTx(flag == 0);
1323 env.close();
1324 }
1325 };
1326
1327 // Can't deposit into Vault if the vault owner is not authorized
1328 forUnauthAuth([&](bool authorized) {
1329 auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS);
1330 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(51)}),
1331 err);
1332 });
1333
1334 // Can't withdraw from Vault if the vault owner is not authorized
1335 forUnauthAuth([&](bool authorized) {
1336 auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS);
1337 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = asset(1)}),
1338 err);
1339 });
1340
1341 auto const brokerKeylet =
1342 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
1343 // Can create LoanBroker if the vault owner is not authorized
1344 forUnauthAuth([&](auto) { env(set(alice, vaultInfo.vaultID)); });
1345
1346 auto const broker = env.le(brokerKeylet);
1347 if (!BEAST_EXPECT(broker))
1348 return;
1349 Account const brokerPseudo("pseudo", broker->at(sfAccount));
1350
1351 // Can't unauthorize LoanBroker pseudo-account
1352 asset.authorize(
1353 {.account = issuer,
1354 .holder = brokerPseudo,
1355 .flags = tfMPTUnauthorize,
1356 .err = tecNO_PERMISSION});
1357
1358 // Can't cover deposit into Vault if the vault owner is not authorized
1359 forUnauthAuth([&](bool authorized) {
1360 auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS);
1361 env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), err);
1362 });
1363
1364 // Can't cover withdraw from Vault if the vault owner is not authorized
1365 forUnauthAuth([&](bool authorized) {
1366 auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS);
1367 env(coverWithdraw(alice, brokerKeylet.key, vaultInfo.asset(5)), err);
1368 });
1369
1370 // Issuer can always cover clawback. The holder authorization is n/a.
1371 forUnauthAuth([&](bool) {
1372 env(coverClawback(issuer),
1373 kLoanBrokerId(brokerKeylet.key),
1374 kAmount(vaultInfo.asset(1)));
1375 });
1376 }
1377
1378 void
1380 {
1381 testcase("testLoanBrokerSetDebtMaximum");
1382 using namespace jtx;
1383 using namespace loan_broker;
1384 Account const issuer{"issuer"};
1385 Account const alice{"alice"};
1386 Env env(*this, all_);
1387 Vault const vault{env};
1388
1389 env.fund(XRP(100'000), issuer, alice);
1390 env.close();
1391
1392 PrettyAsset const asset = [&]() {
1393 MPTTester mptt{env, issuer, kMptInitNoFund};
1394 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
1395 env.close();
1396 PrettyAsset const mptAsset = mptt["MPT"];
1397 mptt.authorize({.account = alice});
1398 env.close();
1399 return mptAsset;
1400 }();
1401
1402 env(pay(issuer, alice, asset(100'000)));
1403 env.close();
1404
1405 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
1406 env(tx);
1407 env.close();
1408 auto const le = env.le(vaultKeylet);
1409 VaultInfo const vaultInfo = [&]() {
1410 if (BEAST_EXPECT(le))
1411 return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
1412 return VaultInfo{asset, {}, {}};
1413 }();
1414 if (vaultInfo.vaultID == UInt256{})
1415 return;
1416
1417 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
1418 env.close();
1419
1420 auto const brokerKeylet =
1421 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
1422 env(set(alice, vaultInfo.vaultID));
1423 env.close();
1424
1425 Account const borrower{"borrower"};
1426 env.fund(XRP(1'000), borrower);
1427 env(loan::set(borrower, brokerKeylet.key, asset(50).value()),
1428 Sig(sfCounterpartySignature, alice),
1429 Fee(env.current()->fees().base * 2));
1430 auto const broker = env.le(brokerKeylet);
1431 if (!BEAST_EXPECT(broker))
1432 return;
1433
1434 BEAST_EXPECT(broker->at(sfDebtTotal) == 50);
1435 auto debtTotal = broker->at(sfDebtTotal);
1436
1437 auto tx2 = set(alice, vaultInfo.vaultID);
1438 tx2[sfLoanBrokerID] = to_string(brokerKeylet.key);
1439 tx2[sfDebtMaximum] = debtTotal - 1;
1440 env(tx2, Ter(tecLIMIT_EXCEEDED));
1441
1442 tx2[sfDebtMaximum] = debtTotal + 1;
1443 env(tx2, Ter(tesSUCCESS));
1444
1445 tx2[sfDebtMaximum] = 0;
1446 env(tx2, Ter(tesSUCCESS));
1447
1448 tx2[sfDebtMaximum] = json::Value::kMaxInt;
1449 env(tx2, Ter(tesSUCCESS));
1450
1451 {
1452 auto const dm = power(2, 64) - 1;
1453 BEAST_EXPECT(dm > kMaxMpTokenAmount);
1454 tx2[sfDebtMaximum] = dm;
1455 env(tx2, Ter(temINVALID));
1456 }
1457
1458 env.setParseFailureExpected(true);
1459 try
1460 {
1461 tx2[sfDebtMaximum] = "9223372036854775808";
1462 env(tx2, Ter(temINVALID));
1463 // should throw in parser
1464 fail();
1465 }
1466 catch (std::exception const& e)
1467 {
1468 BEAST_EXPECT(
1469 std::string(e.what()) ==
1470 "invalidParamsField 'tx_json.DebtMaximum' has invalid data.");
1471 }
1472 env.setParseFailureExpected(false);
1473
1475 {
1476 // For the Large330 scale, 2^63 rounds _down_ to Number::kMaxRep
1477 {
1478 auto const dm = power(2, 63);
1479 BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm));
1480 tx2[sfDebtMaximum] = dm;
1481 env(tx2, Ter(tesSUCCESS));
1482 }
1483
1484 {
1485 auto const dm = power(2, 63) + Number{1, -1};
1486 BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm));
1487 tx2[sfDebtMaximum] = dm;
1488 env(tx2, Ter(tesSUCCESS));
1489 }
1490
1491 {
1492 auto const dm = power(2, 63) - 1;
1493 BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm));
1494 tx2[sfDebtMaximum] = dm;
1495 env(tx2, Ter(tesSUCCESS));
1496 }
1497
1498 {
1499 auto const dm = power(2, 63) - 3;
1500 BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm));
1501 tx2[sfDebtMaximum] = dm;
1502 env(tx2, Ter(tesSUCCESS));
1503 }
1504
1505 {
1506 auto const dm = power(2, 63) + 3;
1507 BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm));
1508 tx2[sfDebtMaximum] = dm;
1509 env(tx2, Ter(temINVALID));
1510 }
1511 }
1512 else
1513 {
1514 // For other scales, 2^63 rounds _up_ to Number::kMaxRepUp. Subtracting 1 rounds up
1515 // again.
1516 {
1517 auto const dm = power(2, 63) - 1;
1518 BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm));
1519 tx2[sfDebtMaximum] = dm;
1520 env(tx2, Ter(temINVALID));
1521 }
1522
1523 {
1524 auto const dm = power(2, 63) - 3;
1525 BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm));
1526 tx2[sfDebtMaximum] = dm;
1527 env(tx2, Ter(tesSUCCESS));
1528 }
1529 }
1530
1531 {
1532 auto const dm = 2 * (power(2, 62) - 1) + 1;
1533 BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm));
1534 tx2[sfDebtMaximum] = dm;
1535 env(tx2, Ter(tesSUCCESS));
1536 }
1537
1538 tx2[sfDebtMaximum] = Number{9223372036854775807, 0};
1539 env(tx2, Ter(tesSUCCESS));
1540 }
1541
1542 void
1544 {
1545 testcase << "RIPD-4323";
1546 using namespace jtx;
1547 Account const issuer("issuer");
1548 Account const holder("holder");
1549 Account const& broker = issuer;
1550
1551 auto test = [&](auto&& getToken) {
1552 Env env(*this, all_);
1553
1554 env.fund(XRP(1'000), issuer, holder);
1555 env.close();
1556
1557 auto const [token, deposit, err] = getToken(env);
1558
1559 Vault const vault(env);
1560 auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
1561 env(tx);
1562 env.close();
1563
1564 env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = deposit}),
1565 Ter(err));
1566 env.close();
1567
1568 auto const brokerKeylet =
1569 keylet::loanBroker(broker, SeqProxy::rawSequence(env.seq(broker)));
1570
1571 env(loan_broker::set(broker, keylet.key));
1572 env.close();
1573
1574 env(loan_broker::coverDeposit(broker, brokerKeylet.key, deposit), Ter(err));
1575 env.close();
1576 };
1577
1578 test([&](Env&) {
1579 // issuer can issue any amount
1580 auto const token = issuer["IOU"];
1581 return std::make_tuple(token, token(1'000), tesSUCCESS);
1582 });
1584 std::uint64_t, // pay to holder
1585 std::optional<std::uint64_t>, // max amount
1586 std::uint64_t, // deposit amount
1587 TER>> // expected error
1588 const mptTests = {
1589 // issuer can issue up to 2'000 tokens
1590 {2'000, 4'000, 1'000, tesSUCCESS},
1591 // issuer can issue 500 tokens (250 VaultDeposit +
1592 // 250 LoanBrokerCoverDeposit)
1593 {2'000, 2'500, 250, tesSUCCESS},
1594 // issuer can issue 500 tokens (250 VaultDeposit +
1595 // 250 LoanBrokerCoverDeposit). MaximumAmount is default.
1596 {kMaxMpTokenAmount - 500, std::nullopt, 250, tesSUCCESS},
1597 // issuer can issue 500, and fails on depositing 1'000
1598 {2'000, 2'500, 1'000, tecINSUFFICIENT_FUNDS},
1599 // issuer has already issued MaximumAmount
1600 {2'000, 2'000, 1'000, tecINSUFFICIENT_FUNDS},
1601 // issuer has already issued MaximumAmount. MaximumAmount is
1602 // default.
1603 {kMaxMpTokenAmount, std::nullopt, 250, tecINSUFFICIENT_FUNDS},
1604 };
1605 for (auto const& [pay, max, deposit, err] : mptTests)
1606 {
1608 MPT const token = MPTTester(
1609 {.env = env,
1610 .issuer = issuer,
1611 .holders = {holder},
1612 .pay = pay,
1613 .flags = kMptDexFlags,
1614 .maxAmt = max});
1615 return std::make_tuple(token, token(deposit), err);
1616 });
1617 }
1618 }
1619
1620 void
1622 {
1623 testcase << "RIPD-4466 - LoanBrokerSet disallows frozen vaults";
1624 using namespace jtx;
1625 Env env(*this, all_);
1626
1627 Account const issuer{"issuer"}, lender{"lender"}, borrower{"borrower"};
1628 env.fund(XRP(20'000), issuer, lender, borrower);
1629 auto const iou = issuer["IOU"];
1630
1631 Vault const vault{env};
1632 auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = iou.asset()});
1633 env(tx);
1634 env.close();
1635
1636 // Get vault pseudo-account and FREEZE it
1637 auto const vaultSle = env.le(vaultKeylet);
1638 auto const vaultPseudo = vaultSle->at(sfAccount);
1639 auto const vaultPseudoAcct = Account("VaultPseudo", vaultPseudo);
1640 env(trust(issuer, vaultPseudoAcct["IOU"](0), tfSetFreeze));
1641
1642 env(loan_broker::set(lender, vaultKeylet.key), Ter(tecFROZEN));
1643 }
1644
1645 void
1647 {
1648 testcase << "LoanBrokerDelete - locked broker pseudo-account MPT";
1649 using namespace jtx;
1650 using namespace loan_broker;
1651
1652 Account const issuer("issuer");
1653 Account const alice("alice");
1654
1655 auto const withFix = features[fixCleanup3_2_0];
1656 Env env(*this, features);
1657 env.fund(XRP(100'000), issuer, alice);
1658 env.close();
1659
1660 // Create MPT with locking enabled
1661 MPTTester mptt{env, issuer, kMptInitNoFund};
1662 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
1663
1664 PrettyAsset const mpt{mptt.issuanceID()};
1665
1666 // Fund alice
1667 mptt.authorize({.account = alice});
1668 env.close();
1669 env(pay(issuer, alice, mpt(100'000)));
1670 env.close();
1671
1672 // Create vault
1673 Vault const vault{env};
1674 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
1675 env(tx);
1676 env.close();
1677
1678 // Deposit into vault
1679 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(10'000)}));
1680 env.close();
1681
1682 // Create loan broker
1683 auto const brokerKeylet =
1684 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
1685 env(set(alice, vaultKeylet.key));
1686 env.close();
1687
1688 // Deposit cover
1689 env(coverDeposit(alice, brokerKeylet.key, mpt(5'000).value()));
1690 env.close();
1691
1692 // Verify cover is deposited
1693 auto const broker = env.le(brokerKeylet);
1694 if (!BEAST_EXPECT(broker))
1695 return;
1696 BEAST_EXPECT(broker->at(sfCoverAvailable) > 0);
1697
1698 // Get the broker pseudo-account ID
1699 auto const brokerPseudoID = broker->at(sfAccount);
1700
1701 // Verify the broker pseudo-account has an MPToken
1702 auto const pseudoMptKey = keylet::mptoken(mptt.issuanceID(), brokerPseudoID);
1703 auto const pseudoMpt = env.le(pseudoMptKey);
1704 if (!BEAST_EXPECT(pseudoMpt))
1705 return;
1706
1707 // Issuer locks the broker pseudo-account's individual MPToken
1708 {
1709 json::Value jv;
1710 jv[jss::Account] = issuer.human();
1711 jv[sfMPTokenIssuanceID] = to_string(mptt.issuanceID());
1712 jv[jss::Holder] = toBase58(brokerPseudoID);
1713 jv[jss::TransactionType] = jss::MPTokenIssuanceSet;
1714 jv[jss::Flags] = tfMPTLock;
1715 env(jv);
1716 env.close();
1717 }
1718
1719 // Verify the pseudo-account's MPToken is now locked
1720 {
1721 auto const sle = env.le(pseudoMptKey);
1722 if (!BEAST_EXPECT(sle))
1723 return;
1724 BEAST_EXPECT(sle->isFlag(lsfMPTLocked));
1725 }
1726
1727 // Record alice's balance before deletion
1728 auto const aliceBalanceBefore = env.balance(alice, mpt);
1729
1730 // With fixCleanup3_2_0, preclaim() checks the broker pseudo-account's
1731 // freeze/lock state via checkFrozen(), so deletion is blocked.
1732 // Without the fix, the check is missing and the locked cover is
1733 // returned to the owner.
1734 if (withFix)
1735 {
1736 env(del(alice, brokerKeylet.key), Ter(tecLOCKED));
1737 env.close();
1738
1739 // Verify the broker is not deleted
1740 BEAST_EXPECT(env.le(brokerKeylet) != nullptr);
1741
1742 // Verify alice did not receive the cover despite the lock
1743 auto const aliceBalanceAfter = env.balance(alice, mpt);
1744 BEAST_EXPECT(aliceBalanceAfter == aliceBalanceBefore);
1745
1746 // Verify the locked MPToken was not deleted
1747 BEAST_EXPECT(env.le(pseudoMptKey) != nullptr);
1748 }
1749 else
1750 {
1751 env(del(alice, brokerKeylet.key), Ter(tesSUCCESS));
1752 env.close();
1753
1754 // Verify the broker is deleted
1755 BEAST_EXPECT(env.le(brokerKeylet) == nullptr);
1756
1757 // Verify alice received the cover despite the lock
1758 auto const aliceBalanceAfter = env.balance(alice, mpt);
1759 BEAST_EXPECT(aliceBalanceAfter > aliceBalanceBefore);
1760
1761 // Verify the locked MPToken was deleted
1762 BEAST_EXPECT(env.le(pseudoMptKey) == nullptr);
1763 }
1764 }
1765
1766 // LoanBrokerDelete should succeed even when an unrelated third party
1767 // holds a confidential balance. The broker pseudo-account carries no
1768 // confidential fields of its own, but pre-fixCleanup3_5_0 the erase
1769 // was rejected.
1770 void
1772 {
1773 testcase << "LoanBrokerDelete - unrelated confidential holder";
1774 using namespace jtx;
1775 using namespace loan_broker;
1776
1777 auto const withFix = features[fixCleanup3_5_0];
1778
1779 Account const issuer("issuer");
1780 Account const alice("alice");
1781 // carol holds the same MPT but has no relationship to the vault, the
1782 // broker, or alice.
1783 Account const carol("carol");
1784
1785 for (bool const carolConverts : {false, true})
1786 {
1787 Env env(*this, features);
1788 env.fund(XRP(100'000), issuer, alice, carol);
1789 env.close();
1790
1791 MPTTester mptt{env, issuer, kMptInitNoFund};
1792 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
1793
1794 PrettyAsset const mpt{mptt.issuanceID()};
1795
1796 mptt.authorize({.account = alice});
1797 mptt.authorize({.account = carol});
1798 env(pay(issuer, alice, mpt(100'000)));
1799 env(pay(issuer, carol, mpt(100)));
1800 env.close();
1801
1802 // The issuer must publish an encryption key before any holder can
1803 // convert into confidential form.
1804 mptt.generateKeyPair(issuer);
1805 mptt.set({.account = issuer, .issuerPubKey = mptt.getPubKey(issuer)});
1806
1807 Vault const vault{env};
1808 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
1809 env(tx);
1810 env.close();
1811
1812 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(10'000)}));
1813 env.close();
1814
1815 auto const brokerKeylet =
1816 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
1817 env(set(alice, vaultKeylet.key));
1818 env.close();
1819
1820 env(coverDeposit(alice, brokerKeylet.key, mpt(5'000).value()));
1821 env.close();
1822
1823 auto const broker = env.le(brokerKeylet);
1824 if (!BEAST_EXPECT(broker))
1825 return;
1826 BEAST_EXPECT(broker->at(sfCoverAvailable) > 0);
1827
1828 auto const brokerPseudoID = broker->at(sfAccount);
1829 auto const pseudoMptKey = keylet::mptoken(mptt.issuanceID(), brokerPseudoID);
1830 if (!BEAST_EXPECT(env.le(pseudoMptKey)))
1831 return;
1832
1833 // The broker pseudo-account has no signing key, so it can never
1834 // submit a confidential transaction and its MPToken holds none of
1835 // the four ciphertext fields.
1836 Account const brokerHolder("broker", brokerPseudoID);
1837 BEAST_EXPECT(!mptt.getEncryptedBalance(brokerHolder, MPTTester::holderEncryptedInbox));
1838 BEAST_EXPECT(
1840 BEAST_EXPECT(
1842 BEAST_EXPECT(
1844
1845 if (carolConverts)
1846 {
1847 mptt.generateKeyPair(carol);
1848 mptt.convert({.account = carol, .amt = 1, .holderPubKey = mptt.getPubKey(carol)});
1849 }
1850
1851 BEAST_EXPECT(mptt.getIssuanceConfidentialBalance() == (carolConverts ? 1 : 0));
1852
1853 if (carolConverts && !withFix)
1854 {
1855 // The cover is non-zero at the start of the transaction, so
1856 // the erase is flagged and carol's COA rejects it.
1857 env(del(alice, brokerKeylet.key), Ter(tecINVARIANT_FAILED));
1858 env.close();
1859 BEAST_EXPECT(env.le(brokerKeylet) != nullptr);
1860 BEAST_EXPECT(env.le(pseudoMptKey) != nullptr);
1861
1862 // Draining the cover in an earlier transaction leaves the
1863 // MPToken's pre-transaction balance at zero, which sidesteps
1864 // the flag entirely.
1865 env(coverWithdraw(alice, brokerKeylet.key, mpt(5'000).value()));
1866 env.close();
1867 }
1868
1869 // With the fix the balance is read at erase time, so returning
1870 // the cover and deleting the broker in one transaction succeeds
1871 // regardless of carol's confidential balance.
1872 env(del(alice, brokerKeylet.key));
1873 env.close();
1874 BEAST_EXPECT(env.le(brokerKeylet) == nullptr);
1875 BEAST_EXPECT(env.le(pseudoMptKey) == nullptr);
1876 }
1877 }
1878
1879 void
1881 {
1882 testcase << "LoanBrokerDelete - frozen broker pseudo-account IOU";
1883 using namespace jtx;
1884 using namespace loan_broker;
1885
1886 Account const issuer("issuer");
1887 Account const alice("alice");
1888
1889 auto const withFix = features[fixCleanup3_2_0];
1890 std::string logs;
1891 Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
1892 env.fund(XRP(100'000), issuer, alice);
1893 env.close();
1894
1895 auto const iou = issuer["IOU"];
1896
1897 // Set up trust lines and fund alice
1898 env(trust(alice, iou(1'000'000)));
1899 env.close();
1900 env(pay(issuer, alice, iou(100'000)));
1901 env.close();
1902
1903 // Create vault
1904 Vault const vault{env};
1905 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = iou.asset()});
1906 env(tx);
1907 env.close();
1908
1909 // Deposit into vault
1910 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = iou(10'000)}));
1911 env.close();
1912
1913 // Create loan broker
1914 auto const brokerKeylet =
1915 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
1916 env(set(alice, vaultKeylet.key));
1917 env.close();
1918
1919 // Deposit cover
1920 env(coverDeposit(alice, brokerKeylet.key, iou(5'000)));
1921 env.close();
1922
1923 // Verify cover is deposited
1924 auto const broker = env.le(brokerKeylet);
1925 if (!BEAST_EXPECT(broker))
1926 return;
1927 BEAST_EXPECT(broker->at(sfCoverAvailable) > 0);
1928
1929 // Get the broker pseudo-account
1930 auto const brokerPseudoID = broker->at(sfAccount);
1931 auto const brokerPseudo = Account("BrokerPseudo", brokerPseudoID);
1932
1933 // Issuer freezes the broker pseudo-account's trust line
1934 env(trust(issuer, brokerPseudo["IOU"](0), tfSetFreeze));
1935 env.close();
1936
1937 // Record alice's balance before deletion attempt
1938 auto const aliceBalanceBefore = env.balance(alice, iou);
1939
1940 // With fixCleanup3_2_0, preclaim() checks the broker
1941 // pseudo-account's freeze state via checkFrozen(), so
1942 // deletion is blocked early with tecFROZEN.
1943 // Without the fix, preclaim() does not check the pseudo-account,
1944 // but the TransfersNotFrozen invariant catches the frozen transfer
1945 // in doApply() and fails with tecINVARIANT_FAILED.
1946 // Either way, the broker survives and alice's balance is unchanged.
1947 if (withFix)
1948 {
1949 env(del(alice, brokerKeylet.key), Ter(tecFROZEN));
1950 }
1951 else
1952 {
1953 env(del(alice, brokerKeylet.key), Ter(tecINVARIANT_FAILED));
1954 }
1955 env.close();
1956
1957 // Broker still exists
1958 BEAST_EXPECT(env.le(brokerKeylet) != nullptr);
1959
1960 // Alice's balance unchanged
1961 auto const aliceBalanceAfter = env.balance(alice, iou);
1962 BEAST_EXPECT(aliceBalanceAfter == aliceBalanceBefore);
1963 }
1964
1965 void
1967 {
1968 testcase << "LoanBrokerDelete - auth-required broker pseudo-account MPT "
1969 << (features[fixCleanup3_4_0] ? "post-fix" : "pre-fix");
1970 using namespace jtx;
1971 using namespace loan_broker;
1972
1973 Account const issuer("issuer");
1974 Account const alice("alice");
1975
1976 Env env(*this, features);
1977 env.fund(XRP(100'000), issuer, alice);
1978 env.close();
1979
1980 // Create an auth-required MPT and authorize alice as a holder. The
1981 // broker pseudo-account's cover MPToken is auto-created later
1982 // (addEmptyHolding -> authorizeMPToken) with lsfMPTAuthorized clear;
1983 // the pseudo-account is implicitly authorized to hold any MPT
1984 // regardless of that flag.
1985 auto tester = MPTTester(
1986 {.env = env,
1987 .issuer = issuer,
1988 .holders = {alice},
1989 .pay = 20'000,
1990 .flags = tfMPTRequireAuth | tfMPTCanTransfer,
1991 .authHolder = true});
1992
1993 PrettyAsset const mpt{tester.issuanceID()};
1994
1995 // Create vault
1996 Vault const vault{env};
1997 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
1998 env(tx);
1999 env.close();
2000
2001 // Deposit into vault
2002 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(10'000)}));
2003 env.close();
2004
2005 // Create loan broker
2006 auto const brokerKeylet =
2007 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2008 env(set(alice, vaultKeylet.key));
2009 env.close();
2010
2011 // Deposit cover
2012 env(coverDeposit(alice, brokerKeylet.key, mpt(5'000).value()));
2013 env.close();
2014
2015 // Verify cover is deposited
2016 auto const broker = env.le(brokerKeylet);
2017 if (!BEAST_EXPECT(broker))
2018 return;
2019 BEAST_EXPECT(broker->at(sfCoverAvailable) > 0);
2020
2021 // Get the broker pseudo-account
2022 auto const brokerPseudoID = broker->at(sfAccount);
2023
2024 // Verify the broker pseudo-account has an MPToken, and that it was
2025 // never explicitly authorized (issuer cannot authorize a
2026 // pseudo-account holder; see MPTokenAuthorize::preclaim).
2027 auto const pseudoMptKey = keylet::mptoken(tester.issuanceID(), brokerPseudoID);
2028 auto const pseudoMpt = env.le(pseudoMptKey);
2029 if (!BEAST_EXPECT(pseudoMpt))
2030 return;
2031 BEAST_EXPECT(!pseudoMpt->isFlag(lsfMPTAuthorized));
2032
2033 // Record alice's balance before deletion
2034 auto const aliceBalanceBefore = env.balance(alice, mpt);
2035
2036 // LoanBrokerDelete sends the remaining cover out of the broker pseudo-account, deletes its
2037 // now-empty MPToken, and erases the pseudo AccountRoot. Before the fix,
2038 // ValidMPTTransfer::isAuthorized evaluates isPseudoAccount() on the post-transaction view
2039 // (where the pseudo-account is already gone) and falls back to the MPToken's
2040 // lsfMPTAuthorized flag, which was never set, so the invariant treats the broker as an
2041 // unauthorized sender and the whole transaction fails once fixCleanup3_4_0 makes the check
2042 // enforcing.
2043 env(del(alice, brokerKeylet.key), Ter(tesSUCCESS));
2044 env.close();
2045
2046 // Broker and its pseudo-account MPToken are gone
2047 BEAST_EXPECT(env.le(brokerKeylet) == nullptr);
2048 BEAST_EXPECT(env.le(pseudoMptKey) == nullptr);
2049
2050 // Alice received the cover
2051 auto const aliceBalanceAfter = env.balance(alice, mpt);
2052 BEAST_EXPECT(aliceBalanceAfter > aliceBalanceBefore);
2053 }
2054
2055 void
2057 {
2058 using namespace jtx;
2059 using namespace loan_broker;
2060
2061 Account const issuer{"issuer"};
2062 Account const alice{"alice"};
2063
2064 // === IOU ===
2065 {
2066 testcase("LoanBrokerCoverDeposit IOU freeze checks");
2067 Env env(*this, all_);
2068 Vault const vault{env};
2069
2070 env.fund(XRP(100'000), issuer, alice);
2071 env(trust(alice, issuer["IOU"](1'000'000)));
2072 env.close();
2073 PrettyAsset const asset(issuer["IOU"]);
2074 env(pay(issuer, alice, asset(100'000)));
2075 env.close();
2076
2077 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
2078 env(tx);
2079 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
2080 env.close();
2081
2082 auto const brokerKeylet =
2083 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2084 env(set(alice, vaultKeylet.key));
2085 env.close();
2086
2087 auto const broker = env.le(brokerKeylet);
2088 if (!BEAST_EXPECT(broker))
2089 return;
2090 Account const brokerPseudo("pseudo", broker->at(sfAccount));
2091
2092 env(coverDeposit(alice, brokerKeylet.key, asset(10)));
2093 env.close();
2094
2095 auto runTests = [&]() {
2096 auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
2097
2098 // Global freeze
2099 env(fset(issuer, asfGlobalFreeze));
2100 env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
2101 env(fclear(issuer, asfGlobalFreeze));
2102
2103 // Source regular freeze
2104 env(trust(issuer, asset(0), alice, tfSetFreeze));
2105 env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
2106 env(trust(issuer, asset(0), alice, tfClearFreeze));
2107
2108 // Source deep freeze
2109 env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze));
2110 env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
2111 env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze));
2112
2113 // Pseudo regular freeze — post-fix blocks, pre-fix allows (BUG)
2114 TER const pseudoTer = fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS);
2115 env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze));
2116 env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(pseudoTer));
2117 env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze));
2118
2119 // Pseudo deep freeze
2120 env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze | tfSetDeepFreeze));
2121 env(coverDeposit(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
2122 env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze | tfClearDeepFreeze));
2123 };
2124
2125 runTests();
2126 env.disableFeature(fixCleanup3_3_0);
2127 runTests();
2128 env.enableFeature(fixCleanup3_3_0);
2129 }
2130
2131 // === MPT ===
2132 {
2133 testcase("LoanBrokerCoverDeposit MPT lock checks");
2134 Env env(*this, all_);
2135 Vault const vault{env};
2136
2137 env.fund(XRP(100'000), issuer, alice);
2138 env.close();
2139
2140 MPTTester mptt{env, issuer, kMptInitNoFund};
2141 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
2142 PrettyAsset const mpt{mptt.issuanceID()};
2143
2144 mptt.authorize({.account = alice});
2145 env(pay(issuer, alice, mpt(100'000)));
2146 env.close();
2147
2148 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
2149 env(tx);
2150 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(50)}));
2151 env.close();
2152
2153 auto const brokerKeylet =
2154 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2155 env(set(alice, vaultKeylet.key));
2156 env.close();
2157
2158 auto const broker = env.le(brokerKeylet);
2159 if (!BEAST_EXPECT(broker))
2160 return;
2161 Account const brokerPseudo("pseudo", broker->at(sfAccount));
2162
2163 env(coverDeposit(alice, brokerKeylet.key, mpt(10)));
2164 env.close();
2165
2166 // For MPT isDeepFrozen == isFrozen, so all locks block in
2167 // both pre- and post-fix. No behavioral difference.
2168 auto runTests = [&]() {
2169 // Global lock
2170 mptt.set({.flags = tfMPTLock});
2171 env.close();
2172 env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED));
2173 mptt.set({.flags = tfMPTUnlock});
2174 env.close();
2175
2176 // Source (alice) individual lock
2177 mptt.set({.holder = alice, .flags = tfMPTLock});
2178 env.close();
2179 env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED));
2180 mptt.set({.holder = alice, .flags = tfMPTUnlock});
2181 env.close();
2182
2183 // Pseudo individual lock
2184 mptt.set({.holder = brokerPseudo, .flags = tfMPTLock});
2185 env.close();
2186 env(coverDeposit(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED));
2187 mptt.set({.holder = brokerPseudo, .flags = tfMPTUnlock});
2188 env.close();
2189 };
2190
2191 runTests();
2192 env.disableFeature(fixCleanup3_3_0);
2193 runTests();
2194 env.enableFeature(fixCleanup3_3_0);
2195 }
2196 }
2197
2198 // Focused demonstration: a cover-withdraw submitter under a regular
2199 // individual IOU freeze can still withdraw to themselves (self-withdrawal).
2200 //
2201 // Pre-fixCleanup3_3_0: the old code only checked the pseudo-account source
2202 // and the destination for deep-freeze; it did not check the submitter's
2203 // individual freeze at all. Self-withdrawal therefore always succeeded.
2204 // Post-fixCleanup3_3_0: checkWithdrawFreeze explicitly skips the submitter
2205 // freeze check when submitter == destination, preserving the same result.
2206 void
2208 {
2209 testcase("LoanBrokerCoverWithdraw IOU self-withdrawal while individually frozen");
2210
2211 using namespace jtx;
2212 using namespace loan_broker;
2213
2214 Account const issuer{"issuer"};
2215 Account const alice{"alice"};
2216 Account const dest{"dest"};
2217 Env env{*this, all_};
2218 Vault const vault{env};
2219
2220 env.fund(XRP(100'000), issuer, alice, dest);
2221 env(trust(alice, issuer["IOU"](1'000'000)));
2222 env(trust(dest, issuer["IOU"](1'000'000)));
2223 env.close();
2224
2225 PrettyAsset const asset(issuer["IOU"]);
2226 env(pay(issuer, alice, asset(100'000)));
2227 env.close();
2228
2229 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
2230 env(vaultTx);
2231 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
2232 env.close();
2233
2234 auto const brokerKeylet =
2235 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2236 env(set(alice, vaultKeylet.key));
2237 env.close();
2238
2239 env(coverDeposit(alice, brokerKeylet.key, asset(10)));
2240 env.close();
2241
2242 auto runTests = [&]() {
2243 auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
2244
2245 // Set a regular individual freeze on alice's IOU trustline.
2246 env(trust(issuer, asset(0), alice, tfSetFreeze));
2247 env.close();
2248
2249 // Self-withdrawal: submitter == destination (no sfDestination in tx).
2250 // Both pre- and post-fixCleanup3_3_0 this succeeds:
2251 // pre-fix: old code never checked the submitter's freeze.
2252 // post-fix: checkWithdrawFreeze skips submitter when submitter==dst.
2253 env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tesSUCCESS));
2254
2255 // Withdrawal to a third party is blocked by the submitter freeze
2256 // under fixCleanup3_3_0; pre-fix it was not checked.
2257 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2258 kDestination(dest),
2259 Ter(fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS)));
2260
2261 env(trust(issuer, asset(0), alice, tfClearFreeze));
2262 env.close();
2263 };
2264
2265 runTests();
2266 env.disableFeature(fixCleanup3_3_0);
2267 runTests();
2268 env.enableFeature(fixCleanup3_3_0);
2269 }
2270
2271 void
2273 {
2274 using namespace jtx;
2275 using namespace loan_broker;
2276
2277 Account const issuer{"issuer"};
2278 Account const alice{"alice"};
2279
2280 // === IOU ===
2281 {
2282 testcase("LoanBrokerCoverWithdraw IOU freeze checks");
2283 Env env(*this, all_);
2284 Vault const vault{env};
2285
2286 env.fund(XRP(100'000), issuer, alice);
2287 env(trust(alice, issuer["IOU"](1'000'000)));
2288 env.close();
2289 PrettyAsset const asset(issuer["IOU"]);
2290 env(pay(issuer, alice, asset(100'000)));
2291 env.close();
2292
2293 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
2294 env(tx);
2295 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
2296 env.close();
2297
2298 auto const brokerKeylet =
2299 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2300 env(set(alice, vaultKeylet.key));
2301 env.close();
2302
2303 auto const broker = env.le(brokerKeylet);
2304 if (!BEAST_EXPECT(broker))
2305 return;
2306 Account const brokerPseudo("pseudo", broker->at(sfAccount));
2307
2308 env(coverDeposit(alice, brokerKeylet.key, asset(10)));
2309 env.close();
2310
2311 Account const dest{"dest"};
2312 env.fund(XRP(1'000), dest);
2313 env(trust(dest, asset(1'000)));
2314
2315 auto runTests = [&]() {
2316 auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
2317 TER const expectedTec = fix330Enabled ? TER(tecFROZEN) : TER(tesSUCCESS);
2318
2319 // Global freeze
2320 env(fset(issuer, asfGlobalFreeze));
2321 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2322 kDestination(dest),
2323 Ter(tecFROZEN));
2324 env(fclear(issuer, asfGlobalFreeze));
2325
2326 // Source (pseudo) regular freeze
2327 env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze));
2328 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2329 kDestination(dest),
2330 Ter(tecFROZEN));
2331 env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze));
2332
2333 // Source (pseudo) deep freeze
2334 env(trust(issuer, asset(0), brokerPseudo, tfSetFreeze | tfSetDeepFreeze));
2335 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2336 kDestination(dest),
2337 Ter(tecFROZEN));
2338 env(trust(issuer, asset(0), brokerPseudo, tfClearFreeze | tfClearDeepFreeze));
2339
2340 // Submitter regular freeze → dest
2341 env(trust(issuer, asset(0), alice, tfSetFreeze));
2342 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2343 kDestination(dest),
2344 Ter(expectedTec));
2345 // Submitter regular freeze → self: always allowed
2346 env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tesSUCCESS));
2347 env(trust(issuer, asset(0), alice, tfClearFreeze));
2348 env(coverDeposit(
2349 alice, brokerKeylet.key, asset(isTesSuccess(expectedTec) ? 2 : 1)));
2350
2351 // Submitter deep freeze → dest
2352 env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze));
2353 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2354 kDestination(dest),
2355 Ter(expectedTec));
2356 // Submitter deep freeze → self: blocked (checkDeepFrozen)
2357 env(coverWithdraw(alice, brokerKeylet.key, asset(1)), Ter(tecFROZEN));
2358 env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze));
2359 if (isTesSuccess(expectedTec))
2360 env(coverDeposit(alice, brokerKeylet.key, asset(1)));
2361
2362 // Destination regular freeze: only deep freeze blocks
2363 env(trust(issuer, asset(0), dest, tfSetFreeze));
2364 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2365 kDestination(dest),
2366 Ter(tesSUCCESS));
2367 env(trust(issuer, asset(0), dest, tfClearFreeze));
2368 env(coverDeposit(alice, brokerKeylet.key, asset(1)));
2369
2370 // Destination deep freeze
2371 env(trust(issuer, asset(0), dest, tfSetFreeze | tfSetDeepFreeze));
2372 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2373 kDestination(dest),
2374 Ter(tecFROZEN));
2375 env(trust(issuer, asset(0), dest, tfClearFreeze | tfClearDeepFreeze));
2376
2377 // Submitter frozen → issuer: bypasses all freeze checks
2378 env(trust(issuer, asset(0), alice, tfSetFreeze));
2379 env(coverWithdraw(alice, brokerKeylet.key, asset(1)),
2380 kDestination(issuer),
2381 Ter(tesSUCCESS));
2382 env(trust(issuer, asset(0), alice, tfClearFreeze));
2383 env(coverDeposit(alice, brokerKeylet.key, asset(1)));
2384 };
2385
2386 runTests();
2387 env.disableFeature(fixCleanup3_3_0);
2388 runTests();
2389 env.enableFeature(fixCleanup3_3_0);
2390 }
2391
2392 // === MPT ===
2393 {
2394 testcase("LoanBrokerCoverWithdraw MPT lock checks");
2395 Env env(*this, all_);
2396 Vault const vault{env};
2397
2398 env.fund(XRP(100'000), issuer, alice);
2399 env.close();
2400
2401 MPTTester mptt{env, issuer, kMptInitNoFund};
2402 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
2403 PrettyAsset const mpt{mptt.issuanceID()};
2404
2405 mptt.authorize({.account = alice});
2406 env(pay(issuer, alice, mpt(100'000)));
2407 env.close();
2408
2409 auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
2410 env(tx);
2411 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(50)}));
2412 env.close();
2413
2414 auto const brokerKeylet =
2415 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2416 env(set(alice, vaultKeylet.key));
2417 env.close();
2418
2419 auto const broker = env.le(brokerKeylet);
2420 if (!BEAST_EXPECT(broker))
2421 return;
2422 Account const brokerPseudo("pseudo", broker->at(sfAccount));
2423
2424 env(coverDeposit(alice, brokerKeylet.key, mpt(10)));
2425 env.close();
2426
2427 Account const dest{"dest"};
2428 env.fund(XRP(1'000), dest);
2429 mptt.authorize({.account = dest});
2430 env.close();
2431
2432 auto runTests = [&]() {
2433 auto const withFix = env.current()->rules().enabled(fixCleanup3_3_0);
2434 // Only submitter-to-dest differs: post-fix blocks, pre-fix
2435 // doesn't (BUG). All other locks block in both because for
2436 // MPT isDeepFrozen == isFrozen.
2437 TER const submitterToDest = withFix ? TER(tecLOCKED) : TER(tesSUCCESS);
2438
2439 // Global lock
2440 mptt.set({.flags = tfMPTLock});
2441 env.close();
2442 env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
2443 kDestination(dest),
2444 Ter(tecLOCKED));
2445 mptt.set({.flags = tfMPTUnlock});
2446 env.close();
2447
2448 // Source (pseudo) individual lock
2449 mptt.set({.holder = brokerPseudo, .flags = tfMPTLock});
2450 env.close();
2451 env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
2452 kDestination(dest),
2453 Ter(tecLOCKED));
2454 mptt.set({.holder = brokerPseudo, .flags = tfMPTUnlock});
2455 env.close();
2456
2457 // Submitter individual lock → dest
2458 mptt.set({.holder = alice, .flags = tfMPTLock});
2459 env.close();
2460 env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
2461 kDestination(dest),
2462 Ter(submitterToDest));
2463 // Submitter individual lock → self: blocked
2464 env(coverWithdraw(alice, brokerKeylet.key, mpt(1)), Ter(tecLOCKED));
2465 mptt.set({.holder = alice, .flags = tfMPTUnlock});
2466 env.close();
2467 if (isTesSuccess(submitterToDest))
2468 env(coverDeposit(alice, brokerKeylet.key, mpt(1)));
2469 env.close();
2470
2471 // Dest individual lock: blocked
2472 mptt.set({.holder = dest, .flags = tfMPTLock});
2473 env.close();
2474 env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
2475 kDestination(dest),
2476 Ter(tecLOCKED));
2477 mptt.set({.holder = dest, .flags = tfMPTUnlock});
2478 env.close();
2479
2480 // Submitter locked → issuer: bypasses all freeze checks
2481 mptt.set({.holder = alice, .flags = tfMPTLock});
2482 env.close();
2483 env(coverWithdraw(alice, brokerKeylet.key, mpt(1)),
2484 kDestination(issuer),
2485 Ter(tesSUCCESS));
2486 mptt.set({.holder = alice, .flags = tfMPTUnlock});
2487 env(coverDeposit(alice, brokerKeylet.key, mpt(1)));
2488 env.close();
2489 };
2490
2491 runTests();
2492 env.disableFeature(fixCleanup3_3_0);
2493 runTests();
2494 env.enableFeature(fixCleanup3_3_0);
2495 }
2496 }
2497
2498 void
2500 {
2501 using namespace jtx;
2502 Account const issuer("broker");
2503 Account const broker("issuer");
2504 Account const dest("destination");
2505 auto const token = issuer["IOU"];
2506
2507 enum class TrustState {
2508 RequireAuth,
2509 ZeroLimit,
2510 ReachedLimit,
2511 NearLimit,
2512 NoTrustLine,
2513 };
2514
2515 auto test = [&](TrustState trustState) {
2516 Env env(*this, all_);
2517
2518 testcase << "RIPD-4274 IOU with state: " << static_cast<int>(trustState);
2519
2520 auto setTrustLine = [&](Account const& acct, TrustState state) {
2521 switch (state)
2522 {
2523 case TrustState::RequireAuth:
2524 env(trust(issuer, token(0), acct, tfSetfAuth));
2525 break;
2526 case TrustState::ZeroLimit: {
2527 auto jv = trust(acct, token(0));
2528 // set QualityIn so that the trustline is not
2529 // auto-deleted
2530 jv[sfQualityIn] = 10'000'000;
2531 env(jv);
2532 }
2533 break;
2534 case TrustState::ReachedLimit: {
2535 env(trust(acct, token(1'000)));
2536 env(pay(issuer, acct, token(1'000)));
2537 env.close();
2538 }
2539 break;
2540 case TrustState::NearLimit: {
2541 env(trust(acct, token(1'000)));
2542 env(pay(issuer, acct, token(950)));
2543 env.close();
2544 }
2545 break;
2546 case TrustState::NoTrustLine:
2547 // don't create a trustline
2548 break;
2549 default:
2550 BEAST_EXPECT(false);
2551 }
2552 env.close();
2553 };
2554
2555 env.fund(XRP(1'000), issuer, broker, dest);
2556 env.close();
2557
2558 if (trustState == TrustState::RequireAuth)
2559 {
2560 env(fset(issuer, asfRequireAuth));
2561 env.close();
2562
2563 setTrustLine(broker, TrustState::RequireAuth);
2564 }
2565
2566 setTrustLine(dest, trustState);
2567
2568 env(trust(broker, token(2'000), 0));
2569 env(pay(issuer, broker, token(2'000)));
2570 env.close();
2571
2572 Vault const vault(env);
2573 auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
2574 env(tx);
2575 env.close();
2576
2577 // Test Vault withdraw
2578 env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
2579 env.close();
2580
2581 env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}),
2583 Ter(std::ignore));
2584 BEAST_EXPECT(env.ter() == tecNO_LINE);
2585 env.close();
2586
2587 env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
2588
2589 // Test LoanBroker withdraw
2590 auto const brokerKeylet =
2591 keylet::loanBroker(broker, SeqProxy::rawSequence(env.seq(broker)));
2592
2593 env(loan_broker::set(broker, keylet.key));
2594 env.close();
2595
2596 env(loan_broker::coverDeposit(broker, brokerKeylet.key, token(1'000)));
2597 env.close();
2598
2599 env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)),
2601 Ter(std::ignore));
2602 BEAST_EXPECT(env.ter() == tecNO_LINE);
2603 env.close();
2604
2605 // Clearing RequireAuth shouldn't change the result
2606 if (trustState == TrustState::RequireAuth)
2607 {
2608 env(fclear(issuer, asfRequireAuth));
2609 env.close();
2610
2611 env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)),
2613 Ter(std::ignore));
2614 BEAST_EXPECT(env.ter() == tecNO_LINE);
2615 env.close();
2616 }
2617 };
2618
2619 test(TrustState::RequireAuth);
2620 test(TrustState::ZeroLimit);
2621 test(TrustState::ReachedLimit);
2622 test(TrustState::NearLimit);
2623 test(TrustState::NoTrustLine);
2624 }
2625
2626 void
2628 {
2629 using namespace jtx;
2630 Account const issuer("broker");
2631 Account const broker("issuer");
2632 Account const dest("destination");
2633
2634 enum class MPTState {
2635 RequireAuth,
2636 ReachedMAX,
2637 NoMPT,
2638 };
2639
2640 auto test = [&](MPTState mptState) {
2641 Env env(*this, all_);
2642
2643 testcase << "RIPD-4274 MPT with state: " << static_cast<int>(mptState);
2644
2645 env.fund(XRP(1'000), issuer, broker, dest);
2646 env.close();
2647
2648 auto const maybeToken = [&]() -> std::optional<MPT> {
2649 switch (mptState)
2650 {
2651 case MPTState::RequireAuth: {
2652 auto tester = MPTTester(
2653 {.env = env,
2654 .issuer = issuer,
2655 .holders = {broker, dest},
2656 .pay = 2'000,
2657 .flags = kMptDexFlags | tfMPTRequireAuth,
2658 .authHolder = true,
2659 .maxAmt = 5'000});
2660 // unauthorize dest
2661 tester.authorize(
2662 {.account = issuer, .holder = dest, .flags = tfMPTUnauthorize});
2663 return tester;
2664 }
2665 case MPTState::ReachedMAX: {
2666 auto tester = MPTTester(
2667 {.env = env,
2668 .issuer = issuer,
2669 .holders = {broker, dest},
2670 .pay = 2'000,
2671 .flags = kMptDexFlags,
2672 .maxAmt = 4'000});
2673 BEAST_EXPECT(env.balance(issuer, tester) == tester(-4'000));
2674 return tester;
2675 }
2676 case MPTState::NoMPT: {
2677 return MPTTester(
2678 {.env = env,
2679 .issuer = issuer,
2680 .holders = {broker},
2681 .pay = 2'000,
2682 .flags = kMptDexFlags,
2683 .maxAmt = 4'000});
2684 }
2685 default:
2686 return std::nullopt;
2687 }
2688 }();
2689 if (!BEAST_EXPECT(maybeToken))
2690 return;
2691
2692 auto const& token = *maybeToken;
2693
2694 Vault const vault(env);
2695 auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
2696 env(tx);
2697 env.close();
2698
2699 // Test Vault withdraw
2700 env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
2701 env.close();
2702
2703 env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}),
2705 Ter(std::ignore));
2706
2707 // Shouldn't fail if at MaximumAmount since no new tokens are issued
2708 TER const err = mptState == MPTState::ReachedMAX ? TER(tesSUCCESS) : tecNO_AUTH;
2709 BEAST_EXPECT(env.ter() == err);
2710 env.close();
2711
2712 if (!isTesSuccess(err))
2713 {
2714 env(vault.withdraw(
2715 {.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
2716 }
2717
2718 // Test LoanBroker withdraw
2719 auto const brokerKeylet =
2720 keylet::loanBroker(broker, SeqProxy::rawSequence(env.seq(broker)));
2721
2722 env(loan_broker::set(broker, keylet.key));
2723 env.close();
2724
2725 env(loan_broker::coverDeposit(broker, brokerKeylet.key, token(1'000)));
2726 env.close();
2727
2728 env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)),
2730 Ter(std::ignore));
2731 BEAST_EXPECT(env.ter() == err);
2732 env.close();
2733 };
2734
2735 test(MPTState::RequireAuth);
2736 test(MPTState::ReachedMAX);
2737 test(MPTState::NoMPT);
2738 }
2739
2740 void
2742 {
2745 }
2746
2747 void
2749 {
2750 testcase(
2751 std::string{"CoverWithdraw with credential-based deposit preauth "} +
2752 (features[fixCleanup3_4_0] ? "post-fix" : "pre-fix"));
2753 using namespace jtx;
2754 using namespace std::chrono_literals;
2755
2756 bool const fix340Enabled = features[fixCleanup3_4_0];
2757
2758 Env env(*this, features);
2759
2760 Account const broker{"broker"};
2761 Account const dest{"dest"};
2762 Account const credIssuer{"credIssuer"};
2763 char const credType[] = "abcde";
2764
2765 env.fund(XRP(10'000), broker, dest, credIssuer);
2766 env(fset(dest, asfDepositAuth));
2767 env.close();
2768
2769 PrettyAsset const asset{xrpIssue(), 1'000'000};
2770
2771 Vault const vault(env);
2772 auto const [vaultTx, vaultKeylet] = vault.create({.owner = broker, .asset = asset});
2773 env(vaultTx);
2774 env.close();
2775
2776 env(vault.deposit({.depositor = broker, .id = vaultKeylet.key, .amount = asset(1'000)}));
2777 env.close();
2778
2779 auto const brokerKeylet =
2780 keylet::loanBroker(broker.id(), SeqProxy::rawSequence(env.seq(broker)));
2781 env(loan_broker::set(broker, vaultKeylet.key));
2782 env.close();
2783
2784 env(loan_broker::coverDeposit(broker, brokerKeylet.key, asset(500)));
2785 env.close();
2786
2787 auto coverWithdrawToDest = [&]() {
2788 return loan_broker::coverWithdraw(broker, brokerKeylet.key, asset(10));
2789 };
2790
2791 // Without any preauth, coverWithdraw to dest fails
2792 env(coverWithdrawToDest(), loan_broker::kDestination(dest), Ter{tecNO_PERMISSION});
2793 env.close();
2794
2795 // Issue and accept a credential for the broker (with expiration)
2796 auto jv = credentials::create(broker, credIssuer, credType);
2797 std::uint32_t const expiration =
2798 env.current()->header().parentCloseTime.time_since_epoch().count() + 100;
2799 jv[sfExpiration.jsonName] = expiration;
2800 env(jv);
2801 env(credentials::accept(broker, credIssuer, credType));
2802 env.close();
2803
2804 auto const credKeylet = credentials::keylet(broker, credIssuer, credType);
2805 auto const credIdx =
2806 credentials::ledgerEntry(env, broker, credIssuer, credType)[jss::result][jss::index]
2807 .asString();
2808
2809 // dest authorizes deposits from holders of credentials issued by credIssuer
2810 env(deposit::authCredentials(dest, {{.issuer = credIssuer, .credType = credType}}));
2811 env.close();
2812
2813 // Without supplying credentials, still fails
2814 env(coverWithdrawToDest(), loan_broker::kDestination(dest), Ter{tecNO_PERMISSION});
2815 env.close();
2816
2817 if (!fix340Enabled)
2818 {
2819 // Pre-fix: sfCredentialIDs in LoanBrokerCoverWithdraw is disabled
2820 env(coverWithdrawToDest(),
2822 credentials::Ids({credIdx}),
2823 Ter{temDISABLED});
2824 env.close();
2825 return;
2826 }
2827
2828 // With credentials, succeeds
2829 env(coverWithdrawToDest(), loan_broker::kDestination(dest), credentials::Ids({credIdx}));
2830 env.close();
2831
2832 // Bad credential id is rejected
2833 std::string const invalidIdx =
2834 "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E01E034";
2835 env(coverWithdrawToDest(),
2837 credentials::Ids({invalidIdx}),
2839 env.close();
2840
2841 // Malformed credential array (duplicates) is rejected by checkFields
2842 env(coverWithdrawToDest(),
2844 credentials::Ids({credIdx, credIdx}),
2845 Ter{temMALFORMED});
2846 env.close();
2847
2848 // Valid credential not authorized by dest hits authorizedDepositPreauth error path
2849 char const credType2[] = "fghij";
2850 env(credentials::create(broker, credIssuer, credType2));
2851 env(credentials::accept(broker, credIssuer, credType2));
2852 env.close();
2853 auto const credIdx2 =
2854 credentials::ledgerEntry(env, broker, credIssuer, credType2)[jss::result][jss::index]
2855 .asString();
2856 env(coverWithdrawToDest(),
2858 credentials::Ids({credIdx2}),
2860 env.close();
2861
2862 // Advance time past expiration: credentials yield tecEXPIRED and are deleted
2863 env.close(150s);
2864 BEAST_EXPECT(env.le(credKeylet));
2865 env(coverWithdrawToDest(),
2867 credentials::Ids({credIdx}),
2868 Ter{tecEXPIRED});
2869 env.close();
2870 BEAST_EXPECT(!env.le(credKeylet));
2871 }
2872
2873 // Exercises canApplyToBrokerCover (fixCleanup3_2_0): a deposit, withdraw,
2874 // or clawback whose amount rounds to zero at sfCoverAvailable's precision
2875 // scale must be rejected with tecPRECISION_LOSS once the amendment is on,
2876 // and must silently succeed without changing sfCoverAvailable when off.
2877 void
2879 {
2880 using namespace jtx;
2881 using namespace loan_broker;
2882
2883 Account const issuer{"issuer"};
2884 Account const alice{"alice"};
2885
2886 // sfCoverAvailable = 10 IOU → STAmount exponent = -14.
2887 // Anything < 5e-15 rounds to zero at that scale.
2888 // 1e-16 is the representative sub-ULP probe amount.
2889
2890 // Shared setup: funds accounts, creates a vault + broker with 10 IOU
2891 // cover, and returns {brokerKeylet, iou}.
2892 auto const setup = [&](Env& env) -> std::pair<Keylet, PrettyAsset> {
2893 Vault const vault{env};
2894
2895 env.fund(XRP(100'000), issuer, alice);
2896 env.close();
2897 env(fset(issuer, asfAllowTrustLineClawback));
2898 env.close();
2899
2900 PrettyAsset const iou = issuer["IOU"];
2901 env(trust(alice, iou(1'000'000)));
2902 env.close();
2903 env(pay(issuer, alice, iou(1'000)));
2904 env.close();
2905
2906 auto [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = iou});
2907 env(createTx);
2908 env.close();
2909
2910 auto const brokerKeylet =
2911 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2912 env(set(alice, vaultKeylet.key));
2913 env.close();
2914
2915 env(coverDeposit(alice, brokerKeylet.key, iou(10)));
2916 env.close();
2917
2918 return {brokerKeylet, iou};
2919 };
2920
2921 auto runTestCases = [&](FeatureBitset features) {
2922 TER const expected =
2923 features[fixCleanup3_2_0] ? TER{tecPRECISION_LOSS} : TER{tesSUCCESS};
2924
2925 {
2926 testcase("Cover precision guard: Deposit zero-at-scale");
2927 Env env{*this, features};
2928 auto const [brokerKeylet, iou] = setup(env);
2929 PrettyAmount const subUlpAmt = iou(Number{1, -16});
2930 auto const coverBefore = env.le(brokerKeylet)->at(sfCoverAvailable);
2931 env(coverDeposit(alice, brokerKeylet.key, subUlpAmt), Ter(expected));
2932 env.close();
2933 if (expected == tesSUCCESS)
2934 {
2935 if (auto const broker = env.le(brokerKeylet); BEAST_EXPECT(broker))
2936 BEAST_EXPECT(broker->at(sfCoverAvailable) == coverBefore);
2937 }
2938 }
2939
2940 {
2941 testcase("Cover precision guard: Deposit rounds down");
2942 // Both cases succeed; post-fix the amount is rounded DOWN to
2943 // cover scale first, so the delta differs from pre-fix
2944 // Input: 1.8e-14 IOU (sub-scale at cover scale -14)
2945 // Pre-fix: 10 + 1.8e-14 → round-to-nearest →
2946 // 10.00000000000002 → delta 2e-14
2947 // Post-fix: roundToScale(1.8e-14, -14, Downward) = 1e-14;
2948 // 10 + 1e-14 = 10.00000000000001 → delta 1e-14
2949 Env env{*this, features};
2950 auto const [brokerKeylet, iou] = setup(env);
2951 PrettyAmount const subUlpAmt = iou(Number{18, -15});
2952 auto const coverBefore = env.le(brokerKeylet)->at(sfCoverAvailable);
2953 env(coverDeposit(alice, brokerKeylet.key, subUlpAmt), Ter(tesSUCCESS));
2954 env.close();
2955 auto const brokerAfter = env.le(brokerKeylet);
2956 if (!BEAST_EXPECT(brokerAfter))
2957 return;
2958
2959 Number const delta = features[fixCleanup3_2_0] ? Number{1, -14} : Number{2, -14};
2960 BEAST_EXPECT(brokerAfter->at(sfCoverAvailable) - coverBefore == delta);
2961 }
2962
2963 // Property: post-fix, when the user deposits `x` and cover
2964 // gains `x'`, we always have 0 <= x - x' < 1 ULP at cover
2965 // scale (cover holds 10 IOU → ULP = 1e-14). Pre-fix uses
2966 // STAmount's default round-to-nearest during `+=`, which can
2967 // over-deposit (x' > x), so the property only holds with
2968 // fixCleanup3_2_0 enabled.
2969 if (features[fixCleanup3_2_0])
2970 {
2971 testcase("Cover precision guard: Deposit rounding bound");
2972 Env env{*this, features};
2973 auto const [brokerKeylet, iou] = setup(env);
2974 Number const oneUlp{1, -14};
2975 // Each requested amount lies strictly between 1·ULP and
2976 // 2·ULP at cover scale; post-fix `roundDown` credits
2977 // exactly `oneUlp` and leaves a strictly-positive,
2978 // strictly-sub-ULP residual.
2979 for (Number const requested : {Number{11, -15}, Number{15, -15}, Number{19, -15}})
2980 {
2981 auto const broker = env.le(brokerKeylet);
2982 if (!BEAST_EXPECT(broker))
2983 return;
2984 Number const coverBefore = broker->at(sfCoverAvailable);
2985 env(coverDeposit(alice, brokerKeylet.key, iou(requested)), Ter(tesSUCCESS));
2986 env.close();
2987 auto const brokerAfter = env.le(brokerKeylet);
2988 if (!BEAST_EXPECT(brokerAfter))
2989 return;
2990 Number const coverAfter = brokerAfter->at(sfCoverAvailable);
2991 Number const actual = coverAfter - coverBefore;
2992 Number const lost = requested - actual;
2993 BEAST_EXPECT(lost >= Number{0});
2994 BEAST_EXPECT(lost < oneUlp);
2995 }
2996 }
2997
2998 {
2999 testcase("Cover precision guard: Withdraw");
3000 Env env{*this, features};
3001 auto const [brokerKeylet, iou] = setup(env);
3002 PrettyAmount const subUlpAmt = iou(Number{1, -16});
3003 auto const coverBefore = env.le(brokerKeylet)->at(sfCoverAvailable);
3004 auto const aliceBalanceBefore = env.balance(alice, iou);
3005 env(coverWithdraw(alice, brokerKeylet.key, subUlpAmt), Ter(expected));
3006 env.close();
3007 if (expected == tesSUCCESS)
3008 {
3009 if (auto const broker = env.le(brokerKeylet); BEAST_EXPECT(broker))
3010 BEAST_EXPECT(broker->at(sfCoverAvailable) == coverBefore);
3011 BEAST_EXPECT(env.balance(alice, iou) == aliceBalanceBefore);
3012 }
3013 }
3014
3015 {
3016 testcase("Cover precision guard: Clawback");
3017 Env env{*this, features};
3018 auto const [brokerKeylet, iou] = setup(env);
3019 PrettyAmount const subUlpAmt = iou(Number{1, -16});
3020 auto const coverBefore = env.le(brokerKeylet)->at(sfCoverAvailable);
3021 env(coverClawback(issuer),
3022 kLoanBrokerId(brokerKeylet.key),
3023 kAmount(subUlpAmt),
3024 Ter(expected));
3025 env.close();
3026 if (expected == tesSUCCESS)
3027 {
3028 if (auto const broker = env.le(brokerKeylet); BEAST_EXPECT(broker))
3029 BEAST_EXPECT(broker->at(sfCoverAvailable) == coverBefore);
3030 }
3031 }
3032
3033 // MPT amounts are integers; scale is 0; the guard never rejects a
3034 // positive integer amount. Verify all three callsites pass with amendment on.
3035 {
3036 testcase("Cover precision guard: MPT min amount passes");
3037 Env env{*this, all_};
3038
3039 env.fund(XRP(100'000), issuer, alice);
3040 env.close();
3041
3042 MPTTester mptt{env, issuer, kMptInitNoFund};
3043 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
3044 env.close();
3045
3046 PrettyAsset const mptAsset = mptt["MPT"];
3047 mptt.authorize({.account = alice});
3048 env.close();
3049
3050 env(pay(issuer, alice, mptAsset(100)));
3051 env.close();
3052
3053 Vault const vault{env};
3054 auto [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = mptAsset});
3055 env(createTx);
3056 env.close();
3057
3058 auto const brokerKeylet =
3059 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
3060 env(set(alice, vaultKeylet.key));
3061 env.close();
3062
3063 env(coverDeposit(alice, brokerKeylet.key, mptAsset(10)));
3064 env.close();
3065
3066 env(coverDeposit(alice, brokerKeylet.key, mptAsset(1)), Ter(tesSUCCESS));
3067 env.close();
3068
3069 env(coverWithdraw(alice, brokerKeylet.key, mptAsset(1)), Ter(tesSUCCESS));
3070 env.close();
3071
3072 env(coverClawback(issuer),
3073 kLoanBrokerId(brokerKeylet.key),
3074 kAmount(mptAsset(1)),
3075 Ter(tesSUCCESS));
3076 env.close();
3077 }
3078 };
3079
3080 runTestCases(all_);
3081 runTestCases(all_ - fixCleanup3_2_0);
3082 }
3083
3084public:
3085 void
3086 run() override
3087 {
3094
3096
3099
3100 testDisabled();
3101 testLifecycle();
3105
3106 testRIPD4323();
3108
3109 testRIPD4274();
3110
3113
3115 testLoanBrokerDeleteLockedMPT(all_ - fixCleanup3_2_0);
3116
3118 testLoanBrokerDeleteFrozenIOU(all_ - fixCleanup3_2_0);
3119
3121 testLoanBrokerDeleteConfidentialCOA(all_ - fixCleanup3_5_0);
3122
3123 // featureMPTokensV2 independently makes ValidMPTTransfer enforcing,
3124 // but it's Supported::No (never enabled on real networks); exclude
3125 // it here so fixCleanup3_4_0 alone is the deciding amendment, as it
3126 // would be on mainnet.
3127 testLoanBrokerDeleteRequireAuthMPT(all_ - featureMPTokensV2);
3128 testLoanBrokerDeleteRequireAuthMPT(all_ - featureMPTokensV2 - fixCleanup3_4_0);
3129 // TODO: Write clawback failure tests with an issuer / MPT that doesn't
3130 // have the right flags set.
3131 }
3132};
3133
3135
3136} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
A testsuite class.
Definition suite.h:52
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:554
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
static constexpr Int kMaxInt
Definition json_value.h:130
std::string asString() const
Returns the unquoted string value.
State information when applying a tx.
ApplyView & view()
virtual SLE::pointer peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
virtual void erase(SLE::Ref sle)=0
Remove a peeked SLE.
A currency issued by an account.
Definition Issue.h:18
static TER preclaim(PreclaimContext const &ctx)
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static MantissaRange::MantissaScale getMantissaScale()
Returns which mantissa scale is currently in use for normalization.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
std::shared_ptr< STLedgerEntry const > const & ConstRef
void setFieldAmount(SField const &field, STAmount const &)
Definition STObject.cpp:803
void setAccountID(SField const &field, AccountID const &)
Definition STObject.cpp:785
void setFieldH256(SField const &field, UInt256 const &)
Definition STObject.cpp:767
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
Slice slice() const noexcept
Definition Serializer.h:141
void testLoanBrokerDeleteRequireAuthMPT(FeatureBitset features)
void testLoanBrokerDeleteFrozenIOU(FeatureBitset features)
void run() override
Runs the suite.
void testLoanBroker(std::function< jtx::PrettyAsset(jtx::Env &, jtx::Account const &, jtx::Account const &)> getAsset, LoanBrokerTest brokerTest)
void lifecycle(char const *label, jtx::Env &env, jtx::Account const &issuer, jtx::Account const &alice, jtx::Account const &evan, jtx::Account const &bystander, VaultInfo const &vault, VaultInfo const &badVault, std::function< jtx::JTx(jtx::JTx const &)> modifyJTx, std::function< void(SLE::ConstRef)> checkBroker, std::function< void(SLE::ConstRef)> changeBroker, std::function< void(SLE::ConstRef)> checkChangedBroker)
void testLoanBrokerDeleteConfidentialCOA(FeatureBitset features)
void testCoverWithdrawCredentialDepositPreauth(FeatureBitset features)
void testLoanBrokerDeleteLockedMPT(FeatureBitset features)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
std::string const & human() const
Returns the human readable public key.
AccountID id() const
Returns the Account ID.
A balance matches.
Definition balance.h:24
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
TER ter() const
Return the TER for the last JTx.
Definition Env.h:844
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:752
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
std::uint32_t ownerCount(Account const &account) const
Return the number of objects owned by an account.
Definition Env.cpp:266
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:1058
void setParseFailureExpected(bool b)
Definition Env.h:553
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:723
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void disableFeature(UInt256 const feature)
Definition Env.cpp:717
void enableFeature(UInt256 const feature)
Definition Env.cpp:709
std::shared_ptr< STTx const > ust(JTx const &jt)
Create a STTx from a JTx without sanitizing Use to inject bogus values into test transactions by firs...
Definition Env.cpp:664
beast::Journal const journal
Definition Env.h:204
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:766
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
Set the fee on a JTx.
Definition fee.h:20
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:512
static constexpr auto holderEncryptedInbox
Definition mpt.h:533
std::optional< Buffer > getEncryptedBalance(Account const &account, EncryptedBalanceType option=holderEncryptedInbox) const
Definition mpt.cpp:1136
std::uint32_t generateKeyPair(Account const &account)
Definition mpt.cpp:1877
static constexpr auto issuerEncryptedBalance
Definition mpt.h:532
MPTID const & issuanceID() const
Definition mpt.h:768
static constexpr auto holderEncryptedSpending
Definition mpt.h:534
std::optional< Buffer > getPubKey(Account const &account, std::optional< std::uint32_t > epoch=std::nullopt) const
Definition mpt.cpp:1902
static constexpr auto auditorEncryptedBalance
Definition mpt.h:535
void create(MPTCreate const &arg=MPTCreate{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:343
void set(MPTSet const &set={}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:577
std::int64_t getIssuanceConfidentialBalance() const
Definition mpt.cpp:939
void authorize(MPTAuthorize const &arg=MPTAuthorize{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:455
void convert(MPTConvert const &arg=MPTConvert{}, std::source_location const &loc=std::source_location::current())
Definition mpt.cpp:1266
Converts to MPT Issue or STAmount.
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Set the flags on a JTx.
Definition txflags.h:14
T emplace_back(T... args)
T make_tuple(T... args)
T make_unique(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet computation functions.
Definition Indexes.h:40
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
constexpr TenthBips32 kMaxCoverRate
The maximum coverage rate required of a loan broker in 1/10 bips.
Definition Protocol.h:159
constexpr TenthBips16 kMaxManagementFeeRate(unsafeCast< std::uint16_t >(percentageToTenthBips(10).value()))
The maximum management fee rate allowed by a loan broker in 1/10 bips.
json::Value accept(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:29
json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:16
json::Value ledgerEntry(jtx::Env &env, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:56
Keylet keylet(test::jtx::Account const &subject, test::jtx::Account const &issuer, std::string_view credType)
Definition credentials.h:21
Deposit preauthorize operations.
Definition deposit.h:16
json::Value authCredentials(jtx::Account const &account, std::vector< AuthorizeCredentials > const &auth)
Definition deposit.cpp:38
json::Value set(AccountID const &account, UInt256 const &vaultId, uint32_t flags)
json::Value coverWithdraw(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value coverDeposit(AccountID const &account, UInt256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value set(AccountID const &account, UInt256 const &loanBrokerID, Number principalRequested, std::uint32_t flags)
json::Value del(AccountID const &account, UInt256 const &loanID, std::uint32_t flags)
json::Value pay(AccountID const &account, UInt256 const &loanID, STAmount const &amount, std::uint32_t flags)
static NoneT const kNone
Definition tags.h:9
auto const kMptDexFlags
Definition mpt.h:47
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition flags.h:110
FeatureBitset testableAmendments()
Definition Env.h:92
auto const kData
General field definitions, or fields used in multiple transaction namespaces.
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:86
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
bool checkVL(Slice const &result, std::string const &expected)
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
static MPTInit const kMptInitNoFund
Definition mpt.h:201
static STAmount accountReserve(jtx::Env &env, std::uint32_t count=1)
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
STTx createTx(bool disabling, LedgerIndex seq, PublicKey const &txKey)
Create ttUNL_MODIFY Tx.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_RIPPLE
Definition TER.h:225
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:13
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
static bool authorized(Port const &port, std::map< std::string, std::string > const &h)
Number power(Number const &f, unsigned n)
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
constexpr FlagValue tfUniversal
Definition TxFlags.h:45
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
constexpr std::size_t kMaxDataPayloadLength
The maximum length of Data payload.
Definition Protocol.h:303
BaseUInt< 256 > UInt256
Definition base_uint.h:580
Asset getAsset(T const &amt)
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:735
@ TapNone
Definition ApplyView.h:28
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
@ temINVALID
Definition TER.h:98
@ temINVALID_FLAG
Definition TER.h:99
@ temMALFORMED
Definition TER.h:75
@ temDISABLED
Definition TER.h:102
@ temBAD_AMOUNT
Definition TER.h:77
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecWRONG_ASSET
Definition TER.h:368
@ tecPSEUDO_ACCOUNT
Definition TER.h:370
@ tecLOCKED
Definition TER.h:366
@ tecNO_ENTRY
Definition TER.h:314
@ tecOBJECT_NOT_FOUND
Definition TER.h:334
@ tecNO_AUTH
Definition TER.h:308
@ tecBAD_CREDENTIALS
Definition TER.h:367
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tecFROZEN
Definition TER.h:311
@ tecINSUFFICIENT_FUNDS
Definition TER.h:333
@ tecEXPIRED
Definition TER.h:322
@ tecNO_LINE
Definition TER.h:309
@ tecPRECISION_LOSS
Definition TER.h:371
@ tecINSUFFICIENT_RESERVE
Definition TER.h:315
@ tecLIMIT_EXCEEDED
Definition TER.h:369
@ tecNO_PERMISSION
Definition TER.h:313
@ tecDST_TAG_NEEDED
Definition TER.h:317
@ tecHAS_OBLIGATIONS
Definition TER.h:325
@ tecNO_DST
Definition TER.h:298
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:297
@ tesSUCCESS
Definition TER.h:250
constexpr FlagValue tfFullyCanonicalSig
Definition TxFlags.h:43
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:92
VaultInfo(jtx::PrettyAsset const &asset, UInt256 const &vaultId, AccountID const &pseudo)
Set the destination tag on a JTx.
Definition tag.h:14
Execution context for applying a JSON transaction.
Definition JTx.h:27
Represents an XRP, IOU, or MPT quantity This customizes the string conversion and supports XRP conver...
Set the sequence number on a JTx.
Definition seq.h:16
T what(T... args)