fix: Check trustline limits for share-denominated vault withdrawals#6645
fix: Check trustline limits for share-denominated vault withdrawals#6645
Conversation
VaultWithdraw preclaim skipped the canWithdraw trustline limit check when the withdrawal amount was specified in shares (MPT) rather than assets. Convert shares to the equivalent asset amount before calling canWithdraw so that the destination's trustline limit is enforced regardless of denomination. Gated behind fixAssortedFixes for ledger replay compatibility.
Move sleIssuance ledger read inside the fixAssortedFixes branch where it is actually needed, avoiding an unnecessary read on the pre-amendment and asset-denominated paths. Reuse existing account and dstAcct variables instead of recomputing from and to. Add pre-amendment test to verify the old behavior (share-denominated withdrawal bypasses trustline limit check) alongside the post-fix behavior. Fix stale comments in the test.
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
|
All conflicts have been resolved. Assigned reviewers can now start or resume their review. |
|
/ai-review |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #6645 +/- ##
=======================================
Coverage 81.4% 81.4%
=======================================
Files 998 998
Lines 74443 74452 +9
Branches 7563 7560 -3
=======================================
+ Hits 60632 60640 +8
- Misses 13811 13812 +1
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Gave this a review
Two issues flagged inline: a rounding-direction concern in the share→asset conversion that could allow boundary bypass, and a tec/tef inconsistency on the unreachable error path.
Review by ReviewBot 🤖
Review by Claude Opus 4.6 · Prompt: V12
|
|
||
| if (auto const ret = canWithdraw(ctx.view, ctx.tx)) | ||
| return ret; | ||
| if (ctx.view.rules().enabled(fixSecurity3_1_3) && amount.asset() == vaultShare) |
There was a problem hiding this comment.
Verify sharesToAssetsWithdraw rounds up (conservative) when passed to canWithdraw — rounding down could allow a withdrawal that slightly exceeds the trust-line limit.
If the conversion rounds down, shares worth exactly the limit could pass the check while delivering more assets than permitted. Confirm the rounding direction is conservative for the amount passed to canWithdraw.
|
|
||
| auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, amount); | ||
| if (!maybeAssets) | ||
| return tecINTERNAL; // LCOV_EXCL_LINE |
There was a problem hiding this comment.
tecINTERNAL charges the account a fee; use tefINTERNAL for unreachable internal errors, consistent with line 82:
| return tecINTERNAL; // LCOV_EXCL_LINE | |
| return tefINTERNAL; // LCOV_EXCL_LINE |
There was a problem hiding this comment.
Pull request overview
Fixes a VaultWithdraw preclaim gap where withdrawals specified in vault shares (MPT) could bypass the destination IOU trustline limit check by converting share-denominated amounts into the underlying asset amount before applying canWithdraw (gated by an amendment for replay compatibility).
Changes:
- Update
VaultWithdraw::preclaimto convert share-denominated withdrawals into asset-denominated amounts (when the fix amendment is enabled) before callingcanWithdraw. - Add a regression test reproducing the trustline-limit bypass when withdrawing in shares vs assets.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp |
Converts share-denominated withdrawal amounts to asset amounts (behind fixSecurity3_1_3) so trustline limits are enforced consistently. |
src/test/app/Vault_test.cpp |
Adds a unit test demonstrating the pre-fix bypass and asserting rejection post-fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (ctx.view.rules().enabled(fixSecurity3_1_3) && amount.asset() == vaultShare) | ||
| { | ||
| // Post-fixSecurity3_1_3: if the user specified shares, convert | ||
| // to the equivalent asset amount before checking withdrawal | ||
| // limits. Pre-amendment the limit check was skipped for | ||
| // share-denominated withdrawals. | ||
| auto const mptIssuanceID = vault->at(sfShareMPTID); | ||
| auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID)); | ||
| if (!sleIssuance) | ||
| { | ||
| // LCOV_EXCL_START | ||
| JLOG(ctx.j.error()) << "VaultWithdraw: missing issuance of vault shares."; | ||
| return tefINTERNAL; | ||
| // LCOV_EXCL_STOP | ||
| } | ||
|
|
||
| auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, amount); | ||
| if (!maybeAssets) | ||
| return tecINTERNAL; // LCOV_EXCL_LINE | ||
|
|
||
| if (auto const ret = canWithdraw( | ||
| ctx.view, account, dstAcct, *maybeAssets, ctx.tx.isFieldPresent(sfDestinationTag))) | ||
| return ret; |
There was a problem hiding this comment.
The new share-denominated path calls sharesToAssetsWithdraw() in preclaim without guarding against std::overflow_error. The same conversion is wrapped in a try/catch in doApply(), so an overflow here could propagate as an exception during preclaim instead of returning a TER. Consider wrapping the conversion (and/or the whole share-conversion block) in a try/catch and mapping overflow to the same TER used in doApply (e.g. tecPATH_DRY) or another appropriate code.
| if (ctx.view.rules().enabled(fixSecurity3_1_3) && amount.asset() == vaultShare) | ||
| { | ||
| // Post-fixSecurity3_1_3: if the user specified shares, convert | ||
| // to the equivalent asset amount before checking withdrawal | ||
| // limits. Pre-amendment the limit check was skipped for | ||
| // share-denominated withdrawals. |
There was a problem hiding this comment.
PR description says this behavior is gated behind fixAssortedFixes, but the implementation (and the new test) gates it behind fixSecurity3_1_3. There doesn't appear to be a fixAssortedFixes amendment in the codebase, so either the PR description should be updated to match fixSecurity3_1_3, or the code should use the intended amendment name for replay compatibility.
VaultWithdraw preclaim skipped the canWithdraw trustline limit check when the withdrawal amount was specified in shares (MPT) rather than assets. Convert shares to the equivalent asset amount before calling canWithdraw so that the destination's trustline limit is enforced regardless of denomination.
Gated behind fixAssortedFixes for ledger replay compatibility.
High Level Overview of Change
Context of Change
API Impact
libxrplchange (any change that may affectlibxrplor dependents oflibxrpl)