rippled
Loading...
Searching...
No Matches
VaultClawback.cpp
1#include <xrpld/app/tx/detail/VaultClawback.h>
2//
3#include <xrpl/beast/utility/instrumentation.h>
4#include <xrpl/ledger/View.h>
5#include <xrpl/protocol/AccountID.h>
6#include <xrpl/protocol/MPTIssue.h>
7#include <xrpl/protocol/SField.h>
8#include <xrpl/protocol/STAmount.h>
9#include <xrpl/protocol/STNumber.h>
10#include <xrpl/protocol/TER.h>
11
12#include <optional>
13
14namespace xrpl {
17{
18 if (ctx.tx[sfVaultID] == beast::zero)
19 {
20 JLOG(ctx.j.debug()) << "VaultClawback: zero/empty vault ID.";
21 return temMALFORMED;
22 }
23
24 auto const amount = ctx.tx[~sfAmount];
25 if (amount)
26 {
27 // Note, zero amount is valid, it means "all". It is also the default.
28 if (*amount < beast::zero)
29 return temBAD_AMOUNT;
30 else if (isXRP(amount->asset()))
31 {
32 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
33 return temMALFORMED;
34 }
35 }
36
37 return tesSUCCESS;
38}
39
40[[nodiscard]] STAmount
42 std::shared_ptr<SLE const> const& vault,
43 std::optional<STAmount> const& maybeAmount,
44 AccountID const& account)
45{
46 if (maybeAmount)
47 return *maybeAmount;
48
49 Asset const share = MPTIssue{vault->at(sfShareMPTID)};
50 if (account == vault->at(sfOwner))
51 return STAmount{share};
52
53 return STAmount{vault->at(sfAsset)};
54}
55
56TER
58{
59 auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
60 if (!vault)
61 return tecNO_ENTRY;
62
63 Asset const vaultAsset = vault->at(sfAsset);
64 auto const account = ctx.tx[sfAccount];
65 auto const holder = ctx.tx[sfHolder];
66 auto const maybeAmount = ctx.tx[~sfAmount];
67 auto const mptIssuanceID = vault->at(sfShareMPTID);
68 auto const sleShareIssuance =
69 ctx.view.read(keylet::mptIssuance(mptIssuanceID));
70 if (!sleShareIssuance)
71 {
72 // LCOV_EXCL_START
73 JLOG(ctx.j.error())
74 << "VaultClawback: missing issuance of vault shares.";
75 return tefINTERNAL;
76 // LCOV_EXCL_STOP
77 }
78
79 Asset const share = MPTIssue{mptIssuanceID};
80
81 // Ambiguous case: If Issuer is Owner they must specify the asset
82 if (!maybeAmount && !vaultAsset.native() &&
83 vaultAsset.getIssuer() == vault->at(sfOwner))
84 {
85 JLOG(ctx.j.debug())
86 << "VaultClawback: must specify amount when issuer is owner.";
87 return tecWRONG_ASSET;
88 }
89
90 auto const amount = clawbackAmount(vault, maybeAmount, account);
91
92 // There is a special case that allows the VaultOwner to use clawback to
93 // burn shares when Vault assets total and available are zero, but
94 // shares remain. However, that case is handled in doApply() directly,
95 // so here we just enforce checks.
96 if (amount.asset() == share)
97 {
98 // Only the Vault Owner may clawback shares
99 if (account != vault->at(sfOwner))
100 {
101 JLOG(ctx.j.debug())
102 << "VaultClawback: only vault owner can clawback shares.";
103 return tecNO_PERMISSION;
104 }
105
106 auto const assetsTotal = vault->at(sfAssetsTotal);
107 auto const assetsAvailable = vault->at(sfAssetsAvailable);
108 auto const sharesTotal = sleShareIssuance->at(sfOutstandingAmount);
109
110 // Owner can clawback funds when the vault has shares but no assets
111 if (sharesTotal == 0 || (assetsTotal != 0 || assetsAvailable != 0))
112 {
113 JLOG(ctx.j.debug())
114 << "VaultClawback: vault owner can clawback shares only"
115 " when vault has no assets.";
116 return tecNO_PERMISSION;
117 }
118
119 // If amount is non-zero, the VaultOwner must burn all shares
120 if (amount != beast::zero)
121 {
122 Number const& sharesHeld = accountHolds(
123 ctx.view,
124 holder,
125 share,
128 ctx.j);
129
130 // The VaultOwner must burn all shares
131 if (amount != sharesHeld)
132 {
133 JLOG(ctx.j.debug())
134 << "VaultClawback: vault owner must clawback all "
135 "shares.";
136 return tecLIMIT_EXCEEDED;
137 }
138 }
139
140 return tesSUCCESS;
141 }
142
143 // The asset that is being clawed back is the vault asset
144 if (amount.asset() == vaultAsset)
145 {
146 // XRP cannot be clawed back
147 if (vaultAsset.native())
148 {
149 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
150 return tecNO_PERMISSION;
151 }
152
153 // Only the Asset Issuer may clawback the asset
154 if (account != vaultAsset.getIssuer())
155 {
156 JLOG(ctx.j.debug())
157 << "VaultClawback: only asset issuer can clawback asset.";
158 return tecNO_PERMISSION;
159 }
160
161 // The issuer cannot clawback from itself
162 if (account == holder)
163 {
164 JLOG(ctx.j.debug())
165 << "VaultClawback: issuer cannot be the holder.";
166 return tecNO_PERMISSION;
167 }
168
169 return std::visit(
170 [&]<ValidIssueType TIss>(TIss const& issue) -> TER {
171 if constexpr (std::is_same_v<TIss, MPTIssue>)
172 {
173 auto const mptIssue =
174 ctx.view.read(keylet::mptIssuance(issue.getMptID()));
175 if (mptIssue == nullptr)
176 return tecOBJECT_NOT_FOUND;
177
178 std::uint32_t const issueFlags =
179 mptIssue->getFieldU32(sfFlags);
180 if (!(issueFlags & lsfMPTCanClawback))
181 {
182 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback "
183 "MPT vault asset.";
184 return tecNO_PERMISSION;
185 }
186 }
187 else if constexpr (std::is_same_v<TIss, Issue>)
188 {
189 auto const issuerSle =
190 ctx.view.read(keylet::account(account));
191 if (!issuerSle)
192 {
193 // LCOV_EXCL_START
194 JLOG(ctx.j.error())
195 << "VaultClawback: missing submitter account.";
196 return tefINTERNAL;
197 // LCOV_EXCL_STOP
198 }
199
200 std::uint32_t const issuerFlags =
201 issuerSle->getFieldU32(sfFlags);
202 if (!(issuerFlags & lsfAllowTrustLineClawback) ||
203 (issuerFlags & lsfNoFreeze))
204 {
205 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback "
206 "IOU vault asset.";
207 return tecNO_PERMISSION;
208 }
209 }
210 return tesSUCCESS;
211 },
212 vaultAsset.value());
213 }
214
215 // Invalid asset
216 return tecWRONG_ASSET;
217}
218
221 std::shared_ptr<SLE> const& vault,
222 std::shared_ptr<SLE const> const& sleShareIssuance,
223 AccountID const& holder,
225{
226 if (clawbackAmount.asset() != vault->at(sfAsset))
227 {
228 // preclaim should have blocked this , now it's an internal error
229 // LCOV_EXCL_START
230 JLOG(j_.error()) << "VaultClawback: asset mismatch in clawback.";
231 return Unexpected(tecINTERNAL);
232 // LCOV_EXCL_STOP
233 }
234
235 auto const assetsAvailable = vault->at(sfAssetsAvailable);
236 auto const mptIssuanceID = *vault->at(sfShareMPTID);
237 MPTIssue const share{mptIssuanceID};
238
239 if (clawbackAmount == beast::zero)
240 {
241 auto const sharesDestroyed = accountHolds(
242 view(),
243 holder,
244 share,
247 j_);
248 auto const maybeAssets =
249 sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
250 if (!maybeAssets)
251 return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
252
253 return std::make_pair(*maybeAssets, sharesDestroyed);
254 }
255
256 STAmount sharesDestroyed;
257 STAmount assetsRecovered = clawbackAmount;
258 try
259 {
260 {
261 auto const maybeShares = assetsToSharesWithdraw(
262 vault, sleShareIssuance, assetsRecovered);
263 if (!maybeShares)
264 return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
265 sharesDestroyed = *maybeShares;
266 }
267
268 auto const maybeAssets =
269 sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
270 if (!maybeAssets)
271 return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
272 assetsRecovered = *maybeAssets;
273
274 // Clamp to maximum.
275 if (assetsRecovered > *assetsAvailable)
276 {
277 assetsRecovered = *assetsAvailable;
278 // Note, it is important to truncate the number of shares,
279 // otherwise the corresponding assets might breach the
280 // AssetsAvailable
281 {
282 auto const maybeShares = assetsToSharesWithdraw(
283 vault,
284 sleShareIssuance,
285 assetsRecovered,
287 if (!maybeShares)
288 return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
289 sharesDestroyed = *maybeShares;
290 }
291
292 auto const maybeAssets = sharesToAssetsWithdraw(
293 vault, sleShareIssuance, sharesDestroyed);
294 if (!maybeAssets)
295 return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
296 assetsRecovered = *maybeAssets;
297 if (assetsRecovered > *assetsAvailable)
298 {
299 // LCOV_EXCL_START
300 JLOG(j_.error())
301 << "VaultClawback: invalid rounding of shares.";
302 return Unexpected(tecINTERNAL);
303 // LCOV_EXCL_STOP
304 }
305 }
306 }
307 catch (std::overflow_error const&)
308 {
309 // It's easy to hit this exception from Number with large enough
310 // Scale so we avoid spamming the log and only use debug here.
311 JLOG(j_.debug()) //
312 << "VaultClawback: overflow error with"
313 << " scale=" << (int)vault->at(sfScale).value() //
314 << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
315 << ", sharesTotal=" << sleShareIssuance->at(sfOutstandingAmount)
316 << ", amount=" << clawbackAmount.value();
317 return Unexpected(tecPATH_DRY);
318 }
319
320 return std::make_pair(assetsRecovered, sharesDestroyed);
321}
322
323TER
325{
326 auto const& tx = ctx_.tx;
327 auto const vault = view().peek(keylet::vault(tx[sfVaultID]));
328 if (!vault)
329 return tefINTERNAL; // LCOV_EXCL_LINE
330
331 auto const mptIssuanceID = *vault->at(sfShareMPTID);
332 auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
333 if (!sleIssuance)
334 {
335 // LCOV_EXCL_START
336 JLOG(j_.error()) << "VaultClawback: missing issuance of vault shares.";
337 return tefINTERNAL;
338 // LCOV_EXCL_STOP
339 }
340 MPTIssue const share{mptIssuanceID};
341
342 Asset const vaultAsset = vault->at(sfAsset);
343 STAmount const amount = clawbackAmount(vault, tx[~sfAmount], account_);
344
345 auto assetsAvailable = vault->at(sfAssetsAvailable);
346 auto assetsTotal = vault->at(sfAssetsTotal);
347
348 [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
349 XRPL_ASSERT(
350 lossUnrealized <= (assetsTotal - assetsAvailable),
351 "xrpl::VaultClawback::doApply : loss and assets do balance");
352
353 AccountID holder = tx[sfHolder];
354 STAmount sharesDestroyed = {share};
355 STAmount assetsRecovered = {vault->at(sfAsset)};
356
357 // The Owner is burning shares
358 if (account_ == vault->at(sfOwner) && amount.asset() == share)
359 {
360 sharesDestroyed = accountHolds(
361 view(),
362 holder,
363 share,
366 j_);
367 }
368 else // The Issuer is clawbacking vault assets
369 {
370 XRPL_ASSERT(
371 amount.asset() == vaultAsset,
372 "xrpl::VaultClawback::doApply : matching asset");
373
374 auto const clawbackParts =
375 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::zero)
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(),
394 holder,
395 vaultAccount,
396 sharesDestroyed,
397 j_,
399 !isTesSuccess(ter))
400 return ter;
401
402 // Try to remove MPToken for shares, if the holder balance is zero. Vault
403 // pseudo-account will never set lsfMPTAuthorized, so we ignore flags.
404 // Keep MPToken if holder is the vault owner.
405 if (holder != vault->at(sfOwner))
406 {
407 if (auto const ter =
408 removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
409 isTesSuccess(ter))
410 {
411 JLOG(j_.debug()) //
412 << "VaultClawback: removed empty MPToken for vault shares"
413 << " MPTID=" << to_string(mptIssuanceID) //
414 << " account=" << toBase58(holder);
415 }
416 else if (ter != tecHAS_OBLIGATIONS)
417 {
418 // LCOV_EXCL_START
419 JLOG(j_.error()) //
420 << "VaultClawback: failed to remove MPToken for vault shares"
421 << " MPTID=" << to_string(mptIssuanceID) //
422 << " account=" << toBase58(holder) //
423 << " with result: " << transToken(ter);
424 return ter;
425 // LCOV_EXCL_STOP
426 }
427 // else quietly ignore, holder balance is not zero
428 }
429
430 if (assetsRecovered > beast::zero)
431 {
432 // Transfer assets from vault to issuer.
433 if (auto const ter = accountSend(
434 view(),
435 vaultAccount,
436 account_,
437 assetsRecovered,
438 j_,
440 !isTesSuccess(ter))
441 return ter;
442
443 // Sanity check
444 if (accountHolds(
445 view(),
446 vaultAccount,
447 assetsRecovered.asset(),
450 j_) < beast::zero)
451 {
452 // LCOV_EXCL_START
453 JLOG(j_.error())
454 << "VaultClawback: negative balance of vault assets.";
455 return tefINTERNAL;
456 // LCOV_EXCL_STOP
457 }
458 }
459
460 return tesSUCCESS;
461}
462
463} // namespace xrpl
Stream error() const
Definition Journal.h:327
Stream debug() const
Definition Journal.h:309
STTx const & tx
virtual void update(std::shared_ptr< SLE > const &sle)=0
Indicate changes to a peeked SLE.
virtual std::shared_ptr< SLE > peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
AccountID const & getIssuer() const
Definition Asset.cpp:17
bool native() const
Definition Asset.h:82
constexpr value_type const & value() const
Definition Asset.h:157
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
Asset const & asset() const
Definition STAmount.h:475
STAmount const & value() const noexcept
Definition STAmount.h:586
AccountID const account_
Definition Transactor.h:128
beast::Journal const j_
Definition Transactor.h:126
ApplyView & view()
Definition Transactor.h:144
ApplyContext & ctx_
Definition Transactor.h:124
static NotTEC preflight(PreflightContext const &ctx)
static TER preclaim(PreclaimContext const &ctx)
Expected< std::pair< STAmount, STAmount >, TER > assetsToClawback(std::shared_ptr< SLE > const &vault, std::shared_ptr< SLE const > const &sleShareIssuance, AccountID const &holder, STAmount const &clawbackAmount)
TER doApply() override
T is_same_v
T make_pair(T... args)
Keylet mptIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:508
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:546
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:166
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
@ fhIGNORE_FREEZE
Definition View.h:59
bool isXRP(AccountID const &c)
Definition AccountID.h:71
TER removeEmptyHolding(ApplyView &view, AccountID const &accountID, Issue const &issue, beast::Journal journal)
Definition View.cpp:1762
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:611
@ tefINTERNAL
Definition TER.h:154
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
std::string transToken(TER code)
Definition TER.cpp:245
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j)
Definition View.cpp:461
TER accountSend(ApplyView &view, AccountID const &from, AccountID const &to, STAmount const &saAmount, beast::Journal j, WaiveTransferFee waiveFee=WaiveTransferFee::No)
Calls static accountSendIOU if saAmount represents Issue.
Definition View.cpp:2777
std::optional< STAmount > assetsToSharesWithdraw(std::shared_ptr< SLE const > const &vault, std::shared_ptr< SLE const > const &issuance, STAmount const &assets, TruncateShares truncate=TruncateShares::no)
Definition View.cpp:3597
TERSubset< CanCvtToTER > TER
Definition TER.h:630
@ ahIGNORE_AUTH
Definition View.h:62
@ temMALFORMED
Definition TER.h:68
@ temBAD_AMOUNT
Definition TER.h:70
bool isTesSuccess(TER x) noexcept
Definition TER.h:659
std::optional< STAmount > sharesToAssetsWithdraw(std::shared_ptr< SLE const > const &vault, std::shared_ptr< SLE const > const &issuance, STAmount const &shares)
Definition View.cpp:3626
@ tecWRONG_ASSET
Definition TER.h:342
@ tecNO_ENTRY
Definition TER.h:288
@ tecPATH_DRY
Definition TER.h:276
@ tecOBJECT_NOT_FOUND
Definition TER.h:308
@ tecINTERNAL
Definition TER.h:292
@ tecPRECISION_LOSS
Definition TER.h:345
@ tecLIMIT_EXCEEDED
Definition TER.h:343
@ tecNO_PERMISSION
Definition TER.h:287
@ tecHAS_OBLIGATIONS
Definition TER.h:299
@ lsfAllowTrustLineClawback
@ lsfNoFreeze
@ lsfMPTCanClawback
STAmount clawbackAmount(std::shared_ptr< SLE const > const &vault, std::optional< STAmount > const &maybeAmount, AccountID const &account)
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:590
@ tesSUCCESS
Definition TER.h:226
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:16
beast::Journal const j
Definition Transactor.h:23
T visit(T... args)