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