xrpld
Loading...
Searching...
No Matches
VaultInvariantPrecision_test.cpp
1#include <test/app/vault/VaultPrecisionFixture.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/amount.h>
4#include <test/jtx/envconfig.h>
5#include <test/jtx/ter.h>
6#include <test/jtx/vault.h>
7
8#include <xrpl/basics/Number.h>
9#include <xrpl/beast/unit_test/suite.h>
10#include <xrpl/beast/utility/Journal.h>
11#include <xrpl/protocol/Feature.h>
12#include <xrpl/protocol/MPTIssue.h>
13#include <xrpl/protocol/STAmount.h>
14#include <xrpl/protocol/TER.h>
15
16#include <algorithm>
17#include <array>
18#include <cstdint>
19#include <string>
20#include <tuple>
21
22namespace xrpl::test {
23
24// With fixCleanup3_4_0 disabled the six delta invariants and the
25// lossUnrealized > (assetsTotal - assetsAvailable) gap invariant spuriously
26// fire on legitimate flows; with the amendment enabled the one-unit
27// tolerance absorbs the sub-ULP drift and every one of these transactions
28// must succeed. Exactness (assetsTotal delta == assetsAvailable delta
29// exactly) is covered by VaultTransactorPrecision_test.
31{
32 // Deposit small integer amounts into an A-1 vault. Pre-amendment,
33 // deposits of 1, 7, and 10'000'000 land on assetsTotal/assetsAvailable
34 // grids that disagree by one ULP and the invariant fires. Post-
35 // amendment the tolerance-widened check accepts the same states.
36 void
38 {
39 using namespace jtx;
40
41 bool const fixEnabled = features[fixCleanup3_4_0];
43 std::string("A-1 deposit boundary invariant") +
44 (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
45
46 std::array<int, 3> const kAmounts{1, 7, 10'000'000};
47
48 for (auto const amount : kAmounts)
49 {
50 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
51 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
52 if (!f.asset || !f.broker)
53 {
54 BEAST_EXPECT(f.asset && f.broker);
55 continue;
56 }
57 auto const& asset = *f.asset;
58
59 auto const before = read(env, f);
60
61 Vault const v{env};
62 env(v.deposit(
63 {.depositor = f.depositor,
64 .id = f.vaultKeylet.key,
65 .amount = asset(amount).value()}),
66 Ter(std::ignore));
67 env.close();
68
69 TER const actual = env.ter();
70
71 if (fixEnabled)
72 {
73 BEAST_EXPECTS(
74 actual == tesSUCCESS,
75 "amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
76 transToken(actual));
77
78 auto const after = read(env, f);
79 Number const tDelta = after.assetsTotal - before.assetsTotal;
80 Number const aDelta = after.assetsAvailable - before.assetsAvailable;
81 Number const requested = asset(amount).number();
82
83 BEAST_EXPECT(tDelta <= requested);
84
85 Number const gap = tDelta > aDelta ? tDelta - aDelta : aDelta - tDelta;
86 BEAST_EXPECT(gap <= oneUnit(asset, after.assetsTotal));
87 }
88 else
89 {
90 BEAST_EXPECTS(
91 actual == tecINVARIANT_FAILED,
92 "amount=" + std::to_string(amount) + " expected tecINVARIANT_FAILED, got " +
93 transToken(actual));
94 }
95 }
96 }
97
98 // Withdraw long-mantissa share counts from an A-1 vault. Pre-fix
99 // some counts trip the withdraw delta invariants; post-fix none does.
100 void
102 {
103 using namespace jtx;
104
105 bool const fixEnabled = features[fixCleanup3_4_0];
106 testcase(
107 std::string("A-1 withdraw boundary invariant") +
108 (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
109
110 std::array<std::uint64_t, 6> const kShareCounts{
111 99'999u, 100'001u, 333'333u, 1'234'567u, 142'857'142u, 333'333'333u};
112
113 // Fill the vault with enough shares that every count below is
114 // available to the depositor.
115 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
116 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
117 if (!f.asset || !f.broker)
118 {
119 BEAST_EXPECT(f.asset && f.broker);
120 return;
121 }
122 auto const& asset = *f.asset;
123
124 Vault const v{env};
125 // Deposit a large amount so we can afford every withdrawal below.
126 env(v.deposit(
127 {.depositor = f.depositor,
128 .id = f.vaultKeylet.key,
129 .amount = asset(1'000'000).value()}),
130 Ter(std::ignore));
131 env.close();
132
133 for (auto const count : kShareCounts)
134 {
135 auto const before = read(env, f);
136 if (before.sharesTotal < count)
137 continue;
138
139 STAmount const shareAmount{MPTIssue{f.share}, Number{static_cast<std::int64_t>(count)}};
140 env(v.withdraw(
141 {.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = shareAmount}),
142 Ter(std::ignore));
143 env.close();
144
145 TER const actual = env.ter();
146
147 if (fixEnabled)
148 {
149 BEAST_EXPECTS(
150 actual != tecINVARIANT_FAILED,
151 "shares=" + std::to_string(count) + " unexpected invariant failure");
152
153 if (actual == tesSUCCESS)
154 {
155 auto const after = read(env, f);
156 Number const tDelta = before.assetsTotal - after.assetsTotal;
157 Number const pDelta = before.pseudo - after.pseudo;
158 Number const gap = tDelta > pDelta ? tDelta - pDelta : pDelta - tDelta;
159 // VaultTransactorPrecision_test tightens this to strict
160 // equality.
161 BEAST_EXPECT(gap <= oneUnit(asset, before.assetsTotal));
162 }
163 }
164 // Pre-fix behaviour is fixture-dependent: some share counts may
165 // succeed even without the amendment. The important property is
166 // that post-fix no legitimate withdrawal is rejected by the
167 // widened invariant.
168 }
169 }
170
171 // Clawback of small IOU amounts against a live-loan vault. Pre-fix
172 // some amounts trip the clawback delta invariants; post-fix none does.
173 // Also assert the owner force-burn path returns tecNO_PERMISSION
174 // under both amendment states (it never enters assetsToClawback).
175 void
177 {
178 using namespace jtx;
179
180 bool const fixEnabled = features[fixCleanup3_4_0];
181 testcase(
182 std::string("A-1 clawback boundary invariant") +
183 (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
184
185 std::array<int, 6> const kAmounts{1, 7, 99, 333, 993, 2000};
186
187 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
188 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false, /*allowClawback=*/true);
189 if (!f.asset || !f.broker)
190 {
191 BEAST_EXPECT(f.asset && f.broker);
192 return;
193 }
194 auto const& asset = *f.asset;
195
196 Vault const v{env};
197
198 // Give the depositor a stake so that the issuer has something to
199 // claw back.
200 env(v.deposit(
201 {.depositor = f.depositor,
202 .id = f.vaultKeylet.key,
203 .amount = asset(2'000).value()}),
204 Ter(std::ignore));
205 env.close();
206
207 for (auto const amount : kAmounts)
208 {
209 auto const before = read(env, f);
210 if (before.sharesTotal == 0)
211 continue;
212
213 env(v.clawback(
214 {.issuer = f.issuer,
215 .id = f.vaultKeylet.key,
216 .holder = f.depositor,
217 .amount = asset(amount).value()}),
218 Ter(std::ignore));
219 env.close();
220
221 TER const actual = env.ter();
222
223 if (fixEnabled)
224 {
225 BEAST_EXPECTS(
226 actual != tecINVARIANT_FAILED,
227 "amount=" + std::to_string(amount) + " unexpected invariant failure");
228 }
229 // Pre-fix behaviour is fixture-dependent: some clawback amounts
230 // may succeed even without the amendment. The important
231 // property is that post-fix no legitimate clawback is rejected
232 // by the widened invariant.
233 }
234
235 // Owner force-burn only succeeds against an EMPTY vault (see
236 // VaultClawback::preclaim). Our fixture keeps a live loan, so
237 // this must return tecNO_PERMISSION regardless of the amendment.
238 env(v.clawback({.issuer = f.lender, .id = f.vaultKeylet.key, .holder = f.depositor}),
240 env.close();
241 }
242
243 // Deposit into an A-3 vault where the impaired-loan gap plus the
244 // interest earned from the sibling repayment lands L > (T - A) by
245 // sub-ULP. Pre-fix the loss invariant fires; post-fix it does not.
246 void
248 {
249 using namespace jtx;
250
251 bool const fixEnabled = features[fixCleanup3_4_0];
252 testcase(
253 std::string("A-3 loss invariant sweep") +
254 (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
255
256 std::array<int, 3> const kAmounts{1, 7, 10'000'000};
257
258 for (auto const amount : kAmounts)
259 {
260 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
261 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/true);
262 if (!f.asset || !f.broker)
263 {
264 BEAST_EXPECT(f.asset && f.broker);
265 continue;
266 }
267 auto const& asset = *f.asset;
268
269 Vault const v{env};
270 env(v.deposit(
271 {.depositor = f.depositor,
272 .id = f.vaultKeylet.key,
273 .amount = asset(amount).value()}),
274 Ter(std::ignore));
275 env.close();
276
277 TER const actual = env.ter();
278
279 if (fixEnabled)
280 {
281 BEAST_EXPECTS(
282 actual == tesSUCCESS,
283 "amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
284 transToken(actual));
285
286 auto const after = read(env, f);
287 BEAST_EXPECT(
288 after.lossUnrealized <= (after.assetsTotal - after.assetsAvailable) +
289 oneUnit(asset, after.assetsTotal));
290 }
291 else
292 {
293 BEAST_EXPECTS(
294 actual == tecINVARIANT_FAILED,
295 "amount=" + std::to_string(amount) + " expected tecINVARIANT_FAILED, got " +
296 transToken(actual));
297 }
298 }
299 }
300
301 // Full 17-magnitude A-1 deposit sweep. Pre-fix {1, 7, 10'000'000}
302 // are the boundary amounts that fail; post-fix every amount succeeds.
303 void
305 {
306 using namespace jtx;
307
308 bool const fixEnabled = features[fixCleanup3_4_0];
309 testcase(
310 std::string("A-1 deposit magnitude sweep") +
311 (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
312
313 std::array<int, 17> const kAmounts{
314 1,
315 2,
316 5,
317 7,
318 10,
319 50,
320 100,
321 500,
322 1'000,
323 5'000,
324 10'000,
325 50'000,
326 100'000,
327 500'000,
328 1'000'000,
329 5'000'000,
330 10'000'000};
331 std::array<int, 3> const kPreFixFailures{1, 7, 10'000'000};
332
333 for (auto const amount : kAmounts)
334 {
335 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
336 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
337 if (!f.asset || !f.broker)
338 {
339 BEAST_EXPECT(f.asset && f.broker);
340 continue;
341 }
342 auto const& asset = *f.asset;
343
344 Vault const v{env};
345 env(v.deposit(
346 {.depositor = f.depositor,
347 .id = f.vaultKeylet.key,
348 .amount = asset(amount).value()}),
349 Ter(std::ignore));
350 env.close();
351
352 TER const actual = env.ter();
353
354 if (fixEnabled)
355 {
356 BEAST_EXPECTS(
357 actual == tesSUCCESS,
358 "amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
359 transToken(actual));
360 }
361 else
362 {
363 bool const shouldFail =
364 std::ranges::find(kPreFixFailures, amount) != kPreFixFailures.end();
365 if (shouldFail)
366 {
367 BEAST_EXPECTS(
368 actual == tecINVARIANT_FAILED,
369 "pre-fix amount=" + std::to_string(amount) +
370 " expected tecINVARIANT_FAILED, got " + transToken(actual));
371 }
372 // For other amounts pre-fix, we accept any outcome; the
373 // interesting property is only asserted for the known-failing
374 // ones.
375 }
376 }
377 }
378
379 // A-3 deposit sweep. Pre-fix {1, 7, 10'000, 10'000'000} fail; post-fix
380 // every amount succeeds. 99'999 (delta tolerance) and 10'000'000
381 // (loss tolerance) are the two boundary cases that motivate this PR.
382 void
384 {
385 using namespace jtx;
386
387 bool const fixEnabled = features[fixCleanup3_4_0];
388 testcase(
389 std::string("A-3 deposit magnitude sweep") +
390 (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
391
392 std::array<int, 9> const kAmounts{
393 1, 7, 100, 1'000, 10'000, 100'000, 1'000'000, 10'000'000, 99'999};
394
395 std::array<int, 4> const kPreFixFailures{1, 7, 10'000, 10'000'000};
396
397 for (auto const amount : kAmounts)
398 {
399 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
400 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/true);
401 if (!f.asset || !f.broker)
402 {
403 BEAST_EXPECT(f.asset && f.broker);
404 continue;
405 }
406 auto const& asset = *f.asset;
407
408 Vault const v{env};
409 env(v.deposit(
410 {.depositor = f.depositor,
411 .id = f.vaultKeylet.key,
412 .amount = asset(amount).value()}),
413 Ter(std::ignore));
414 env.close();
415
416 TER const actual = env.ter();
417
418 if (fixEnabled)
419 {
420 BEAST_EXPECTS(
421 actual == tesSUCCESS,
422 "amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
423 transToken(actual));
424 }
425 else
426 {
427 bool const shouldFail =
428 std::ranges::find(kPreFixFailures, amount) != kPreFixFailures.end();
429 if (shouldFail)
430 {
431 BEAST_EXPECTS(
432 actual == tecINVARIANT_FAILED,
433 "pre-fix amount=" + std::to_string(amount) +
434 " expected tecINVARIANT_FAILED, got " + transToken(actual));
435 }
436 }
437 }
438 }
439
440public:
441 void
442 run() override
443 {
444 for (auto const& features : {all_ - fixCleanup3_4_0, all_})
445 {
449 testLossInvariantA3(features);
450 testA1DepositMagnitudes(features);
451 testA3DepositMagnitudes(features);
452 }
453 }
454};
455
456BEAST_DEFINE_TESTSUITE(VaultInvariantPrecision, app, xrpl);
457
458} // namespace xrpl::test
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
FeatureBitset const all_
static Number oneUnit(Asset const &asset, Number const &assetsTotalAfter)
static Numbers read(jtx::Env const &env, Fixture const &f)
static Fixture setupSingleLoanVault(jtx::Env &env, bool impairAndPaySibling, bool allowClawback=false)
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
TER ter() const
Return the TER for the last JTx.
Definition Env.h:844
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
T end(T... args)
T find(T... args)
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:38
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
std::string transToken(TER code)
Definition TER.cpp:257
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tecNO_PERMISSION
Definition TER.h:313
@ tesSUCCESS
Definition TER.h:250
static json::Value withdraw(WithdrawArgs const &args)
Definition vault.cpp:100
static json::Value clawback(ClawbackArgs const &args)
Definition vault.cpp:111
static json::Value deposit(DepositArgs const &args)
Definition vault.cpp:89
T to_string(T... args)