xrpld
Loading...
Searching...
No Matches
VaultTransactorPrecision_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 <array>
17#include <cstddef>
18#include <cstdint>
19#include <optional>
20#include <string>
21#include <tuple>
22
23namespace xrpl::test {
24
25// With fixCleanup3_4_0, deposit/withdraw/clawback apply one amount on the
26// sfAssetsTotal grid. These tests require T, A, and the pseudo-account to
27// change by the same Number; the invariant suite still allows a one-unit gap.
29{
32 {
33 return jtx::Env{*this, jtx::envconfig(), all_, nullptr, beast::Severity::Disabled};
34 }
35
36 bool
37 ready(Fixture const& f)
38 {
39 return BEAST_EXPECT(f.asset && f.broker) && f.asset;
40 }
41
42 void
43 assertEqualDeltas(Numbers const& before, Numbers const& after, std::string const& tag)
44 {
45 Number const tDelta = before.assetsTotal - after.assetsTotal;
46 Number const aDelta = before.assetsAvailable - after.assetsAvailable;
47 Number const pDelta = before.pseudo - after.pseudo;
48 BEAST_EXPECTS(tDelta == aDelta, tag + " tDelta != aDelta");
49 BEAST_EXPECTS(tDelta == pDelta, tag + " tDelta != pDelta");
50 }
51
52 void
54 {
55 using namespace jtx;
56
57 testcase("deposit clamp does not over-credit");
58
59 std::array<int, 4> const kAmounts{1, 7, 1'000, 10'000'000};
60
61 for (auto const amount : kAmounts)
62 {
63 Env env = makeEnv();
64 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
65 if (!ready(f))
66 continue;
67 // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
68 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
69 jtx::PrettyAsset const& asset = f.asset.value();
70
71 auto const before = read(env, f);
72
73 Vault const v{env};
74 env(v.deposit(
75 {.depositor = f.depositor,
76 .id = f.vaultKeylet.key,
77 .amount = asset(amount).value()}),
78 Ter(std::ignore));
79 env.close();
80
81 if (env.ter() != tesSUCCESS)
82 continue;
83
84 auto const after = read(env, f);
85 Number const tDelta = after.assetsTotal - before.assetsTotal;
86 Number const requested = asset(amount).number();
87 BEAST_EXPECTS(
88 tDelta <= requested,
89 "amount=" + std::to_string(amount) + " tDelta exceeds requested");
90
91 Number const sharesMinted = after.sharesTotal - before.sharesTotal;
92 if (before.sharesTotal == Number{0})
93 continue;
94 Number const shareValue = (before.assetsTotal * sharesMinted) / before.sharesTotal;
95 // The depositor is never charged more than the shares they received are worth.
96 BEAST_EXPECTS(
97 tDelta <= shareValue,
98 "amount=" + std::to_string(amount) + " assetsTaken > shareValue");
99 // Discount is strictly less than one ULP of the new AssetsTotal
100 BEAST_EXPECTS(
101 shareValue - tDelta < oneUnit(asset.raw(), after.assetsTotal),
102 "amount=" + std::to_string(amount) + " discount is not below one unit");
103 }
104
105 {
106 Env env = makeEnv();
107 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
108 if (!ready(f))
109 return;
110 // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
111 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
112 jtx::PrettyAsset const& asset = f.asset.value();
113
114 Vault const v{env};
115 env(v.deposit(
116 {.depositor = f.depositor,
117 .id = f.vaultKeylet.key,
118 .amount = asset(99'000'000).value()}),
119 Ter(std::ignore));
120 env.close();
121
122 auto const before = read(env, f);
123 Number const kLowerBound{1, 6};
124 BEAST_EXPECT(before.assetsTotal > kLowerBound);
125
126 auto const tinyAmount = asset(Number{1, -10}).value();
127 env(v.deposit(
128 {.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = tinyAmount}),
129 Ter(std::ignore));
130 env.close();
131
132 BEAST_EXPECTS(
133 env.ter() == tecPRECISION_LOSS,
134 std::string{"expected tecPRECISION_LOSS, got "} + transToken(env.ter()));
135
136 auto const after = read(env, f);
137 BEAST_EXPECT(after.assetsTotal == before.assetsTotal);
138 BEAST_EXPECT(after.assetsAvailable == before.assetsAvailable);
139 BEAST_EXPECT(after.sharesTotal == before.sharesTotal);
140 }
141 }
142
143 void
145 {
146 using namespace jtx;
147
148 testcase("withdraw deltas are equal");
149
150 Env env = makeEnv();
151 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
152 if (!ready(f))
153 return;
154 // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
155 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
156 jtx::PrettyAsset const& asset = f.asset.value();
157
158 Vault const v{env};
159 env(v.deposit(
160 {.depositor = f.depositor,
161 .id = f.vaultKeylet.key,
162 .amount = asset(1'000'000).value()}),
163 Ter(std::ignore));
164 env.close();
165
166 auto checkSuccess = [&](STAmount const& amount, std::string const& tag) {
167 auto const before = read(env, f);
168 env(v.withdraw({.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = amount}),
169 Ter(std::ignore));
170 env.close();
171 if (env.ter() != tesSUCCESS)
172 return;
173
174 auto const after = read(env, f);
175 assertEqualDeltas(before, after, tag);
176
177 Number const sharesBurned = before.sharesTotal - after.sharesTotal;
178 if (before.sharesTotal == Number{0})
179 return;
180 Number const shareValue = (before.assetsTotal * sharesBurned) / before.sharesTotal;
181 Number const tDelta = before.assetsTotal - after.assetsTotal;
182 BEAST_EXPECTS(tDelta <= shareValue, tag + " payout > shareValue");
183 };
184
185 std::array<std::uint64_t, 3> const kShareCounts{99'999u, 333'333u, 1'234'567u};
186 for (auto const count : kShareCounts)
187 {
188 auto const before = read(env, f);
189 if (before.sharesTotal < count)
190 continue;
191 STAmount const shareAmount{MPTIssue{f.share}, Number{static_cast<std::int64_t>(count)}};
192 checkSuccess(shareAmount, "shares=" + std::to_string(count));
193 }
194
195 std::array<int, 3> const kAssetAmounts{1, 7, 99};
196 for (auto const amount : kAssetAmounts)
197 checkSuccess(asset(amount).value(), "assets=" + std::to_string(amount));
198 }
199
200 // Withdraw more than sfAssetsAvailable must return tecINSUFFICIENT_FUNDS,
201 // not tecPRECISION_LOSS.
202 void
203 testWithdrawInsufficientFundsPrecedence()
204 {
205 using namespace jtx;
206
207 testcase("withdraw over available returns insufficient funds, not precision loss");
208
209 Env env = makeEnv();
210 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
211 if (!ready(f))
212 return;
213 // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
214 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
215 jtx::PrettyAsset const& asset = f.asset.value();
216
217 Vault const v{env};
218 env(v.deposit(
219 {.depositor = f.depositor,
220 .id = f.vaultKeylet.key,
221 .amount = asset(1'000'000).value()}),
222 Ter(std::ignore));
223 env.close();
224
225 auto const before = read(env, f);
226 if (!BEAST_EXPECT(before.assetsAvailable > Number{0}))
227 return;
228
229 STAmount const request = asset(before.assetsAvailable + Number{1}).value();
230 env(v.withdraw({.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = request}),
231 Ter(std::ignore));
232 env.close();
233
234 BEAST_EXPECTS(
235 env.ter() == tecINSUFFICIENT_FUNDS,
236 std::string{"expected tecINSUFFICIENT_FUNDS, got "} + transToken(env.ter()));
237 }
238
239 void
240 testClawback()
241 {
242 using namespace jtx;
243
244 testcase("clawback deltas are equal");
245
246 Env env = makeEnv();
247 auto f = setupSingleLoanVault(
248 env,
249 /*impairAndPaySibling=*/false,
250 /*allowClawback=*/true);
251 if (!ready(f))
252 return;
253 // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
254 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
255 jtx::PrettyAsset const& asset = f.asset.value();
256
257 Vault const v{env};
258 env(v.deposit(
259 {.depositor = f.depositor,
260 .id = f.vaultKeylet.key,
261 .amount = asset(2'000).value()}),
262 Ter(std::ignore));
263 env.close();
264
265 auto checkSuccess = [&](std::optional<STAmount> const& amount, std::string const& tag) {
266 auto const before = read(env, f);
267 if (before.sharesTotal == Number{0})
268 return;
269
270 env(v.clawback(
271 {.issuer = f.issuer,
272 .id = f.vaultKeylet.key,
273 .holder = f.depositor,
274 .amount = amount}),
275 Ter(std::ignore));
276 env.close();
277 if (env.ter() != tesSUCCESS)
278 return;
279
280 assertEqualDeltas(before, read(env, f), tag);
281 };
282
283 std::array<int, 3> const kAmounts{1, 7, 99};
284 for (auto const amount : kAmounts)
285 checkSuccess(asset(amount).value(), "amount=" + std::to_string(amount));
286
287 checkSuccess(std::nullopt, "sfAmount absent");
288 }
289
290 void
291 testImpairedVault()
292 {
293 using namespace jtx;
294
295 testcase("impaired vault loss stays within assetsTotal - assetsAvailable");
296
297 Env env = makeEnv();
298 auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/true);
299 if (!ready(f))
300 return;
301 // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
302 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
303 jtx::PrettyAsset const& asset = f.asset.value();
304
305 Vault const v{env};
306 env(v.deposit(
307 {.depositor = f.depositor,
308 .id = f.vaultKeylet.key,
309 .amount = asset(5'000).value()}),
310 Ter(std::ignore));
311 env.close();
312
313 auto checkInvariant = [&](std::string const& tag) {
314 TER const actual = env.ter();
315 BEAST_EXPECTS(actual != tecINVARIANT_FAILED, tag + " unexpected invariant failure");
316 if (actual != tesSUCCESS)
317 return;
318 auto const after = read(env, f);
319 BEAST_EXPECTS(
320 after.lossUnrealized <= after.assetsTotal - after.assetsAvailable,
321 tag + " lossUnrealized exceeds assetsTotal - assetsAvailable");
322 };
323
324 std::array<int, 4> const kAmounts{1, 7, 51, 137};
325 for (std::size_t i = 0; i + 1 < kAmounts.size(); i += 2)
326 {
327 int const depositAmount = kAmounts[i];
328 int const withdrawAmount = kAmounts[i + 1];
329
330 env(v.deposit(
331 {.depositor = f.depositor,
332 .id = f.vaultKeylet.key,
333 .amount = asset(depositAmount).value()}),
334 Ter(std::ignore));
335 env.close();
336 checkInvariant("deposit=" + std::to_string(depositAmount));
337
338 env(v.withdraw(
339 {.depositor = f.depositor,
340 .id = f.vaultKeylet.key,
341 .amount = asset(withdrawAmount).value()}),
342 Ter(std::ignore));
343 env.close();
344 checkInvariant("withdraw=" + std::to_string(withdrawAmount));
345 }
346 }
347
348public:
349 void
350 run() override
351 {
352 testDeposit();
353 testWithdraw();
354 testWithdrawInsufficientFundsPrecedence();
355 testClawback();
356 testImpairedVault();
357 }
358};
359
360BEAST_DEFINE_TESTSUITE(VaultTransactorPrecision, app, xrpl);
361
362} // namespace xrpl::test
bool unexpected(Condition shouldBeFalse, String const &reason)
DEPRECATED.
Definition suite.h:516
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)
void assertEqualDeltas(Numbers const &before, Numbers const &after, std::string const &tag)
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
Deposit preauthorize operations.
Definition deposit.h:16
bool equal(STAmount const &sa1, STAmount const &sa2)
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:38
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
@ tecINSUFFICIENT_FUNDS
Definition TER.h:333
@ tecPRECISION_LOSS
Definition TER.h:371
@ tesSUCCESS
Definition TER.h:250
std::optional< jtx::PrettyAsset > asset
static json::Value withdraw(WithdrawArgs const &args)
Definition vault.cpp:100
static json::Value deposit(DepositArgs const &args)
Definition vault.cpp:89
T to_string(T... args)