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 {
261 if (clawbackAmount == beast::kZero)
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 =
393 accountSend(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 = removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
403 isTesSuccess(ter))
404 {
405 JLOG(j_.debug()) //
406 << "VaultClawback: removed empty MPToken for vault shares"
407 << " MPTID=" << to_string(mptIssuanceID) //
408 << " account=" << toBase58(holder);
409 }
410 else if (ter != tecHAS_OBLIGATIONS)
411 {
412 // LCOV_EXCL_START
413 JLOG(j_.error()) //
414 << "VaultClawback: failed to remove MPToken for vault shares"
415 << " MPTID=" << to_string(mptIssuanceID) //
416 << " account=" << toBase58(holder) //
417 << " with result: " << transToken(ter);
418 return ter;
419 // LCOV_EXCL_STOP
420 }
421 // else quietly ignore, holder balance is not zero
422 }
423
424 if (assetsRecovered > beast::kZero)
425 {
426 // Transfer assets from vault to issuer.
427 if (auto const ter = accountSend(
428 view(), vaultAccount, accountID_, assetsRecovered, j_, WaiveTransferFee::Yes);
429 !isTesSuccess(ter))
430 return ter;
431
432 // Sanity check
433 if (accountHolds(
434 view(),
435 vaultAccount,
436 assetsRecovered.asset(),
439 j_) < beast::kZero)
440 {
441 // LCOV_EXCL_START
442 JLOG(j_.error()) << "VaultClawback: negative balance of vault assets.";
443 return tefINTERNAL;
444 // LCOV_EXCL_STOP
445 }
446 }
447
448 associateAsset(*vault, vaultAsset);
449
450 return tesSUCCESS;
451}
452
453void
455{
456 // No transaction-specific invariants yet (future work).
457}
458
459bool
461 STTx const&,
462 TER,
463 XRPAmount,
464 ReadView const&,
465 beast::Journal const&)
466{
467 // No transaction-specific invariants yet (future work).
468 return true;
469}
470
471} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:38
Stream error() const
Definition Journal.h:315
Stream debug() const
Definition Journal.h:297
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:107
constexpr bool native() const
Definition Asset.h:115
AccountID const & getIssuer() const
Definition Asset.cpp:21
A currency issued by an account.
Definition Issue.h:13
constexpr MPTID const & getMptID() const
Definition MPTIssue.h:33
Number is a floating point type that can represent a wide range of values.
Definition Number.h:306
A view into a ledger.
Definition ReadView.h:31
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:478
std::shared_ptr< STLedgerEntry > const & ref
std::shared_ptr< STLedgerEntry const > const & const_ref
beast::Journal const j_
Definition Transactor.h:118
ApplyView & view()
Definition Transactor.h:136
AccountID const accountID_
Definition Transactor.h:120
ApplyContext & ctx_
Definition Transactor.h:116
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)
Keylet mptokenIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:521
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:551
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
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:70
@ tefINTERNAL
Definition TER.h:163
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:93
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:247
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:594
STAmount clawbackAmount(SLE::const_ref vault, std::optional< STAmount > const &maybeAmount, AccountID const &account)
TER accountSend(ApplyView &view, AccountID const &from, AccountID const &to, STAmount const &saAmount, beast::Journal j, WaiveTransferFee waiveFee=WaiveTransferFee::No, AllowMPTOverflow allowOverflow=AllowMPTOverflow::No)
Calls static accountSendIOU if saAmount represents Issue.
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
@ temMALFORMED
Definition TER.h:73
@ temBAD_AMOUNT
Definition TER.h:75
bool isTesSuccess(TER x) noexcept
Definition TER.h:663
TERSubset< CanCvtToTER > TER
Definition TER.h:634
@ tecWRONG_ASSET
Definition TER.h:358
@ tecNO_ENTRY
Definition TER.h:304
@ tecPATH_DRY
Definition TER.h:292
@ tecOBJECT_NOT_FOUND
Definition TER.h:324
@ tecINTERNAL
Definition TER.h:308
@ tecPRECISION_LOSS
Definition TER.h:361
@ tecLIMIT_EXCEEDED
Definition TER.h:359
@ tecNO_PERMISSION
Definition TER.h:303
@ tecHAS_OBLIGATIONS
Definition TER.h:315
void associateAsset(STLedgerEntry &sle, Asset const &asset)
Associate an Asset with all sMD_NeedsAsset fields in a ledger entry.
TER removeEmptyHolding(ApplyView &view, AccountID const &accountID, MPTIssue const &mptIssue, beast::Journal journal)
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:240
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:61
ReadView const & view
Definition Transactor.h:64
beast::Journal const j
Definition Transactor.h:69
State information when preflighting a tx.
Definition Transactor.h:18
beast::Journal const j
Definition Transactor.h:25
T unexpected(T... args)