Take baseline changes from original implementation

- Won't build
This commit is contained in:
Ed Hennis
2025-11-16 13:10:01 -05:00
parent 13a12c6402
commit 248d267f21
14 changed files with 158 additions and 17 deletions

View File

@@ -13,6 +13,15 @@ class Number;
std::string std::string
to_string(Number const& amount); to_string(Number const& amount);
template <typename T>
constexpr bool
isPowerOfTen(T value)
{
while (value >= 10 && value % 10 == 0)
value /= 10;
return value == 1;
}
class Number class Number
{ {
using rep = std::int64_t; using rep = std::int64_t;
@@ -21,8 +30,13 @@ class Number
public: public:
// The range for the mantissa when normalized // The range for the mantissa when normalized
constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; constexpr static rep minMantissa = 1'000'000'000'000'000LL;
constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; static_assert(isPowerOfTen(minMantissa));
constexpr static rep maxMantissa = minMantissa * 10 - 1;
static_assert(maxMantissa == 9'999'999'999'999'999LL);
constexpr static rep maxIntValue = maxMantissa / 100;
static_assert(maxIntValue == 99'999'999'999'999LL);
// The range for the exponent when normalized // The range for the exponent when normalized
constexpr static int minExponent = -32768; constexpr static int minExponent = -32768;

View File

@@ -84,6 +84,12 @@ public:
return holds<Issue>() && get<Issue>().native(); return holds<Issue>() && get<Issue>().native();
} }
bool
integral() const
{
return !holds<Issue>() || get<Issue>().native();
}
friend constexpr bool friend constexpr bool
operator==(Asset const& lhs, Asset const& rhs); operator==(Asset const& lhs, Asset const& rhs);

View File

@@ -155,6 +155,9 @@ public:
int int
exponent() const noexcept; exponent() const noexcept;
bool
integral() const noexcept;
bool bool
native() const noexcept; native() const noexcept;
@@ -435,6 +438,12 @@ STAmount::exponent() const noexcept
return mOffset; return mOffset;
} }
inline bool
STAmount::integral() const noexcept
{
return mAsset.integral();
}
inline bool inline bool
STAmount::native() const noexcept STAmount::native() const noexcept
{ {
@@ -553,7 +562,7 @@ STAmount::clear()
{ {
// The -100 is used to allow 0 to sort less than a small positive values // The -100 is used to allow 0 to sort less than a small positive values
// which have a negative exponent. // which have a negative exponent.
mOffset = native() ? 0 : -100; mOffset = integral() ? 0 : -100;
mValue = 0; mValue = 0;
mIsNegative = false; mIsNegative = false;
} }

View File

@@ -482,6 +482,8 @@ public:
value_type value_type
operator*() const; operator*() const;
/// Do not use operator->() unless the field is required, or you've checked
/// that it's set.
T const* T const*
operator->() const; operator->() const;
@@ -718,6 +720,8 @@ STObject::Proxy<T>::operator*() const -> value_type
return this->value(); return this->value();
} }
/// Do not use operator->() unless the field is required, or you've checked that
/// it's set.
template <class T> template <class T>
T const* T const*
STObject::Proxy<T>::operator->() const STObject::Proxy<T>::operator->() const

View File

@@ -23,6 +23,7 @@ systemName()
/** Number of drops in the genesis account. */ /** Number of drops in the genesis account. */
constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP}; constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP};
static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000);
/** Returns true if the amount does not exceed the initial XRP in existence. */ /** Returns true if the amount does not exceed the initial XRP in existence. */
inline bool inline bool

View File

@@ -479,10 +479,10 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
{sfAccount, soeREQUIRED}, {sfAccount, soeREQUIRED},
{sfData, soeOPTIONAL}, {sfData, soeOPTIONAL},
{sfAsset, soeREQUIRED}, {sfAsset, soeREQUIRED},
{sfAssetsTotal, soeREQUIRED}, {sfAssetsTotal, soeDEFAULT},
{sfAssetsAvailable, soeREQUIRED}, {sfAssetsAvailable, soeDEFAULT},
{sfAssetsMaximum, soeDEFAULT}, {sfAssetsMaximum, soeDEFAULT},
{sfLossUnrealized, soeREQUIRED}, {sfLossUnrealized, soeDEFAULT},
{sfShareMPTID, soeREQUIRED}, {sfShareMPTID, soeREQUIRED},
{sfWithdrawalPolicy, soeREQUIRED}, {sfWithdrawalPolicy, soeREQUIRED},
{sfScale, soeDEFAULT}, {sfScale, soeDEFAULT},

View File

@@ -1384,7 +1384,7 @@ private:
// equal asset deposit: unit test to exercise the rounding-down of // equal asset deposit: unit test to exercise the rounding-down of
// LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations
// The LPTokens need to have 16 significant digits and a fractional part // The LPTokens need to have 16 significant digits and a fractional part
for (Number const deltaLPTokens : for (Number const& deltaLPTokens :
{Number{UINT64_C(100000'0000000009), -10}, {Number{UINT64_C(100000'0000000009), -10},
Number{UINT64_C(100000'0000000001), -10}}) Number{UINT64_C(100000'0000000001), -10}})
{ {

View File

@@ -3684,7 +3684,32 @@ class Vault_test : public beast::unit_test::suite
}); });
testCase(18, [&, this](Env& env, Data d) { testCase(18, [&, this](Env& env, Data d) {
testcase("Scale deposit overflow on second deposit"); testcase("MPT scale deposit overflow");
// The computed number of shares can not be represented as an MPT
// without truncation
{
auto tx = d.vault.deposit(
{.depositor = d.depositor,
.id = d.keylet.key,
.amount = d.asset(5)});
env(tx, ter{tecPRECISION_LOSS});
env.close();
}
});
testCase(13, [&, this](Env& env, Data d) {
testcase("MPT scale deposit overflow on first deposit");
auto tx = d.vault.deposit(
{.depositor = d.depositor,
.id = d.keylet.key,
.amount = d.asset(10)});
env(tx, ter{tecPRECISION_LOSS});
env.close();
});
testCase(13, [&, this](Env& env, Data d) {
testcase("MPT scale deposit overflow on second deposit");
{ {
auto tx = d.vault.deposit( auto tx = d.vault.deposit(
@@ -3705,8 +3730,8 @@ class Vault_test : public beast::unit_test::suite
} }
}); });
testCase(18, [&, this](Env& env, Data d) { testCase(13, [&, this](Env& env, Data d) {
testcase("Scale deposit overflow on total shares"); testcase("No MPT scale deposit overflow on total shares");
{ {
auto tx = d.vault.deposit( auto tx = d.vault.deposit(
@@ -3722,7 +3747,7 @@ class Vault_test : public beast::unit_test::suite
{.depositor = d.depositor, {.depositor = d.depositor,
.id = d.keylet.key, .id = d.keylet.key,
.amount = d.asset(5)}); .amount = d.asset(5)});
env(tx, ter{tecPATH_DRY}); env(tx);
env.close(); env.close();
} }
}); });
@@ -4006,6 +4031,28 @@ class Vault_test : public beast::unit_test::suite
testCase(18, [&, this](Env& env, Data d) { testCase(18, [&, this](Env& env, Data d) {
testcase("Scale withdraw overflow"); testcase("Scale withdraw overflow");
{
auto tx = d.vault.deposit(
{.depositor = d.depositor,
.id = d.keylet.key,
.amount = d.asset(5)});
env(tx, ter{tecPRECISION_LOSS});
env.close();
}
{
auto tx = d.vault.withdraw(
{.depositor = d.depositor,
.id = d.keylet.key,
.amount = STAmount(d.asset, Number(10, 0))});
env(tx, ter{tecPRECISION_LOSS});
env.close();
}
});
testCase(13, [&, this](Env& env, Data d) {
testcase("MPT scale withdraw overflow");
{ {
auto tx = d.vault.deposit( auto tx = d.vault.deposit(
{.depositor = d.depositor, {.depositor = d.depositor,
@@ -4224,6 +4271,29 @@ class Vault_test : public beast::unit_test::suite
testCase(18, [&, this](Env& env, Data d) { testCase(18, [&, this](Env& env, Data d) {
testcase("Scale clawback overflow"); testcase("Scale clawback overflow");
{
auto tx = d.vault.deposit(
{.depositor = d.depositor,
.id = d.keylet.key,
.amount = d.asset(5)});
env(tx, ter(tecPRECISION_LOSS));
env.close();
}
{
auto tx = d.vault.clawback(
{.issuer = d.issuer,
.id = d.keylet.key,
.holder = d.depositor,
.amount = STAmount(d.asset, Number(10, 0))});
env(tx, ter{tecPRECISION_LOSS});
env.close();
}
});
testCase(13, [&, this](Env& env, Data d) {
testcase("MPT Scale clawback overflow");
{ {
auto tx = d.vault.deposit( auto tx = d.vault.deposit(
{.depositor = d.depositor, {.depositor = d.depositor,
@@ -4525,7 +4595,8 @@ class Vault_test : public beast::unit_test::suite
BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50"));
BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000"));
BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50"));
BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); // Since this field is default, it is not returned.
BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName()));
auto const strShareID = strHex(sle->at(sfShareMPTID)); auto const strShareID = strHex(sle->at(sfShareMPTID));
BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID));

View File

@@ -2413,6 +2413,19 @@ ValidVault::finalize(
beforeVault_.empty() || beforeVault_[0].key == afterVault.key, beforeVault_.empty() || beforeVault_[0].key == afterVault.key,
"ripple::ValidVault::finalize : single vault operation"); "ripple::ValidVault::finalize : single vault operation");
if (!afterVault.assetsTotal.representable() ||
!afterVault.assetsAvailable.representable() ||
!afterVault.assetsMaximum.representable() ||
!afterVault.lossUnrealized.representable())
{
JLOG(j.fatal()) << "Invariant failed: vault overflowed maximum current "
"representable integer value";
XRPL_ASSERT(
enforce,
"ripple::ValidVault::finalize : vault integer limit invariant");
return !enforce; // That's all we can do here
}
auto const updatedShares = [&]() -> std::optional<Shares> { auto const updatedShares = [&]() -> std::optional<Shares> {
// At this moment we only know that a vault is being updated and there // At this moment we only know that a vault is being updated and there
// might be some MPTokenIssuance objects which are also updated in the // might be some MPTokenIssuance objects which are also updated in the

View File

@@ -71,9 +71,13 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
} }
Asset const vaultAsset = vault->at(sfAsset); Asset const vaultAsset = vault->at(sfAsset);
if (auto const amount = ctx.tx[~sfAmount]; if (auto const amount = ctx.tx[~sfAmount])
amount && vaultAsset != amount->asset()) {
if (vaultAsset != amount->asset())
return tecWRONG_ASSET; return tecWRONG_ASSET;
else if (!amount->validNumber())
return tecPRECISION_LOSS;
}
if (vaultAsset.native()) if (vaultAsset.native())
{ {

View File

@@ -204,6 +204,13 @@ VaultCreate::doApply()
vault->at(sfWithdrawalPolicy) = vaultStrategyFirstComeFirstServe; vault->at(sfWithdrawalPolicy) = vaultStrategyFirstComeFirstServe;
if (scale) if (scale)
vault->at(sfScale) = scale; vault->at(sfScale) = scale;
if (asset.integral())
{
// Only the Maximum can be a non-zero value, so only it needs to be
// checked.
if (!vault->at(sfAssetsMaximum).value().valid(Number::compatible))
return tecLIMIT_EXCEEDED;
}
view().insert(vault); view().insert(vault);
// Explicitly create MPToken for the vault owner // Explicitly create MPToken for the vault owner

View File

@@ -42,6 +42,9 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
if (assets.asset() != vaultAsset) if (assets.asset() != vaultAsset)
return tecWRONG_ASSET; return tecWRONG_ASSET;
if (!assets.validNumber())
return tecPRECISION_LOSS;
if (vaultAsset.native()) if (vaultAsset.native())
; // No special checks for XRP ; // No special checks for XRP
else if (vaultAsset.holds<MPTIssue>()) else if (vaultAsset.holds<MPTIssue>())
@@ -227,14 +230,14 @@ VaultDeposit::doApply()
return tecINTERNAL; // LCOV_EXCL_LINE return tecINTERNAL; // LCOV_EXCL_LINE
sharesCreated = *maybeShares; sharesCreated = *maybeShares;
} }
if (sharesCreated == beast::zero) if (sharesCreated == beast::zero || !sharesCreated.validNumber())
return tecPRECISION_LOSS; return tecPRECISION_LOSS;
auto const maybeAssets = auto const maybeAssets =
sharesToAssetsDeposit(vault, sleIssuance, sharesCreated); sharesToAssetsDeposit(vault, sleIssuance, sharesCreated);
if (!maybeAssets) if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE return tecINTERNAL; // LCOV_EXCL_LINE
else if (*maybeAssets > amount) else if (*maybeAssets > amount || !maybeAssets->validNumber())
{ {
// LCOV_EXCL_START // LCOV_EXCL_START
JLOG(j_.error()) << "VaultDeposit: would take more than offered."; JLOG(j_.error()) << "VaultDeposit: would take more than offered.";

View File

@@ -144,6 +144,11 @@ VaultSet::doApply()
tx[sfAssetsMaximum] < *vault->at(sfAssetsTotal)) tx[sfAssetsMaximum] < *vault->at(sfAssetsTotal))
return tecLIMIT_EXCEEDED; return tecLIMIT_EXCEEDED;
vault->at(sfAssetsMaximum) = tx[sfAssetsMaximum]; vault->at(sfAssetsMaximum) = tx[sfAssetsMaximum];
if (vault->at(sfAsset).value().integral())
{
if (!vault->at(sfAssetsMaximum).value().valid(Number::compatible))
return tecLIMIT_EXCEEDED;
}
} }
if (auto const domainId = tx[~sfDomainID]; domainId) if (auto const domainId = tx[~sfDomainID]; domainId)

View File

@@ -174,6 +174,8 @@ VaultWithdraw::doApply()
if (!maybeAssets) if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE return tecINTERNAL; // LCOV_EXCL_LINE
assetsWithdrawn = *maybeAssets; assetsWithdrawn = *maybeAssets;
if (!assetsWithdrawn.validNumber())
return tecPRECISION_LOSS;
} }
else if (amount.asset() == share) else if (amount.asset() == share)
{ {
@@ -184,6 +186,8 @@ VaultWithdraw::doApply()
if (!maybeAssets) if (!maybeAssets)
return tecINTERNAL; // LCOV_EXCL_LINE return tecINTERNAL; // LCOV_EXCL_LINE
assetsWithdrawn = *maybeAssets; assetsWithdrawn = *maybeAssets;
if (!assetsWithdrawn.validNumber())
return tecPRECISION_LOSS;
} }
else else
return tefINTERNAL; // LCOV_EXCL_LINE return tefINTERNAL; // LCOV_EXCL_LINE