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