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