rippled
Loading...
Searching...
No Matches
VaultWithdraw.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/VaultWithdraw.h>
21
22#include <xrpl/ledger/CredentialHelpers.h>
23#include <xrpl/ledger/View.h>
24#include <xrpl/protocol/AccountID.h>
25#include <xrpl/protocol/Feature.h>
26#include <xrpl/protocol/SField.h>
27#include <xrpl/protocol/STNumber.h>
28#include <xrpl/protocol/TER.h>
29#include <xrpl/protocol/TxFlags.h>
30
31namespace ripple {
32
35{
36 if (!ctx.rules.enabled(featureSingleAssetVault))
37 return temDISABLED;
38
39 if (auto const ter = preflight1(ctx))
40 return ter;
41
42 if (ctx.tx.getFlags() & tfUniversalMask)
43 return temINVALID_FLAG;
44
45 if (ctx.tx[sfVaultID] == beast::zero)
46 {
47 JLOG(ctx.j.debug()) << "VaultWithdraw: zero/empty vault ID.";
48 return temMALFORMED;
49 }
50
51 if (ctx.tx[sfAmount] <= beast::zero)
52 return temBAD_AMOUNT;
53
54 if (auto const destination = ctx.tx[~sfDestination];
55 destination.has_value())
56 {
57 if (*destination == beast::zero)
58 {
59 JLOG(ctx.j.debug())
60 << "VaultWithdraw: zero/empty destination account.";
61 return temMALFORMED;
62 }
63 }
64 else if (ctx.tx.isFieldPresent(sfDestinationTag))
65 {
66 JLOG(ctx.j.debug()) << "VaultWithdraw: sfDestinationTag is set but "
67 "sfDestination is not";
68 return temMALFORMED;
69 }
70
71 return preflight2(ctx);
72}
73
74TER
76{
77 auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
78 if (!vault)
79 return tecNO_ENTRY;
80
81 auto const assets = ctx.tx[sfAmount];
82 auto const vaultAsset = vault->at(sfAsset);
83 auto const vaultShare = vault->at(sfShareMPTID);
84 if (assets.asset() != vaultAsset && assets.asset() != vaultShare)
85 return tecWRONG_ASSET;
86
87 if (vaultAsset.native())
88 ; // No special checks for XRP
89 else if (vaultAsset.holds<MPTIssue>())
90 {
91 auto mptID = vaultAsset.get<MPTIssue>().getMptID();
92 auto issuance = ctx.view.read(keylet::mptIssuance(mptID));
93 if (!issuance)
95 if (!issuance->isFlag(lsfMPTCanTransfer))
96 {
97 // LCOV_EXCL_START
98 JLOG(ctx.j.error())
99 << "VaultWithdraw: vault assets are non-transferable.";
100 return tecNO_AUTH;
101 // LCOV_EXCL_STOP
102 }
103 }
104 else if (vaultAsset.holds<Issue>())
105 {
106 auto const issuer =
107 ctx.view.read(keylet::account(vaultAsset.getIssuer()));
108 if (!issuer)
109 {
110 // LCOV_EXCL_START
111 JLOG(ctx.j.error())
112 << "VaultWithdraw: missing issuer of vault assets.";
113 return tefINTERNAL;
114 // LCOV_EXCL_STOP
115 }
116 }
117
118 // Enforce valid withdrawal policy
119 if (vault->at(sfWithdrawalPolicy) != vaultStrategyFirstComeFirstServe)
120 {
121 // LCOV_EXCL_START
122 JLOG(ctx.j.error()) << "VaultWithdraw: invalid withdrawal policy.";
123 return tefINTERNAL;
124 // LCOV_EXCL_STOP
125 }
126
127 auto const account = ctx.tx[sfAccount];
128 auto const dstAcct = [&]() -> AccountID {
129 if (ctx.tx.isFieldPresent(sfDestination))
130 return ctx.tx.getAccountID(sfDestination);
131 return account;
132 }();
133
134 // Withdrawal to a 3rd party destination account is essentially a transfer,
135 // via shares in the vault. Enforce all the usual asset transfer checks.
136 AuthType authType = AuthType::Legacy;
137 if (account != dstAcct)
138 {
139 auto const sleDst = ctx.view.read(keylet::account(dstAcct));
140 if (sleDst == nullptr)
141 return tecNO_DST;
142
143 if (sleDst->isFlag(lsfRequireDestTag) &&
144 !ctx.tx.isFieldPresent(sfDestinationTag))
145 return tecDST_TAG_NEEDED; // Cannot send without a tag
146
147 if (sleDst->isFlag(lsfDepositAuth))
148 {
149 if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account)))
150 return tecNO_PERMISSION;
151 }
152 // The destination account must have consented to receive the asset by
153 // creating a RippleState or MPToken
154 authType = AuthType::StrongAuth;
155 }
156
157 // Destination MPToken (for an MPT) or trust line (for an IOU) must exist
158 // if not sending to Account.
159 if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType);
160 !isTesSuccess(ter))
161 return ter;
162
163 // Cannot withdraw from a Vault an Asset frozen for the destination account
164 if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset))
165 return ret;
166
167 if (auto const ret = checkFrozen(ctx.view, account, vaultShare))
168 return ret;
169
170 return tesSUCCESS;
171}
172
173TER
175{
176 auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
177 if (!vault)
178 return tefINTERNAL; // LCOV_EXCL_LINE
179
180 auto const mptIssuanceID = *((*vault)[sfShareMPTID]);
181 auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
182 if (!sleIssuance)
183 {
184 // LCOV_EXCL_START
185 JLOG(j_.error()) << "VaultWithdraw: missing issuance of vault shares.";
186 return tefINTERNAL;
187 // LCOV_EXCL_STOP
188 }
189
190 // Note, we intentionally do not check lsfVaultPrivate flag on the Vault. If
191 // you have a share in the vault, it means you were at some point authorized
192 // to deposit into it, and this means you are also indefinitely authorized
193 // to withdraw from it.
194
195 auto const amount = ctx_.tx[sfAmount];
196 Asset const vaultAsset = vault->at(sfAsset);
197 MPTIssue const share{mptIssuanceID};
198 STAmount sharesRedeemed = {share};
199 STAmount assetsWithdrawn;
200 try
201 {
202 if (amount.asset() == vaultAsset)
203 {
204 // Fixed assets, variable shares.
205 {
206 auto const maybeShares =
207 assetsToSharesWithdraw(vault, sleIssuance, amount);
208 if (!maybeShares)
209 return tecINTERNAL; // LCOV_EXCL_LINE
210 sharesRedeemed = *maybeShares;
211 }
212
213 if (sharesRedeemed == beast::zero)
214 return tecPRECISION_LOSS;
215 auto const maybeAssets =
216 sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed);
217 if (!maybeAssets)
218 return tecINTERNAL; // LCOV_EXCL_LINE
219 assetsWithdrawn = *maybeAssets;
220 }
221 else if (amount.asset() == share)
222 {
223 // Fixed shares, variable assets.
224 sharesRedeemed = amount;
225 auto const maybeAssets =
226 sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed);
227 if (!maybeAssets)
228 return tecINTERNAL; // LCOV_EXCL_LINE
229 assetsWithdrawn = *maybeAssets;
230 }
231 else
232 return tefINTERNAL; // LCOV_EXCL_LINE
233 }
234 catch (std::overflow_error const&)
235 {
236 // It's easy to hit this exception from Number with large enough Scale
237 // so we avoid spamming the log and only use debug here.
238 JLOG(j_.debug()) //
239 << "VaultWithdraw: overflow error with"
240 << " scale=" << (int)vault->at(sfScale).value() //
241 << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
242 << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
243 << ", amount=" << amount.value();
244 return tecPATH_DRY;
245 }
246
247 if (accountHolds(
248 view(),
249 account_,
250 share,
253 j_) < sharesRedeemed)
254 {
255 JLOG(j_.debug()) << "VaultWithdraw: account doesn't hold enough shares";
257 }
258
259 auto assetsAvailable = vault->at(sfAssetsAvailable);
260 auto assetsTotal = vault->at(sfAssetsTotal);
261 [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
262 XRPL_ASSERT(
263 lossUnrealized <= (assetsTotal - assetsAvailable),
264 "ripple::VaultWithdraw::doApply : loss and assets do balance");
265
266 // The vault must have enough assets on hand. The vault may hold assets
267 // that it has already pledged. That is why we look at AssetAvailable
268 // instead of the pseudo-account balance.
269 if (*assetsAvailable < assetsWithdrawn)
270 {
271 JLOG(j_.debug()) << "VaultWithdraw: vault doesn't hold enough assets";
273 }
274
275 assetsTotal -= assetsWithdrawn;
276 assetsAvailable -= assetsWithdrawn;
277 view().update(vault);
278
279 auto const& vaultAccount = vault->at(sfAccount);
280 // Transfer shares from depositor to vault.
281 if (auto const ter = accountSend(
282 view(),
283 account_,
284 vaultAccount,
285 sharesRedeemed,
286 j_,
288 !isTesSuccess(ter))
289 return ter;
290
291 // Try to remove MPToken for shares, if the account balance is zero. Vault
292 // pseudo-account will never set lsfMPTAuthorized, so we ignore flags.
293 // Keep MPToken if holder is the vault owner.
294 if (account_ != vault->at(sfOwner))
295 {
296 if (auto const ter = removeEmptyHolding(
297 view(), account_, sharesRedeemed.asset(), j_);
298 isTesSuccess(ter))
299 {
300 JLOG(j_.debug()) //
301 << "VaultWithdraw: removed empty MPToken for vault shares"
302 << " MPTID=" << to_string(mptIssuanceID) //
303 << " account=" << toBase58(account_);
304 }
305 else if (ter != tecHAS_OBLIGATIONS)
306 {
307 // LCOV_EXCL_START
308 JLOG(j_.error()) //
309 << "VaultWithdraw: failed to remove MPToken for vault shares"
310 << " MPTID=" << to_string(mptIssuanceID) //
311 << " account=" << toBase58(account_) //
312 << " with result: " << transToken(ter);
313 return ter;
314 // LCOV_EXCL_STOP
315 }
316 // else quietly ignore, account balance is not zero
317 }
318
319 auto const dstAcct = [&]() -> AccountID {
320 if (ctx_.tx.isFieldPresent(sfDestination))
321 return ctx_.tx.getAccountID(sfDestination);
322 return account_;
323 }();
324
325 // Transfer assets from vault to depositor or destination account.
326 if (auto const ter = accountSend(
327 view(),
328 vaultAccount,
329 dstAcct,
330 assetsWithdrawn,
331 j_,
333 !isTesSuccess(ter))
334 return ter;
335
336 // Sanity check
337 if (accountHolds(
338 view(),
339 vaultAccount,
340 assetsWithdrawn.asset(),
343 j_) < beast::zero)
344 {
345 // LCOV_EXCL_START
346 JLOG(j_.error()) << "VaultWithdraw: negative balance of vault assets.";
347 return tefINTERNAL;
348 // LCOV_EXCL_STOP
349 }
350
351 return tesSUCCESS;
352}
353
354} // 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.
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.
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
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
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:651
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:484
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 TER preclaim(PreclaimContext const &ctx)
static NotTEC preflight(PreflightContext 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
Keylet depositPreauth(AccountID const &owner, AccountID const &preauthorized) noexcept
A DepositPreauth.
Definition Indexes.cpp:342
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.
TER checkFrozen(ReadView const &view, AccountID const &account, Issue const &issue)
Definition View.h:178
@ fhZERO_IF_FROZEN
Definition View.h:77
@ fhIGNORE_FREEZE
Definition View.h:77
@ lsfMPTCanTransfer
@ lsfRequireDestTag
AuthType
Definition View.h:786
@ 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
TER requireAuth(ReadView const &view, Issue const &issue, AccountID const &account, AuthType authType=AuthType::Legacy)
Check if the account lacks required authorization.
Definition View.cpp:2464
@ 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
@ tecNO_DST
Definition TER.h:290
@ tecOBJECT_NOT_FOUND
Definition TER.h:326
@ tecINSUFFICIENT_FUNDS
Definition TER.h:325
@ tecINTERNAL
Definition TER.h:310
@ tecNO_PERMISSION
Definition TER.h:305
@ tecDST_TAG_NEEDED
Definition TER.h:309
@ tecPRECISION_LOSS
Definition TER.h:363
@ tecHAS_OBLIGATIONS
Definition TER.h:317
@ tecWRONG_ASSET
Definition TER.h:360
@ tecPATH_DRY
Definition TER.h:294
@ tecNO_AUTH
Definition TER.h:300
@ 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
std::uint8_t constexpr vaultStrategyFirstComeFirstServe
Vault withdrawal policies.
Definition Protocol.h:122
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