fix: Clamp Vault Deposit, Withdraw, and Clawback to assetsTotal grid

sfAssetsTotal is stored on a coarser STAmount grid than sfAssetsAvailable
and the vault's trust line, so adding the same amount to all three
quantizes differently on each and leaves the vault's books disagreeing
with its actual holdings by a sub-ULP amount. Fix by clamping the
credited/withdrawn amount to what sfAssetsTotal can represent before
applying it to the other rails, via a shared clampToAssetsTotalScale
helper used by all three transactors.

- Deposit: clamp assetsDeposited downward to the assetsTotal grid, then
  re-derive shares from the clamped amount so the depositor cannot
  receive shares worth more than they paid. Return tecPRECISION_LOSS if
  the clamp rounds the deposit to zero.
- Withdraw: clamp assetsWithdrawn upward (i.e. the vault pays out
  slightly less) so it never pays out more than it can account for.
  Shares are not re-derived, so the withdrawer receives slightly less
  per share, favouring remaining holders.
- Clawback: same pattern as withdraw, guarded on assetsRecovered > 0 and
  placed after the existing clamp-to-available.

Gated on fixCleanup3_4_0.
This commit is contained in:
Vito
2026-08-19 19:54:50 +02:00
parent a6983f8bf3
commit ec8a9cdbf8
8 changed files with 1109 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Protocol.h>
@@ -41,6 +42,27 @@ assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
[[nodiscard]] std::optional<STAmount>
sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& shares);
/**
* Clamps `delta` (positive when crediting the vault, negative when debiting
* it) to the largest magnitude that changes sfAssetsTotal by an exact
* multiple of its own STAmount grid step, rounding in `mode`. The returned
* delta has the same sign convention as `delta` (i.e. this is a magnitude
* adjustment toward zero, never away from it). Applying the returned delta
* to both sfAssetsTotal and any other field derived from the same raw amount
* (e.g. sfAssetsAvailable) keeps them exactly in sync, since those fields'
* scale is always at least as fine as sfAssetsTotal's.
*
* @param vault The vault SLE (mutable, since sfAssetsTotal is read through a
* non-const field proxy).
* @param delta The signed amount by which sfAssetsTotal is about to change.
* @param mode The rounding mode to apply when quantizing to sfAssetsTotal's
* scale.
*
* @return The clamped delta.
*/
[[nodiscard]] STAmount
clampToAssetsTotalScale(SLE::ref vault, STAmount const& delta, Number::RoundingMode mode);
/**
* Controls whether to truncate shares instead of rounding.
*/