mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-28 06:27:12 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92c1761d27 | ||
|
|
7326fbd144 | ||
|
|
56d6ff3067 | ||
|
|
6c07421e4f | ||
|
|
05ffa06c21 | ||
|
|
bd7fda4853 | ||
|
|
844c250aa2 | ||
|
|
15a57a917a | ||
|
|
f64d034292 |
No files matched your search
@@ -312,12 +312,22 @@ checkCreateMPT(
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// MaximumAmount doesn't exceed 2**63-1
|
||||
/**
|
||||
* MaximumAmount doesn't exceed 2**63-1
|
||||
*/
|
||||
std::int64_t
|
||||
maxMPTAmount(SLE const& sleIssuance);
|
||||
|
||||
// OutstandingAmount may overflow and available amount might be negative.
|
||||
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
|
||||
/**
|
||||
* std::nullopt if the issuance doesn't exist.
|
||||
*/
|
||||
std::optional<std::int64_t>
|
||||
maxMPTAmount(ReadView const& view, MPTID const& mptID);
|
||||
|
||||
/**
|
||||
* OutstandingAmount may overflow and available amount might be negative.
|
||||
* But available amount is always <= |MaximumAmount - OutstandingAmount|.
|
||||
*/
|
||||
std::int64_t
|
||||
availableMPTAmount(SLE const& sleIssuance);
|
||||
|
||||
@@ -331,7 +341,12 @@ availableMPTAmount(ReadView const& view, MPTID const& mptID);
|
||||
* directSendNoFee transactions that bypass the payment engine.
|
||||
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
|
||||
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
|
||||
* for `accountSend` and payments processed via the payment engine.
|
||||
* for `accountSend` and payments processed via the payment engine, whose
|
||||
* reverse pass may issue before it redeems.
|
||||
*
|
||||
* In both cases a single `sendAmount > MaximumAmount` is rejected. Such a send
|
||||
* can never settle: redemptions within the transaction are bounded by the
|
||||
* outstanding amount, so the final OutstandingAmount would exceed the cap.
|
||||
*/
|
||||
bool
|
||||
isMPTOverflow(
|
||||
@@ -353,6 +368,12 @@ issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
|
||||
/**
|
||||
* Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
|
||||
* See ApplyView::issuerSelfDebitHookMPT().
|
||||
*
|
||||
* `amount` may be 0. BookStep removes an offer whose fee-adjusted funded
|
||||
* output is 0, but other clips (for example MaximumAmount via limitStepIn)
|
||||
* can still consume a zero slice of a funded remainder. The hook must
|
||||
* accept 0: first call still snapshots origBalance; a zero debit adds
|
||||
* nothing to selfDebit.
|
||||
*/
|
||||
void
|
||||
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
|
||||
|
||||
@@ -435,8 +435,6 @@ PaymentSandbox::issuerSelfDebitHookMPT(
|
||||
std::uint64_t amount,
|
||||
std::int64_t origBalance)
|
||||
{
|
||||
XRPL_ASSERT(amount > 0, "PaymentSandbox::issuerSelfDebitHookMPT: amount must be > 0");
|
||||
|
||||
tab_.issuerSelfDebitMPT(issue, amount, origBalance);
|
||||
}
|
||||
|
||||
|
||||
@@ -1047,6 +1047,14 @@ maxMPTAmount(SLE const& sleIssuance)
|
||||
return sleIssuance[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
|
||||
}
|
||||
|
||||
std::optional<std::int64_t>
|
||||
maxMPTAmount(ReadView const& view, MPTID const& mptID)
|
||||
{
|
||||
if (auto const sle = view.read(keylet::mptokenIssuance(mptID)))
|
||||
return maxMPTAmount(*sle);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::int64_t
|
||||
availableMPTAmount(SLE const& sleIssuance)
|
||||
{
|
||||
|
||||
@@ -43,8 +43,9 @@ setCurrentTransactionRules(std::optional<Rules> r)
|
||||
auto const range = [&r]() {
|
||||
// If any new conditions with new amendments are added to "enableLargeNumbers", those
|
||||
// amendments must also be added to useRulesGuards.
|
||||
bool const enableLargeNumbers =
|
||||
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
|
||||
bool const enableLargeNumbers = !r ||
|
||||
(r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol) ||
|
||||
r->enabled(featureMPTokensV2));
|
||||
// If enableLargeNumbers is true, then useRulesGuards must also return true.
|
||||
// However, the reverse is not true. Other amendments can cause the rules guard to be used,
|
||||
// even though large numbers are _not_ used.
|
||||
@@ -84,7 +85,8 @@ useRulesGuards(Rules const& rules)
|
||||
// with createGuards, and any other callers, and the first set of guards can be created directly
|
||||
// at the call site, without using optional.
|
||||
return rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol) ||
|
||||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0);
|
||||
rules.enabled(fixCleanup3_2_0) || rules.enabled(fixCleanup3_3_0) ||
|
||||
rules.enabled(featureMPTokensV2);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1232,6 +1232,19 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
|
||||
if (num == beast::kZero)
|
||||
return {asset};
|
||||
|
||||
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is
|
||||
// the raw 63-bit value and is never scaled down, so an MPT numerator can
|
||||
// overflow the quotient and an MPT denominator shortens it, moving the +5
|
||||
// nudge into a significant digit. Use Number arithmetic under
|
||||
// MPTokensV2 whenever an MPT is involved, as mulRound/divRound do. Pin
|
||||
// the rounding mode so the result does not depend on the caller's.
|
||||
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
|
||||
(asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()))
|
||||
{
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return STAmount{asset, Number{num} / Number{den}};
|
||||
}
|
||||
|
||||
std::uint64_t numVal = num.mantissa();
|
||||
std::uint64_t denVal = den.mantissa();
|
||||
int numOffset = num.exponent();
|
||||
@@ -1261,11 +1274,21 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
|
||||
// numerator by 10^17 (the product is in the range of
|
||||
// 10^32 to 10^33) followed by a division, so the result
|
||||
// is in the range of 10^16 to 10^15.
|
||||
return STAmount(
|
||||
asset,
|
||||
muldiv(numVal, kTenTO17, denVal) + 5,
|
||||
numOffset - denOffset - 17,
|
||||
num.negative() != den.negative());
|
||||
std::uint64_t quotient = muldiv(numVal, kTenTO17, denVal);
|
||||
int offset = numOffset - denOffset - 17;
|
||||
|
||||
// An integral (XRP) numerator is not scaled down to 16 digits, so the
|
||||
// quotient can land in [2^63, 2^64). The STAmount constructor reads
|
||||
// that as a negative IOU mantissa, and the +5 can wrap. Drop a digit
|
||||
// first; the result is canonicalized to 16 digits anyway.
|
||||
if (quotient > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) - 5 &&
|
||||
isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true))
|
||||
{
|
||||
quotient /= 10;
|
||||
++offset;
|
||||
}
|
||||
|
||||
return STAmount(asset, quotient + 5, offset, num.negative() != den.negative());
|
||||
}
|
||||
|
||||
STAmount
|
||||
@@ -1541,11 +1564,19 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
|
||||
|
||||
bool const resultNegative = v1.negative() != v2.negative();
|
||||
|
||||
if (asset.holds<MPTIssue>() && isFeatureEnabled(featureMPTokensV2, false))
|
||||
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is the
|
||||
// raw 63-bit balance, so any MPT operand (not just an MPT result) can
|
||||
// overflow it: a large MPT amount times an IOU-shaped rate into an XRP or
|
||||
// IOU result is the offer-crossing remainder and partial-fill case. Use
|
||||
// Number arithmetic under MPTokensV2 whenever an MPT is involved.
|
||||
//
|
||||
// Outside a transaction (no current rules, e.g. pathfinding) default to
|
||||
// the Number path: an MPT operand can only reach here through MPT
|
||||
// offers or AMMs, which require MPTokensV2, so this keeps RPC quotes on
|
||||
// the same arithmetic as the transaction they describe.
|
||||
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
|
||||
(asset.holds<MPTIssue>() || v1.holds<MPTIssue>() || v2.holds<MPTIssue>()))
|
||||
{
|
||||
// MPT DEX can combine 63-bit MPT amounts with IOU-shaped transfer
|
||||
// rates. Use Number arithmetic under MPTokensV2 so the rounded
|
||||
// operation is not limited by the legacy uint64_t scaled mantissa.
|
||||
Number result;
|
||||
{
|
||||
NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp));
|
||||
@@ -1643,7 +1674,10 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
|
||||
|
||||
bool const resultNegative = (num.negative() != den.negative());
|
||||
|
||||
if (asset.holds<MPTIssue>() && isFeatureEnabled(featureMPTokensV2, false))
|
||||
// See mulRoundImpl: any MPT operand, not just an MPT result, and the
|
||||
// Number path when there are no current rules.
|
||||
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
|
||||
(asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()))
|
||||
{
|
||||
// Match the multiply path above: Number performs the rounded
|
||||
// operation, then STAmount materializes the final MPT amount using the
|
||||
|
||||
@@ -83,6 +83,11 @@ protected:
|
||||
std::optional<AMMLiquidity<TIn, TOut>> ammLiquidity_;
|
||||
beast::Journal const j_;
|
||||
Asset const strandDeliver_;
|
||||
// MaximumAmount of book_.in if it is an MPT. Taking an MPT offer has the
|
||||
// issuer send TakerPays to the owner, and a send above the cap is rejected
|
||||
// (isMPTOverflow). forEachOffer limits the offer's input to the cap so it
|
||||
// fills partially instead of failing the strand.
|
||||
std::optional<TIn> maxIn_;
|
||||
|
||||
struct Cache
|
||||
{
|
||||
@@ -106,6 +111,12 @@ private:
|
||||
, j_(ctx.j)
|
||||
, strandDeliver_(ctx.strandDeliver)
|
||||
{
|
||||
if constexpr (std::is_same_v<TIn, MPTAmount>)
|
||||
{
|
||||
if (auto const max = maxMPTAmount(ctx.view, in.get<MPTIssue>().getMptID()))
|
||||
maxIn_.emplace(*max);
|
||||
}
|
||||
|
||||
if (auto const ammSle = ctx.view.read(keylet::amm(in, out));
|
||||
ammSle && ammSle->getFieldAmount(sfLPTokenBalance) != beast::kZero)
|
||||
{
|
||||
@@ -817,31 +828,59 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false);
|
||||
|
||||
stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
|
||||
// Limit offer's input if MPT, BookStep is the first step (an issuer
|
||||
// is making a cross-currency payment), and this offer is not owned
|
||||
// by the issuer. Otherwise, OutstandingAmount may overflow.
|
||||
auto const& issuer = assetIn.getIssuer();
|
||||
if (isAssetInMPT && !prevStep_ && offer.owner() != issuer)
|
||||
{
|
||||
// Funds available to issue
|
||||
auto const available = toAmount<TIn>(accountFunds(
|
||||
sb,
|
||||
issuer,
|
||||
assetIn, // STAmount{0}, but the default is not used
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_));
|
||||
if (stpAmt.in > available)
|
||||
// Fee clip can floor in or out to 0. OfferStream is
|
||||
// fee-blind, so it misses both: out==0 blocks the book;
|
||||
// in==0 / out>0 consumes ownerGives for free. MPT at
|
||||
// ordinary sizes; IOU only at the smallest out — V2
|
||||
// because that IOU outcome changes.
|
||||
if (sb.rules().enabled(featureMPTokensV2) &&
|
||||
(stpAmt.out <= beast::kZero || ofrAmt.in <= beast::kZero))
|
||||
{
|
||||
limitStepIn(
|
||||
offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available);
|
||||
removeOffer(
|
||||
"Removing offer whose fee-clipped amount rounds to "
|
||||
"zero");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit offer's input if MPT and the offer is not owned by the
|
||||
// issuer (an issuer's send to itself is a no-op).
|
||||
bool inLimited = false;
|
||||
std::optional<TIn> limitIn;
|
||||
auto const& issuer = assetIn.getIssuer();
|
||||
if (isAssetInMPT && offer.owner() != issuer)
|
||||
{
|
||||
if (!prevStep_)
|
||||
{
|
||||
// BookStep is the first step (an issuer is making a
|
||||
// cross-currency payment). Limit to the funds available to
|
||||
// issue, otherwise OutstandingAmount may overflow.
|
||||
auto const available = toAmount<TIn>(accountFunds(
|
||||
sb,
|
||||
issuer,
|
||||
assetIn, // STAmount{0}, but the default is not used
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j_));
|
||||
if (stpAmt.in > available)
|
||||
limitIn = available;
|
||||
}
|
||||
else if (maxIn_ && ofrAmt.in > *maxIn_)
|
||||
{
|
||||
// Limit to MaximumAmount (see maxIn_). The remainder is
|
||||
// still funded, so the callback keeps the offer instead of
|
||||
// consuming it. The grossed-up cap can't overflow since
|
||||
// ofrAmt.in > maxIn_ did not.
|
||||
inLimited = true;
|
||||
limitIn = mulRatio(*maxIn_, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
}
|
||||
if (limitIn)
|
||||
limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, *limitIn);
|
||||
|
||||
offerAttempted = true;
|
||||
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate);
|
||||
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, inLimited);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
@@ -1091,7 +1130,8 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
TAmounts<TIn, TOut> const& stpAmt,
|
||||
TOut const& ownerGives,
|
||||
std::uint32_t transferRateIn,
|
||||
std::uint32_t transferRateOut) mutable -> bool {
|
||||
std::uint32_t transferRateOut,
|
||||
bool inLimited) mutable -> bool {
|
||||
if (remainingOut <= beast::kZero)
|
||||
return false;
|
||||
|
||||
@@ -1103,8 +1143,10 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
remainingOut = out - result.out;
|
||||
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
// return true b/c even if the payment is satisfied,
|
||||
// we need to consume the offer
|
||||
return true;
|
||||
// we need to consume the offer. An offer limited to the issuance
|
||||
// cap is not consumed, though: its remainder is still funded, and
|
||||
// no more of that MPT can flow through this step anyway.
|
||||
return !inLimited;
|
||||
}
|
||||
|
||||
auto ofrAdjAmt = ofrAmt;
|
||||
@@ -1207,7 +1249,8 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
TAmounts<TIn, TOut> const& stpAmt,
|
||||
TOut const& ownerGives,
|
||||
std::uint32_t transferRateIn,
|
||||
std::uint32_t transferRateOut) mutable -> bool {
|
||||
std::uint32_t transferRateOut,
|
||||
bool inLimited) mutable -> bool {
|
||||
XRPL_ASSERT(cache_, "xrpl::BookStep::fwdImp::eachOffer : cache is set");
|
||||
|
||||
if (remainingIn <= beast::kZero)
|
||||
@@ -1237,8 +1280,10 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
savedInsAdj.insert(stpAmt.in);
|
||||
lastOut = savedOutsAdj.insert(stpAmt.out);
|
||||
resultAdj = TAmounts<TIn, TOut>(sum(savedInsAdj), sum(savedOutsAdj));
|
||||
// consume the offer even if stepAmt.in == remainingIn
|
||||
processMore = true;
|
||||
// consume the offer even if stepAmt.in == remainingIn, unless it
|
||||
// was limited to the issuance cap: its remainder is still funded
|
||||
// and must stay on the book.
|
||||
processMore = !inLimited;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
@@ -51,6 +52,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
@@ -414,19 +416,47 @@ OfferCreate::flowCross(
|
||||
// offer taker. Set sendMax to allow for the gateway's cut.
|
||||
Rate gatewayXferRate{QUALITY_ONE};
|
||||
STAmount sendMax = takerAmount.in;
|
||||
// Payment flow code compares quality after the transfer rate is
|
||||
// included, so the threshold is computed from the grossed-up input.
|
||||
STAmount thresholdIn = sendMax;
|
||||
if (!sendMax.native() && (accountID_ != sendMax.getIssuer()))
|
||||
{
|
||||
gatewayXferRate = transferRate(psb, sendMax);
|
||||
if (gatewayXferRate.value != QUALITY_ONE)
|
||||
{
|
||||
sendMax =
|
||||
multiplyRound(takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
|
||||
try
|
||||
{
|
||||
sendMax = multiplyRound(
|
||||
takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
|
||||
thresholdIn = sendMax;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// Only an MPT amount can overflow here (IOU rescales and
|
||||
// XRP has no transfer rate), and MPT offers require
|
||||
// featureMPTokensV2 at preflight, so this legacy re-throw
|
||||
// is unreachable in practice.
|
||||
// LCOV_EXCL_START
|
||||
if (!takerAmount.in.holds<MPTIssue>() ||
|
||||
!psb.rules().enabled(featureMPTokensV2))
|
||||
throw;
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
// The grossed-up MPT amount exceeds 2^63-1. The taker can't
|
||||
// send more than its balance anyway. The threshold only
|
||||
// needs the ratio, so compute it as an IOU, which can't
|
||||
// overflow.
|
||||
sendMax = inStartBalance;
|
||||
thresholdIn = multiplyRound(
|
||||
STAmount{noIssue(), takerAmount.in.mantissa()},
|
||||
gatewayXferRate,
|
||||
noIssue(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Payment flow code compares quality after the transfer rate is
|
||||
// included. Since transfer rate is incorporated compute threshold.
|
||||
Quality threshold{takerAmount.out, sendMax};
|
||||
Quality threshold{takerAmount.out, thresholdIn};
|
||||
|
||||
// If we're creating a passive offer adjust the threshold so we only
|
||||
// cross offers that have a better quality than this one.
|
||||
@@ -871,15 +901,30 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
|
||||
return {tesSUCCESS, true};
|
||||
}
|
||||
|
||||
// A taker redeeming an MPT into this offer pays the transfer fee, and
|
||||
// BookStep grosses up TakerPays by the rate for the whole offer before
|
||||
// anything else. If that doesn't fit in an MPT amount, no taker can take
|
||||
// the offer and BookStep would remove it on the first attempt. Checked on
|
||||
// the remainder: it may fit even if the original amount didn't.
|
||||
bool cannotRest = false;
|
||||
if (mptV2 && saTakerPays.holds<MPTIssue>())
|
||||
{
|
||||
auto const rate = transferRate(sb, saTakerPays);
|
||||
cannotRest = rate != kParityRate &&
|
||||
!tryMulRatio(saTakerPays.mpt(), rate.value, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
|
||||
// The remainder rests at uRate, the original pre-crossing rate. A zero
|
||||
// rate (quality not representable) puts it in the directory whose index
|
||||
// equals getBookBase(book), and BookTip scans keys strictly greater, so it
|
||||
// could never be crossed while holding the owner's reserve. Don't place
|
||||
// it; anything that crossed is kept, and a fully crossed offer has already
|
||||
// returned above. Gated to preserve pre-amendment behavior.
|
||||
if (mptV2 && uRate == 0)
|
||||
// could never be crossed while holding the owner's reserve. Likewise, a
|
||||
// bid whose fee-grossed TakerPays doesn't fit would be removed by BookStep
|
||||
// on the first take. Don't place it; anything that crossed is kept, and a
|
||||
// fully crossed offer has already returned above. Gated to preserve
|
||||
// pre-amendment behavior.
|
||||
if ((mptV2 && uRate == 0) || cannotRest)
|
||||
{
|
||||
JLOG(j_.debug()) << "Unrepresentable quality: remainder not placed";
|
||||
JLOG(j_.debug()) << "Unrepresentable quality or fee: remainder not placed";
|
||||
if (!crossed)
|
||||
return {tecKILLED, false};
|
||||
return {tesSUCCESS, true};
|
||||
|
||||
@@ -31,6 +31,15 @@
|
||||
namespace xrpl::test {
|
||||
class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
{
|
||||
// SingleAssetVault, LendingProtocol and MPTokensV2 enable the large
|
||||
// Number mantissa.
|
||||
static bool
|
||||
largeMantissa(FeatureBitset const& features)
|
||||
{
|
||||
return features[featureSingleAssetVault] || features[featureLendingProtocol] ||
|
||||
features[featureMPTokensV2];
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidRequest(FeatureBitset features)
|
||||
{
|
||||
@@ -1724,7 +1733,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
.asString();
|
||||
auto const lpTokenBalance =
|
||||
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
|
||||
if (features[featureSingleAssetVault] || features[featureLendingProtocol])
|
||||
if (largeMantissa(features))
|
||||
{
|
||||
BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.4142135623741");
|
||||
}
|
||||
@@ -1741,16 +1750,14 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
|
||||
BEAST_EXPECT(!amm.ammExists());
|
||||
}
|
||||
else if (
|
||||
features[fixAMMv1_3] &&
|
||||
(features[featureSingleAssetVault] || features[featureLendingProtocol]))
|
||||
else if (features[fixAMMv1_3] && largeMantissa(features))
|
||||
{
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
|
||||
// Without the Rounding feature and with new Number a dust pool
|
||||
// amount remains
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
}
|
||||
else if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
|
||||
else if (!largeMantissa(features))
|
||||
{
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
@@ -1794,7 +1801,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
.asString();
|
||||
auto const lpTokenBalance =
|
||||
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
|
||||
if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
|
||||
if (!largeMantissa(features))
|
||||
{
|
||||
BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.414213562374");
|
||||
}
|
||||
@@ -1811,16 +1818,14 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
|
||||
BEAST_EXPECT(!amm.ammExists());
|
||||
}
|
||||
else if (
|
||||
features[fixAMMv1_3] &&
|
||||
(features[featureSingleAssetVault] || features[featureLendingProtocol]))
|
||||
else if (features[fixAMMv1_3] && largeMantissa(features))
|
||||
{
|
||||
// Without the Rounding feature and with new Number a dust pool
|
||||
// amount remains
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
}
|
||||
else if (!features[featureSingleAssetVault] && !features[featureLendingProtocol])
|
||||
else if (!largeMantissa(features))
|
||||
{
|
||||
env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
|
||||
BEAST_EXPECT(amm.ammExists());
|
||||
|
||||
@@ -2857,8 +2857,8 @@ class AMMClawback_test : public beast::unit_test::Suite
|
||||
{
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas
|
||||
FeatureBitset const all =
|
||||
jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
|
||||
FeatureBitset const all = jtx::testableAmendments() - featureSingleAssetVault -
|
||||
featureLendingProtocol - featureMPTokensV2;
|
||||
|
||||
testInvalidRequest(all);
|
||||
testInvalidRequest(all - featureMPTokensV2);
|
||||
|
||||
@@ -116,7 +116,7 @@ private:
|
||||
Txflags(tfPartialPayment));
|
||||
|
||||
BEAST_EXPECT(ammCarol.expectBalances(
|
||||
btc(1'001'000'000'374'816), eth(100'000'000'000'000'000), ammCarol.tokens()));
|
||||
btc(1'001'000'000'374'814), eth(100'000'000'000'000'000), ammCarol.tokens()));
|
||||
|
||||
env.require(Balance(bob_, eth(200'100'000'000'000'000)));
|
||||
BEAST_EXPECT(isOffer(env, carol_, btc(49'000'000'000'000), XRP(49)));
|
||||
@@ -2269,13 +2269,15 @@ private:
|
||||
// on 24e12GBP
|
||||
// 1,200e12 - 24e12*1.25 =~ 1,170e12GBP
|
||||
env.require(Balance(alice_, gbp(1'170'000'000'000'000)));
|
||||
// 24e12GBP is swapped in for 28.125e12BTC
|
||||
// 24e12GBP is swapped in for 28.125e12BTC, less one unit: the
|
||||
// limit quality 120/90 is rounded to nearest, so a trade at
|
||||
// exactly 4/3 is just over the limit
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
gbp(1'024'000'000'000'000), btc(1'171'875'000'000'000), amm.tokens()));
|
||||
gbp(1'024'000'000'000'000), btc(1'171'875'000'000'001), amm.tokens()));
|
||||
|
||||
// 25% on 22.5e12BTC is paid in tr fee
|
||||
// 22.5*1.25 = 28.125e12BTC
|
||||
env.require(Balance(carol_, btc(1'222'500'000'000'000)));
|
||||
env.require(Balance(carol_, btc(1'222'499'999'999'999)));
|
||||
}
|
||||
{
|
||||
// Payment via offer and AMM with limit quality, deliver less
|
||||
|
||||
@@ -73,7 +73,8 @@ class AMMExtended_test : public jtx::AMMTest
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas
|
||||
FeatureBitset const all_{
|
||||
testableAmendments() - featureSingleAssetVault - featureLendingProtocol};
|
||||
testableAmendments() - featureSingleAssetVault - featureLendingProtocol -
|
||||
featureMPTokensV2};
|
||||
|
||||
private:
|
||||
void
|
||||
|
||||
@@ -1451,12 +1451,14 @@ private:
|
||||
auto carolXRP = env.balance(carol_, XRP);
|
||||
auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
|
||||
|
||||
// The exact deposit is 201; rounding in the pool's favor at
|
||||
// the large mantissa takes one more unit.
|
||||
ammAlice.deposit(carol_, 100'000, MPT(ammAlice[1])(205));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10'000), MPT(ammAlice[1])(10'201), IOUAmount{10'100'000, 0}));
|
||||
XRP(10'000), MPT(ammAlice[1])(10'202), IOUAmount{10'100'000, 0}));
|
||||
|
||||
env.require(Balance(carol_, carolXRP - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(201)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(202)));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -1467,11 +1469,13 @@ private:
|
||||
auto carolXRP = env.balance(carol_, XRP);
|
||||
auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
|
||||
|
||||
// The exact deposit is 201 XRP; rounding in the pool's favor
|
||||
// at the large mantissa takes one more drop.
|
||||
ammAlice.deposit(carol_, 100'000, XRP(205));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10'201), MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
|
||||
XRPAmount{10'201'000'001}, MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
|
||||
|
||||
env.require(Balance(carol_, carolXRP - XRP(201) - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolXRP - XRPAmount{201'000'001} - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolMPT));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
@@ -1551,10 +1555,10 @@ private:
|
||||
STAmount{ammAlice.lptIssue(), 2004, -6});
|
||||
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
|
||||
XRP(10'000), MPT(ammAlice[1])(10'080), IOUAmount{10'039'920'3184089, -7}));
|
||||
|
||||
env.require(Balance(carol_, carolXRP - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(80)));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -1573,10 +1577,10 @@ private:
|
||||
STAmount{ammAlice.lptIssue(), 2004, -6});
|
||||
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
|
||||
XRP(10'000), MPT(ammAlice[1])(10'080), IOUAmount{10'039'920'3184089, -7}));
|
||||
|
||||
env.require(Balance(carol_, carolXRP - drops(baseFee)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
|
||||
env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(80)));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -1797,9 +1801,9 @@ private:
|
||||
|
||||
ammAlice.deposit(gw_, MPT(ammAlice[0])(1000));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
MPT(ammAlice[0])(1'010'999),
|
||||
MPT(ammAlice[0])(1'011'000),
|
||||
MPT(ammAlice[1])(1'010'000),
|
||||
IOUAmount{1'010'499'376546071, -9}));
|
||||
IOUAmount{1'010'499'876298854, -9}));
|
||||
},
|
||||
{{gAmmmpt(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -2212,7 +2216,7 @@ private:
|
||||
if (env.enabled(fixAMMv1_3))
|
||||
{
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
MPT(ammAlice[0])(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
|
||||
MPT(ammAlice[0])(1), STAmount{USD, 1, -12}, IOUAmount{1, -9}));
|
||||
}
|
||||
},
|
||||
{{gAmmmpt(10'000'000'000), USD(10'000)}},
|
||||
@@ -2541,11 +2545,12 @@ private:
|
||||
// XRP amount to withdraw is 0
|
||||
ammAlice.withdraw(
|
||||
alice_, IOUAmount{1, -5}, std::nullopt, std::nullopt, Ter(tecAMM_FAILED));
|
||||
// Calculated tokens to withdraw are 0
|
||||
// Calculated tokens to withdraw are 0: 1e-16 of 10,000 USD is
|
||||
// below the large Number mantissa's precision
|
||||
ammAlice.withdraw(
|
||||
alice_,
|
||||
std::nullopt,
|
||||
STAmount{USD, 1, -11},
|
||||
STAmount{USD, 1, -16},
|
||||
std::nullopt,
|
||||
Ter(tecAMM_INVALID_TOKENS));
|
||||
ammAlice.deposit(carol_, STAmount{USD, 1, -10});
|
||||
@@ -2707,7 +2712,7 @@ private:
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRPAmount(9000'000001),
|
||||
MPT(ammAlice[1])(10'000),
|
||||
IOUAmount{9'486'832'98050514, -8}));
|
||||
IOUAmount{9'486'832'980505138, -9}));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
testAMM(
|
||||
@@ -2715,7 +2720,7 @@ private:
|
||||
// single withdraw MPT from XRP/MPT
|
||||
ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'98050514, -8}));
|
||||
XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'980505138, -9}));
|
||||
},
|
||||
{{XRP(10'000), gAmmmpt(10'000)}});
|
||||
testAMM(
|
||||
@@ -2723,9 +2728,7 @@ private:
|
||||
// single withdraw IOU from IOU/MPT
|
||||
ammAlice.withdraw(alice_, USD(1'000));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
STAmount{USD, UINT64_C(9000'000000000004), -12},
|
||||
MPT(ammAlice[1])(10'000),
|
||||
IOUAmount{9486'83298050514, -11}));
|
||||
USD(9'000), MPT(ammAlice[1])(10'000), IOUAmount{9486'832980505138, -12}));
|
||||
},
|
||||
{{USD(10'000), gAmmmpt(10'000)}});
|
||||
testAMM(
|
||||
@@ -2733,7 +2736,7 @@ private:
|
||||
// single withdraw MPT from IOU/MPT
|
||||
ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'83298050514, -11}));
|
||||
USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'832980505138, -12}));
|
||||
},
|
||||
{{USD(10'000), gAmmmpt(10'000)}});
|
||||
testAMM(
|
||||
@@ -2743,7 +2746,7 @@ private:
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
MPT(ammAlice[0])(9001),
|
||||
MPT(ammAlice[1])(10'000),
|
||||
IOUAmount{9486'83298050514, -11}));
|
||||
IOUAmount{9486'832980505138, -12}));
|
||||
},
|
||||
{{gAmmmpt(10'000), gAmmmpt(10'000)}});
|
||||
|
||||
@@ -3024,7 +3027,7 @@ private:
|
||||
ammAlice.deposit(carol_, 1'000);
|
||||
ammAlice.withdrawAll(carol_, USD(0));
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
STAmount{USD, UINT64_C(9'090'909090909092), -12},
|
||||
STAmount{USD, UINT64_C(9'090'909090909091), -12},
|
||||
MPT(ammAlice[1])(11'000),
|
||||
IOUAmount{10'000}));
|
||||
},
|
||||
@@ -3249,7 +3252,7 @@ private:
|
||||
BEAST_EXPECT(ammAlice.expectBalances(
|
||||
MPT(ammAlice[0])(10'000'000'000),
|
||||
STAmount{USD, 1, -11},
|
||||
IOUAmount{316227765, -9}));
|
||||
IOUAmount{316227766, -9}));
|
||||
},
|
||||
{{gAmmmpt(10'000'000'000), USD(10'000)}});
|
||||
// XRP
|
||||
@@ -5960,7 +5963,7 @@ private:
|
||||
if (!features[fixAMMv1_3])
|
||||
{
|
||||
BEAST_EXPECT(tokensFee == IOUAmount(98'019'80198019, -8));
|
||||
BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933556, -8));
|
||||
BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933557, -8));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -84,7 +84,8 @@ private:
|
||||
{
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas
|
||||
return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
|
||||
return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol -
|
||||
featureMPTokensV2;
|
||||
}
|
||||
|
||||
// Seed from the local testableAmendments() which strips SAV and Lending.
|
||||
|
||||
@@ -573,8 +573,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
|
||||
env(pay(gw, bob, usd(1)));
|
||||
env.close();
|
||||
|
||||
bool const largeMantissa =
|
||||
features[featureSingleAssetVault] || features[featureLendingProtocol];
|
||||
bool const largeMantissa = features[featureSingleAssetVault] ||
|
||||
features[featureLendingProtocol] || features[featureMPTokensV2];
|
||||
|
||||
// alice cannot create escrow for 1/10 iou - precision loss
|
||||
env(escrow::create(alice, bob, usd(1)),
|
||||
@@ -2294,8 +2294,8 @@ struct EscrowToken_test : public beast::unit_test::Suite
|
||||
env(pay(gw, bob, usd(1)));
|
||||
env.close();
|
||||
|
||||
bool const largeMantissa =
|
||||
features[featureSingleAssetVault] || features[featureLendingProtocol];
|
||||
bool const largeMantissa = features[featureSingleAssetVault] ||
|
||||
features[featureLendingProtocol] || features[featureMPTokensV2];
|
||||
|
||||
// alice cannot create escrow for 1/10 iou - precision loss
|
||||
env(escrow::create(alice, bob, usd(1)),
|
||||
@@ -4334,7 +4334,7 @@ public:
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
for (FeatureBitset const& feats :
|
||||
{all - featureSingleAssetVault - featureLendingProtocol, all})
|
||||
{all - featureSingleAssetVault - featureLendingProtocol - featureMPTokensV2, all})
|
||||
{
|
||||
testIOUWithFeats(feats);
|
||||
testIOUWithFeats(feats - fixCleanup3_2_0);
|
||||
|
||||
+336
-17
@@ -41,9 +41,11 @@
|
||||
#include <xrpl/tx/paths/detail/Steps.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
@@ -782,6 +784,335 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
test(maxRepresentable + 1, kMaxMpTokenAmount, "limiting");
|
||||
}
|
||||
|
||||
void
|
||||
testBookStepInputAboveMaximumAmount(FeatureBitset features)
|
||||
{
|
||||
// OfferCreate doesn't compare TakerPays with the MPT's MaximumAmount,
|
||||
// so a bid for more than the cap rests. Taking it has the issuer send
|
||||
// TakerPays to the owner, and isMPTOverflow() rejects a single send
|
||||
// above the cap. BookStep now limits the offer's input to the cap so
|
||||
// the bid fills partially, like an underfunded offer, instead of
|
||||
// throwing out of consumeOffer() and drying the strand (which left the
|
||||
// bid on the book to dead-end every payment that crossed it).
|
||||
//
|
||||
// XRP -> x -> y. carol asks the whole 1,000 x supply for XRP(1,000);
|
||||
// mallory bids x for 2,000 y; alice pays dave y.
|
||||
testcase("BookStep input above MaximumAmount");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Account const gwX("gwX");
|
||||
Account const gwY("gwY");
|
||||
Account const alice("alice");
|
||||
Account const carol("carol");
|
||||
Account const mallory("mallory");
|
||||
Account const dave("dave");
|
||||
|
||||
std::int64_t constexpr cap = 1'000;
|
||||
std::int64_t constexpr bidGets = 2'000;
|
||||
// A bid above the cap at a non-unit price.
|
||||
std::int64_t constexpr unevenBidPays = (3 * cap) + 100;
|
||||
|
||||
auto test = [&](Account const& bidder,
|
||||
std::int64_t bidPays,
|
||||
std::int64_t deliver,
|
||||
bool partial,
|
||||
std::int64_t delivered,
|
||||
std::optional<TER> const& ter = std::nullopt) {
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(100'000), gwX, gwY, alice, carol, mallory, dave);
|
||||
env.close();
|
||||
|
||||
MPT const x = MPTTester(
|
||||
{.env = env, .issuer = gwX, .holders = {carol, mallory, dave}, .maxAmt = cap});
|
||||
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {mallory, dave, gwX}});
|
||||
env(pay(gwX, carol, x(cap)));
|
||||
env(pay(gwY, bidder, y(bidGets)));
|
||||
env.close();
|
||||
|
||||
env(offer(carol, XRP(cap), x(cap)));
|
||||
auto const bidSeq = env.seq(bidder);
|
||||
env(offer(bidder, x(bidPays), y(bidGets)));
|
||||
env.close();
|
||||
auto const bidKeylet = keylet::offer(bidder.id(), SeqProxy::rawSequence(bidSeq));
|
||||
BEAST_EXPECT(env.le(bidKeylet) != nullptr);
|
||||
|
||||
auto const flags = partial ? tfNoRippleDirect | tfPartialPayment : tfNoRippleDirect;
|
||||
env(pay(alice, dave, y(deliver)),
|
||||
Path(~x, ~y),
|
||||
Sendmax(XRP(5'000)),
|
||||
Txflags(flags),
|
||||
ter ? Ter(*ter) : Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(dave, y) == y(delivered));
|
||||
// Each x delivered through the bid costs bidGets/bidPays y.
|
||||
std::int64_t const xTaken = delivered * bidPays / bidGets;
|
||||
if (bidder != gwX)
|
||||
BEAST_EXPECT(env.balance(bidder, x) == x(xTaken));
|
||||
BEAST_EXPECT(env.balance(carol, x) == x(cap - xTaken));
|
||||
|
||||
auto const sle = env.le(bidKeylet);
|
||||
if (xTaken == bidPays)
|
||||
{
|
||||
BEAST_EXPECT(!sle);
|
||||
}
|
||||
else if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT((*sle)[sfTakerPays] == x(bidPays - xTaken));
|
||||
BEAST_EXPECT((*sle)[sfTakerGets] == y(bidGets - delivered));
|
||||
}
|
||||
|
||||
auto const issuance = env.le(keylet::mptokenIssuance(x.mpt()));
|
||||
if (BEAST_EXPECT(issuance))
|
||||
{
|
||||
// x taken by the issuer's own bid is redeemed.
|
||||
std::int64_t const outstanding = bidder == gwX ? cap - xTaken : cap;
|
||||
BEAST_EXPECT(
|
||||
(*issuance)[sfOutstandingAmount] == static_cast<std::uint64_t>(outstanding));
|
||||
}
|
||||
};
|
||||
|
||||
// Bid above the cap: the input is limited to the cap, so at most
|
||||
// 1,000 x (= 1,000 y at this bid's price) can flow through it.
|
||||
test(mallory, 2 * cap, 1'001, true, cap);
|
||||
test(mallory, 2 * cap, bidGets, true, cap);
|
||||
// Not partial: the strand no longer throws, but 1,001 y can't be
|
||||
// delivered.
|
||||
test(mallory, 2 * cap, 1'001, false, 0, tecPATH_PARTIAL);
|
||||
// Controls: a request within the cap, and a bid at the cap.
|
||||
test(mallory, 2 * cap, 500, true, 500);
|
||||
test(mallory, cap, bidGets, true, bidGets);
|
||||
// The issuer's own bid above the cap isn't limited: the issuer's send
|
||||
// to itself is a no-op, so upstream supply bounds the fill as usual.
|
||||
test(gwX, 2 * cap, 1'001, true, cap);
|
||||
|
||||
// A bid above the cap at a non-unit price, crossed repeatedly. Each
|
||||
// crossing is limited to the cap and the output is rounded down, so
|
||||
// the remainder's quality is no worse than the original's. The
|
||||
// remainder stays on the book and is eventually consumed.
|
||||
{
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(100'000), gwX, gwY, alice, carol, mallory, dave);
|
||||
env.close();
|
||||
|
||||
MPT const x = MPTTester(
|
||||
{.env = env, .issuer = gwX, .holders = {carol, mallory, dave}, .maxAmt = cap});
|
||||
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {mallory, dave}});
|
||||
env(pay(gwX, carol, x(cap)));
|
||||
env(pay(gwY, mallory, y(bidGets)));
|
||||
env.close();
|
||||
|
||||
auto const bidSeq = env.seq(mallory);
|
||||
env(offer(mallory, x(unevenBidPays), y(bidGets)));
|
||||
env.close();
|
||||
auto const bidKeylet = keylet::offer(mallory.id(), SeqProxy::rawSequence(bidSeq));
|
||||
|
||||
// {x taken, y delivered} per round; the first three are capped.
|
||||
// 1,000 x buys floor(1,000 * gets / pays) y at the remainder's
|
||||
// price: 2,000/3,100, 1,355/2,100, 710/1,100. The last 100 x buy
|
||||
// the remaining 65 y and consume the bid.
|
||||
std::int64_t totalY = 0;
|
||||
std::int64_t bidPays = unevenBidPays;
|
||||
std::int64_t bidGetsLeft = bidGets;
|
||||
for (auto const& [xTaken, yDelivered] :
|
||||
std::initializer_list<std::pair<std::int64_t, std::int64_t>>{
|
||||
{cap, 645}, {cap, 645}, {cap, 645}, {100, 65}})
|
||||
{
|
||||
auto const askSeq = env.seq(carol);
|
||||
env(offer(carol, XRP(cap), x(cap)));
|
||||
env.close();
|
||||
env(pay(alice, dave, y(1'001)),
|
||||
Path(~x, ~y),
|
||||
Sendmax(XRP(5'000)),
|
||||
Txflags(tfNoRippleDirect | tfPartialPayment));
|
||||
env.close();
|
||||
|
||||
totalY += yDelivered;
|
||||
bidPays -= xTaken;
|
||||
bidGetsLeft -= yDelivered;
|
||||
BEAST_EXPECT(env.balance(dave, y) == y(totalY));
|
||||
BEAST_EXPECT(env.balance(mallory, x) == x(xTaken));
|
||||
BEAST_EXPECT(env.balance(carol, x) == x(cap - xTaken));
|
||||
auto const sle = env.le(bidKeylet);
|
||||
if (bidPays == 0)
|
||||
{
|
||||
BEAST_EXPECT(!sle);
|
||||
}
|
||||
else if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT((*sle)[sfTakerPays] == x(bidPays));
|
||||
BEAST_EXPECT((*sle)[sfTakerGets] == y(bidGetsLeft));
|
||||
// The remainder's quality is no worse than the original's.
|
||||
BEAST_EXPECT(bidGetsLeft * unevenBidPays >= bidGets * bidPays);
|
||||
}
|
||||
|
||||
// Return x to carol for the next round, and cancel her unfilled
|
||||
// ask remainder.
|
||||
env(pay(mallory, carol, x(xTaken)));
|
||||
env(offerCancel(carol, askSeq));
|
||||
env.close();
|
||||
}
|
||||
BEAST_EXPECT(totalY == bidGets);
|
||||
}
|
||||
|
||||
// A transfer fee on x: the cap is grossed up by the rate when limiting
|
||||
// the taker's input, and the owner receives exactly the cap. The
|
||||
// previous step must redeem for the fee to apply, so alice pays with
|
||||
// x directly (MPTEndpoint alice -> gwX, then the x/y book). alice can
|
||||
// hold at most the cap, so upstream still bounds the fill: she pays
|
||||
// 1,000 gross, mallory receives floor(1,000 / 1.1) = 909 x, dave gets
|
||||
// floor(909 * 2,000 / 3,100) = 586 y.
|
||||
{
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(100'000), gwX, gwY, alice, mallory, dave);
|
||||
env.close();
|
||||
|
||||
MPT const x = MPTTester(
|
||||
{.env = env,
|
||||
.issuer = gwX,
|
||||
.holders = {alice, mallory, dave},
|
||||
.transferFee = 10'000,
|
||||
.maxAmt = cap});
|
||||
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {mallory, dave}});
|
||||
env(pay(gwX, alice, x(cap)));
|
||||
env(pay(gwY, mallory, y(bidGets)));
|
||||
env.close();
|
||||
|
||||
auto const bidSeq = env.seq(mallory);
|
||||
env(offer(mallory, x(unevenBidPays), y(bidGets)));
|
||||
env.close();
|
||||
auto const bidKeylet = keylet::offer(mallory.id(), SeqProxy::rawSequence(bidSeq));
|
||||
|
||||
env(pay(alice, dave, y(1'001)),
|
||||
Path(~y),
|
||||
Sendmax(x(cap)),
|
||||
Txflags(tfNoRippleDirect | tfPartialPayment));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(dave, y) == y(586));
|
||||
BEAST_EXPECT(env.balance(mallory, x) == x(909));
|
||||
BEAST_EXPECT(env.balance(alice, x) == x(0));
|
||||
auto const sle = env.le(bidKeylet);
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT((*sle)[sfTakerPays] == x(unevenBidPays - 909));
|
||||
BEAST_EXPECT((*sle)[sfTakerGets] == y(bidGets - 586));
|
||||
}
|
||||
auto const issuance = env.le(keylet::mptokenIssuance(x.mpt()));
|
||||
if (BEAST_EXPECT(issuance))
|
||||
BEAST_EXPECT((*issuance)[sfOutstandingAmount] == 909);
|
||||
}
|
||||
|
||||
// Two bids above the cap at the same quality, mallory's ahead of
|
||||
// bob's. Stopping after a cap-limited bid doesn't strand bob's: the
|
||||
// step's input is already at the cap, which is all carol can supply,
|
||||
// so delivery equals the single-bid case. If mallory's bid is
|
||||
// underfunded below the cap instead, it isn't cap-limited: it is
|
||||
// consumed, removed, and bob's bid is crossed in the same pass.
|
||||
auto twoBids = [&](std::int64_t malloryY,
|
||||
std::int64_t delivered,
|
||||
std::int64_t malloryX,
|
||||
std::int64_t malloryYSold,
|
||||
std::int64_t bobX,
|
||||
std::int64_t bobYSold,
|
||||
std::optional<std::pair<std::int64_t, std::int64_t>> malloryBid) {
|
||||
Env env(*this, features);
|
||||
Account const bob("bob");
|
||||
env.fund(XRP(100'000), gwX, gwY, alice, bob, carol, mallory, dave);
|
||||
env.close();
|
||||
|
||||
MPT const x = MPTTester(
|
||||
{.env = env, .issuer = gwX, .holders = {bob, carol, mallory, dave}, .maxAmt = cap});
|
||||
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {bob, mallory, dave}});
|
||||
env(pay(gwX, carol, x(cap)));
|
||||
env(pay(gwY, mallory, y(malloryY)));
|
||||
env(pay(gwY, bob, y(bidGets)));
|
||||
env.close();
|
||||
|
||||
env(offer(carol, XRP(cap), x(cap)));
|
||||
// Close between the bids: within one ledger, transactions apply in
|
||||
// canonical order, and mallory's bid must be ahead of bob's.
|
||||
auto const mallorySeq = env.seq(mallory);
|
||||
env(offer(mallory, x(unevenBidPays), y(bidGets)));
|
||||
env.close();
|
||||
auto const bobSeq = env.seq(bob);
|
||||
env(offer(bob, x(unevenBidPays), y(bidGets)));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, dave, y(bidGets)),
|
||||
Path(~x, ~y),
|
||||
Sendmax(XRP(5'000)),
|
||||
Txflags(tfNoRippleDirect | tfPartialPayment));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(dave, y) == y(delivered));
|
||||
BEAST_EXPECT(env.balance(mallory, x) == x(malloryX));
|
||||
BEAST_EXPECT(env.balance(mallory, y) == y(malloryY - malloryYSold));
|
||||
BEAST_EXPECT(env.balance(bob, x) == x(bobX));
|
||||
BEAST_EXPECT(env.balance(bob, y) == y(bidGets - bobYSold));
|
||||
BEAST_EXPECT(env.balance(carol, x) == x(cap - malloryX - bobX));
|
||||
|
||||
auto const mallorySle =
|
||||
env.le(keylet::offer(mallory.id(), SeqProxy::rawSequence(mallorySeq)));
|
||||
if (!malloryBid)
|
||||
{
|
||||
BEAST_EXPECT(!mallorySle);
|
||||
}
|
||||
else if (BEAST_EXPECT(mallorySle))
|
||||
{
|
||||
BEAST_EXPECT((*mallorySle)[sfTakerPays] == x(malloryBid->first));
|
||||
BEAST_EXPECT((*mallorySle)[sfTakerGets] == y(malloryBid->second));
|
||||
}
|
||||
auto const bobSle = env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq)));
|
||||
if (BEAST_EXPECT(bobSle))
|
||||
{
|
||||
BEAST_EXPECT((*bobSle)[sfTakerPays] == x(unevenBidPays - bobX));
|
||||
BEAST_EXPECT((*bobSle)[sfTakerGets] == y(bidGets - bobYSold));
|
||||
}
|
||||
auto const issuance = env.le(keylet::mptokenIssuance(x.mpt()));
|
||||
if (BEAST_EXPECT(issuance))
|
||||
BEAST_EXPECT((*issuance)[sfOutstandingAmount] == static_cast<std::uint64_t>(cap));
|
||||
};
|
||||
// Both funded: mallory's bid takes the cap for 645 y and rests at
|
||||
// 2,100/1,355; bob's is untouched.
|
||||
twoBids(bidGets, 645, cap, 645, 0, 0, std::pair{unevenBidPays - cap, bidGets - 645});
|
||||
// mallory holds 500 y: funds limit her bid to 775 x, under the cap, so
|
||||
// it is consumed and removed; bob's bid takes the remaining 225 x for
|
||||
// 145 y in the same pass. Same 645 y total.
|
||||
twoBids(500, 645, 775, 500, 225, 145, std::nullopt);
|
||||
}
|
||||
|
||||
void
|
||||
testBookStepMissingIssuance(FeatureBitset features)
|
||||
{
|
||||
// A path through a book whose MPT issuance doesn't exist. BookStep's
|
||||
// constructor looks the issuance up for MaximumAmount before check()
|
||||
// rejects the step, so the lookup must tolerate a missing entry.
|
||||
testcase("BookStep on a missing MPT issuance");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Env env(*this, features);
|
||||
Account const gw("gw");
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
env.fund(XRP(10'000), gw, alice, bob);
|
||||
env.close();
|
||||
|
||||
MPT const y = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
|
||||
MPT const missing{"missing", makeMptID(99, gw)};
|
||||
BEAST_EXPECT(!env.le(keylet::mptokenIssuance(missing.mpt())));
|
||||
|
||||
env(pay(alice, bob, y(10)),
|
||||
Path(~missing, ~y),
|
||||
Sendmax(XRP(100)),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter(tecOBJECT_NOT_FOUND));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob, y) == y(0));
|
||||
}
|
||||
|
||||
void
|
||||
testFalseDry(FeatureBitset features)
|
||||
{
|
||||
@@ -1112,14 +1443,7 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
{
|
||||
auto const offer = *offerPtr;
|
||||
BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(eur)>, IOU>)
|
||||
{
|
||||
BEAST_EXPECT(offer[sfTakerGets] == eur(5'988));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(offer[sfTakerGets] == eur(5'989));
|
||||
}
|
||||
BEAST_EXPECT(offer[sfTakerGets] == eur(5'988));
|
||||
BEAST_EXPECT(offer[sfTakerPays] == usd(4'990));
|
||||
}
|
||||
};
|
||||
@@ -1158,10 +1482,8 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
// OutstandingAmount since it doesn't know if the
|
||||
// BookStep redeems or not. The BookStep then has 600EUR
|
||||
// available. Consequently, the entire offer is crossed.
|
||||
// Note remaining takerGets is 541 rather than 540 due to integral
|
||||
// rounding. XRP has a similar result.
|
||||
return TokenData<MPT, MPT>{
|
||||
.gets = eur, .pays = usd, .remTakerGets = eur(541), .remTakerPays = usd(450)};
|
||||
.gets = eur, .pays = usd, .remTakerGets = eur(540), .remTakerPays = usd(450)};
|
||||
};
|
||||
|
||||
auto initXRP = [&](Env& env) {
|
||||
@@ -1171,13 +1493,8 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
// OutstandingAmount since it doesn't know if the
|
||||
// BookStep redeems or not. The BookStep then has 600EUR
|
||||
// available. Consequently, the entire offer is crossed.
|
||||
// Note remaining takerGets is 540.000001 rather than 540 due to
|
||||
// integral rounding.
|
||||
return TokenData<XrpT, MPT>{
|
||||
.gets = XRP,
|
||||
.pays = usd,
|
||||
.remTakerGets = XRP(540.000001),
|
||||
.remTakerPays = usd(450)};
|
||||
.gets = XRP, .pays = usd, .remTakerGets = XRP(540), .remTakerPays = usd(450)};
|
||||
};
|
||||
|
||||
auto initIOU = [&](Env& env) {
|
||||
@@ -2525,6 +2842,8 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
testTransferRate(features);
|
||||
testMPTEndpointTransferRateOverflow(features);
|
||||
testMPTEndpointRipplingInputOverflow(features);
|
||||
testBookStepInputAboveMaximumAmount(features);
|
||||
testBookStepMissingIssuance(features);
|
||||
testSelfPayment1(features);
|
||||
testSelfPayment2(features);
|
||||
testSelfFundedXRPEndpoint(false, features);
|
||||
|
||||
+1286
-143
File diff suppressed because it is too large.
Load diff
@@ -139,14 +139,15 @@ AMMTestBase::testAMM(std::function<void(jtx::AMM&, jtx::Env&)> const& cb, TestAM
|
||||
// Use small Number mantissas for the life of this test.
|
||||
NumberMantissaScaleGuard const sg{xrpl::MantissaRange::MantissaScale::Small};
|
||||
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas
|
||||
Env env{
|
||||
*this,
|
||||
features - featureSingleAssetVault - featureLendingProtocol,
|
||||
arg.noLog ? std::make_unique<CaptureLogs>(&logs) : nullptr};
|
||||
|
||||
auto const [asset1, asset2] = arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000));
|
||||
|
||||
// For now, just disable SAV entirely, which locks in the small Number
|
||||
// mantissas. MPTokensV2 enables them too, so keep it for MPT pools
|
||||
// only, which require it.
|
||||
auto envFeatures = features - featureSingleAssetVault - featureLendingProtocol;
|
||||
if (!asset1.holds<MPTIssue>() && !asset2.holds<MPTIssue>())
|
||||
envFeatures = envFeatures - featureMPTokensV2;
|
||||
Env env{*this, envFeatures, arg.noLog ? std::make_unique<CaptureLogs>(&logs) : nullptr};
|
||||
auto toFund = [&](STAmount const& a) -> STAmount {
|
||||
if (a.native())
|
||||
{
|
||||
|
||||
@@ -1072,6 +1072,39 @@ public:
|
||||
BEAST_EXPECT(down.signum() > 0);
|
||||
BEAST_EXPECT(up >= down);
|
||||
}
|
||||
|
||||
{
|
||||
// An MPT operand with a non-MPT result. The legacy path assumes
|
||||
// 16-digit mantissas and overflows on a 63-bit MPT mantissa; under
|
||||
// MPTokensV2 any MPT operand, not only an MPT result, takes the
|
||||
// Number path. This is OfferCreate::flowCross recomputing the
|
||||
// remainder of a partially crossed large MPT offer, and
|
||||
// Quality::ceilIn/ceilOut partially filling one.
|
||||
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
|
||||
STAmount const bigMpt{asset, UINT64_C(5'000'000'000'000'000'000)};
|
||||
STAmount const milli{noIssue(), UINT64_C(1'000'000'000'000'000), -18}; // 1e-3
|
||||
STAmount const kilo{noIssue(), UINT64_C(1'000'000'000'000'000), -12}; // 1e3
|
||||
STAmount const tenMicro{noIssue(), UINT64_C(1'000'000'000'000'000), -20}; // 1e-5
|
||||
STAmount const usdResult{usd, UINT64_C(5'000'000'000'000'000)};
|
||||
XRPAmount const xrpResult{50'000'000'000'000};
|
||||
|
||||
{
|
||||
CurrentTransactionRulesGuard const rg(rules(false));
|
||||
throwsOverflow([&] { (void)mulRound(bigMpt, milli, usd, true); });
|
||||
throwsOverflow([&] { (void)mulRound(bigMpt, tenMicro, xrpIssue(), true); });
|
||||
throwsOverflow([&] { (void)divRound(bigMpt, kilo, usd, true); });
|
||||
throwsOverflow([&] { (void)divRoundStrict(bigMpt, kilo, usd, false); });
|
||||
}
|
||||
|
||||
{
|
||||
CurrentTransactionRulesGuard const rg(rules(true));
|
||||
BEAST_EXPECT(mulRound(bigMpt, milli, usd, true) == usdResult);
|
||||
BEAST_EXPECT(mulRound(bigMpt, milli, usd, false) == usdResult);
|
||||
BEAST_EXPECT(mulRound(bigMpt, tenMicro, xrpIssue(), true) == STAmount{xrpResult});
|
||||
BEAST_EXPECT(divRound(bigMpt, kilo, usd, true) == usdResult);
|
||||
BEAST_EXPECT(divRoundStrict(bigMpt, kilo, usd, false) == usdResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1336,6 +1369,100 @@ public:
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
testMPTGetRate()
|
||||
{
|
||||
testcase("getRate with a large integral numerator");
|
||||
|
||||
MPTIssue const asset{makeMptID(1, AccountID(0x4985601))};
|
||||
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
|
||||
|
||||
auto rules = [](bool const mptV2) {
|
||||
static std::unordered_set<uint256, beast::Uhash<>> const kNoFeatures;
|
||||
// MPTokensV2 with the large Number mantissa (SAV enables it).
|
||||
static std::unordered_set<uint256, beast::Uhash<>> const kMptV2Features{
|
||||
featureMPTokensV2, featureSingleAssetVault, fixCleanup3_3_0};
|
||||
return Rules{mptV2 ? kMptV2Features : kNoFeatures};
|
||||
};
|
||||
|
||||
// TakerPays / TakerGets = 184467440737095516 / 1e-7. The legacy
|
||||
// divide() quotient is 2^64 - 11, which the signed IOU mantissa reads
|
||||
// as -11: getRate records 1.1e6 instead of ~1.8e24.
|
||||
STAmount const dust{usd, UINT64_C(1'000'000'000'000'000), -22}; // 1e-7
|
||||
STAmount const largeMpt{asset, UINT64_C(184'467'440'737'095'516)};
|
||||
STAmount const trueRate{noIssue(), Number{largeMpt} / Number{dust}};
|
||||
|
||||
// Quotient above 2^64: muldiv throws, so getRate returns 0
|
||||
// pre-amendment.
|
||||
STAmount const one{usd, UINT64_C(1'000'000'000'000'000), -15};
|
||||
STAmount const hugeMpt{asset, UINT64_C(5'000'000'000'000'000'000)};
|
||||
STAmount const hugeMptRate{noIssue(), UINT64_C(5'000'000'000'000'000), 3};
|
||||
|
||||
// Over the smallest IOU the rate is beyond the IOU exponent range, so
|
||||
// getRate returns 0 either way.
|
||||
STAmount const tiny{usd, STAmount::kMinValue, STAmount::kMinOffset};
|
||||
|
||||
// An integral result keeps every digit, so it must come out exact.
|
||||
STAmount const maxMpt{asset, static_cast<std::uint64_t>(kMaxMpTokenAmount)};
|
||||
STAmount const oneAndHalf{noIssue(), UINT64_C(1'500'000'000'000'000), -15};
|
||||
STAmount const e18Mpt{asset, UINT64_C(1'000'000'000'000'000'000)};
|
||||
|
||||
// XRP numerator in the same band: 9.5e16 drops over a mantissa of
|
||||
// 1e15 gives a legacy quotient of 9.5e18.
|
||||
STAmount const hugeXrp{XRPAmount{95'000'000'000'000'000}};
|
||||
STAmount const hugeXrpRate{noIssue(), UINT64_C(9'500'000'000'000'000), 1};
|
||||
|
||||
{
|
||||
CurrentTransactionRulesGuard const rg(rules(false));
|
||||
|
||||
BEAST_EXPECT(
|
||||
amountFromQuality(getRate(dust, largeMpt)) ==
|
||||
STAmount(noIssue(), UINT64_C(1'100'000'000'000'000), -9));
|
||||
BEAST_EXPECT(getRate(one, hugeMpt) == 0);
|
||||
BEAST_EXPECT(getRate(tiny, hugeMpt) == 0);
|
||||
BEAST_EXPECT(getRate(one, hugeXrp) != 0);
|
||||
BEAST_EXPECT(amountFromQuality(getRate(one, hugeXrp)) != hugeXrpRate);
|
||||
|
||||
bool threw = false;
|
||||
try
|
||||
{
|
||||
(void)divide(hugeMpt, one, asset);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
threw = true;
|
||||
}
|
||||
BEAST_EXPECT(threw);
|
||||
}
|
||||
|
||||
{
|
||||
CurrentTransactionRulesGuard const rg(rules(true));
|
||||
|
||||
BEAST_EXPECT(amountFromQuality(getRate(dust, largeMpt)) == trueRate);
|
||||
BEAST_EXPECT(amountFromQuality(getRate(one, hugeMpt)) == hugeMptRate);
|
||||
BEAST_EXPECT(getRate(tiny, hugeMpt) == 0);
|
||||
BEAST_EXPECT(amountFromQuality(getRate(one, hugeXrp)) == hugeXrpRate);
|
||||
BEAST_EXPECT(!divide(largeMpt, dust, noIssue()).negative());
|
||||
|
||||
BEAST_EXPECT(divide(hugeMpt, one, asset) == hugeMpt);
|
||||
BEAST_EXPECT(divide(maxMpt, one, asset) == maxMpt);
|
||||
BEAST_EXPECT(
|
||||
divide(STAmount{asset, UINT64_C(9'000'000'000'000'000'000)}, oneAndHalf, asset) ==
|
||||
STAmount(asset, UINT64_C(6'000'000'000'000'000'000)));
|
||||
|
||||
// An unscaled MPT denominator no longer moves the +5 nudge into
|
||||
// the 15th digit: 9e15 / 1e18 is exactly 0.009.
|
||||
BEAST_EXPECT(
|
||||
divide(STAmount{usd, UINT64_C(9'000'000'000'000'000)}, e18Mpt, noIssue()) ==
|
||||
STAmount(noIssue(), UINT64_C(9'000'000'000'000'000), -18));
|
||||
}
|
||||
|
||||
// No rules (RPC): same as post-amendment.
|
||||
BEAST_EXPECT(amountFromQuality(getRate(dust, largeMpt)) == trueRate);
|
||||
BEAST_EXPECT(amountFromQuality(getRate(one, hugeMpt)) == hugeMptRate);
|
||||
BEAST_EXPECT(amountFromQuality(getRate(one, hugeXrp)) == hugeXrpRate);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -1352,6 +1479,7 @@ public:
|
||||
testCanAddIOU();
|
||||
testCanAddMPT();
|
||||
testMPTRateRounding();
|
||||
testMPTGetRate();
|
||||
testCanSubtractXRP();
|
||||
testCanSubtractIOU();
|
||||
testCanSubtractMPT();
|
||||
|
||||
@@ -149,33 +149,34 @@ public:
|
||||
auto const big = MPT{gw.id(), 1};
|
||||
auto const usd = iouGw["USD"];
|
||||
|
||||
// This metadata represents a partial MPT/IOU offer fill whose deltas
|
||||
// make divide(deltaGets, deltaPays) overflow before MPTokensV2 skips
|
||||
// the unrepresentable book-change rate.
|
||||
STObject finalFields = STObject::makeInnerObject(sfFinalFields);
|
||||
finalFields.setFieldU32(sfSequence, 1);
|
||||
finalFields.setFieldAmount(sfTakerGets, big(1'800'000'000'000'000'000ull));
|
||||
finalFields.setFieldAmount(sfTakerPays, usd(9));
|
||||
// Metadata for a partial MPT/IOU offer fill. The rate is
|
||||
// divide(deltaGets, deltaPays): 1.8e18 MPT over `pays` IOU.
|
||||
auto const test = [&](std::unordered_set<uint256, beast::Uhash<>> const& features,
|
||||
STAmount const& pays,
|
||||
std::size_t expectedChanges) {
|
||||
STObject finalFields = STObject::makeInnerObject(sfFinalFields);
|
||||
finalFields.setFieldU32(sfSequence, 1);
|
||||
finalFields.setFieldAmount(sfTakerGets, big(1'800'000'000'000'000'000ull));
|
||||
finalFields.setFieldAmount(sfTakerPays, pays);
|
||||
|
||||
STObject previousFields = STObject::makeInnerObject(sfPreviousFields);
|
||||
previousFields.setFieldU32(sfSequence, 1);
|
||||
previousFields.setFieldAmount(sfTakerGets, big(3'600'000'000'000'000'000ull));
|
||||
previousFields.setFieldAmount(sfTakerPays, usd(18));
|
||||
STObject previousFields = STObject::makeInnerObject(sfPreviousFields);
|
||||
previousFields.setFieldU32(sfSequence, 1);
|
||||
previousFields.setFieldAmount(sfTakerGets, big(3'600'000'000'000'000'000ull));
|
||||
previousFields.setFieldAmount(sfTakerPays, pays + pays);
|
||||
|
||||
STObject modifiedOffer{sfModifiedNode};
|
||||
modifiedOffer.setFieldU16(sfLedgerEntryType, ltOFFER);
|
||||
modifiedOffer.setFieldObject(sfFinalFields, finalFields);
|
||||
modifiedOffer.setFieldObject(sfPreviousFields, previousFields);
|
||||
STObject modifiedOffer{sfModifiedNode};
|
||||
modifiedOffer.setFieldU16(sfLedgerEntryType, ltOFFER);
|
||||
modifiedOffer.setFieldObject(sfFinalFields, finalFields);
|
||||
modifiedOffer.setFieldObject(sfPreviousFields, previousFields);
|
||||
|
||||
STArray affectedNodes{sfAffectedNodes};
|
||||
affectedNodes.pushBack(std::move(modifiedOffer));
|
||||
STArray affectedNodes{sfAffectedNodes};
|
||||
affectedNodes.pushBack(std::move(modifiedOffer));
|
||||
|
||||
auto metadata = std::make_shared<STObject>(sfTransactionMetaData);
|
||||
metadata->setFieldArray(sfAffectedNodes, affectedNodes);
|
||||
auto metadata = std::make_shared<STObject>(sfTransactionMetaData);
|
||||
metadata->setFieldArray(sfAffectedNodes, affectedNodes);
|
||||
|
||||
auto tx = std::make_shared<STTx const>(ttOFFER_CREATE, [](STObject&) {});
|
||||
auto tx = std::make_shared<STTx const>(ttOFFER_CREATE, [](STObject&) {});
|
||||
|
||||
auto const test = [&](std::unordered_set<uint256, beast::Uhash<>> const& features) {
|
||||
auto ledger = std::make_shared<Ledger>(
|
||||
2,
|
||||
NetClock::time_point{},
|
||||
@@ -198,7 +199,7 @@ public:
|
||||
auto const result =
|
||||
xrpl::rpc::computeBookChanges(std::static_pointer_cast<Ledger const>(ledger));
|
||||
BEAST_EXPECT(result[jss::type] == "bookChanges");
|
||||
BEAST_EXPECT(result[jss::changes].size() == 0);
|
||||
BEAST_EXPECT(result[jss::changes].size() == expectedChanges);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
@@ -206,8 +207,20 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
test(std::unordered_set<uint256, beast::Uhash<>>{});
|
||||
test(std::unordered_set<uint256, beast::Uhash<>>{featureMPTokensV2});
|
||||
// Over the smallest IOU the rate is beyond the IOU exponent range:
|
||||
// the change is skipped.
|
||||
STAmount const tiny{usd.issue(), STAmount::kMinValue, STAmount::kMinOffset};
|
||||
// Over 9 USD the rate is 2e17. divide() used to overflow here; with
|
||||
// Number arithmetic for MPT operands it is reported.
|
||||
STAmount const nine = usd(9);
|
||||
|
||||
for (auto const& features :
|
||||
{std::unordered_set<uint256, beast::Uhash<>>{},
|
||||
std::unordered_set<uint256, beast::Uhash<>>{featureMPTokensV2}})
|
||||
{
|
||||
test(features, tiny, 0);
|
||||
test(features, nine, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Build a ledger whose transactions are OfferCreates carrying the supplied
|
||||
|
||||
Reference in new issue
Block a user