xrpld
Loading...
Searching...
No Matches
VaultClawback.cpp
1#include <xrpl/tx/transactors/vault/VaultClawback.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Number.h>
5#include <xrpl/basics/base_uint.h>
6#include <xrpl/beast/utility/Zero.h>
7#include <xrpl/beast/utility/instrumentation.h>
8#include <xrpl/core/ServiceRegistry.h>
9#include <xrpl/ledger/helpers/TokenHelpers.h>
10#include <xrpl/ledger/helpers/VaultHelpers.h>
11#include <xrpl/protocol/AccountID.h>
12#include <xrpl/protocol/Asset.h>
13#include <xrpl/protocol/Feature.h>
14#include <xrpl/protocol/Indexes.h>
15#include <xrpl/protocol/Issue.h>
16#include <xrpl/protocol/LedgerFormats.h>
17#include <xrpl/protocol/MPTIssue.h>
18#include <xrpl/protocol/SField.h>
19#include <xrpl/protocol/STAmount.h>
20#include <xrpl/protocol/STLedgerEntry.h>
21#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
22#include <xrpl/protocol/STTakesAsset.h>
23#include <xrpl/protocol/STTx.h>
24#include <xrpl/protocol/TER.h>
25#include <xrpl/protocol/XRPAmount.h>
26#include <xrpl/tx/Transactor.h>
27
28#include <expected>
29#include <optional>
30#include <stdexcept>
31#include <utility>
32
33namespace xrpl {
36{
37 if (ctx.tx[sfVaultID] == beast::kZero)
38 {
39 JLOG(ctx.j.debug()) << "VaultClawback: zero/empty vault ID.";
40 return temMALFORMED;
41 }
42
43 auto const amount = ctx.tx[~sfAmount];
44 if (amount)
45 {
46 // Note, zero amount is valid, it means "all". It is also the default.
47 if (*amount < beast::kZero)
48 {
49 return temBAD_AMOUNT;
50 }
51 if (isXRP(amount->asset()))
52 {
53 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
54 return temMALFORMED;
55 }
56 }
57
58 return tesSUCCESS;
59}
60
61[[nodiscard]] STAmount
63 SLE::const_ref vault,
64 std::optional<STAmount> const& maybeAmount,
65 AccountID const& account)
66{
67 if (maybeAmount)
68 return *maybeAmount;
69
70 Asset const share = MPTIssue{vault->at(sfShareMPTID)};
71 if (account == vault->at(sfOwner))
72 return STAmount{share};
73
74 return STAmount{vault->at(sfAsset)};
75}
76
77TER
79{
80 auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
81 if (!vault)
82 return tecNO_ENTRY;
83
84 Asset const vaultAsset = vault->at(sfAsset);
85 auto const account = ctx.tx[sfAccount];
86 auto const holder = ctx.tx[sfHolder];
87 auto const maybeAmount = ctx.tx[~sfAmount];
88 auto const mptIssuanceID = vault->at(sfShareMPTID);
89 auto const sleShareIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
90 if (!sleShareIssuance)
91 {
92 // LCOV_EXCL_START
93 JLOG(ctx.j.error()) << "VaultClawback: missing issuance of vault shares.";
94 return tefINTERNAL;
95 // LCOV_EXCL_STOP
96 }
97
98 Asset const share = MPTIssue{mptIssuanceID};
99
100 // Ambiguous case: If Issuer is Owner they must specify the asset
101 if (!maybeAmount && !vaultAsset.native() && vaultAsset.getIssuer() == vault->at(sfOwner))
102 {
103 JLOG(ctx.j.debug()) << "VaultClawback: must specify amount when issuer is owner.";
104 return tecWRONG_ASSET;
105 }
106
107 auto const amount = clawbackAmount(vault, maybeAmount, account);
108
109 // There is a special case that allows the VaultOwner to use clawback to
110 // burn shares when Vault assets total and available are zero, but
111 // shares remain. However, that case is handled in doApply() directly,
112 // so here we just enforce checks.
113 if (amount.asset() == share)
114 {
115 // Only the Vault Owner may clawback shares
116 if (account != vault->at(sfOwner))
117 {
118 JLOG(ctx.j.debug()) << "VaultClawback: only vault owner can clawback shares.";
119 return tecNO_PERMISSION;
120 }
121
122 auto const assetsTotal = vault->at(sfAssetsTotal);
123 auto const assetsAvailable = vault->at(sfAssetsAvailable);
124 auto const sharesTotal = sleShareIssuance->at(sfOutstandingAmount);
125
126 // Owner can clawback funds when the vault has shares but no assets
127 if (sharesTotal == 0 || (assetsTotal != 0 || assetsAvailable != 0))
128 {
129 JLOG(ctx.j.debug()) << "VaultClawback: vault owner can clawback shares only"
130 " when vault has no assets.";
131 return tecNO_PERMISSION;
132 }
133
134 // If amount is non-zero, the VaultOwner must burn all shares
135 if (amount != beast::kZero)
136 {
137 Number const& sharesHeld = accountHolds(
138 ctx.view,
139 holder,
140 share,
143 ctx.j);
144
145 // The VaultOwner must burn all shares
146 if (amount != sharesHeld)
147 {
148 JLOG(ctx.j.debug()) << "VaultClawback: vault owner must clawback all "
149 "shares.";
150 return tecLIMIT_EXCEEDED;
151 }
152 }
153
154 return tesSUCCESS;
155 }
156
157 // The asset that is being clawed back is the vault asset
158 if (amount.asset() == vaultAsset)
159 {
160 // XRP cannot be clawed back
161 if (vaultAsset.native())
162 {
163 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
164 return tecNO_PERMISSION;
165 }
166
167 // Only the Asset Issuer may clawback the asset
168 if (account != vaultAsset.getIssuer())
169 {
170 JLOG(ctx.j.debug()) << "VaultClawback: only asset issuer can clawback asset.";
171 return tecNO_PERMISSION;
172 }
173
174 // The issuer cannot clawback from itself
175 if (account == holder)
176 {
177 JLOG(ctx.j.debug()) << "VaultClawback: issuer cannot be the holder.";
178 return tecNO_PERMISSION;
179 }
180
181 return vaultAsset.visit(
182 [&](MPTIssue const& issue) -> TER {
183 auto const mptIssue = ctx.view.read(keylet::mptokenIssuance(issue.getMptID()));
184 if (mptIssue == nullptr)
185 return tecOBJECT_NOT_FOUND;
186
187 if (!mptIssue->isFlag(lsfMPTCanClawback))
188 {
189 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback "
190 "MPT vault asset.";
191 return tecNO_PERMISSION;
192 }
193
194 return tesSUCCESS;
195 },
196 [&](Issue const&) -> TER {
197 auto const issuerSle = ctx.view.read(keylet::account(account));
198 if (!issuerSle)
199 {
200 // LCOV_EXCL_START
201 JLOG(ctx.j.error()) << "VaultClawback: missing submitter account.";
202 return tefINTERNAL;
203 // LCOV_EXCL_STOP
204 }
205
206 if (!issuerSle->isFlag(lsfAllowTrustLineClawback) || issuerSle->isFlag(lsfNoFreeze))
207 {
208 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback "
209 "IOU vault asset.";
210 return tecNO_PERMISSION;
211 }
212
213 return tesSUCCESS;
214 });
215 }
216
217 // Invalid asset
218 return tecWRONG_ASSET;
219}
220
221std::expected<std::pair<STAmount, STAmount>, TER>
223 SLE::ref vault,
224 SLE::const_ref sleShareIssuance,
225 AccountID const& holder,
227{
228 if (clawbackAmount.asset() != vault->at(sfAsset))
229 {
230 // preclaim should have blocked this , now it's an internal error
231 // LCOV_EXCL_START
232 JLOG(j_.error()) << "VaultClawback: asset mismatch in clawback.";
234 // LCOV_EXCL_STOP
235 }
236
237 auto const assetsAvailable = vault->at(sfAssetsAvailable);
238 auto const mptIssuanceID = *vault->at(sfShareMPTID);
239 MPTIssue const share{mptIssuanceID};
240
241 // Pre-fixCleanup3_1_3: zero-amount clawback returned early without
242 // clamping to assetsAvailable, allowing more assets to be recovered
243 // than available when there was an outstanding loan. Retained for
244 // ledger replay compatibility.
245 if (!ctx_.view().rules().enabled(fixCleanup3_1_3) && clawbackAmount == beast::kZero)
246 {
247 auto const sharesDestroyed = accountHolds(
249 auto const maybeAssets = sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
250 if (!maybeAssets)
251 return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
252
253 return std::make_pair(*maybeAssets, sharesDestroyed);
254 }
255
256 STAmount sharesDestroyed;
257 STAmount assetsRecovered;
258
259 try
260 {
262 {
263 sharesDestroyed = accountHolds(
265 auto const maybeAssets =
266 sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
267 if (!maybeAssets)
268 return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
269
270 assetsRecovered = *maybeAssets;
271 }
272 else
273 {
274 auto const maybeShares =
275 assetsToSharesWithdraw(vault, sleShareIssuance, clawbackAmount);
276 if (!maybeShares)
277 return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
278 sharesDestroyed = *maybeShares;
279
280 auto const maybeAssets =
281 sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
282 if (!maybeAssets)
283 return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
284 assetsRecovered = *maybeAssets;
285 }
286 // Clamp to maximum.
287 if (assetsRecovered > *assetsAvailable)
288 {
289 assetsRecovered = *assetsAvailable;
290 // Note, it is important to truncate the number of shares,
291 // otherwise the corresponding assets might breach the
292 // AssetsAvailable
293 {
294 auto const maybeShares = assetsToSharesWithdraw(
295 vault, sleShareIssuance, assetsRecovered, TruncateShares::Yes);
296 if (!maybeShares)
297 return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
298 sharesDestroyed = *maybeShares;
299 }
300
301 auto const maybeAssets =
302 sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
303 if (!maybeAssets)
304 return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
305 assetsRecovered = *maybeAssets;
306 if (assetsRecovered > *assetsAvailable)
307 {
308 // LCOV_EXCL_START
309 JLOG(j_.error()) << "VaultClawback: invalid rounding of shares.";
311 // LCOV_EXCL_STOP
312 }
313 }
314 }
315 catch (std::overflow_error const&)
316 {
317 // It's easy to hit this exception from Number with large enough
318 // Scale so we avoid spamming the log and only use debug here.
319 JLOG(j_.debug()) //
320 << "VaultClawback: overflow error with"
321 << " scale=" << (int)vault->at(sfScale).value() //
322 << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
323 << ", sharesTotal=" << sleShareIssuance->at(sfOutstandingAmount)
324 << ", amount=" << clawbackAmount.value();
326 }
327
328 return std::make_pair(assetsRecovered, sharesDestroyed);
329}
330
331TER
333{
334 auto const& tx = ctx_.tx;
335 auto const vault = view().peek(keylet::vault(tx[sfVaultID]));
336 if (!vault)
337 return tefINTERNAL; // LCOV_EXCL_LINE
338
339 auto const mptIssuanceID = *vault->at(sfShareMPTID);
340 auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID));
341 if (!sleIssuance)
342 {
343 // LCOV_EXCL_START
344 JLOG(j_.error()) << "VaultClawback: missing issuance of vault shares.";
345 return tefINTERNAL;
346 // LCOV_EXCL_STOP
347 }
348 MPTIssue const share{mptIssuanceID};
349
350 Asset const vaultAsset = vault->at(sfAsset);
351 STAmount const amount = clawbackAmount(vault, tx[~sfAmount], accountID_);
352
353 auto assetsAvailable = vault->at(sfAssetsAvailable);
354 auto assetsTotal = vault->at(sfAssetsTotal);
355
356 [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
357 XRPL_ASSERT(
358 lossUnrealized <= (assetsTotal - assetsAvailable),
359 "xrpl::VaultClawback::doApply : loss and assets do balance");
360
361 AccountID const holder = tx[sfHolder];
362 STAmount sharesDestroyed = {share};
363 STAmount assetsRecovered = {vault->at(sfAsset)};
364
365 // The Owner is burning shares
366 if (accountID_ == vault->at(sfOwner) && amount.asset() == share)
367 {
368 sharesDestroyed = accountHolds(
370 }
371 else // The Issuer is clawbacking vault assets
372 {
373 XRPL_ASSERT(amount.asset() == vaultAsset, "xrpl::VaultClawback::doApply : matching asset");
374
375 auto const clawbackParts = assetsToClawback(vault, sleIssuance, holder, amount);
376 if (!clawbackParts)
377 return clawbackParts.error();
378
379 assetsRecovered = clawbackParts->first;
380 sharesDestroyed = clawbackParts->second;
381 }
382
383 if (sharesDestroyed == beast::kZero)
384 return tecPRECISION_LOSS;
385
386 assetsTotal -= assetsRecovered;
387 assetsAvailable -= assetsRecovered;
388 view().update(vault);
389
390 auto const& vaultAccount = vault->at(sfAccount);
391 // Transfer shares from holder to vault.
392 if (auto const ter = accountSend(
393 view(), holder, vaultAccount, sharesDestroyed, j_, {}, WaiveTransferFee::Yes);
394 !isTesSuccess(ter))
395 return ter;
396
397 // Try to remove MPToken for shares, if the holder balance is zero. Vault
398 // pseudo-account will never set lsfMPTAuthorized, so we ignore flags.
399 // Keep MPToken if holder is the vault owner.
400 if (holder != vault->at(sfOwner))
401 {
402 if (auto const ter =
403 removeEmptyHolding(ctx_.getApplyViewContext(), holder, sharesDestroyed.asset(), j_);
404 isTesSuccess(ter))
405 {
406 JLOG(j_.debug()) //
407 << "VaultClawback: removed empty MPToken for vault shares"
408 << " MPTID=" << to_string(mptIssuanceID) //
409 << " account=" << toBase58(holder);
410 }
411 else if (ter != tecHAS_OBLIGATIONS)
412 {
413 // LCOV_EXCL_START
414 JLOG(j_.error()) //
415 << "VaultClawback: failed to remove MPToken for vault shares"
416 << " MPTID=" << to_string(mptIssuanceID) //
417 << " account=" << toBase58(holder) //
418 << " with result: " << transToken(ter);
419 return ter;
420 // LCOV_EXCL_STOP
421 }
422 // else quietly ignore, holder balance is not zero
423 }
424
425 if (assetsRecovered > beast::kZero)
426 {
427 // Transfer assets from vault to issuer.
428 if (auto const ter = accountSend(
429 view(), vaultAccount, accountID_, assetsRecovered, j_, {}, WaiveTransferFee::Yes);
430 !isTesSuccess(ter))
431 return ter;
432
433 // Sanity check
434 if (accountHolds(
435 view(),
436 vaultAccount,
437 assetsRecovered.asset(),
440 j_) < beast::kZero)
441 {
442 // LCOV_EXCL_START
443 JLOG(j_.error()) << "VaultClawback: negative balance of vault assets.";
444 return tefINTERNAL;
445 // LCOV_EXCL_STOP
446 }
447 }
448
449 associateAsset(*vault, vaultAsset);
450
451 return tesSUCCESS;
452}
453
454void
456{
457 // No transaction-specific invariants yet (future work).
458}
459
460bool
462 STTx const&,
463 TER,
464 XRPAmount,
465 ReadView const&,
466 beast::Journal const&)
467{
468 // No transaction-specific invariants yet (future work).
469 return true;
470}
471
472} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Stream error() const
Definition Journal.h:362
Stream debug() const
Definition Journal.h:344
virtual SLE::pointer peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
virtual void update(SLE::ref sle)=0
Indicate changes to a peeked SLE.
constexpr auto visit(Visitors &&... visitors) const -> decltype(auto)
Definition Asset.h:117
constexpr bool native() const
Definition Asset.h:125
AccountID const & getIssuer() const
Definition Asset.cpp:21
A currency issued by an account.
Definition Issue.h:18
constexpr MPTID const & getMptID() const
Definition MPTIssue.h:43
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
A view into a ledger.
Definition ReadView.h:41
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
Asset const & asset() const
Definition STAmount.h:496
std::shared_ptr< STLedgerEntry > const & ref
std::shared_ptr< STLedgerEntry const > const & const_ref
beast::Journal const j_
Definition Transactor.h:155
ApplyView & view()
Definition Transactor.h:175
AccountID const accountID_
Definition Transactor.h:157
ApplyContext & ctx_
Definition Transactor.h:153
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
void visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override
Inspect a single ledger entry modified by this transaction.
static NotTEC preflight(PreflightContext const &ctx)
static TER preclaim(PreclaimContext const &ctx)
std::expected< std::pair< STAmount, STAmount >, TER > assetsToClawback(SLE::ref vault, SLE::const_ref sleShareIssuance, AccountID const &holder, STAmount const &clawbackAmount)
TER doApply() override
T make_pair(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:561
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:537
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
std::optional< STAmount > assetsToSharesWithdraw(SLE::const_ref vault, SLE::const_ref 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...
bool isXRP(AccountID const &c)
Definition AccountID.h:84
TER accountSend(ApplyView &view, AccountID const &from, AccountID const &to, STAmount const &saAmount, beast::Journal j, SLE::ref sponsorSle={}, WaiveTransferFee waiveFee=WaiveTransferFee::No, AllowMPTOverflow allowOverflow=AllowMPTOverflow::No)
Calls static accountSendIOU if saAmount represents Issue.
TER removeEmptyHolding(ApplyViewContext ctx, AccountID const &accountID, MPTIssue const &mptIssue, beast::Journal journal)
@ tefINTERNAL
Definition TER.h:165
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
std::optional< STAmount > sharesToAssetsWithdraw(SLE::const_ref vault, SLE::const_ref issuance, STAmount const &shares, WaiveUnrealizedLoss waive=WaiveUnrealizedLoss::No)
From the perspective of a vault, return the number of assets to give the depositor when they redeem a...
std::string transToken(TER code)
Definition TER.cpp:251
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:607
STAmount clawbackAmount(SLE::const_ref vault, std::optional< STAmount > const &maybeAmount, AccountID const &account)
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
@ temMALFORMED
Definition TER.h:75
@ temBAD_AMOUNT
Definition TER.h:77
bool isTesSuccess(TER x) noexcept
Definition TER.h:676
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecWRONG_ASSET
Definition TER.h:363
@ tecNO_ENTRY
Definition TER.h:309
@ tecPATH_DRY
Definition TER.h:297
@ tecOBJECT_NOT_FOUND
Definition TER.h:329
@ tecINTERNAL
Definition TER.h:313
@ tecPRECISION_LOSS
Definition TER.h:366
@ tecLIMIT_EXCEEDED
Definition TER.h:364
@ tecNO_PERMISSION
Definition TER.h:308
@ tecHAS_OBLIGATIONS
Definition TER.h:320
void associateAsset(STLedgerEntry &sle, Asset const &asset)
Associate an Asset with all sMD_NeedsAsset fields in a ledger entry.
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j, SpendableHandling includeFullBalance=SpendableHandling::SimpleBalance)
@ tesSUCCESS
Definition TER.h:245
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:83
ReadView const & view
Definition Transactor.h:86
beast::Journal const j
Definition Transactor.h:91
State information when preflighting a tx.
Definition Transactor.h:38
beast::Journal const j
Definition Transactor.h:45
T unexpected(T... args)