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