xrpld
Loading...
Searching...
No Matches
VaultBugs_test.cpp
1#include <test/app/vault/VaultTestBase.h>
2#include <test/jtx/Account.h>
3#include <test/jtx/CaptureLogs.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/credentials.h>
8#include <test/jtx/fee.h>
9#include <test/jtx/flags.h>
10#include <test/jtx/mpt.h>
11#include <test/jtx/pay.h>
12#include <test/jtx/permissioned_domains.h>
13#include <test/jtx/sig.h>
14#include <test/jtx/sponsor.h>
15#include <test/jtx/ter.h>
16#include <test/jtx/trust.h>
17#include <test/jtx/vault.h>
18
19#include <xrpl/basics/Number.h>
20#include <xrpl/basics/base_uint.h>
21#include <xrpl/basics/chrono.h>
22#include <xrpl/beast/unit_test/suite.h>
23#include <xrpl/json/json_forwards.h>
24#include <xrpl/json/json_value.h>
25#include <xrpl/ledger/helpers/VaultHelpers.h>
26#include <xrpl/protocol/Asset.h>
27#include <xrpl/protocol/Feature.h>
28#include <xrpl/protocol/Indexes.h>
29#include <xrpl/protocol/Issue.h>
30#include <xrpl/protocol/Keylet.h>
31#include <xrpl/protocol/MPTIssue.h>
32#include <xrpl/protocol/Protocol.h>
33#include <xrpl/protocol/SField.h>
34#include <xrpl/protocol/STAmount.h>
35#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
36#include <xrpl/protocol/SeqProxy.h>
37#include <xrpl/protocol/TER.h>
38#include <xrpl/protocol/TxFlags.h>
39#include <xrpl/protocol/UintTypes.h>
40
41#include <chrono>
42#include <cstdint>
43#include <memory>
44#include <optional>
45#include <string>
46#include <tuple>
47#include <utility>
48
49namespace xrpl {
50
52{
53private:
54 // Bug: the equality check (vault outflow == destination inflow) was
55 // skipped whenever the destination delta rounded to zero at localMinScale,
56 // including cases where the vault outflow rounded to a non-zero value and
57 // a representable amount of value was genuinely destroyed.
58 //
59 // Scenario: Bob's IOU balance sits 5 units below the 10^16 STAmount
60 // precision boundary (atEdge2 = 9,999,999,999,999,995). A withdrawal of
61 // 6 USD shifts his balance across that boundary: the exponent increments
62 // (0 → 1), so his effective inflow in Number space is only +5 — 1 USD is
63 // consumed by the precision-boundary rounding and cannot be credited.
64 //
65 // The destroyed amount (1 USD) is sub-ULP at destinationScale=1 (step=10),
66 // so the check treats it as an unavoidable IOU-precision artefact and
67 // lets the transaction succeed.
68 //
69 // Contrast: if 15 USD were destroyed at the same scale (destroyed ≥ step),
70 // floor(15/10)=1 ≠ 0 and the invariant would fire — that discrepancy IS
71 // representable and indicates a real accounting bug.
72 //
73 // Pre-fixCleanup3_2_0: the "must increase destination balance" check fires
74 // because roundedDestinationDelta = 0 ≤ 0.
75 void
77 {
78 using namespace test::jtx;
79
80 auto runScenario = [this](FeatureBitset features, TER expected) {
81 std::string logs;
82 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
83
84 Account const issuer{"issuer"};
85 Account const alice{"alice"};
86 Account const bob{"bob"};
87
88 env.fund(XRP(100'000), issuer, alice, bob);
89 env.close();
90 env(fset(issuer, asfDefaultRipple));
91 env.close();
92
93 PrettyAsset const usd{issuer["USD"]};
94 STAmount const aliceLimit{usd.raw(), 2, 16};
95 STAmount const bobLimit{usd.raw(), 2, 16};
96 // Bob's balance sits 5 units below the 10^16 STAmount precision
97 // boundary. Receiving 6 USD shifts his exponent 0 → 1; the
98 // STAmount records +5, not +6 (1 USD is lost to rounding).
99 STAmount const atEdge2{usd.raw(), Number{9'999'999'999'999'995LL}};
100
101 env(trust(alice, aliceLimit));
102 env(trust(bob, bobLimit));
103 env.close();
104
105 env(pay(issuer, alice, usd(1'000)));
106 env(pay(issuer, bob, atEdge2));
107 env.close();
108
109 Vault const vault{env};
110 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
111 vaultTx[sfScale] = 0;
112 env(vaultTx);
113 env.close();
114
115 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
116 env.close();
117
118 // Withdraw 6 USD to Bob: vault loses 6, Bob gains only 5.
119 // Destroyed amount = 1 USD, which is sub-ULP at destinationScale=1.
120 auto tx = vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(6)});
121 tx[sfDestination] = bob.human();
122 env(tx, Ter(expected));
123 env.close();
124 };
125
126 {
127 testcase(
128 "bug: VaultWithdraw to destination at IOU precision boundary fires "
129 "invariant (pre-fixCleanup3_2_0)");
130 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
131 }
132 {
133 testcase(
134 "bug: VaultWithdraw to destination at IOU precision boundary succeeds "
135 "when destroyed amount is sub-ULP (post-fixCleanup3_2_0)");
136 runScenario(testableAmendments(), tesSUCCESS);
137 }
138 }
139
140 // VaultDeposit by issuer with the vault parked at the IOU 16-digit
141 // edge (9.999e15). Issuer mints 2 more USD; the vault trust line
142 // goes 9.999e15 → 10^16, gaining 1 unit instead of 2 (canonicalization).
143 //
144 // Pre-fixCleanup3_2_0: the proactive check is absent; the deposit
145 // applies, then VaultInvariant's "deposit must increase vault
146 // balance" assertion fires at finalize time on the rounded vault
147 // delta of zero, returning tecINVARIANT_FAILED.
148 // Post-amendment: reject deposit that is not representable at Vault scale.
149 void
151 {
152 using namespace test::jtx;
153
154 auto runScenario = [this](FeatureBitset features, TER expected) {
155 std::string logs;
156 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
157
158 Account const issuer{"issuer"};
159 Account const owner{"owner"};
160
161 env.fund(XRP(100'000), issuer, owner);
162 env.close();
163 env(fset(issuer, asfDefaultRipple));
164 env.close();
165
166 PrettyAsset const usd{issuer["USD"]};
167 STAmount const trustLimit{usd.raw(), 2, 16};
168 STAmount const ownerFund{usd.raw(), Number{9'999'999'999'999'999LL}};
169
170 env(trust(owner, trustLimit));
171 env.close();
172 env(pay(issuer, owner, ownerFund));
173 env.close();
174
175 Vault const vault{env};
176 auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = usd});
177 vaultTx[sfScale] = 0;
178 env(vaultTx);
179 env.close();
180 env(vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = ownerFund}));
181 env.close();
182
183 // Vault pseudo-account is now at 9.999e15. Issuer mints 2
184 // more USD. Pre: tecINVARIANT_FAILED at finalize. Post:
185 // tecPRECISION_LOSS proactively. Either way, no value moves.
186 env(vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = usd(2)}),
187 Ter(expected));
188 env.close();
189 };
190
191 {
192 testcase(
193 "bug: VaultDeposit by issuer at IOU edge fires "
194 "tecINVARIANT_FAILED at finalize (pre-fixCleanup3_2_0)");
195 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
196 }
197 {
198 testcase(
199 "bug: VaultDeposit by issuer at IOU edge rejects with "
200 "tecPRECISION_LOSS proactively (post-fixCleanup3_2_0)");
201 runScenario(testableAmendments(), tecPRECISION_LOSS);
202 }
203 }
204
205 // Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for
206 // sfAssetsTotal/Available deltas. This is symmetric to
207 // testBugMakeDeltaAnteriorScale but in the opposite direction: a deposit
208 // pushes assetsTotal from just below 1e16 (IOU exponent 0, ULP = 1) to just
209 // above it (exponent 1, ULP = 10). makeDelta picks the coarser *posterior*
210 // scale 1. The trust line balance rounds from atEdge + 2 = 10,000,000,000,000,001
211 // → 1e16, so the pseudo-account delta is only +1 in IOU space.
212 // roundToAsset(+1, scale=1) = 0 fires "deposit must increase vault balance"
213 // even though the state change is consistent at every precision boundary.
214 //
215 // Fix (fixCleanup3_2_0): computeVaultMinScale uses the posterior Number-space
216 // scale of sfAssetsTotal (which retains the full value 10,000,000,000,000,001,
217 // exponent 0), giving minScale = 0. roundToAsset(+1, scale=0) = 1 > 0 and
218 // the invariant passes. However the transactor's own precision guard fires
219 // first (bob pays 2 USD, vault receives only 1 due to IOU rounding), so the
220 // post-amendment result is tecPRECISION_LOSS rather than tesSUCCESS —
221 // the depositor is protected from silently losing 1 USD to rounding.
222 void
224 {
225 using namespace test::jtx;
226
227 auto runScenario = [this](FeatureBitset features, TER expected) {
228 std::string logs;
229 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
230
231 Account const issuer{"issuer"};
232 Account const alice{"alice"};
233 Account const bob{"bob"};
234
235 env.fund(XRP(100'000), issuer, alice, bob);
236 env.close();
237 env(fset(issuer, asfDefaultRipple));
238 env.close();
239
240 PrettyAsset const usd{issuer["USD"]};
241 // atEdge is the largest IOU value with exponent 0 (ULP = 1).
242 // A deposit of 2 USD brings assetsTotal to 10,000,000,000,000,001
243 // in Number space, crossing the 1e16 boundary in IOU space.
244 STAmount const atEdge{usd.raw(), Number{9'999'999'999'999'999LL}};
245
246 env(trust(alice, STAmount{usd.raw(), 2, 16}));
247 env(trust(bob, usd(100)));
248 env.close();
249 env(pay(issuer, alice, atEdge));
250 env(pay(issuer, bob, usd(2)));
251 env.close();
252
253 Vault const vault{env};
254 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
255 vaultTx[sfScale] = 0;
256 env(vaultTx);
257 env.close();
258
259 // sfAssetsTotal = sfAssetsAvailable = atEdge (exponent 0, ULP = 1)
260 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = atEdge}));
261 env.close();
262
263 // Deposit 2 USD: +2 is sub-ULP at the posterior IOU scale (ULP = 10)
264 // but exact at the Number scale retained by sfAssetsTotal.
265 env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(2)}),
266 Ter(expected));
267 env.close();
268 };
269
270 {
271 testcase(
272 "bug: VaultDeposit across IOU scale boundary fires invariant "
273 "(pre-fixCleanup3_2_0)");
274 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
275 }
276 {
277 testcase(
278 "bug: VaultDeposit across IOU scale boundary succeeds "
279 "(post-fixCleanup3_2_0)");
280 runScenario(testableAmendments(), tecPRECISION_LOSS);
281 }
282 }
283
284 // Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for the
285 // sfAssetsTotal and sfAssetsAvailable deltas, and visitEntry applies the
286 // same max() for the vault pseudo-account RippleState. When
287 // sfAssetsTotal sits exactly at 1e16 (IOU exponent 1, ULP = 10) and a
288 // withdrawal of 5 USD brings it to 9.999...995e15 (IOU exponent 0,
289 // ULP = 1), all three computations pick the anterior coarser scale 1.
290 // roundToAsset(-5, scale=1) collapses to 0, so the invariant check
291 // vaultPseudoDeltaAssets >= kZero fires even though the state change is
292 // valid and fully consistent at IOU precision.
293 //
294 // Fix (fixCleanup3_2_0): finalize compares the vault pseudo-account and
295 // sfAssetsTotal/Available deltas directly in Number space, bypassing
296 // scale-coarsened rounding.
297 void
299 {
300 using namespace test::jtx;
301
302 auto runScenario = [this](FeatureBitset features, TER expected) {
303 std::string logs;
304 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
305
306 Account const issuer{"issuer"};
307 Account const alice{"alice"};
308
309 env.fund(XRP(100'000), issuer, alice);
310 env.close();
311 env(fset(issuer, asfDefaultRipple));
312 env.close();
313
314 PrettyAsset const usd{issuer["USD"]};
315 // Trust limit of 2e16, fund exactly 1e16 so deposit lands at the
316 // IOU scale-1 boundary (exponent 1, ULP = 10).
317 STAmount const fundAndDeposit{usd.raw(), Number{1, 16}};
318
319 env(trust(alice, STAmount{usd.raw(), 2, 16}));
320 env.close();
321 env(pay(issuer, alice, fundAndDeposit));
322 env.close();
323
324 Vault const vault{env};
325 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
326 vaultTx[sfScale] = 0;
327 env(vaultTx);
328 env.close();
329
330 // sfAssetsTotal = sfAssetsAvailable = 1e16 (exponent 1, ULP = 10).
331 env(vault.deposit(
332 {.depositor = alice, .id = vaultKeylet.key, .amount = fundAndDeposit}));
333 env.close();
334
335 // Withdraw 5 USD: -5 is sub-ULP at the anterior scale (ULP = 10)
336 // but exact at the posterior scale (ULP = 1). The state change is
337 // consistent; only the invariant's scale selection is wrong.
338 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(5)}),
339 Ter(expected));
340 env.close();
341 };
342
343 {
344 testcase(
345 "bug: VaultWithdraw across IOU scale boundary fires invariant "
346 "(pre-fixCleanup3_2_0)");
347 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
348 }
349 {
350 testcase(
351 "bug: VaultWithdraw across IOU scale boundary succeeds "
352 "(post-fixCleanup3_2_0)");
353 runScenario(testableAmendments(), tesSUCCESS);
354 }
355 }
356
357 // Bug: when a depositor's IOU trustline balance is very large (e.g.
358 // ~1e17), adding a small deposit (e.g. 1 USD) leaves sfAssetsTotal
359 // unchanged at IOU precision because the increment is sub-ULP at the
360 // vault's current asset scale. The vault records the deposit, mints
361 // shares, and decrements the depositor's trustline, but sfAssetsTotal
362 // does not change — the conservation invariant fires because the rail
363 // delta is zero.
364 //
365 // Two sub-cases are exercised:
366 // 1. First-ever deposit into an empty vault: the depositor's own
367 // trustline has a large balance so 1 USD canonicalizes to zero
368 // when written back through the IOU rail.
369 // 2. Subsequent deposit after the vault already holds a large
370 // sfAssetsTotal: a different depositor (bob, with a small balance)
371 // sends 1 USD, which again rounds to zero at the vault's coarse
372 // asset scale.
373 //
374 // Fix (fixCleanup3_2_0): the deposit transactor checks whether
375 // roundToAsset(amount, vault_scale) == 0 and rejects early with
376 // tecPRECISION_LOSS before any state is modified.
377 void
379 {
380 using namespace test::jtx;
381 auto runScenario = [this](FeatureBitset features, TER expected) {
382 std::string logs;
383 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
384
385 Account const issuer{"issuer"};
386 Account const alice{"alice"};
387 Account const bob{"bob"};
388
389 env.fund(XRP(100'000), issuer, alice, bob);
390 env.close();
391
392 env(fset(issuer, asfDefaultRipple));
393 env.close();
394
395 PrettyAsset const usd{issuer["USD"]};
396
397 STAmount const trustLimit{usd.raw(), Number{99'999'999'999'999'999LL}};
398 STAmount const aliceFund{usd.raw(), Number{99'999'999'999'999'999LL}};
399
400 env(trust(alice, trustLimit));
401 env(trust(bob, trustLimit));
402 env.close();
403
404 env(pay(issuer, alice, aliceFund));
405 env(pay(issuer, bob, usd(1000)));
406 env.close();
407
408 Vault const vault{env};
409
410 // Scale=0 so sfAssetsTotal stores whole USD
411 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
412 vaultTx[sfScale] = 0;
413 env(vaultTx);
414 env.close();
415
416 // Alice's deposit canonicalizes to zero at her own trustline scale
417 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1)}),
418 Ter(expected));
419
420 // Increase vault-scale
421 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = aliceFund}));
422 env.close();
423
424 env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(1)}),
425 Ter(expected));
426 env.close();
427 };
428
429 {
430 // fixCleanup3_4_0 has to be off as well: its depositor-side check
431 // rejects alice's deposit for the same reason, so the invariant is
432 // only reachable with neither guard in place.
433 testcase(
434 "bug: VaultDeposit below Vault precision canonicalized to zero "
435 "(pre-fixCleanup3_2_0)");
436 // Also remove fixCleanup3_4_0 so the VaultDeposit clamp
437 // introduced by that amendment does not short-circuit this
438 // pre-fixCleanup3_2_0 scenario with tecPRECISION_LOSS.
439 runScenario(
440 testableAmendments() - fixCleanup3_2_0 - fixCleanup3_4_0, tecINVARIANT_FAILED);
441 }
442 {
443 testcase(
444 "bug: VaultDeposit below Vault precision canonicalized to zero "
445 "(post-fixCleanup3_2_0)");
446 runScenario(testableAmendments(), tecPRECISION_LOSS);
447 }
448 }
449
450 // A deposit does not transfer the requested amount. It transfers the
451 // request truncated to a whole number of shares and converted back, which
452 // can be strictly smaller. When that smaller value is below half a ULP at
453 // the depositor's own trust-line scale, the debit rounds away to nothing:
454 // the depositor pays nothing, while the vault books the assets and mints
455 // shares. ValidVault catches the desync at finalize time.
456 //
457 // Only a non-power-of-ten assets-to-shares ratio is needed, and that
458 // happens through ordinary use: LoanPay books accrued interest into
459 // sfAssetsTotal without minting shares.
460 //
461 // The fixCleanup3_2_0 guard in preclaim does not help, because it tests the
462 // raw requested amount, which is large enough to survive the rounding.
463 // Post-fixCleanup3_4_0 the post-truncation value is checked as well and the
464 // deposit is rejected with tecPRECISION_LOSS before anything moves.
465 void
467 {
468 using namespace test::jtx;
469 using namespace loan_broker;
470 using namespace loan;
471
472 // How bob's trust line is set up before he deposits. Holding is the plain case: a large
473 // positive balance whose ULP swallows the debit. InDebt is the case where the stored
474 // balance and the spendable amount diverge: bob owes the issuer 1e16, and the issuer's
475 // limit on the same line lets him spend 1000 anyway. Reading the spendable amount there
476 // reports a small, finely scaled number, while the rounding of the debit is still governed
477 // by the 1e16 he actually holds.
478 enum class Line { Holding, InDebt };
479
480 auto runScenario = [this](FeatureBitset features, Line line, TER expected) {
481 std::string logs;
482 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
483
484 Account const issuer{"issuer"};
485 Account const alice{"alice"};
486 Account const carol{"carol"};
487 Account const bob{"bob"};
488
489 env.fund(XRP(100'000), issuer, alice, carol, bob);
490 env.close();
491 env(fset(issuer, asfDefaultRipple));
492 env.close();
493
494 PrettyAsset const usd{issuer["USD"]};
495 PrettyAsset const bobUsd{bob["USD"]};
496 STAmount const trustLimit{usd.raw(), Number{99'999'999'999'999'999LL}};
497 // Bob's balance sits exactly on a multiple-of-10 boundary at the
498 // 1e16 IOU precision cusp, where one ULP is 10.
499 STAmount const bobEdge{usd.raw(), Number{10'000'000'000'000'010LL}};
500 STAmount const bobDebt{bobUsd.raw(), Number{10'000'000'000'000'000LL}};
501 STAmount const oppositeLimit{bobUsd.raw(), Number{10'000'000'000'001'000LL}};
502
503 env(trust(alice, trustLimit));
504 env(trust(carol, trustLimit));
505 env(trust(bob, trustLimit));
506 env.close();
507
508 env(pay(issuer, alice, usd(1'000)));
509 env(pay(issuer, carol, usd(1'000)));
510 if (line == Line::Holding)
511 {
512 env(pay(issuer, bob, bobEdge));
513 }
514 else
515 {
516 // The issuer trusts bob's own USD, so bob can issue 1e16 back and still have
517 // 1000 of spendable room left on the same line.
518 env(trust(issuer, oppositeLimit));
519 env.close();
520 env(pay(bob, issuer, bobDebt));
521 }
522 env.close();
523
524 Vault const vault{env};
525 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
526 vaultTx[sfScale] = 0;
527 env(vaultTx);
528 env.close();
529
530 // Alice deposits 1000 USD, minting 1000 shares 1:1.
531 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
532 env.close();
533
534 // A loan broker on the vault, then a bullet loan at 24% interest:
535 // a single payment, one year out.
536 auto const brokerKeylet =
537 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
538 env(set(alice, vaultKeylet.key));
539 env.close();
540
541 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
542 env(set(carol, brokerKeylet.key, usd(1'000).value()),
543 loan::kInterestRate(percentageToTenthBips(24)),
544 kGracePeriod(60),
545 kPaymentInterval(365 * 24 * 60 * 60),
546 kPaymentTotal(1),
547 Sig(sfCounterpartySignature, alice),
548 Fee(env.current()->fees().base * 2),
549 Ter(tesSUCCESS));
550 env.close();
551
552 // Advance to just before the single payment falls due and let carol
553 // repay principal plus interest. LoanPay is what books the accrued
554 // interest into sfAssetsTotal; under cash-basis accounting LoanSet
555 // alone does not. Share supply stays at 1000, so
556 // assetsTotal/sharesTotal becomes 1240/1000.
557 env.close(std::chrono::seconds{(365 * 24 * 60 * 60) - 3600});
558 env(pay(carol, loanKeylet.key, usd(2'000).value()), Ter(tesSUCCESS));
559 env.close();
560
561 // Pin the ratio the rest of the scenario reasons about, so the test cannot quietly
562 // stop exercising the bug if the setup drifts.
563 auto const sleVault = env.le(vaultKeylet);
564 BEAST_EXPECT(sleVault && sleVault->at(sfAssetsTotal) == Number{1'240});
565 auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
566 BEAST_EXPECT(sleIssuance && sleIssuance->at(sfOutstandingAmount) == 1'000);
567
568 // Bob deposits 6 USD, which rounds to 10 at his own trust-line
569 // scale and so clears the fixCleanup3_2_0 guard. But
570 // floor(1000 * 6 / 1240) is 4 shares, worth 4 * 1240 / 1000 = 4.96,
571 // and that is below half a ULP of his balance, so it rounds away to
572 // nothing when subtracted.
573 env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(6)}),
574 Ter(expected));
575 env.close();
576 };
577
578 // Strip featureLendingProtocolV1_1: this scenario runs an
579 // open-ended vault through deposit/broker/loan/repay/deposit,
580 // which spans both Subscription and post-loan lifetime — a phase
581 // pattern that only makes sense on open-ended vaults. The gate
582 // added by LP V1.1 is unrelated to the truncation bug asserted
583 // here.
584 auto const legacy = testableAmendments() - featureLendingProtocolV1_1;
585 {
586 testcase(
587 "bug: VaultDeposit share truncation lets depositor debit "
588 "round away to zero (pre-fixCleanup3_4_0)");
589 runScenario(legacy - fixCleanup3_4_0, Line::Holding, tecINVARIANT_FAILED);
590 }
591 {
592 testcase(
593 "bug: VaultDeposit share truncation lets depositor debit "
594 "round away to zero (pre-fixCleanup3_2_0 and pre-fixCleanup3_4_0)");
595 runScenario(
596 legacy - fixCleanup3_2_0 - fixCleanup3_4_0, Line::Holding, tecINVARIANT_FAILED);
597 }
598 {
599 testcase(
600 "bug: VaultDeposit share truncation rejected with "
601 "tecPRECISION_LOSS (post-fixCleanup3_4_0)");
602 runScenario(legacy, Line::Holding, tecPRECISION_LOSS);
603 }
604 {
605 testcase(
606 "bug: VaultDeposit share truncation rejected with "
607 "tecPRECISION_LOSS (post-fixCleanup3_4_0, pre-fixCleanup3_2_0)");
608 runScenario(legacy - fixCleanup3_2_0, Line::Holding, tecPRECISION_LOSS);
609 }
610 {
611 testcase(
612 "bug: VaultDeposit share truncation against a debt balance "
613 "round away to zero (pre-fixCleanup3_4_0)");
614 runScenario(legacy - fixCleanup3_4_0, Line::InDebt, tecINVARIANT_FAILED);
615 }
616 {
617 testcase(
618 "bug: VaultDeposit share truncation against a debt balance rejected with "
619 "tecPRECISION_LOSS (post-fixCleanup3_4_0)");
620 runScenario(legacy, Line::InDebt, tecPRECISION_LOSS);
621 }
622 }
623
624 // Bug: ValidVault::visitEntry computes destinationDelta.scale as
625 // max(before_exponent, after_exponent) for RippleState entries. When a
626 // withdrawal credits a destination whose IOU balance sits just below a
627 // power-of-10 boundary (atEdge = 9'999'999'999'999'999), the post-credit
628 // STAmount rounds up one exponent (exponent 0 → 1), making
629 // destinationDelta.scale = 1. The invariant then calls
630 // roundToAsset(+2 USD, scale=1) = 0 and incorrectly fires
631 // "withdrawal must increase destination balance".
632 //
633 // Fix (fixCleanup3_2_0): finalize compares destination delta directly in
634 // Number space, bypassing scale-coarsened rounding. The transaction
635 // itself succeeds because the effective IOU credit is non-trivial at
636 // Number precision even though the STAmount exponent shifted.
637 void
639 {
640 using namespace test::jtx;
641
642 enum class DestKind : bool { ThirdParty = false, Self = true };
643
644 auto runScenario = [this](FeatureBitset features, DestKind destKind, TER expected) {
645 std::string logs;
646 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
647
648 Account const issuer{"issuer"};
649 Account const alice{"alice"};
650 Account const bob{"bob"};
651
652 env.fund(XRP(100'000), issuer, alice, bob);
653 env.close();
654 env(fset(issuer, asfDefaultRipple));
655 env.close();
656
657 PrettyAsset const usd{issuer["USD"]};
658 STAmount const aliceLimit{usd.raw(), 2, 16};
659 STAmount const bobLimit{usd.raw(), 2, 16};
660 STAmount const atEdge{usd.raw(), Number{9'999'999'999'999'999LL}};
661
662 env(trust(alice, aliceLimit));
663 if (destKind == DestKind::ThirdParty)
664 env(trust(bob, bobLimit));
665 env.close();
666
667 env(pay(issuer, alice, usd(1'000)));
668 if (destKind == DestKind::ThirdParty)
669 env(pay(issuer, bob, atEdge));
670 env.close();
671
672 Vault const vault{env};
673 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
674 vaultTx[sfScale] = 0;
675 env(vaultTx);
676 env.close();
677
678 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
679 env.close();
680
681 // For the self-destination case, push alice's own trust line to
682 // the IOU edge so the next withdraw inflow crosses the boundary.
683 if (destKind == DestKind::Self)
684 {
685 env(pay(issuer, alice, atEdge));
686 env.close();
687 }
688
689 auto tx = vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(2)});
690 if (destKind == DestKind::ThirdParty)
691 tx[sfDestination] = bob.human();
692 env(tx, Ter(expected));
693 env.close();
694 };
695
696 {
697 testcase(
698 "bug: VaultWithdraw to third-party at IOU edge fires invariant "
699 "(pre-fixCleanup3_2_0)");
700 runScenario(
701 testableAmendments() - fixCleanup3_2_0, DestKind::ThirdParty, tecINVARIANT_FAILED);
702 }
703 {
704 testcase(
705 "bug: VaultWithdraw to third-party at IOU edge succeeds "
706 "(post-fixCleanup3_2_0)");
707 runScenario(testableAmendments(), DestKind::ThirdParty, tesSUCCESS);
708 }
709 {
710 testcase(
711 "bug: VaultWithdraw to self at IOU edge fires invariant "
712 "(pre-fixCleanup3_2_0)");
713 runScenario(
714 testableAmendments() - fixCleanup3_2_0, DestKind::Self, tecINVARIANT_FAILED);
715 }
716 {
717 testcase(
718 "bug: VaultWithdraw to self at IOU edge succeeds "
719 "(post-fixCleanup3_2_0)");
720 runScenario(testableAmendments(), DestKind::Self, tesSUCCESS);
721 }
722 }
723
724 // Bug: a debit can be genuinely non-zero yet still be dust relative to a
725 // sfAssetsTotal/sfAssetsAvailable large enough to exceed STAmount's precision, e.g.
726 // AssetsTotal 2e12 minus a 1e-6 debit needs 19 significant digits and rounds straight
727 // back to 2e12. The shares still move, so ValidVault later fails with "must decrease
728 // vault balance" instead of a clean upfront rejection.
729 //
730 // Fix (fixCleanup3_4_0): reject upfront with tecPRECISION_LOSS if the debit would
731 // canonicalize back to the prior stored value.
732 //
733 // With a single depositor AssetsTotal == AssetsAvailable, so both
734 // debitIsNonZeroDust operands trip together here. LoanRounding_test's
735 // "dust debit vs AssetsTotal only" case isolates the AssetsTotal operand
736 // via a heavily-loaned vault.
737 void
739 {
740 using namespace test::jtx;
741
742 // Fund a single depositor and have them deposit `total` USD in one shot (default
743 // scale 6, so shares mint at exactly total*1e6).
744 auto const seedVault = [](Env& env, Number const& total) {
745 Account const issuer{"issuer"};
746 Account const owner{"owner"};
747 Account const holder{"holder"};
748
749 env.fund(XRP(1'000'000), issuer, owner, holder);
750 env.close();
751 env(fset(issuer, asfAllowTrustLineClawback));
752 env.close();
753
754 PrettyAsset const usd{issuer["USD"]};
755 env(trust(holder, usd(100'000'000'000'000LL)));
756 env.close();
757 env(pay(issuer, holder, usd(total)));
758 env.close();
759
760 Vault const vault{env};
761 auto const [tx, keylet] = vault.create({.owner = owner, .asset = usd.raw()});
762 env(tx);
763 env.close();
764 env(vault.deposit({.depositor = holder, .id = keylet.key, .amount = usd(total)}),
765 Ter(tesSUCCESS));
766 env.close();
767
768 return keylet;
769 };
770
771 {
772 auto runScenario = [&](FeatureBitset features, TER expected) {
773 Env env(*this, features);
774 Number const total{2, 12};
775 auto const keylet = seedVault(env, total);
776
777 Account const issuer{"issuer"};
778 PrettyAsset const usd{issuer["USD"]};
779
780 // 1 share's worth of assets: 1e-6, below AssetsTotal's storage precision.
781 env(Vault::clawback(
782 {.issuer = issuer,
783 .id = keylet.key,
784 .holder = Account{"holder"},
785 .amount = usd(Number{1, -6}).value()}),
786 Ter(expected));
787 env.close();
788 };
789
790 testcase("bug: VaultClawback dust debit fires invariant (pre-fixCleanup3_4_0)");
791 runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
792 testcase("bug: VaultClawback dust debit rejected cleanly (post-fixCleanup3_4_0)");
793 runScenario(all_, tecPRECISION_LOSS);
794 }
795
796 {
797 auto runScenario = [&](FeatureBitset features, TER expected) {
798 Env env(*this, features);
799 Number const total{2, 12};
800 auto const keylet = seedVault(env, total);
801
802 MPTIssue const share{env.le(keylet)->at(sfShareMPTID)};
803
804 // Redeem 1 share, worth 1e-6 assets, below AssetsTotal's storage precision.
805 env(Vault::withdraw(
806 {.depositor = Account{"holder"},
807 .id = keylet.key,
808 .amount = STAmount{share, 1}}),
809 Ter(expected));
810 env.close();
811 };
812
813 testcase("bug: VaultWithdraw dust debit fires invariant (pre-fixCleanup3_4_0)");
814 runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
815 testcase("bug: VaultWithdraw dust debit rejected cleanly (post-fixCleanup3_4_0)");
816 runScenario(all_, tecPRECISION_LOSS);
817 }
818 }
819
820 // Scale 15 seed + deposit 5: pre-fix credited > paid; post-fix credited <= paid.
821 // fixCleanup3_2_0 is off so roundToVaultScale does not shrink the deposit first.
822 void
824 {
825 using namespace test::jtx;
826
827 auto runScenario = [this](FeatureBitset features, bool expectOvercredit) {
828 Env env(*this, features);
829 Account const owner{"owner"};
830 Account const issuer{"issuer"};
831 Account const depositor{"depositor"};
832 env.fund(XRP(1'000'000), owner, issuer, depositor);
833 env.close();
834
835 PrettyAsset const usd{issuer["USD"]};
836 Number const seed{9'999'999'999'999'999LL, -15};
837 Number const deposit{5};
838
839 env(trust(depositor, usd(1'000'000'000)));
840 env.close();
841 env(pay(issuer, depositor, usd(deposit)));
842 env.close();
843
844 Vault const vault{env};
845 auto [tx, keylet] = vault.create({.owner = owner, .asset = usd.raw()});
846 tx[sfScale] = 15;
847 env(tx);
848 env.close();
849 env(vault.deposit({.depositor = issuer, .id = keylet.key, .amount = usd(seed)}));
850 env.close();
851
852 Number const totalBefore = env.le(keylet)->at(sfAssetsTotal);
853 Number const depositorBefore = env.balance(depositor, usd.raw()).number();
854
855 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = usd(deposit)}));
856 env.close();
857
858 Number const totalAfter = env.le(keylet)->at(sfAssetsTotal);
859 Number const depositorAfter = env.balance(depositor, usd.raw()).number();
860 Number const paid = depositorBefore - depositorAfter;
861 Number const credited = totalAfter - totalBefore;
862
863 if (expectOvercredit)
864 {
865 BEAST_EXPECTS(
866 credited > paid,
867 "AssetsTotal credited " + to_string(credited) + " for a payment of " +
868 to_string(paid) + ", expected an overcredit");
869 }
870 else
871 {
872 BEAST_EXPECTS(
873 credited <= paid,
874 "AssetsTotal credited " + to_string(credited) + " for a payment of " +
875 to_string(paid));
876 }
877 };
878
879 testcase(
880 "bug: VaultDeposit overcredits across an IOU scale boundary "
881 "(pre-fixCleanup3_4_0)");
882 runScenario(all_ - fixCleanup3_2_0 - fixCleanup3_4_0, true);
883
884 testcase(
885 "bug: VaultDeposit no longer overcredits across an IOU scale boundary "
886 "(post-fixCleanup3_4_0)");
887 runScenario(all_, false);
888 }
889
890 // 1e17 IOU at scale 0. Withdraw all-but-one, then the last share:
891 // pre-fix tecINVARIANT_FAILED, post-fix tesSUCCESS.
892 void
894 {
895 using namespace test::jtx;
896
897 auto runScenario = [this](FeatureBitset features, TER expected) {
898 Env env(*this, features);
899 Account const owner{"owner"};
900 Account const issuer{"issuer"};
901 Account const holder{"holder"};
902 env.fund(XRP(1'000'000), owner, issuer, holder);
903 env.close();
904
905 PrettyAsset const usd{issuer["USD"]};
906 env(trust(holder, usd(Number{1, 18})));
907 env.close();
908 env(pay(issuer, holder, usd(Number{1, 17})));
909 env.close();
910
911 Vault const vault{env};
912 auto [tx, keylet] = vault.create({.owner = owner, .asset = usd.raw()});
913 tx[sfScale] = 0;
914 env(tx);
915 env.close();
916 env(vault.deposit(
917 {.depositor = holder, .id = keylet.key, .amount = usd(Number{1, 17})}));
918 env.close();
919
920 MPTIssue const share{env.le(keylet)->at(sfShareMPTID)};
921 std::int64_t const allButOne = 100'000'000'000'000'000LL - 1;
922 env(vault.withdraw(
923 {.depositor = holder, .id = keylet.key, .amount = STAmount{share, allButOne}}));
924 env.close();
925
926 env(vault.withdraw(
927 {.depositor = holder, .id = keylet.key, .amount = STAmount{share, 1}}),
928 Ter(expected));
929 env.close();
930 };
931
932 testcase(
933 "bug: VaultWithdraw permanently locks a large IOU vault "
934 "(pre-fixCleanup3_4_0)");
935 runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
936 testcase(
937 "bug: VaultWithdraw no longer locks a large IOU vault "
938 "(post-fixCleanup3_4_0)");
939 runScenario(all_, tesSUCCESS);
940 }
941
942 // VaultDeposit::preclaim uses accountHolds(..., SpendableHandling::
943 // shFULL_BALANCE), which for an IOU asset adds the counterparty's
944 // LowLimit/HighLimit to the depositor's raw balance (TokenHelpers.cpp:
945 // getTrustLineBalance with includeOppositeLimit=true). When the
946 // depositor's raw balance < deposit amount but raw + opposite limit >=
947 // amount, preclaim is satisfied. doApply then calls
948 // directSendNoFeeIOU, which unconditionally subtracts saAmount from
949 // saBalance — driving the trust line negative — and returns tesSUCCESS.
950 // The post-send sanity check uses the default shSIMPLE_BALANCE (no
951 // opposite-limit add), sees a negative balance, and returns tefINTERNAL.
952 void
954 {
955 auto runTest = [&](FeatureBitset f, TER expected) {
956 using namespace test::jtx;
957 using namespace std::literals;
958
959 Env env{*this, f};
960 Account const gw{"gateway"};
961 Account const owner{"owner"};
962 Account const depositor{"depositor"};
963
964 env.fund(XRP(10000), gw, owner, depositor);
965 env.close();
966
967 // Gateway with DefaultRipple so vault creation on its IOU works.
968 env(fset(gw, asfDefaultRipple));
969 env.close();
970
971 // Depositor opens a trust line to gateway and receives a small
972 // balance.
973 PrettyAsset const usd = gw["USD"];
974 env.trust(usd(1000), depositor);
975 env(pay(gw, depositor, usd(100))); // raw trust-line balance: 100
976 env.close();
977
978 // Key precondition: gateway sets a non-zero limit on the same
979 // RippleState — the "opposite field" from depositor's perspective.
980 // This is what inflates shFULL_BALANCE in preclaim above the raw
981 // balance.
982 env(trust(gw, depositor["USD"](1000)));
983 env.close();
984
985 // Create the IOU vault.
986 Vault const vault{env};
987 auto [vaultTx, keylet] = vault.create({.owner = owner, .asset = usd});
988 env(vaultTx);
989 env.close();
990
991 // Submit a deposit of 500 USD:
992 // - raw balance: 100 USD
993 // - opposite limit (gw's side): 1000 USD
994 // - preclaim sees 100 + 1000 = 1100, passes (>= 500)
995 // - doApply transfers 500, depositor's trust-line balance
996 // becomes -400
997 // - sanity check at VaultDeposit.cpp:256 fires
998 // - tx returns tefINTERNAL (BUG — should be tesSUCCESS.
999 auto depositTx =
1000 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = usd(500)});
1001 env(depositTx, Ter(expected));
1002 env.close();
1003 };
1004
1005 {
1006 testcase(
1007 "IOU vault deposit exceeding depositor's balance but "
1008 "within counterparty's trust limit, pre-fixCleanup3_2_0 "
1009 "(tefINTERNAL)");
1010 runTest(test::jtx::testableAmendments() - fixCleanup3_2_0, tefINTERNAL);
1011 }
1012 {
1013 testcase(
1014 "IOU vault deposit exceeding depositor's balance but "
1015 "within counterparty's trust limit, post-fixCleanup3_2_0 "
1016 "(tesSUCCESS)");
1018 }
1019 }
1020
1021 // Reproduction: canWithdraw IOU limit check bypassed when
1022 // withdrawal amount is specified in shares (MPT) rather than in assets.
1023 void
1025 {
1026 using namespace test::jtx;
1027 testcase("Bug6 - limit bypass with share-denominated withdrawal");
1028
1029 auto const allAmendments = testableAmendments() | featureSingleAssetVault;
1030
1031 for (auto const& features : {allAmendments, allAmendments - fixCleanup3_1_3})
1032 {
1033 bool const withFix = features[fixCleanup3_1_3];
1034
1035 Env env{*this, features};
1036 Account const owner{"owner"};
1037 Account const issuer{"issuer"};
1038 Account const depositor{"depositor"};
1039 Account const charlie{"charlie"};
1040 Vault const vault{env};
1041
1042 env.fund(XRP(1000), issuer, owner, depositor, charlie);
1043 env(fset(issuer, asfAllowTrustLineClawback));
1044 env.close();
1045
1046 PrettyAsset const asset = issuer["IOU"];
1047 env.trust(asset(1000), owner);
1048 env.trust(asset(1000), depositor);
1049 env(pay(issuer, owner, asset(200)));
1050 env(pay(issuer, depositor, asset(200)));
1051 env.close();
1052
1053 // Charlie gets a LOW trustline limit of 5
1054 env.trust(asset(5), charlie);
1055 env.close();
1056
1057 auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1058 env(tx);
1059 env.close();
1060
1061 auto const depositTx =
1062 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
1063 env(depositTx);
1064 env.close();
1065
1066 // Get the share MPT info
1067 auto const vaultSle = env.le(keylet);
1068 if (!BEAST_EXPECT(vaultSle))
1069 return;
1070 auto const mptIssuanceID = vaultSle->at(sfShareMPTID);
1071 MPTIssue const shares(mptIssuanceID);
1072 PrettyAsset const share(shares);
1073
1074 // CONTROL: Withdraw 10 IOU (asset-denominated) to charlie.
1075 // Charlie's limit is 5, so this should be rejected with tecNO_LINE
1076 // regardless of the amendment.
1077 {
1078 auto withdrawTx =
1079 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
1080 withdrawTx[sfDestination] = charlie.human();
1081 env(withdrawTx, Ter{tecNO_LINE});
1082 env.close();
1083 }
1084 auto const charlieBalanceBefore = env.balance(charlie, asset.raw().get<Issue>());
1085
1086 // Withdraw the equivalent amount in shares to charlie.
1087 // Post-fix: rejected (tecNO_LINE) because the share amount is
1088 // converted to assets and the trustline limit is checked.
1089 // Pre-fix: succeeds (tesSUCCESS) because the limit check was
1090 // skipped for share-denominated withdrawals.
1091 {
1092 auto withdrawTx = vault.withdraw(
1093 {.depositor = depositor,
1094 .id = keylet.key,
1095 .amount = STAmount(share, 10'000'000)});
1096 withdrawTx[sfDestination] = charlie.human();
1097 env(withdrawTx, Ter{withFix ? TER{tecNO_LINE} : TER{tesSUCCESS}});
1098 env.close();
1099
1100 auto const charlieBalanceAfter = env.balance(charlie, asset.raw().get<Issue>());
1101 if (withFix)
1102 {
1103 // Post-fix: charlie's balance is unchanged — the withdrawal
1104 // was correctly rejected despite being share-denominated.
1105 BEAST_EXPECT(charlieBalanceAfter == charlieBalanceBefore);
1106 }
1107 else
1108 {
1109 // Pre-fix: charlie received the assets, bypassing the
1110 // trustline limit.
1111 BEAST_EXPECT(charlieBalanceAfter > charlieBalanceBefore);
1112 }
1113 }
1114 }
1115 }
1116
1117 // Shared setup for testBugClawbackRoundTripOvershoot and
1118 // testBugWithdrawRoundTripOvershoot, which both need a vault at
1119 // assetsTotal=7, sharesTotal=5 and differ only in what they do once
1120 // that state is reached.
1121 //
1122 // The (7, 5) state is reached through ordinary transactions: a 5 USD
1123 // deposit mints 5 shares 1:1, then a loan broker on the vault issues a
1124 // single-payment bullet loan for the full 5 USD at 40% interest. When
1125 // the borrower repays a year later, LoanPay books the 2 USD of accrued
1126 // interest into sfAssetsTotal without minting shares, leaving
1127 // assetsTotal=7 against sharesTotal=5 (see
1128 // testBugDepositShareTruncationSubUlp for the same technique in more
1129 // detail).
1140
1143 {
1144 using namespace test::jtx;
1145 using namespace loan_broker;
1146 using namespace loan;
1147
1148 Account const issuer{"issuer"};
1149 Account const owner{"owner"};
1150 Account const holder{"holder"};
1151 Account const borrower{"borrower"};
1152
1153 env.fund(XRP(10'000), issuer, owner, holder, borrower);
1154 env.close();
1155
1156 env(fset(issuer, asfAllowTrustLineClawback));
1157 env.close();
1158
1159 PrettyAsset const usd = issuer["USD"];
1160 env.trust(usd(1'000), owner);
1161 env.trust(usd(1'000), holder);
1162 env.trust(usd(1'000), borrower);
1163 env.close();
1164
1165 env(pay(issuer, holder, usd(100)));
1166 env(pay(issuer, borrower, usd(100)));
1167 env.close();
1168
1169 Vault const vault{env};
1170 auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = usd});
1171 vaultTx[sfScale] = 0;
1172 env(vaultTx);
1173 env.close();
1174
1175 // Holder deposits 5 USD, minting 5 shares 1:1.
1176 env(vault.deposit({.depositor = holder, .id = vaultKeylet.key, .amount = usd(5)}));
1177 env.close();
1178
1179 // A loan broker on the vault, then a single bullet loan for the
1180 // entire deposit at 40% interest, one payment, one year out.
1181 auto const brokerKeylet =
1182 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
1183 env(set(owner, vaultKeylet.key));
1184 env.close();
1185
1186 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
1187 env(set(borrower, brokerKeylet.key, usd(5).value()),
1188 loan::kInterestRate(percentageToTenthBips(40)),
1189 kGracePeriod(60),
1190 kPaymentInterval(365 * 24 * 60 * 60),
1191 kPaymentTotal(1),
1192 Sig(sfCounterpartySignature, owner),
1193 Fee(env.current()->fees().base * 2),
1194 Ter(tesSUCCESS));
1195 env.close();
1196
1197 // Advance to just before the single payment falls due and let the
1198 // borrower repay principal plus interest. Share supply stays at 5,
1199 // so assetsTotal/sharesTotal becomes 7/5.
1200 env.close(std::chrono::seconds{(365 * 24 * 60 * 60) - 3600});
1201 env(pay(borrower, loanKeylet.key, usd(10).value()), Ter(tesSUCCESS));
1202 env.close();
1203
1204 auto const vaultSle = env.le(vaultKeylet);
1205 if (!BEAST_EXPECT(vaultSle))
1206 return std::nullopt;
1207 auto const mptIssuanceID = vaultSle->at(sfShareMPTID);
1208
1209 Number const initialAssetsTotal = vaultSle->at(sfAssetsTotal);
1210 Number const initialAssetsAvailable = vaultSle->at(sfAssetsAvailable);
1211 BEAST_EXPECT(initialAssetsTotal == usd(7).number());
1212 BEAST_EXPECT(initialAssetsAvailable == usd(7).number());
1213 {
1214 auto const sleIssuance = env.le(keylet::mptokenIssuance(mptIssuanceID));
1215 if (!BEAST_EXPECT(sleIssuance))
1216 return std::nullopt;
1217 BEAST_EXPECT(sleIssuance->getFieldU64(sfOutstandingAmount) == 5);
1218 }
1219
1221 .issuer = issuer,
1222 .holder = holder,
1223 .usd = usd,
1224 .vault = vault,
1225 .vaultKeylet = vaultKeylet,
1226 .initialAssetsTotal = initialAssetsTotal,
1227 .initialAssetsAvailable = initialAssetsAvailable};
1228 }
1229
1230 // VaultClawback::assetsToClawback converts clawbackAmount to shares
1231 // with round-to-nearest, then round-trips back to assets. When shares
1232 // round up, assetsRecovered can exceed clawbackAmount.
1233 //
1234 // Repro: assetsTotal=7, sharesTotal=5, request 4:
1235 // shares = round(20/7) = 3, assets = 7*3/5 = 4.2 > 4.
1236 //
1237 // Post-fixCleanup3_4_0: truncate shares so assetsRecovered <=
1238 // clawbackAmount by construction.
1239 void
1241 {
1242 using namespace test::jtx;
1243
1244 auto runScenario = [this](FeatureBitset features, bool withFix) {
1245 // This regression requires the open-ended vault lifecycle: deposit,
1246 // originate and repay a loan, then claw back shares. LP V1.1
1247 // independently rejects attaching a broker to an open-ended vault.
1248 Env env{*this, features - featureLendingProtocolV1_1};
1249
1250 auto const setup = makeRoundTripOvershootVault(env);
1251 if (!BEAST_EXPECT(setup))
1252 return;
1253
1254 auto const clawbackAmount = setup->usd(4);
1255 env(setup->vault.clawback(
1256 {.issuer = setup->issuer,
1257 .id = setup->vaultKeylet.key,
1258 .holder = setup->holder,
1259 .amount = clawbackAmount.value()}));
1260
1261 auto const vaultSleAfter = env.current()->read(setup->vaultKeylet);
1262 if (!BEAST_EXPECT(vaultSleAfter))
1263 return;
1264 Number const finalAssetsTotal = vaultSleAfter->at(sfAssetsTotal);
1265 Number const assetsRecovered = setup->initialAssetsTotal - finalAssetsTotal;
1266 Number const clawbackNum = clawbackAmount.number();
1267
1268 Number const expectedPost{28LL, -1};
1269 Number const expectedPre{42LL, -1};
1270 if (withFix)
1271 {
1272 BEAST_EXPECT(assetsRecovered <= clawbackNum);
1273 BEAST_EXPECT(assetsRecovered == expectedPost);
1274 }
1275 else
1276 {
1277 BEAST_EXPECT(assetsRecovered > clawbackNum);
1278 BEAST_EXPECT(assetsRecovered == expectedPre);
1279 }
1280 };
1281
1282 {
1283 testcase(
1284 "bug: VaultClawback round-trip overshoot lets issuer recover "
1285 "more than requested (pre-fixCleanup3_4_0)");
1286 runScenario(testableAmendments() - fixCleanup3_4_0, false);
1287 }
1288 {
1289 testcase(
1290 "bug: VaultClawback round-trip overshoot is clamped so "
1291 "assetsRecovered <= clawbackAmount (post-fixCleanup3_4_0)");
1292 runScenario(testableAmendments(), true);
1293 }
1294 }
1295
1296 // Same root cause as testBugClawbackRoundTripOvershoot on the
1297 // withdraw path. Also bypasses the preclaim canWithdraw check, which
1298 // validates destination limits against the requested amount only.
1299 //
1300 // Repro: assetsTotal=7, sharesTotal=5, request 4:
1301 // pre-fix : shares = round(20/7) = 3, assets = 7*3/5 = 4.2 > 4.
1302 // post-fix: shares = floor(20/7) = 2, assets = 7*2/5 = 2.8 <= 4.
1303 void
1305 {
1306 using namespace test::jtx;
1307
1308 auto runScenario = [this](FeatureBitset features, bool withFix) {
1309 // This regression requires the open-ended vault lifecycle: deposit,
1310 // originate and repay a loan, then withdraw shares. LP V1.1
1311 // independently rejects attaching a broker to an open-ended vault.
1312 Env env{*this, features - featureLendingProtocolV1_1};
1313
1314 auto const setup = makeRoundTripOvershootVault(env);
1315 if (!BEAST_EXPECT(setup))
1316 return;
1317
1318 auto const requested = setup->usd(4);
1319 env(setup->vault.withdraw(
1320 {.depositor = setup->holder,
1321 .id = setup->vaultKeylet.key,
1322 .amount = requested.value()}));
1323
1324 auto const vaultSleAfter = env.current()->read(setup->vaultKeylet);
1325 if (!BEAST_EXPECT(vaultSleAfter))
1326 return;
1327 Number const finalAssetsTotal = vaultSleAfter->at(sfAssetsTotal);
1328 Number const assetsWithdrawn = setup->initialAssetsTotal - finalAssetsTotal;
1329 Number const requestedNum = requested.number();
1330
1331 Number const expectedPost{28LL, -1};
1332 Number const expectedPre{42LL, -1};
1333 if (withFix)
1334 {
1335 BEAST_EXPECT(assetsWithdrawn <= requestedNum);
1336 BEAST_EXPECT(assetsWithdrawn == expectedPost);
1337 }
1338 else
1339 {
1340 BEAST_EXPECT(assetsWithdrawn > requestedNum);
1341 BEAST_EXPECT(assetsWithdrawn == expectedPre);
1342 }
1343 };
1344
1345 {
1346 testcase(
1347 "bug: VaultWithdraw round-trip overshoot delivers more than "
1348 "requested (pre-fixCleanup3_4_0)");
1349 runScenario(testableAmendments() - fixCleanup3_4_0, false);
1350 }
1351 {
1352 testcase(
1353 "bug: VaultWithdraw round-trip overshoot is clamped so "
1354 "assetsWithdrawn <= requested (post-fixCleanup3_4_0)");
1355 runScenario(testableAmendments(), true);
1356 }
1357 }
1358
1368
1369 // Impairing a 1,000 loan in a 10,000 vault leaves AssetsAvailable=9,000
1370 // and AssetsTotal=10,000. otherDeposit > 0 splits the shares, 0 leaves
1371 // holder as the sole shareholder.
1374 {
1375 using namespace test::jtx;
1376 using namespace loan_broker;
1377 using namespace loan;
1378
1379 Account const issuer{"issuer"};
1380 Account const owner{"owner"};
1381 Account const holder{"holder"};
1382 Account const other{"other"};
1383 Account const borrower{"borrower"};
1384
1385 env.fund(XRP(100'000), issuer, owner, holder, other, borrower);
1386 env.close();
1387
1388 env(fset(issuer, asfAllowTrustLineClawback));
1389 env(fset(issuer, asfDefaultRipple));
1390 env.close();
1391
1392 PrettyAsset const usd = issuer["USD"];
1393 env.trust(usd(100'000), owner);
1394 env.trust(usd(100'000), holder);
1395 env.trust(usd(100'000), other);
1396 env.trust(usd(100'000), borrower);
1397 env.close();
1398
1399 int const holderDeposit = 10'000 - otherDeposit;
1400 env(pay(issuer, holder, usd(holderDeposit)));
1401 if (otherDeposit != 0)
1402 {
1403 env(pay(issuer, other, usd(otherDeposit)));
1404 }
1405 env.close();
1406
1407 Vault const vault{env};
1408 auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
1409 {.owner = owner, .asset = usd, .subscriptionOffset = std::chrono::seconds{60}});
1410 env(createTx);
1411 env.close();
1412
1413 auto const vaultSle = env.le(vaultKeylet);
1414 if (!BEAST_EXPECT(vaultSle))
1415 return std::nullopt;
1416 MPTID const shareId = vaultSle->at(sfShareMPTID);
1417
1418 env(vault.deposit(
1419 {.depositor = holder, .id = vaultKeylet.key, .amount = usd(holderDeposit)}));
1420 if (otherDeposit != 0)
1421 {
1422 env(vault.deposit(
1423 {.depositor = other, .id = vaultKeylet.key, .amount = usd(otherDeposit)}));
1424 }
1425 env.close();
1426
1427 vault.closePastSubscription(subscriptionDate);
1428
1429 auto const brokerKeylet =
1430 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
1431 env(set(owner, vaultKeylet.key));
1432 env.close();
1433
1434 auto const sleBroker = env.le(brokerKeylet);
1435 if (!BEAST_EXPECT(sleBroker))
1436 return std::nullopt;
1437 auto const loanKeylet =
1438 keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
1439
1440 env(set(borrower, brokerKeylet.key, usd(1'000).value()),
1441 loan::kInterestRate(percentageToTenthBips(0)),
1442 kGracePeriod(60),
1443 kPaymentInterval(120),
1444 kPaymentTotal(10),
1445 Sig(sfCounterpartySignature, owner),
1446 Fee(env.current()->fees().base * 2),
1447 Ter(tesSUCCESS));
1448 env.close();
1449
1450 // Under fixCleanup3_4_0, LoanManage rejects tfLoanImpair with
1451 // tecTOO_SOON unless the payment is already late; advance the ledger
1452 // past sfNextPaymentDueDate so impairment succeeds. No-op otherwise.
1453 if (env.current()->rules().enabled(fixCleanup3_4_0))
1454 {
1455 auto const loanBefore = env.le(loanKeylet);
1456 if (!BEAST_EXPECT(loanBefore))
1457 return std::nullopt;
1458 std::uint32_t const dueDate = loanBefore->at(sfNextPaymentDueDate);
1460 }
1461
1462 env(manage(owner, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
1463 env.close();
1464
1465 auto const vaultAfter = env.le(vaultKeylet);
1466 if (!BEAST_EXPECT(vaultAfter))
1467 return std::nullopt;
1468 BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == usd(9'000).value());
1469 BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == usd(1'000).value());
1470
1471 return ImpairedLoanVault{
1472 .issuer = issuer,
1473 .holder = holder,
1474 .usd = usd,
1475 .vault = vault,
1476 .vaultKeylet = vaultKeylet,
1477 .shareId = shareId};
1478 }
1479
1480 // Legacy clawback pricing burns every share; fixCleanup3_4_0 leaves 10%
1481 // outstanding, backed by the impaired receivable.
1482 void
1484 {
1485 using namespace test::jtx;
1486
1487 auto clawbackHolder = [](ImpairedLoanVault const& setup, STAmount const& amount) {
1488 return setup.vault.clawback(
1489 {.issuer = setup.issuer,
1490 .id = setup.vaultKeylet.key,
1491 .holder = setup.holder,
1492 .amount = amount});
1493 };
1494
1495 auto runSole = [this, &clawbackHolder](FeatureBitset features, TER expected) {
1496 testcase(
1497 features[fixCleanup3_4_0]
1498 ? "VaultClawback after impaired loan (post-fixCleanup3_4_0)"
1499 : "VaultClawback after impaired loan (pre-fixCleanup3_4_0)");
1500
1501 Env env(*this, features);
1502 auto const maybeSetup = makeImpairedLoanVault(env, 0);
1503 if (!maybeSetup)
1504 {
1505 BEAST_EXPECT(false);
1506 return;
1507 }
1508 ImpairedLoanVault const& setup = *maybeSetup;
1509
1510 auto const tokenBefore = env.le(keylet::mptoken(setup.shareId, setup.holder.id()));
1511 auto const vaultBefore = env.le(setup.vaultKeylet);
1512 auto const issuanceBefore = env.le(keylet::mptokenIssuance(setup.shareId));
1513 if (!BEAST_EXPECT(tokenBefore) || !BEAST_EXPECT(vaultBefore) ||
1514 !BEAST_EXPECT(issuanceBefore))
1515 return;
1516 std::uint64_t const sharesBefore = tokenBefore->getFieldU64(sfMPTAmount);
1517
1518 // The clawback of 19,000 exceeds AssetsAvailable (9,000), so
1519 // VaultClawback clamps sharesDestroyed to whatever redeems
1520 // exactly AssetsAvailable; compute that expected value using the
1521 // same conversion helper VaultClawback itself uses, rather than
1522 // assuming an exact 90/10 split holds under truncation.
1523 auto const maybeSharesDestroyed = assetsToSharesWithdraw(
1524 vaultBefore,
1525 issuanceBefore,
1526 setup.usd(9'000).value(),
1529 if (!BEAST_EXPECT(maybeSharesDestroyed))
1530 return;
1531 std::uint64_t const expectedSharesAfter =
1532 sharesBefore - maybeSharesDestroyed->mpt().value();
1533
1534 env(clawbackHolder(setup, setup.usd(19'000).value()), Ter(expected));
1535 env.close();
1536 if (expected != tesSUCCESS)
1537 return;
1538
1539 auto const vaultAfter = env.le(setup.vaultKeylet);
1540 if (!BEAST_EXPECT(vaultAfter))
1541 return;
1542 BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == setup.usd(0).value());
1543 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == setup.usd(1'000).value());
1544 BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == setup.usd(1'000).value());
1545 auto const tokenAfter = env.le(keylet::mptoken(setup.shareId, setup.holder.id()));
1546 if (!BEAST_EXPECT(tokenAfter))
1547 return;
1548 BEAST_EXPECT(tokenAfter->getFieldU64(sfMPTAmount) == expectedSharesAfter);
1549 };
1550
1551 runSole(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
1552 runSole(all_, tesSUCCESS);
1553
1554 testcase("VaultClawback after impaired loan, non-sole holder");
1555 {
1556 Env env(*this, all_);
1557 auto const maybeSetup = makeImpairedLoanVault(env, 1'000);
1558 if (!maybeSetup)
1559 {
1560 BEAST_EXPECT(false);
1561 return;
1562 }
1563 ImpairedLoanVault const& setup = *maybeSetup;
1564 // The waiver does not apply, so the holder's 9,000 shares are
1565 // still priced at the discounted rate and cannot cover 9,000.
1566 env(clawbackHolder(setup, setup.usd(9'000).value()), Ter(tecINSUFFICIENT_FUNDS));
1567 }
1568 }
1569
1570 // Bug: a fully impaired vault may pay zero assets for a share burn.
1571 // Sending zero MPT is a no-op, so the vault pseudo-account's asset
1572 // MPToken is never written and ValidVault, which only records deltas for
1573 // created, modified or deleted entries, sees no vault delta at all.
1574 //
1575 // Pre-fixCleanup3_4_0 that alone makes the withdrawal impossible:
1576 // zeroDeltaIsLegitimate is gated on the amendment, so the absent vault
1577 // delta fails "withdrawal must change vault balance". Every pre-amendment
1578 // arm below dies there, before any destination-side check runs.
1579 //
1580 // The destination side differs per arm, and only the vault-delta return
1581 // hides that pre-amendment. With Alice's asset MPToken already present
1582 // nothing touches it, so she has no delta either. With it missing,
1583 // doWithdraw still called addEmptyHolding for a self-destination on a
1584 // zero payout and created her MPToken at amount 0; a created MPToken is
1585 // recorded even at zero, so she arrives with a present-and-zero delta,
1586 // which for an integral MPT asset the destination check would reject if
1587 // it were reached.
1588 //
1589 // ValidMPTIssuance: pre-fixCleanup3_4_0, a VaultWithdraw that both
1590 // creates and deletes an MPToken fails. Post-fixCleanup3_4_0 that is
1591 // allowed.
1592
1593 //
1594 // Post-fixCleanup3_4_0, doWithdraw skips addEmptyHolding on a zero
1595 // payout and zeroDeltaIsLegitimate lets the vault-delta and
1596 // missing-recipient-delta checks accept the transfer. A present
1597 // destination delta of zero is still rejected.
1598 void
1600 {
1601 using namespace test::jtx;
1602 using namespace loan_broker;
1603 using namespace loan;
1604 using namespace std::chrono_literals;
1605
1606 auto runScenario = [this](
1607 FeatureBitset features,
1608 bool removeAssetToken,
1609 bool withdrawAllAliceShares,
1610 TER expected) {
1611 testcase(
1612 std::string{"bug: MPT vault zero-value withdraw "} +
1613 (removeAssetToken ? "without asset MPToken" : "with asset MPToken") +
1614 (withdrawAllAliceShares ? ", Alice's last share" : ", Alice has leftover shares") +
1615 (features[fixCleanup3_4_0] ? " (post-fixCleanup3_4_0)" : " (pre-fixCleanup3_4_0)"));
1616
1617 Env env(*this, features);
1618
1619 Account const issuer{"issuer"};
1620 Account const owner{"owner"};
1621 Account const alice{"alice"};
1622 Account const bob{"bob"};
1623 Account const borrower{"borrower"};
1624
1625 env.fund(XRP(100'000), issuer, owner, alice, bob, borrower);
1626 env.close();
1627
1628 MPTTester mptt{env, issuer, kMptInitNoFund};
1629 mptt.create({.flags = tfMPTCanTransfer});
1630 PrettyAsset const asset = mptt.issuanceID();
1631 mptt.authorize({.account = owner});
1632 mptt.authorize({.account = alice});
1633 mptt.authorize({.account = bob});
1634 mptt.authorize({.account = borrower});
1635 env.close();
1636
1637 env(pay(issuer, alice, asset(2)));
1638 env(pay(issuer, bob, asset(8)));
1639 env.close();
1640
1641 Vault const vault{env};
1642 auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
1643 {.owner = owner, .asset = asset, .subscriptionOffset = 60s});
1644 env(createTx);
1645 env.close();
1646
1647 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(2)}));
1648 env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = asset(8)}));
1649 env.close();
1650
1651 vault.closePastSubscription(subscriptionDate);
1652
1653 auto const brokerKeylet =
1654 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
1655 env(set(owner, vaultKeylet.key));
1656 env.close();
1657
1658 auto const sleBroker = env.le(brokerKeylet);
1659 if (!BEAST_EXPECT(sleBroker))
1660 return;
1661 auto const loanKeylet = keylet::loan(
1662 brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
1663
1664 env(set(borrower, brokerKeylet.key, asset(10).value()),
1665 kInterestRate(percentageToTenthBips(0)),
1666 kGracePeriod(60),
1667 kPaymentInterval(120),
1668 kPaymentTotal(10),
1669 Sig(sfCounterpartySignature, owner),
1670 Fee(env.current()->fees().base * 2),
1671 Ter(tesSUCCESS));
1672 env.close();
1673
1674 auto const loanBefore = env.le(loanKeylet);
1675 if (!BEAST_EXPECT(loanBefore))
1676 return;
1677 std::uint32_t const dueDate = loanBefore->at(sfNextPaymentDueDate);
1678 env.close(NetClock::time_point{NetClock::duration{dueDate}} + 1s);
1679
1680 env(manage(owner, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
1681 env.close();
1682
1683 auto const vaultImpaired = env.le(vaultKeylet);
1684 if (!BEAST_EXPECT(vaultImpaired))
1685 return;
1686 BEAST_EXPECT(vaultImpaired->at(sfAssetsAvailable) == asset(0).value());
1687 BEAST_EXPECT(vaultImpaired->at(sfAssetsTotal) == vaultImpaired->at(sfLossUnrealized));
1688 Number const totalBefore = vaultImpaired->at(sfAssetsTotal);
1689 Number const lossBefore = vaultImpaired->at(sfLossUnrealized);
1690
1691 MPTID const shareId = vaultImpaired->at(sfShareMPTID);
1692 auto const issuanceBefore = env.le(keylet::mptokenIssuance(shareId));
1693 if (!BEAST_EXPECT(issuanceBefore))
1694 return;
1695 std::uint64_t const outstandingBefore =
1696 issuanceBefore->getFieldU64(sfOutstandingAmount);
1697
1698 auto const tokenAlice = env.le(keylet::mptoken(shareId, alice.id()));
1699 if (!BEAST_EXPECT(tokenAlice))
1700 return;
1701 std::uint64_t const sharesBefore = tokenAlice->getFieldU64(sfMPTAmount);
1702 BEAST_EXPECT(sharesBefore == 2);
1703 std::uint64_t const sharesToRedeem = withdrawAllAliceShares ? sharesBefore : 1;
1704 STAmount const redeemShares{MPTIssue{shareId}, Number(sharesToRedeem)};
1705
1706 auto const assetTokenKeylet = keylet::mptoken(mptt.issuanceID(), alice.id());
1707 if (removeAssetToken)
1708 {
1709 mptt.authorize({.account = alice, .flags = tfMPTUnauthorize});
1710 env.close();
1711 BEAST_EXPECT(!env.le(assetTokenKeylet));
1712 }
1713 else
1714 {
1715 auto const existing = env.le(assetTokenKeylet);
1716 if (!BEAST_EXPECT(existing))
1717 return;
1718 BEAST_EXPECT(existing->getFieldU64(sfMPTAmount) == 0);
1719 }
1720
1721 std::uint32_t const redemptionDate = vaultImpaired->at(sfRedemptionDate);
1722 env.close(NetClock::time_point{NetClock::duration{redemptionDate}} + 1s);
1723
1724 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = redeemShares}),
1725 Ter(expected));
1726 env.close();
1727 if (expected != tesSUCCESS)
1728 return;
1729
1730 if (removeAssetToken)
1731 {
1732 BEAST_EXPECT(!env.le(assetTokenKeylet));
1733 }
1734 else
1735 {
1736 auto const assetAfter = env.le(assetTokenKeylet);
1737 if (!BEAST_EXPECT(assetAfter))
1738 return;
1739 BEAST_EXPECT(assetAfter->getFieldU64(sfMPTAmount) == 0);
1740 }
1741
1742 auto const shareAfter = env.le(keylet::mptoken(shareId, alice.id()));
1743 if (withdrawAllAliceShares)
1744 {
1745 BEAST_EXPECT(!shareAfter);
1746 }
1747 else if (BEAST_EXPECT(shareAfter))
1748 {
1749 BEAST_EXPECT(shareAfter->getFieldU64(sfMPTAmount) == sharesBefore - sharesToRedeem);
1750 }
1751
1752 auto const vaultAfter = env.le(vaultKeylet);
1753 if (!BEAST_EXPECT(vaultAfter))
1754 return;
1755 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
1756 BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
1757 BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == asset(0).value());
1758
1759 auto const issuanceAfter = env.le(keylet::mptokenIssuance(shareId));
1760 if (!BEAST_EXPECT(issuanceAfter))
1761 return;
1762 BEAST_EXPECT(
1763 issuanceAfter->getFieldU64(sfOutstandingAmount) ==
1764 outstandingBefore - sharesToRedeem);
1765 };
1766
1767 runScenario(
1768 all_, false /* removeAssetToken */, false /* withdrawAllAliceShares */, tesSUCCESS);
1769 runScenario(
1770 all_, false /* removeAssetToken */, true /* withdrawAllAliceShares */, tesSUCCESS);
1771 runScenario(
1772 all_, true /* removeAssetToken */, false /* withdrawAllAliceShares */, tesSUCCESS);
1773 runScenario(
1774 all_, true /* removeAssetToken */, true /* withdrawAllAliceShares */, tesSUCCESS);
1775 runScenario(
1776 all_ - fixCleanup3_4_0,
1777 false /* removeAssetToken */,
1778 false /* withdrawAllAliceShares */,
1780 runScenario(
1781 all_ - fixCleanup3_4_0,
1782 false /* removeAssetToken */,
1783 true /* withdrawAllAliceShares */,
1785 runScenario(
1786 all_ - fixCleanup3_4_0,
1787 true /* removeAssetToken */,
1788 false /* withdrawAllAliceShares */,
1790 runScenario(
1791 all_ - fixCleanup3_4_0,
1792 true /* removeAssetToken */,
1793 true /* withdrawAllAliceShares */,
1795 }
1796
1797 // IOU analogue of the missing-MPToken case above. Alice removes her
1798 // zero-balance trust line after depositing, then burns one unit from her
1799 // scaled share balance after the vault is fully impaired. Bob's share
1800 // balance keeps this out of the sole-shareholder loss-waiver and
1801 // final-outstanding-share paths. A zero payout must not recreate Alice's
1802 // unsolicited trust line.
1803 void
1805 {
1806 using namespace test::jtx;
1807 using namespace loan_broker;
1808 using namespace loan;
1809 using namespace std::chrono_literals;
1810
1811 Env env(*this, all_);
1812
1813 Account const issuer{"issuer"};
1814 Account const owner{"owner"};
1815 Account const alice{"alice"};
1816 Account const bob{"bob"};
1817 Account const borrower{"borrower"};
1818
1819 env.fund(XRP(100'000), issuer, owner, alice, bob, borrower);
1820 env.close();
1821 env(fset(issuer, asfDefaultRipple));
1822 env.close();
1823
1824 PrettyAsset const asset = issuer["USD"];
1825 env.trust(asset(100), owner);
1826 env.trust(asset(100), alice);
1827 env.trust(asset(100), bob);
1828 env.trust(asset(100), borrower);
1829 env.close();
1830
1831 env(pay(issuer, alice, asset(2)));
1832 env(pay(issuer, bob, asset(8)));
1833 env.close();
1834
1835 Vault const vault{env};
1836 auto const [createTx, vaultKeylet, subscriptionDate] =
1837 vault.createClosedEnded({.owner = owner, .asset = asset, .subscriptionOffset = 60s});
1838 env(createTx);
1839 env.close();
1840
1841 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(2)}));
1842 env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = asset(8)}));
1843 env.close();
1844
1845 auto const assetLine = keylet::trustLine(alice, asset.raw().get<Issue>());
1846 if (!BEAST_EXPECT(env.le(assetLine)))
1847 return;
1848 env.trust(asset(0), alice);
1849 env.close();
1850 BEAST_EXPECT(!env.le(assetLine));
1851
1852 vault.closePastSubscription(subscriptionDate);
1853
1854 auto const brokerKeylet =
1855 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
1856 env(set(owner, vaultKeylet.key));
1857 env.close();
1858
1859 auto const sleBroker = env.le(brokerKeylet);
1860 if (!BEAST_EXPECT(sleBroker))
1861 return;
1862 auto const loanKeylet =
1863 keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
1864
1865 env(set(borrower, brokerKeylet.key, asset(10).value()),
1866 kInterestRate(percentageToTenthBips(0)),
1867 kGracePeriod(60),
1868 kPaymentInterval(120),
1869 kPaymentTotal(10),
1870 Sig(sfCounterpartySignature, owner),
1871 Fee(env.current()->fees().base * 2),
1872 Ter(tesSUCCESS));
1873 env.close();
1874
1875 auto const loanBefore = env.le(loanKeylet);
1876 if (!BEAST_EXPECT(loanBefore))
1877 return;
1878 std::uint32_t const dueDate = loanBefore->at(sfNextPaymentDueDate);
1879 env.close(NetClock::time_point{NetClock::duration{dueDate}} + 1s);
1880
1881 env(manage(owner, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
1882 env.close();
1883
1884 auto const vaultImpaired = env.le(vaultKeylet);
1885 if (!BEAST_EXPECT(vaultImpaired))
1886 return;
1887 BEAST_EXPECT(vaultImpaired->at(sfAssetsAvailable) == asset(0).value());
1888 BEAST_EXPECT(vaultImpaired->at(sfAssetsTotal) == vaultImpaired->at(sfLossUnrealized));
1889 Number const totalBefore = vaultImpaired->at(sfAssetsTotal);
1890 Number const lossBefore = vaultImpaired->at(sfLossUnrealized);
1891
1892 MPTID const shareId = vaultImpaired->at(sfShareMPTID);
1893 auto const tokenAlice = env.le(keylet::mptoken(shareId, alice.id()));
1894 if (!BEAST_EXPECT(tokenAlice))
1895 return;
1896 std::uint64_t const sharesBefore = tokenAlice->getFieldU64(sfMPTAmount);
1897 // Default IOU vault scale is 6, so 2 USD mints 2e6 shares. Redeem one
1898 // leftover share; do not require 1:1 like the MPT case.
1899 BEAST_EXPECT(sharesBefore > 1);
1900 STAmount const redeemShares{MPTIssue{shareId}, Number(1)};
1901
1902 std::uint32_t const redemptionDate = vaultImpaired->at(sfRedemptionDate);
1903 env.close(NetClock::time_point{NetClock::duration{redemptionDate}} + 1s);
1904
1905 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = redeemShares}),
1906 Ter(tesSUCCESS));
1907 env.close();
1908
1909 // A regression in the View guard would recreate this line even though
1910 // no asset value was paid.
1911 BEAST_EXPECT(!env.le(assetLine));
1912
1913 auto const shareAfter = env.le(keylet::mptoken(shareId, alice.id()));
1914 if (!BEAST_EXPECT(shareAfter))
1915 return;
1916 BEAST_EXPECT(shareAfter->getFieldU64(sfMPTAmount) == sharesBefore - 1);
1917
1918 auto const vaultAfter = env.le(vaultKeylet);
1919 if (!BEAST_EXPECT(vaultAfter))
1920 return;
1921 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
1922 BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
1923 BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == asset(0).value());
1924 }
1925
1926 // Same zero-payout withdrawal as testBugMptZeroWithdrawMissingHolding, but
1927 // the vault asset is XRP. addEmptyHolding is a no-op for native assets.
1928 // Sequence processing still touches the sender AccountRoot; a sponsored
1929 // fee leaves that XRP balance economically unchanged. After the
1930 // sponsored-withdraw fee-payer fix, deltaAssetsForParty collapses that
1931 // economically-zero XRP delta to absence, so tesSUCCESS takes the
1932 // missing-recipient-delta arm gated by zeroDeltaIsLegitimate. This test
1933 // covers that live SUCCESS path. Pre-fixCleanup3_4_0 still fails the
1934 // invariant.
1935 void
1937 {
1938 using namespace test::jtx;
1939 using namespace loan_broker;
1940 using namespace loan;
1941 using namespace std::chrono_literals;
1942
1943 auto runScenario = [this](FeatureBitset features, TER expected) {
1944 testcase(
1945 std::string{"bug: XRP vault zero-value withdraw with sponsored fee"} +
1946 (features[fixCleanup3_4_0] ? " (post-fixCleanup3_4_0)" : " (pre-fixCleanup3_4_0)"));
1947
1948 Env env(*this, features);
1949
1950 Account const owner{"owner"};
1951 Account const alice{"alice"};
1952 Account const bob{"bob"};
1953 Account const borrower{"borrower"};
1954 Account const sponsor{"sponsor"};
1955
1956 env.fund(XRP(100'000), owner, alice, bob, borrower, sponsor);
1957 env.close();
1958
1959 PrettyAsset const asset{xrpIssue()};
1960 Vault const vault{env};
1961 auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
1962 {.owner = owner, .asset = asset, .subscriptionOffset = 60s});
1963 env(createTx);
1964 env.close();
1965
1966 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(2)}));
1967 env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = asset(8)}));
1968 env.close();
1969
1970 vault.closePastSubscription(subscriptionDate);
1971
1972 auto const brokerKeylet =
1973 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
1974 env(set(owner, vaultKeylet.key));
1975 env.close();
1976
1977 auto const sleBroker = env.le(brokerKeylet);
1978 if (!BEAST_EXPECT(sleBroker))
1979 return;
1980 auto const loanKeylet = keylet::loan(
1981 brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
1982
1983 env(set(borrower, brokerKeylet.key, asset(10).value()),
1984 kInterestRate(percentageToTenthBips(0)),
1985 kGracePeriod(60),
1986 kPaymentInterval(120),
1987 kPaymentTotal(10),
1988 Sig(sfCounterpartySignature, owner),
1989 Fee(env.current()->fees().base * 2),
1990 Ter(tesSUCCESS));
1991 env.close();
1992
1993 auto const loanBefore = env.le(loanKeylet);
1994 if (!BEAST_EXPECT(loanBefore))
1995 return;
1996 std::uint32_t const dueDate = loanBefore->at(sfNextPaymentDueDate);
1997 env.close(NetClock::time_point{NetClock::duration{dueDate}} + 1s);
1998
1999 env(manage(owner, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
2000 env.close();
2001
2002 auto const vaultImpaired = env.le(vaultKeylet);
2003 if (!BEAST_EXPECT(vaultImpaired))
2004 return;
2005 BEAST_EXPECT(vaultImpaired->at(sfAssetsAvailable) == asset(0).value());
2006 BEAST_EXPECT(vaultImpaired->at(sfAssetsTotal) == vaultImpaired->at(sfLossUnrealized));
2007 Number const totalBefore = vaultImpaired->at(sfAssetsTotal);
2008 Number const lossBefore = vaultImpaired->at(sfLossUnrealized);
2009
2010 MPTID const shareId = vaultImpaired->at(sfShareMPTID);
2011 auto const tokenAlice = env.le(keylet::mptoken(shareId, alice.id()));
2012 if (!BEAST_EXPECT(tokenAlice))
2013 return;
2014 std::uint64_t const sharesBefore = tokenAlice->getFieldU64(sfMPTAmount);
2015 BEAST_EXPECT(sharesBefore == 2);
2016 STAmount const redeemShares{MPTIssue{shareId}, Number(1)};
2017
2018 std::uint32_t const redemptionDate = vaultImpaired->at(sfRedemptionDate);
2019 env.close(NetClock::time_point{NetClock::duration{redemptionDate}} + 1s);
2020
2021 auto const aliceBalanceBefore = env.balance(alice);
2022 auto const sponsorBalanceBefore = env.balance(sponsor);
2023 auto const fee = env.current()->fees().base;
2024
2025 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = redeemShares}),
2026 Fee(fee),
2027 sponsor::As(sponsor, spfSponsorFee),
2028 Sig(sfSponsorSignature, sponsor),
2029 Ter(expected));
2030 env.close();
2031
2032 BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore - fee);
2033 BEAST_EXPECT(env.balance(alice) == aliceBalanceBefore);
2034
2035 if (expected != tesSUCCESS)
2036 return;
2037
2038 auto const shareAfter = env.le(keylet::mptoken(shareId, alice.id()));
2039 if (!BEAST_EXPECT(shareAfter))
2040 return;
2041 BEAST_EXPECT(shareAfter->getFieldU64(sfMPTAmount) == sharesBefore - 1);
2042
2043 auto const vaultAfter = env.le(vaultKeylet);
2044 if (!BEAST_EXPECT(vaultAfter))
2045 return;
2046 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
2047 BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
2048 BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == asset(0).value());
2049 };
2050
2051 runScenario(all_, tesSUCCESS);
2052 runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
2053 }
2054
2055 // addEmptyHolding() used to check isGlobalFrozen(issuer) and
2056 // !lsfDefaultRipple before the "line already exists" tecDUPLICATE
2057 // short circuit. doWithdraw() calls addEmptyHolding() for a
2058 // self-destination payout and only tolerates tecDUPLICATE, so
2059 // tecINTERNAL from a missing DefaultRipple flag aborted the
2060 // withdrawal. fixCleanup3_4_0 checks existence first and maps the
2061 // create-path DefaultRipple miss to terNO_RIPPLE. Global freeze on an
2062 // existing line is still rejected later by checkWithdrawFreeze.
2063 void
2065 {
2066 using namespace test::jtx;
2067
2068 auto runExistingLine = [this](
2069 FeatureBitset features,
2070 TER selfExpected,
2071 bool issuerGlobalFreeze = false) {
2072 Env env(*this, features);
2073 Account const issuer{"issuer"};
2074 Account const alice{"alice"};
2075 Account const bob{"bob"};
2076
2077 env.fund(XRP(10'000), issuer, alice, bob);
2078 env.close();
2079 env(fset(issuer, asfDefaultRipple));
2080 env.close();
2081
2082 PrettyAsset const usd{issuer["USD"]};
2083 Issue const usdIssue = usd.raw().get<Issue>();
2084 env(trust(alice, usd(10'000)));
2085 env(trust(bob, usd(10'000)));
2086 env.close();
2087 env(pay(issuer, alice, usd(1'000)));
2088 env.close();
2089
2090 Vault const vault{env};
2091 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
2092 env(vaultTx);
2093 env.close();
2094
2095 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
2096 env.close();
2097
2098 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)}));
2099 env.close();
2100
2101 env(fclear(issuer, asfDefaultRipple));
2102 env.close();
2103 if (issuerGlobalFreeze)
2104 {
2105 env(fset(issuer, asfGlobalFreeze));
2106 env.close();
2107 }
2108
2109 BEAST_EXPECT(env.le(keylet::trustLine(alice.id(), usdIssue)));
2110
2111 // Alice's USD line is unchanged; a later deposit still succeeds
2112 // unless the issuer is globally frozen.
2113 if (!issuerGlobalFreeze)
2114 {
2115 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(10)}));
2116 env.close();
2117 }
2118
2119 Number const destBefore = env.balance(alice, usd.raw()).number();
2120 Number const vaultBefore = env.le(vaultKeylet)->at(sfAssetsTotal);
2121 Number const withdrawAmt{50};
2122
2123 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)}),
2124 Ter(selfExpected));
2125 env.close();
2126
2127 Number const destAfter = env.balance(alice, usd.raw()).number();
2128 Number const vaultAfter = env.le(vaultKeylet)->at(sfAssetsTotal);
2129 if (isTesSuccess(selfExpected))
2130 {
2131 BEAST_EXPECT(destAfter == destBefore + withdrawAmt);
2132 BEAST_EXPECT(vaultAfter == vaultBefore - withdrawAmt);
2133 }
2134 else
2135 {
2136 BEAST_EXPECT(destAfter == destBefore);
2137 BEAST_EXPECT(vaultAfter == vaultBefore);
2138 }
2139
2140 if (!issuerGlobalFreeze)
2141 {
2142 auto destTx =
2143 vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)});
2144 destTx[sfDestination] = bob.human();
2145 env(destTx);
2146 env.close();
2147 }
2148 };
2149
2150 auto runDeletedLine = [this](FeatureBitset features, TER selfExpected) {
2151 Env env(*this, features);
2152 Account const issuer{"issuer"};
2153 Account const alice{"alice"};
2154
2155 env.fund(XRP(10'000), issuer, alice);
2156 env.close();
2157 env(fset(issuer, asfDefaultRipple));
2158 env.close();
2159
2160 PrettyAsset const usd{issuer["USD"]};
2161 Issue const usdIssue = usd.raw().get<Issue>();
2162 env(trust(alice, usd(10'000)));
2163 env.close();
2164 env(pay(issuer, alice, usd(500)));
2165 env.close();
2166
2167 Vault const vault{env};
2168 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
2169 env(vaultTx);
2170 env.close();
2171
2172 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
2173 env.close();
2174
2175 env(trust(alice, usd(0)));
2176 env.close();
2177 BEAST_EXPECT(!env.le(keylet::trustLine(alice.id(), usdIssue)));
2178 env(fclear(issuer, asfDefaultRipple));
2179 env.close();
2180
2181 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)}),
2182 Ter(selfExpected));
2183 env.close();
2184 };
2185
2186 auto runCoverWithdraw = [this](FeatureBitset features, TER selfExpected) {
2187 using namespace loan_broker;
2188
2189 Env env(*this, features);
2190 Account const issuer{"issuer"};
2191 Account const alice{"alice"};
2192
2193 env.fund(XRP(10'000), issuer, alice);
2194 env.close();
2195 env(fset(issuer, asfDefaultRipple));
2196 env.close();
2197
2198 PrettyAsset const usd{issuer["USD"]};
2199 Issue const usdIssue = usd.raw().get<Issue>();
2200 env(trust(alice, usd(10'000)));
2201 env.close();
2202 env(pay(issuer, alice, usd(1'000)));
2203 env.close();
2204
2205 Vault const vault{env};
2206 auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
2207 {.owner = alice, .asset = usd, .subscriptionOffset = std::chrono::seconds{60}});
2208 (void)subscriptionDate;
2209 env(createTx);
2210 env.close();
2211
2212 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
2213 env.close();
2214
2215 auto const brokerKeylet =
2216 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2217 env(set(alice, vaultKeylet.key));
2218 env.close();
2219 env(coverDeposit(alice, brokerKeylet.key, usd(100).value()));
2220 env.close();
2221
2222 env(fclear(issuer, asfDefaultRipple));
2223 env.close();
2224 BEAST_EXPECT(env.le(keylet::trustLine(alice.id(), usdIssue)));
2225
2226 Number const destBefore = env.balance(alice, usd.raw()).number();
2227 Number const coverBefore = env.le(brokerKeylet)->at(sfCoverAvailable);
2228 Number const withdrawAmt{50};
2229
2230 env(coverWithdraw(alice, brokerKeylet.key, usd(50).value()), Ter(selfExpected));
2231 env.close();
2232
2233 Number const destAfter = env.balance(alice, usd.raw()).number();
2234 Number const coverAfter = env.le(brokerKeylet)->at(sfCoverAvailable);
2235 if (isTesSuccess(selfExpected))
2236 {
2237 BEAST_EXPECT(destAfter == destBefore + withdrawAmt);
2238 BEAST_EXPECT(coverAfter == coverBefore - withdrawAmt);
2239 }
2240 else
2241 {
2242 BEAST_EXPECT(destAfter == destBefore);
2243 BEAST_EXPECT(coverAfter == coverBefore);
2244 }
2245 };
2246
2247 auto runDeletedCoverWithdraw = [this](FeatureBitset features, TER selfExpected) {
2248 using namespace loan_broker;
2249
2250 Env env(*this, features);
2251 Account const issuer{"issuer"};
2252 Account const alice{"alice"};
2253
2254 env.fund(XRP(10'000), issuer, alice);
2255 env.close();
2256 env(fset(issuer, asfDefaultRipple));
2257 env.close();
2258
2259 PrettyAsset const usd{issuer["USD"]};
2260 Issue const usdIssue = usd.raw().get<Issue>();
2261 env(trust(alice, usd(10'000)));
2262 env.close();
2263 env(pay(issuer, alice, usd(600)));
2264 env.close();
2265
2266 Vault const vault{env};
2267 auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
2268 {.owner = alice, .asset = usd, .subscriptionOffset = std::chrono::seconds{60}});
2269 (void)subscriptionDate;
2270 env(createTx);
2271 env.close();
2272
2273 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
2274 env.close();
2275
2276 auto const brokerKeylet =
2277 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
2278 env(set(alice, vaultKeylet.key));
2279 env.close();
2280 env(coverDeposit(alice, brokerKeylet.key, usd(100).value()));
2281 env.close();
2282
2283 env(trust(alice, usd(0)));
2284 env.close();
2285 BEAST_EXPECT(!env.le(keylet::trustLine(alice.id(), usdIssue)));
2286 env(fclear(issuer, asfDefaultRipple));
2287 env.close();
2288
2289 env(coverWithdraw(alice, brokerKeylet.key, usd(50).value()), Ter(selfExpected));
2290 env.close();
2291 };
2292
2293 auto runPrivateVault = [this](FeatureBitset features, TER selfExpected) {
2294 Env env(*this, features);
2295 Account const issuer{"issuer"};
2296 Account const alice{"alice"};
2297 Account const pdOwner{"pdOwner"};
2298 Account const credIssuer{"credIssuer"};
2299 std::string const credType = "credential";
2300
2301 env.fund(XRP(10'000), issuer, alice, pdOwner, credIssuer);
2302 env.close();
2303 env(fset(issuer, asfDefaultRipple));
2304 env.close();
2305
2306 PrettyAsset const usd{issuer["USD"]};
2307 env(trust(alice, usd(10'000)));
2308 env.close();
2309 env(pay(issuer, alice, usd(1'000)));
2310 env.close();
2311
2312 Vault const vault{env};
2313 auto [vaultTx, vaultKeylet] =
2314 vault.create({.owner = alice, .asset = usd, .flags = tfVaultPrivate});
2315 env(vaultTx);
2316 env.close();
2317
2318 pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}};
2319 env(pdomain::setTx(pdOwner, credentials));
2320 auto const domainId = pdomain::getNewDomain(env.meta());
2321 {
2322 auto domainTx = vault.set({.owner = alice, .id = vaultKeylet.key});
2323 domainTx[sfDomainID] = to_string(domainId);
2324 env(domainTx);
2325 env.close();
2326 }
2327
2328 env(credentials::create(alice, credIssuer, credType));
2329 env(credentials::accept(alice, credIssuer, credType));
2330 env.close();
2331
2332 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
2333 env.close();
2334
2335 env(fclear(issuer, asfDefaultRipple));
2336 env.close();
2337
2338 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)}),
2339 Ter(selfExpected));
2340 env.close();
2341 };
2342
2343 testcase(
2344 "bug: VaultWithdraw to self fails with tecINTERNAL after issuer "
2345 "clears asfDefaultRipple even though the trust line exists "
2346 "(pre-fixCleanup3_4_0)");
2347 runExistingLine(all_ - fixCleanup3_4_0, tecINTERNAL);
2348
2349 testcase(
2350 "bug: VaultWithdraw to self succeeds after issuer clears "
2351 "asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
2352 runExistingLine(all_, tesSUCCESS);
2353
2354 testcase(
2355 "bug: VaultWithdraw to self with an existing line still gets "
2356 "tecFROZEN under asfGlobalFreeze (post-fixCleanup3_4_0)");
2357 runExistingLine(all_, tecFROZEN, true);
2358
2359 testcase(
2360 "bug: VaultWithdraw to self fails with tecINTERNAL after issuer "
2361 "clears asfDefaultRipple and the trust line was deleted "
2362 "(pre-fixCleanup3_4_0)");
2363 runDeletedLine(all_ - fixCleanup3_4_0, tecINTERNAL);
2364
2365 testcase(
2366 "bug: VaultWithdraw to self fails with terNO_RIPPLE after issuer "
2367 "clears asfDefaultRipple and the trust line was deleted "
2368 "(post-fixCleanup3_4_0)");
2369 runDeletedLine(all_, terNO_RIPPLE);
2370
2371 testcase(
2372 "bug: LoanBrokerCoverWithdraw to self fails with tecINTERNAL after "
2373 "issuer clears asfDefaultRipple even though the trust line exists "
2374 "(pre-fixCleanup3_4_0)");
2375 runCoverWithdraw(all_ - fixCleanup3_4_0, tecINTERNAL);
2376
2377 testcase(
2378 "bug: LoanBrokerCoverWithdraw to self succeeds after issuer clears "
2379 "asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
2380 runCoverWithdraw(all_, tesSUCCESS);
2381
2382 testcase(
2383 "bug: LoanBrokerCoverWithdraw to self fails with tecINTERNAL after "
2384 "issuer clears asfDefaultRipple and the trust line was deleted "
2385 "(pre-fixCleanup3_4_0)");
2386 runDeletedCoverWithdraw(all_ - fixCleanup3_4_0, tecINTERNAL);
2387
2388 testcase(
2389 "bug: LoanBrokerCoverWithdraw to self fails with terNO_RIPPLE after "
2390 "issuer clears asfDefaultRipple and the trust line was deleted "
2391 "(post-fixCleanup3_4_0)");
2392 runDeletedCoverWithdraw(all_, terNO_RIPPLE);
2393
2394 testcase(
2395 "bug: private VaultWithdraw to self fails with tecINTERNAL after "
2396 "issuer clears asfDefaultRipple even though the trust line exists "
2397 "(pre-fixCleanup3_4_0)");
2398 runPrivateVault(all_ - fixCleanup3_4_0, tecINTERNAL);
2399
2400 testcase(
2401 "bug: private VaultWithdraw to self succeeds after issuer clears "
2402 "asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
2403 runPrivateVault(all_, tesSUCCESS);
2404 }
2405
2406 // Bug 1: a sponsored XRP VaultWithdraw to a distinct destination is
2407 // rejected because the vault invariant treats the holder's touched
2408 // but economically unchanged AccountRoot as a second payout
2409 // recipient. Sequence/ticket processing still touches the holder
2410 // while the sponsor pays the fee, so the holder's XRP delta is
2411 // present-zero and is not normalized away. If this happens on the
2412 // last Subscription ledger of a closed-ended vault, the holder
2413 // cannot retry until Redemption (tecTOO_SOON during Investment).
2414 //
2415 // Fixed by ValidVault::deltaAssetsForParty always collapsing an
2416 // economically-zero XRP delta to absence, regardless of who paid the
2417 // fee.
2418 void
2420 {
2421 using namespace test::jtx;
2422
2423 auto runScenario = [this](FeatureBitset features, TER expected) {
2424 Env env{*this, features};
2425 Account const owner{"owner"};
2426 Account const holder{"holder"};
2427 Account const destination{"destination"};
2428 Account const sponsor{"sponsor"};
2429 env.fund(XRP(10'000), owner, holder, destination, sponsor);
2430 env.close();
2431
2432 constexpr std::uint32_t investmentPeriod = 14u * 24u * 60u * 60u;
2433 auto const [vault, vaultKeylet, subscriptionDate, redemptionDate] =
2434 makeClosedEndedVault(env, owner, xrpIssue(), 120u, investmentPeriod);
2435 BEAST_EXPECT(redemptionDate - subscriptionDate == investmentPeriod);
2436
2437 env(vault.deposit(
2438 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()}));
2439 env.close();
2440
2441 // Inclusive SubscriptionDate boundary: still Subscription, so an
2442 // ordinary withdrawal is allowed.
2443 closeToTime(env, Tp{D{subscriptionDate}});
2444
2445 auto const vaultBefore = env.le(vaultKeylet);
2446 if (!BEAST_EXPECT(vaultBefore))
2447 return;
2448 auto const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
2449 auto const holderBalanceBefore = env.balance(holder);
2450 auto const destinationBalanceBefore = env.balance(destination);
2451 auto const sponsorBalanceBefore = env.balance(sponsor);
2452 auto const fee = env.current()->fees().base;
2453
2454 auto withdraw = vault.withdraw(
2455 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
2456 withdraw[sfDestination] = destination.human();
2457 env(withdraw,
2458 Fee(fee),
2459 sponsor::As(sponsor, spfSponsorFee),
2460 Sig(sfSponsorSignature, sponsor),
2461 Ter(expected));
2462 env.close();
2463
2464 auto const vaultAfter = env.le(vaultKeylet);
2465 if (!BEAST_EXPECT(vaultAfter))
2466 return;
2467 BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore - fee);
2468
2469 if (expected == tesSUCCESS)
2470 {
2471 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - XRP(100).value());
2472 BEAST_EXPECT(env.balance(holder) == holderBalanceBefore);
2473 BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore + XRP(100));
2474 return;
2475 }
2476
2477 // Invariant rollback: the payout and share burn are undone, but
2478 // sequence processing and the sponsored fee charge remain.
2479 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore);
2480 BEAST_EXPECT(env.balance(holder) == holderBalanceBefore);
2481 BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore);
2482
2483 // Once the ledger advances into Investment, the same holder
2484 // cannot retry until Redemption.
2485 auto retry = vault.withdraw(
2486 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
2487 retry[sfDestination] = destination.human();
2488 env(retry, Ter(tecTOO_SOON));
2489 };
2490
2491 testcase(
2492 "bug: sponsored XRP withdrawal to a distinct destination misreads a "
2493 "touched-but-zero sender delta as a second recipient "
2494 "(pre-fixCleanup3_4_0)");
2495 runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
2496
2497 testcase(
2498 "bug: sponsored XRP withdrawal to a distinct destination succeeds "
2499 "(post-fixCleanup3_4_0)");
2500 runScenario(all_, tesSUCCESS);
2501 }
2502
2503 // Bug 2: a co-signed fee sponsor named as the withdrawal's own
2504 // destination pays its fee from the same AccountRoot it is paid into,
2505 // so its net XRP delta is (payout - fee). The invariant never fee-
2506 // corrected the destination side at all, so this always failed the
2507 // equal-amount check against the vault's outflow (payout).
2508 //
2509 // Fixed by ValidVault::deltaAssetsForParty adding the fee back onto
2510 // whichever inspected party's AccountRoot actually paid it -- the
2511 // sender, or a distinct destination -- not just the sender.
2512 void
2514 {
2515 using namespace test::jtx;
2516
2517 auto runScenario = [this](FeatureBitset features, TER expected) {
2518 Env env{*this, features};
2519 Account const owner{"owner"};
2520 Account const holder{"holder"};
2521 Account const sponsor{"sponsor"};
2522 env.fund(XRP(10'000), owner, holder, sponsor);
2523 env.close();
2524
2525 Vault const vault{env};
2526 auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = xrpIssue()});
2527 env(vaultTx);
2528 env.close();
2529
2530 env(vault.deposit(
2531 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()}));
2532 env.close();
2533
2534 auto const vaultBefore = env.le(vaultKeylet);
2535 if (!BEAST_EXPECT(vaultBefore))
2536 return;
2537 auto const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
2538 auto const sponsorBalanceBefore = env.balance(sponsor);
2539 auto const fee = env.current()->fees().base;
2540
2541 // The sponsor both receives the withdrawal (as sfDestination)
2542 // and pays its own fee (co-signed) from the same AccountRoot.
2543 auto withdraw = vault.withdraw(
2544 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
2545 withdraw[sfDestination] = sponsor.human();
2546 env(withdraw,
2547 Fee(fee),
2548 sponsor::As(sponsor, spfSponsorFee),
2549 Sig(sfSponsorSignature, sponsor),
2550 Ter(expected));
2551 env.close();
2552
2553 auto const vaultAfter = env.le(vaultKeylet);
2554 if (!BEAST_EXPECT(vaultAfter))
2555 return;
2556
2557 if (expected == tesSUCCESS)
2558 {
2559 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - XRP(100).value());
2560 // Paid the withdrawal, then separately debited for the fee
2561 // it chose to cover; net effect is payout minus fee.
2562 BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore + XRP(100) - fee);
2563 return;
2564 }
2565
2566 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore);
2567 BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore - fee);
2568 };
2569
2570 testcase(
2571 "bug: co-signed sponsor named as withdrawal destination has its "
2572 "own fee debit misread as breaking the payout equality "
2573 "(pre-fixCleanup3_4_0)");
2574 runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
2575
2576 testcase(
2577 "bug: co-signed sponsor named as withdrawal destination succeeds "
2578 "(post-fixCleanup3_4_0)");
2579 runScenario(all_, tesSUCCESS);
2580 }
2581
2582 // Pre-funded fee sponsorship draws the fee from ltSponsorship.sfFeeAmount,
2583 // so feePayerAccountRoot must return nullopt rather than the sponsor's
2584 // AccountRoot. A bystander sponsor leaves that branch unexercised: the
2585 // result is only consulted by deltaAssetsForParty via `payer && *payer ==
2586 // id`. Naming the sponsor as sfDestination makes the early return
2587 // load-bearing -- returning the sponsor's id instead of nullopt would add
2588 // the fee back onto a balance that never paid it, and the equal-amount
2589 // check against the vault outflow would fail.
2590 //
2591 // Contrast testBugSponsorAsDestinationFeeMisappliedToPayout, where the
2592 // sponsor co-signs and so really does pay from its own AccountRoot.
2593 void
2595 {
2596 using namespace test::jtx;
2597
2598 auto runScenario = [this](
2599 FeatureBitset features,
2600 TER expected,
2601 bool const sponsorIsDestination) {
2602 Env env{*this, features};
2603 Account const owner{"owner"};
2604 Account const holder{"holder"};
2605 Account const destination{"destination"};
2606 Account const sponsor{"sponsor"};
2607 env.fund(XRP(10'000), owner, holder, destination, sponsor);
2608 env.close();
2609
2610 Vault const vault{env};
2611 auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = xrpIssue()});
2612 env(vaultTx);
2613 env.close();
2614
2615 env(vault.deposit(
2616 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()}));
2617 env.close();
2618
2619 auto const fee = env.current()->fees().base;
2620 env(sponsor::set_fee(sponsor, 0, fee), sponsor::SponseeAcc(holder));
2621 env.close();
2622
2623 auto const vaultBefore = env.le(vaultKeylet);
2624 if (!BEAST_EXPECT(vaultBefore))
2625 return;
2626 auto const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
2627 auto const holderBalanceBefore = env.balance(holder);
2628 auto const destinationBalanceBefore = env.balance(destination);
2629 auto const sponsorBalanceBefore = env.balance(sponsor);
2630
2631 Account const& recipient = sponsorIsDestination ? sponsor : destination;
2632 auto withdraw = vault.withdraw(
2633 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
2634 withdraw[sfDestination] = recipient.human();
2635 env(withdraw, Fee(fee), sponsor::As(sponsor, spfSponsorFee), Ter(expected));
2636 env.close();
2637
2638 auto const vaultAfter = env.le(vaultKeylet);
2639 if (!BEAST_EXPECT(vaultAfter))
2640 return;
2641 // Holder is economically unchanged (sequence only); the fee is
2642 // taken from the sponsorship object, not any AccountRoot.
2643 BEAST_EXPECT(env.balance(holder) == holderBalanceBefore);
2644
2645 if (expected == tesSUCCESS)
2646 {
2647 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - XRP(100).value());
2648 if (sponsorIsDestination)
2649 {
2650 // The sponsor receives the payout and is not debited for
2651 // the fee. The sponsor has to BE the destination for
2652 // FeePayerType::SponsorPreFunded to matter.
2653 BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore + XRP(100));
2654 }
2655 else
2656 {
2657 BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore + XRP(100));
2658 BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore);
2659 }
2660 auto const sponsorship = env.le(keylet::sponsorship(sponsor, holder));
2661 if (!BEAST_EXPECT(sponsorship))
2662 return;
2663 BEAST_EXPECT(!sponsorship->isFieldPresent(sfFeeAmount));
2664 return;
2665 }
2666
2667 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore);
2668 BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore);
2669 if (!sponsorIsDestination)
2670 BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore);
2671 };
2672
2673 testcase(
2674 "pre-funded fee XRP withdrawal to a distinct destination succeeds "
2675 "(post-fixCleanup3_4_0)");
2676 runScenario(all_, tesSUCCESS, false);
2677
2678 testcase(
2679 "bug: pre-funded sponsor named as withdrawal destination misreads "
2680 "the sender's touched-but-zero delta as a second recipient "
2681 "(pre-fixCleanup3_4_0)");
2682 runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED, true);
2683
2684 testcase(
2685 "bug: pre-funded sponsor named as withdrawal destination receives "
2686 "the full payout (post-fixCleanup3_4_0)");
2687 runScenario(all_, tesSUCCESS, true);
2688 }
2689
2690 // Unsponsored third-party XRP withdrawal: the sender's AccountRoot moves
2691 // by exactly -fee. Pre-amendment, the sender-only fee correction then
2692 // collapses that to absence so the dual-recipient guard does not fire.
2693 void
2695 {
2696 using namespace test::jtx;
2697
2698 testcase(
2699 "unsponsored XRP withdrawal to a distinct destination succeeds "
2700 "(pre-fixCleanup3_4_0)");
2701
2702 Env env{*this, all_ - fixCleanup3_4_0};
2703 Account const owner{"owner"};
2704 Account const holder{"holder"};
2705 Account const destination{"destination"};
2706 env.fund(XRP(10'000), owner, holder, destination);
2707 env.close();
2708
2709 Vault const vault{env};
2710 auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = xrpIssue()});
2711 env(vaultTx);
2712 env.close();
2713
2714 env(vault.deposit(
2715 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()}));
2716 env.close();
2717
2718 auto const vaultBefore = env.le(vaultKeylet);
2719 if (!BEAST_EXPECT(vaultBefore))
2720 return;
2721 auto const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
2722 auto const holderBalanceBefore = env.balance(holder);
2723 auto const destinationBalanceBefore = env.balance(destination);
2724 auto const fee = env.current()->fees().base;
2725
2726 auto withdraw = vault.withdraw(
2727 {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
2728 withdraw[sfDestination] = destination.human();
2729 env(withdraw, Fee(fee), Ter(tesSUCCESS));
2730 env.close();
2731
2732 auto const vaultAfter = env.le(vaultKeylet);
2733 if (!BEAST_EXPECT(vaultAfter))
2734 return;
2735 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - XRP(100).value());
2736 BEAST_EXPECT(env.balance(holder) == holderBalanceBefore - fee);
2737 BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore + XRP(100));
2738 }
2739
2740public:
2741 void
2768};
2769
2771
2772} // namespace xrpl
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
constexpr TIss const & get() const
A currency issued by an account.
Definition Issue.h:18
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
std::chrono::duration< rep, period > duration
Definition chrono.h:47
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
void testBugSelfWithdrawAfterIssuerClearsDefaultRipple()
void testBugSponsorAsDestinationFeeMisappliedToPayout()
void testBugVaultLockedByPartialWithdraw()
void testVaultWithdrawEqualityEnforced()
void testVaultDepositNegativeBalanceFromOppositeLimit()
std::optional< RoundTripOvershootVault > makeRoundTripOvershootVault(test::jtx::Env &env)
void testBugSponsoredWithdrawZeroDeltaMisclassifiedAsSecondRecipient()
std::optional< ImpairedLoanVault > makeImpairedLoanVault(test::jtx::Env &env, int otherDeposit)
void testBugVaultDepositOvercreditsAcrossScaleBoundary()
void testBugVaultDustDebitCanonicalizesToNoOp()
void testUnsponsoredWithdrawToDistinctDestinationPreAmendment()
void testVaultDepositCanonicalizeToZero()
void testBugMptZeroWithdrawMissingHolding()
void run() override
Runs the suite.
void testVaultWithdrawCanonicalizeToZero()
void testBugIouZeroWithdrawMissingTrustLine()
void testBugDepositShareTruncationSubUlp()
Shared base for the Vault*_test family under src/test/app/vault/.
NetClock::duration D
NetClock::time_point Tp
FeatureBitset const all_
static ClosedEndedSetup makeClosedEndedVault(test::jtx::Env &env, test::jtx::Account const &owner, Asset const &asset, std::uint32_t subOffset, std::uint32_t gap)
void closeToTime(test::jtx::Env &env, NetClock::time_point time, std::source_location const &loc=std::source_location::current())
test::jtx::PrettyAsset PrettyAsset
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:354
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
T make_unique(T... args)
Keylet computation functions.
Definition Indexes.h:40
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet sponsorship(AccountID const &sponsor, AccountID const &sponsee) noexcept
A Sponsorship.
Definition Indexes.cpp:354
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:275
FeatureBitset testableAmendments()
Definition Env.h:92
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr FlagValue spfSponsorFee
Definition TxFlags.h:465
@ terNO_RIPPLE
Definition TER.h:225
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
std::optional< STAmount > assetsToSharesWithdraw(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const &assets, TruncateShares truncate=TruncateShares::No, WaiveUnrealizedLoss waive=WaiveUnrealizedLoss::No)
From the perspective of a vault, return the number of shares to demand from the depositor when they a...
STAmount clawbackAmount(SLE::ConstRef vault, std::optional< STAmount > const &maybeAmount, AccountID const &account)
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:127
@ tefINTERNAL
Definition TER.h:168
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:54
std::map< std::string, AmendmentSupport > const & allAmendments()
All amendments libxrpl knows about.
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecINTERNAL
Definition TER.h:318
@ tecTOO_SOON
Definition TER.h:326
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tecFROZEN
Definition TER.h:311
@ tecINSUFFICIENT_FUNDS
Definition TER.h:333
@ tecNO_LINE
Definition TER.h:309
@ tecPRECISION_LOSS
Definition TER.h:371
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
@ tesSUCCESS
Definition TER.h:250
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21
static json::Value clawback(ClawbackArgs const &args)
Definition vault.cpp:111