mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 18:45:52 +00:00
fix: floating point representation errors in vault (#5997)
This change fixes floating point errors in conversion of shares to assets and other way, used in `VaultDeposit`, `VaultWithdraw` and `VaultClawback`. In the floating point calculations the division introduces a larger error than multiplication. If we do division first, then the error introduced will be increased by the multiplication that follows, which is therefore the wrong order to perform these two operations. This change flips the order of arithmetic operations, which minimizes the error.
This commit is contained in:
@@ -2904,7 +2904,7 @@ assetsToSharesDeposit(
|
||||
.truncate()};
|
||||
|
||||
Number const shareTotal = issuance->at(sfOutstandingAmount);
|
||||
shares = (shareTotal * (assets / assetTotal)).truncate();
|
||||
shares = ((shareTotal * assets) / assetTotal).truncate();
|
||||
return shares;
|
||||
}
|
||||
|
||||
@@ -2933,7 +2933,7 @@ sharesToAssetsDeposit(
|
||||
false};
|
||||
|
||||
Number const shareTotal = issuance->at(sfOutstandingAmount);
|
||||
assets = assetTotal * (shares / shareTotal);
|
||||
assets = (assetTotal * shares) / shareTotal;
|
||||
return assets;
|
||||
}
|
||||
|
||||
@@ -2959,7 +2959,7 @@ assetsToSharesWithdraw(
|
||||
if (assetTotal == 0)
|
||||
return shares;
|
||||
Number const shareTotal = issuance->at(sfOutstandingAmount);
|
||||
Number result = shareTotal * (assets / assetTotal);
|
||||
Number result = (shareTotal * assets) / assetTotal;
|
||||
if (truncate == TruncateShares::yes)
|
||||
result = result.truncate();
|
||||
shares = result;
|
||||
@@ -2987,7 +2987,7 @@ sharesToAssetsWithdraw(
|
||||
if (assetTotal == 0)
|
||||
return assets;
|
||||
Number const shareTotal = issuance->at(sfOutstandingAmount);
|
||||
assets = assetTotal * (shares / shareTotal);
|
||||
assets = (assetTotal * shares) / shareTotal;
|
||||
return assets;
|
||||
}
|
||||
|
||||
|
||||
@@ -2473,6 +2473,7 @@ class Vault_test : public beast::unit_test::suite
|
||||
struct CaseArgs
|
||||
{
|
||||
int initialXRP = 1000;
|
||||
Number initialIOU = 200;
|
||||
double transferRate = 1.0;
|
||||
};
|
||||
|
||||
@@ -2500,7 +2501,7 @@ class Vault_test : public beast::unit_test::suite
|
||||
PrettyAsset const asset = issuer["IOU"];
|
||||
env.trust(asset(1000), owner);
|
||||
env.trust(asset(1000), charlie);
|
||||
env(pay(issuer, owner, asset(200)));
|
||||
env(pay(issuer, owner, asset(args.initialIOU)));
|
||||
env(rate(issuer, args.transferRate));
|
||||
env.close();
|
||||
|
||||
@@ -2878,6 +2879,86 @@ class Vault_test : public beast::unit_test::suite
|
||||
env(tx1);
|
||||
});
|
||||
|
||||
testCase(
|
||||
[&, this](
|
||||
Env& env,
|
||||
Account const& owner,
|
||||
Account const& issuer,
|
||||
Account const& charlie,
|
||||
auto const& vaultAccount,
|
||||
Vault& vault,
|
||||
PrettyAsset const& asset,
|
||||
auto&&...) {
|
||||
testcase("IOU calculation rounding");
|
||||
|
||||
auto [tx, keylet] =
|
||||
vault.create({.owner = owner, .asset = asset});
|
||||
tx[sfScale] = 1;
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto const startingOwnerBalance = env.balance(owner, asset);
|
||||
BEAST_EXPECT(
|
||||
(startingOwnerBalance.value() ==
|
||||
STAmount{asset, 11875, -2}));
|
||||
|
||||
// This operation (first deposit 100, then 3.75 x 5) is known to
|
||||
// have triggered calculation rounding errors in Number
|
||||
// (addition and division), causing the last deposit to be
|
||||
// blocked by Vault invariants.
|
||||
env(vault.deposit(
|
||||
{.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = asset(100)}));
|
||||
|
||||
auto const tx1 = vault.deposit(
|
||||
{.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = asset(Number(375, -2))});
|
||||
for (auto i = 0; i < 5; ++i)
|
||||
{
|
||||
env(tx1);
|
||||
}
|
||||
env.close();
|
||||
|
||||
{
|
||||
STAmount const xfer{asset, 1185, -1};
|
||||
BEAST_EXPECT(
|
||||
env.balance(owner, asset) ==
|
||||
startingOwnerBalance.value() - xfer);
|
||||
BEAST_EXPECT(
|
||||
env.balance(vaultAccount(keylet), asset) == xfer);
|
||||
|
||||
auto const vault = env.le(keylet);
|
||||
BEAST_EXPECT(vault->at(sfAssetsAvailable) == xfer);
|
||||
BEAST_EXPECT(vault->at(sfAssetsTotal) == xfer);
|
||||
}
|
||||
|
||||
// Total vault balance should be 118.5 IOU. Withdraw and delete
|
||||
// the vault to verify this exact amount was deposited and the
|
||||
// owner has matching shares
|
||||
env(vault.withdraw(
|
||||
{.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = asset(Number(1000 + 37 * 5, -1))}));
|
||||
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
env.balance(owner, asset) ==
|
||||
startingOwnerBalance.value());
|
||||
BEAST_EXPECT(
|
||||
env.balance(vaultAccount(keylet), asset) ==
|
||||
beast::zero);
|
||||
auto const vault = env.le(keylet);
|
||||
BEAST_EXPECT(vault->at(sfAssetsAvailable) == beast::zero);
|
||||
BEAST_EXPECT(vault->at(sfAssetsTotal) == beast::zero);
|
||||
}
|
||||
|
||||
env(vault.del({.owner = owner, .id = keylet.key}));
|
||||
env.close();
|
||||
},
|
||||
{.initialIOU = Number(11875, -2)});
|
||||
|
||||
auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
|
||||
Env env{*this, testable_amendments()};
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user