fix: Round vault deposit and withdraw conversions to prevent shareholder dilution - #7824
fix: Round vault deposit and withdraw conversions to prevent shareholder dilution#7824muhammadsalah wants to merge 1 commit into
Conversation
|
If only the most recent commit is unsigned, you can run:
If multiple commits are unsigned, you can run:
If you're new to commit signing, there are different ways to set it up: Sign commits with
|
310dd72 to
81dbb1b
Compare
…der dilution Under the fixCleanup3_3_0 amendment, round the vault's share<->asset conversions in the pool's favor so that a single depositor or withdrawer can no longer dilute the other shareholders through round-to-nearest. Deposit: sharesToAssetsDeposit() rounds the assets charged for freshly minted shares Upward, so the depositor pays at least the fair value of those shares. Round-to-nearest could undercharge, overvaluing the new shares. VaultDeposit::doApply() passes Upward when the amendment is on. Withdraw: sharesToAssetsWithdraw() rounds the assets paid for burned shares Downward, so the withdrawer receives at most the fair value of those shares. Round-to-nearest could overpay, dropping the assets-per- share price for the remaining holders. VaultWithdraw::doApply() passes Downward on both the fixed-asset and fixed-share branches, and preclaim matches it so the withdrawal-limit check estimates the same amount. Both helpers take a rounding mode defaulting to ToNearest to preserve pre-amendment behavior; the quantization to the asset's precision honors the mode for integral (XRP/MPT) and IOU assets alike. Clawback is left unchanged. Direct unit tests on an MPT asset exercise each conversion under ToNearest vs the pool-favoring mode. This addresses catches found by the Common Prefix formal verification public project.
81dbb1b to
0d2ac58
Compare
|
Hello, this is my first contribution to RIPPLED |
High Level Overview of Change
Under the
fixCleanup3_3_0amendment, the vault's share↔asset conversions are rounded in the pool's favor so that a single depositor or withdrawer can no longer dilute the other shareholders through round-to-nearest:sharesToAssetsDeposit()rounds the assets charged for freshly minted shares Upward, so the depositor pays at least the fair value of those shares.sharesToAssetsWithdraw()rounds the assets paid for burned shares Downward, so the withdrawer receives at most the fair value of those shares.Both helpers take a rounding-mode parameter defaulting to
ToNearest(pre-amendment behavior); the paired transactors pass the pool-favoring mode only when the amendment is enabled.Context of Change
Both defects predate this PR and are symmetric. The conversions computed
assetsTotal · shares / shareTotaland quantized to the asset's precision with the defaultToNearestrounding:Both are corrected only behind
fixCleanup3_3_0, so pre-amendment ledgers replay identically. Part of the ongoing vault/lending rounding cleanup (cf.fixCleanup3_2_0,fixCleanup3_1_3).Scope notes:
assetsToSharesDepositalready truncates (down) andassetsToSharesWithdrawneeds no change — flooring the withdraw payout alone guaranteesassetsWithdrawn ≤ fairValue(sharesBurned)regardless of how the shares were derived, so the "equal balance" invariant is preserved by construction (assetsWithdrawndrives both the vault decrement and the destination credit).sharesToAssetsWithdraware intentionally left onToNearest(different economic direction).VaultWithdraw::preclaimpasses the sameDownwardmode so the withdrawal-limit check estimates the same amount the apply will pay.Bug discovery — Common Prefix formal verification. These fixes address findings from Common Prefix's public formal verification of the XRPL single-asset vault / lending protocol. Their
testVaultDepositOvervaluedSharessnippet states the deposit finding directly:The withdraw payout corrected here is the exact symmetric case: round-to-nearest can overpay a withdrawer, diluting the holders who remain.
API Impact
libxrplchange —sharesToAssetsDeposit()andsharesToAssetsWithdraw()ininclude/xrpl/ledger/helpers/VaultHelpers.heach gain a defaultedNumber::RoundingModeparameter (source-compatible; the default preserves prior behavior).Transaction-processing behavior changes only when
fixCleanup3_3_0is enabled, so no peer-protocol/version impact.Before / After
Inexact rate, pool
100assets /3shares:ToNearest)33.33…33(33·3=99 < 100→ dilution)34Upward (102 ≥ 100)66.67…67(67·3=201 > 200→ dilution)66Downward (198 ≤ 200)Exact rates, and cases where
ToNearestalready rounds the safe way, are unchanged — the fix only ever moves the amount toward the pool.Test Plan
Direct unit tests of both conversion helpers (
src/test/app/Vault_test.cpp) build minimalltVAULT+ltMPTOKEN_ISSUANCESLEs and call the helper withToNearestvs the pool-favoring mode:testDepositRoundingFavorsPool— asserts the undercharge/dilution case (33vs34) and the invariantcharged · shareTotal ≥ assetsTotal · shares, plus no-op cases.testWithdrawRoundingFavorsPool— asserts the overpay/dilution case (67vs66) and the invariantpaid · shareTotal ≤ assetsTotal · shares, plus exact-rate and large awkward-rate cases.Both target MPT assets deliberately: for IOU the fair value fits within STAmount's 16-digit quantization so the rounding difference is sub-ULP and unobservable; on integral (MPT/XRP) assets a wrong rounding is a full unit.
Local regression run (Debug):
xrpl.app.Vault(409 cases / 11,358 tests),xrpl.tx.Loan(1,055 / 115,934 — the unrealized-loss withdraw path), andxrpl.app.Invariants(85 / 13,978) all pass with 0 failures.