rippled
Loading...
Searching...
No Matches
VaultClawback.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2025 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpld/app/tx/detail/VaultClawback.h>
21
22#include <xrpl/beast/utility/instrumentation.h>
23#include <xrpl/ledger/View.h>
24#include <xrpl/protocol/AccountID.h>
25#include <xrpl/protocol/Feature.h>
26#include <xrpl/protocol/MPTIssue.h>
27#include <xrpl/protocol/SField.h>
28#include <xrpl/protocol/STAmount.h>
29#include <xrpl/protocol/STNumber.h>
30#include <xrpl/protocol/TER.h>
31#include <xrpl/protocol/TxFlags.h>
32
33namespace ripple {
34
37{
38 if (!ctx.rules.enabled(featureSingleAssetVault))
39 return temDISABLED;
40
41 if (auto const ter = preflight1(ctx))
42 return ter;
43
44 if (ctx.tx.getFlags() & tfUniversalMask)
45 return temINVALID_FLAG;
46
47 if (ctx.tx[sfVaultID] == beast::zero)
48 {
49 JLOG(ctx.j.debug()) << "VaultClawback: zero/empty vault ID.";
50 return temMALFORMED;
51 }
52
53 AccountID const issuer = ctx.tx[sfAccount];
54 AccountID const holder = ctx.tx[sfHolder];
55
56 if (issuer == holder)
57 {
58 JLOG(ctx.j.debug()) << "VaultClawback: issuer cannot be holder.";
59 return temMALFORMED;
60 }
61
62 auto const amount = ctx.tx[~sfAmount];
63 if (amount)
64 {
65 // Note, zero amount is valid, it means "all". It is also the default.
66 if (*amount < beast::zero)
67 return temBAD_AMOUNT;
68 else if (isXRP(amount->asset()))
69 {
70 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
71 return temMALFORMED;
72 }
73 else if (amount->asset().getIssuer() != issuer)
74 {
75 JLOG(ctx.j.debug())
76 << "VaultClawback: only asset issuer can clawback.";
77 return temMALFORMED;
78 }
79 }
80
81 return preflight2(ctx);
82}
83
84TER
86{
87 auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
88 if (!vault)
89 return tecNO_ENTRY;
90
91 auto account = ctx.tx[sfAccount];
92 auto const issuer = ctx.view.read(keylet::account(account));
93 if (!issuer)
94 {
95 // LCOV_EXCL_START
96 JLOG(ctx.j.error()) << "VaultClawback: missing issuer account.";
97 return tefINTERNAL;
98 // LCOV_EXCL_STOP
99 }
100
101 Asset const vaultAsset = vault->at(sfAsset);
102 if (auto const amount = ctx.tx[~sfAmount];
103 amount && vaultAsset != amount->asset())
104 return tecWRONG_ASSET;
105
106 if (vaultAsset.native())
107 {
108 JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
109 return tecNO_PERMISSION; // Cannot clawback XRP.
110 }
111 else if (vaultAsset.getIssuer() != account)
112 {
113 JLOG(ctx.j.debug()) << "VaultClawback: only asset issuer can clawback.";
114 return tecNO_PERMISSION; // Only issuers can clawback.
115 }
116
117 if (vaultAsset.holds<MPTIssue>())
118 {
119 auto const mpt = vaultAsset.get<MPTIssue>();
120 auto const mptIssue =
121 ctx.view.read(keylet::mptIssuance(mpt.getMptID()));
122 if (mptIssue == nullptr)
123 return tecOBJECT_NOT_FOUND;
124
125 std::uint32_t const issueFlags = mptIssue->getFieldU32(sfFlags);
126 if (!(issueFlags & lsfMPTCanClawback))
127 {
128 JLOG(ctx.j.debug())
129 << "VaultClawback: cannot clawback MPT vault asset.";
130 return tecNO_PERMISSION;
131 }
132 }
133 else if (vaultAsset.holds<Issue>())
134 {
135 std::uint32_t const issuerFlags = issuer->getFieldU32(sfFlags);
136 if (!(issuerFlags & lsfAllowTrustLineClawback) ||
137 (issuerFlags & lsfNoFreeze))
138 {
139 JLOG(ctx.j.debug())
140 << "VaultClawback: cannot clawback IOU vault asset.";
141 return tecNO_PERMISSION;
142 }
143 }
144
145 return tesSUCCESS;
146}
147
148TER
150{
151 auto const& tx = ctx_.tx;
152 auto const vault = view().peek(keylet::vault(tx[sfVaultID]));
153 if (!vault)
154 return tefINTERNAL; // LCOV_EXCL_LINE
155
156 auto const mptIssuanceID = *((*vault)[sfShareMPTID]);
157 auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
158 if (!sleIssuance)
159 {
160 // LCOV_EXCL_START
161 JLOG(j_.error()) << "VaultClawback: missing issuance of vault shares.";
162 return tefINTERNAL;
163 // LCOV_EXCL_STOP
164 }
165
166 Asset const vaultAsset = vault->at(sfAsset);
167 STAmount const amount = [&]() -> STAmount {
168 auto const maybeAmount = tx[~sfAmount];
169 if (maybeAmount)
170 return *maybeAmount;
171 return {sfAmount, vaultAsset, 0};
172 }();
173 XRPL_ASSERT(
174 amount.asset() == vaultAsset,
175 "ripple::VaultClawback::doApply : matching asset");
176
177 auto assetsAvailable = vault->at(sfAssetsAvailable);
178 auto assetsTotal = vault->at(sfAssetsTotal);
179 [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
180 XRPL_ASSERT(
181 lossUnrealized <= (assetsTotal - assetsAvailable),
182 "ripple::VaultClawback::doApply : loss and assets do balance");
183
184 AccountID holder = tx[sfHolder];
185 MPTIssue const share{mptIssuanceID};
186 STAmount sharesDestroyed = {share};
187 STAmount assetsRecovered;
188 try
189 {
190 if (amount == beast::zero)
191 {
192 sharesDestroyed = accountHolds(
193 view(),
194 holder,
195 share,
198 j_);
199
200 auto const maybeAssets =
201 sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
202 if (!maybeAssets)
203 return tecINTERNAL; // LCOV_EXCL_LINE
204 assetsRecovered = *maybeAssets;
205 }
206 else
207 {
208 assetsRecovered = amount;
209 {
210 auto const maybeShares =
211 assetsToSharesWithdraw(vault, sleIssuance, assetsRecovered);
212 if (!maybeShares)
213 return tecINTERNAL; // LCOV_EXCL_LINE
214 sharesDestroyed = *maybeShares;
215 }
216
217 auto const maybeAssets =
218 sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
219 if (!maybeAssets)
220 return tecINTERNAL; // LCOV_EXCL_LINE
221 assetsRecovered = *maybeAssets;
222 }
223
224 // Clamp to maximum.
225 if (assetsRecovered > *assetsAvailable)
226 {
227 assetsRecovered = *assetsAvailable;
228 // Note, it is important to truncate the number of shares, otherwise
229 // the corresponding assets might breach the AssetsAvailable
230 {
231 auto const maybeShares = assetsToSharesWithdraw(
232 vault, sleIssuance, assetsRecovered, TruncateShares::yes);
233 if (!maybeShares)
234 return tecINTERNAL; // LCOV_EXCL_LINE
235 sharesDestroyed = *maybeShares;
236 }
237
238 auto const maybeAssets =
239 sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
240 if (!maybeAssets)
241 return tecINTERNAL; // LCOV_EXCL_LINE
242 assetsRecovered = *maybeAssets;
243 if (assetsRecovered > *assetsAvailable)
244 {
245 // LCOV_EXCL_START
246 JLOG(j_.error())
247 << "VaultClawback: invalid rounding of shares.";
248 return tecINTERNAL;
249 // LCOV_EXCL_STOP
250 }
251 }
252 }
253 catch (std::overflow_error const&)
254 {
255 // It's easy to hit this exception from Number with large enough Scale
256 // so we avoid spamming the log and only use debug here.
257 JLOG(j_.debug()) //
258 << "VaultClawback: overflow error with"
259 << " scale=" << (int)vault->at(sfScale).value() //
260 << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
261 << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
262 << ", amount=" << amount.value();
263 return tecPATH_DRY;
264 }
265
266 if (sharesDestroyed == beast::zero)
267 return tecPRECISION_LOSS;
268
269 assetsTotal -= assetsRecovered;
270 assetsAvailable -= assetsRecovered;
271 view().update(vault);
272
273 auto const& vaultAccount = vault->at(sfAccount);
274 // Transfer shares from holder to vault.
275 if (auto const ter = accountSend(
276 view(),
277 holder,
278 vaultAccount,
279 sharesDestroyed,
280 j_,
282 !isTesSuccess(ter))
283 return ter;
284
285 // Try to remove MPToken for shares, if the holder balance is zero. Vault
286 // pseudo-account will never set lsfMPTAuthorized, so we ignore flags.
287 // Keep MPToken if holder is the vault owner.
288 if (holder != vault->at(sfOwner))
289 {
290 if (auto const ter =
291 removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
292 isTesSuccess(ter))
293 {
294 JLOG(j_.debug()) //
295 << "VaultClawback: removed empty MPToken for vault shares"
296 << " MPTID=" << to_string(mptIssuanceID) //
297 << " account=" << toBase58(holder);
298 }
299 else if (ter != tecHAS_OBLIGATIONS)
300 {
301 // LCOV_EXCL_START
302 JLOG(j_.error()) //
303 << "VaultClawback: failed to remove MPToken for vault shares"
304 << " MPTID=" << to_string(mptIssuanceID) //
305 << " account=" << toBase58(holder) //
306 << " with result: " << transToken(ter);
307 return ter;
308 // LCOV_EXCL_STOP
309 }
310 // else quietly ignore, holder balance is not zero
311 }
312
313 // Transfer assets from vault to issuer.
314 if (auto const ter = accountSend(
315 view(),
316 vaultAccount,
317 account_,
318 assetsRecovered,
319 j_,
321 !isTesSuccess(ter))
322 return ter;
323
324 // Sanity check
325 if (accountHolds(
326 view(),
327 vaultAccount,
328 assetsRecovered.asset(),
331 j_) < beast::zero)
332 {
333 // LCOV_EXCL_START
334 JLOG(j_.error()) << "VaultClawback: negative balance of vault assets.";
335 return tefINTERNAL;
336 // LCOV_EXCL_STOP
337 }
338
339 return tesSUCCESS;
340}
341
342} // namespace ripple
Stream error() const
Definition Journal.h:346
Stream debug() const
Definition Journal.h:328
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.
bool native() const
Definition Asset.h:101
constexpr TIss const & get() const
AccountID const & getIssuer() const
Definition Asset.cpp:36
constexpr bool holds() const
Definition Asset.h:132
A currency issued by an account.
Definition Issue.h:33
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:130
Asset const & asset() const
Definition STAmount.h:483
STAmount const & value() const noexcept
Definition STAmount.h:594
std::uint32_t getFlags() const
Definition STObject.cpp:537
AccountID const account_
Definition Transactor.h:145
ApplyView & view()
Definition Transactor.h:161
beast::Journal const j_
Definition Transactor.h:143
ApplyContext & ctx_
Definition Transactor.h:141
static NotTEC preflight(PreflightContext const &ctx)
static TER preclaim(PreclaimContext const &ctx)
Keylet mptIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:526
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:564
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:184
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
@ fhIGNORE_FREEZE
Definition View.h:77
bool isXRP(AccountID const &c)
Definition AccountID.h:90
@ lsfAllowTrustLineClawback
@ lsfMPTCanClawback
@ ahIGNORE_AUTH
Definition View.h:80
NotTEC preflight1(PreflightContext const &ctx)
Performs early sanity checks on the account and fee fields.
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:2914
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:2174
@ tefINTERNAL
Definition TER.h:173
std::optional< STAmount > sharesToAssetsWithdraw(std::shared_ptr< SLE const > const &vault, std::shared_ptr< SLE const > const &issuance, STAmount const &shares)
Definition View.cpp:2943
std::string transToken(TER code)
Definition TER.cpp:264
NotTEC preflight2(PreflightContext const &ctx)
Checks whether the signature appears valid.
@ tecNO_ENTRY
Definition TER.h:306
@ tecOBJECT_NOT_FOUND
Definition TER.h:326
@ tecINTERNAL
Definition TER.h:310
@ tecNO_PERMISSION
Definition TER.h:305
@ tecPRECISION_LOSS
Definition TER.h:363
@ tecHAS_OBLIGATIONS
Definition TER.h:317
@ tecWRONG_ASSET
Definition TER.h:360
@ tecPATH_DRY
Definition TER.h:294
@ tesSUCCESS
Definition TER.h:244
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j)
Definition View.cpp:384
bool isTesSuccess(TER x) noexcept
Definition TER.h:674
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:630
constexpr std::uint32_t tfUniversalMask
Definition TxFlags.h:63
TER removeEmptyHolding(ApplyView &view, AccountID const &accountID, Issue const &issue, beast::Journal journal)
Definition View.cpp:1508
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:605
@ temBAD_AMOUNT
Definition TER.h:89
@ temMALFORMED
Definition TER.h:87
@ temINVALID_FLAG
Definition TER.h:111
@ temDISABLED
Definition TER.h:114
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:80
ReadView const & view
Definition Transactor.h:83
beast::Journal const j
Definition Transactor.h:88
State information when preflighting a tx.
Definition Transactor.h:35
beast::Journal const j
Definition Transactor.h:42