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