From c549c9dff09398508ac0a938ef3744b77dd4ea12 Mon Sep 17 00:00:00 2001 From: seelabs Date: Thu, 21 Apr 2016 07:17:40 -0400 Subject: [PATCH] Transfer fee changes: An offer owner pays the transfer fee and only change a transfer fee when transfering an IOU (as the old code does). --- src/ripple/app/paths/Flow.cpp | 3 +- src/ripple/app/paths/Flow.h | 1 + src/ripple/app/paths/RippleCalc.cpp | 41 +++-- src/ripple/app/paths/impl/BookStep.cpp | 181 +++++++++++++++---- src/ripple/app/paths/impl/DirectStep.cpp | 85 ++++++--- src/ripple/app/paths/impl/PaySteps.cpp | 24 ++- src/ripple/app/paths/impl/Steps.h | 16 ++ src/ripple/app/paths/impl/StrandFlow.h | 2 +- src/ripple/app/tests/Flow_test.cpp | 211 ++++++++++++++++++----- src/ripple/ledger/impl/View.cpp | 4 +- src/ripple/protocol/Feature.h | 1 + src/ripple/protocol/impl/Feature.cpp | 1 + 12 files changed, 436 insertions(+), 134 deletions(-) diff --git a/src/ripple/app/paths/Flow.cpp b/src/ripple/app/paths/Flow.cpp index 1c9bd7c4f3..9e68f8e988 100644 --- a/src/ripple/app/paths/Flow.cpp +++ b/src/ripple/app/paths/Flow.cpp @@ -62,6 +62,7 @@ flow ( STPathSet const& paths, bool defaultPaths, bool partialPayment, + bool ownerPaysTransferFee, boost::optional const& limitQuality, boost::optional const& sendMax, beast::Journal j) @@ -83,7 +84,7 @@ flow ( // convert the paths to a collection of strands. Each strand is the collection // of account->account steps and book steps that may be used in this payment. auto sr = toStrands (sb, src, dst, dstIssue, sendMaxIssue, paths, - defaultPaths, j); + defaultPaths, ownerPaysTransferFee, j); if (sr.first != tesSUCCESS) { diff --git a/src/ripple/app/paths/Flow.h b/src/ripple/app/paths/Flow.h index 5a1dda0a8e..ecf6225857 100644 --- a/src/ripple/app/paths/Flow.h +++ b/src/ripple/app/paths/Flow.h @@ -51,6 +51,7 @@ flow (PaymentSandbox& view, STPathSet const& paths, bool defaultPaths, bool partialPayment, + bool ownerPaysTransferFee, boost::optional const& limitQuality, boost::optional const& sendMax, beast::Journal j); diff --git a/src/ripple/app/paths/RippleCalc.cpp b/src/ripple/app/paths/RippleCalc.cpp index 1a65573efd..a433ae9a7a 100644 --- a/src/ripple/app/paths/RippleCalc.cpp +++ b/src/ripple/app/paths/RippleCalc.cpp @@ -133,9 +133,11 @@ RippleCalc::Output RippleCalc::rippleCalculate ( try { + bool const ownerPaysTransferFee = + view.rules ().enabled (featureOwnerPaysFee, config.features); flowV2Out = flow (flowV2SB, saDstAmountReq, uSrcAccountID, uDstAccountID, spsPaths, defaultPaths, partialPayment, - limitQuality, sendMax, j); + ownerPaysTransferFee, limitQuality, sendMax, j); } catch (std::exception& e) { @@ -144,25 +146,28 @@ RippleCalc::Output RippleCalc::rippleCalculate ( Throw(); } - if (callFlowV2 && callFlowV1 && - (flowV2Out.result () != flowV1Out.result () || - (flowV2Out.result () == tesSUCCESS && - (flowV2Out.actualAmountIn != flowV1Out.actualAmountIn || - flowV2Out.actualAmountOut != flowV1Out.actualAmountOut)))) + if (j.debug()) { - JLOG (j.trace()) << - "Mismatch: New Flow and RippleCalc" << - " Old actualIn: " << flowV1Out.actualAmountIn << - " New actualIn: " << flowV2Out.actualAmountIn << - " Old actualOut: " << flowV1Out.actualAmountOut << - " New actualOut: " << flowV2Out.actualAmountOut << - " Old result: " << flowV1Out.result () << - " New result: " << flowV2Out.result(); - } - else - { - JLOG (j.trace()) << "Match: New Flow and RippleCalc"; + auto logResult = [&] (std::string const& algoName, Output const& result) + { + j.debug() << "RippleCalc Result> " << + " actualIn: " << result.actualAmountIn << + ", actualOut: " << result.actualAmountOut << + ", result: " << result.result () << + ", dstAmtReq: " << saDstAmountReq << + ", sendMax: " << saMaxAmountReq << + ", algo: " << algoName; + }; + if (callFlowV1) + { + logResult ("V1", flowV1Out); + } + if (callFlowV2) + { + logResult ("V2", flowV2Out); + } } + JLOG (j.trace()) << "Using old flow: " << useFlowV1Output; } diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index 6e4d4c8b08..2ee6143112 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -52,6 +52,9 @@ private: Book book_; AccountID strandSrc_; AccountID strandDst_; + // Charge transfer fees whan the prev step redeems + Step const* const prevStep_ = nullptr; + bool const ownerPaysTransferFee_; beast::Journal j_; struct Cache @@ -59,7 +62,6 @@ private: TIn in; TOut out; - Cache () = default; Cache (TIn const& in_, TOut const& out_) : in (in_), out (out_) { @@ -73,10 +75,14 @@ public: Issue const& out, AccountID const& strandSrc, AccountID const& strandDst, + Step const* prevStep, + bool ownerPaysTransferFee, beast::Journal j) : book_ (in, out) , strandSrc_ (strandSrc) , strandDst_ (strandDst) + , prevStep_ (prevStep) + , ownerPaysTransferFee_ (ownerPaysTransferFee) , j_ (j) { } @@ -102,6 +108,12 @@ public: return EitherAmount (cache_->out); } + bool + redeems (ReadView const& sb, bool fwd) const override + { + return !ownerPaysTransferFee_; + } + boost::optional bookStepBook () const override { @@ -147,7 +159,8 @@ private: void consumeOffer (PaymentSandbox& sb, TOffer& offer, TAmounts const& ofrAmt, - TAmounts const& stepAmt) const; + TAmounts const& stepAmt, + TOut const& ownerGives) const; std::string logString () const override { @@ -178,7 +191,9 @@ static void limitStepIn (Quality const& ofrQ, TAmounts& ofrAmt, TAmounts& stpAmt, + TOut& ownerGives, std::uint32_t transferRateIn, + std::uint32_t transferRateOut, TIn const& limit) { if (limit < stpAmt.in) @@ -188,6 +203,8 @@ void limitStepIn (Quality const& ofrQ, stpAmt.in, QUALITY_ONE, transferRateIn, /*roundUp*/ false); ofrAmt = ofrQ.ceil_in (ofrAmt, inLmt); stpAmt.out = ofrAmt.out; + ownerGives = mulRatio ( + ofrAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false); } } @@ -197,13 +214,17 @@ static void limitStepOut (Quality const& ofrQ, TAmounts& ofrAmt, TAmounts& stpAmt, + TOut& ownerGives, std::uint32_t transferRateIn, + std::uint32_t transferRateOut, TOut const& limit) { if (limit < stpAmt.out) { stpAmt.out = limit; - ofrAmt = ofrQ.ceil_out (ofrAmt, limit); + ownerGives = mulRatio ( + stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false); + ofrAmt = ofrQ.ceil_out (ofrAmt, stpAmt.out); stpAmt.in = mulRatio ( ofrAmt.in, transferRateIn, QUALITY_ONE, /*roundUp*/ true); } @@ -225,18 +246,28 @@ forEachOffer ( Book const& book, AccountID const& src, AccountID const& dst, + bool prevStepRedeems, + bool ownerPaysTransferFee, Callback& callback, std::uint32_t limit, beast::Journal j) { + // Charge the offer owner, not the sender + // Charge a fee even if the owner is the same as the issuer + // (the old code does not charge a fee) + // Calculate amount that goes to the taker and the amount charged the offer owner auto transferRate = [&](AccountID const& id)->std::uint32_t { - if (isXRP (id) || id == src || id == dst) + if (isXRP (id) || id == dst) return QUALITY_ONE; return rippleTransferRate (sb, id); }; - std::uint32_t const trIn = transferRate (book.in.account); + std::uint32_t const trIn = + prevStepRedeems ? transferRate (book.in.account) : QUALITY_ONE; + // Always charge the transfer fee, even if the owner is the issuer + std::uint32_t const trOut = + ownerPaysTransferFee ? transferRate (book.out.account) : QUALITY_ONE; typename FlowOfferStream::StepCounter counter (limit, j); FlowOfferStream offers ( @@ -251,16 +282,31 @@ forEachOffer ( else if (*ofrQ != offer.quality ()) break; - auto const funds = offers.ownerFunds (); auto ofrAmt = offer.amount (); auto stpAmt = make_Amounts ( mulRatio (ofrAmt.in, trIn, QUALITY_ONE, /*roundUp*/ true), ofrAmt.out); + // owner pays the transfer fee + auto ownerGives = + mulRatio (ofrAmt.out, trOut, QUALITY_ONE, /*roundUp*/ false); - if (funds < stpAmt.out) - limitStepOut (*ofrQ, ofrAmt, stpAmt, trIn, funds); + auto const funds = + (offer.owner () == offer.issueOut ().account) + ? ownerGives // Offer owner is issuer; they have unlimited funds + : offers.ownerFunds (); - if (!callback (offer, ofrAmt, stpAmt, trIn)) + if (funds < ownerGives) + { + // We already know offer.owner()!=offer.issueOut().account + ownerGives = funds; + stpAmt.out = mulRatio ( + ownerGives, QUALITY_ONE, trOut, /*roundUp*/ false); + ofrAmt = ofrQ->ceil_out (ofrAmt, stpAmt.out); + stpAmt.in = mulRatio ( + ofrAmt.in, trIn, QUALITY_ONE, /*roundUp*/ true); + } + + if (!callback (offer, ofrAmt, stpAmt, ownerGives, trIn, trOut)) break; } @@ -272,7 +318,8 @@ void BookStep::consumeOffer ( PaymentSandbox& sb, TOffer& offer, TAmounts const& ofrAmt, - TAmounts const& stepAmt) const + TAmounts const& stepAmt, + TOut const& ownerGives) const { // The offer owner gets the ofrAmt. The difference between ofrAmt and stepAmt // is a transfer fee that goes to book_.in.account @@ -283,9 +330,11 @@ void BookStep::consumeOffer ( Throw (dr); } + // The offer owner pays `ownerGives`. The difference between ownerGives and + // stepAmt is a transfer fee that goes to book_.out.account { auto const cr = accountSend (sb, offer.owner (), book_.out.account, - toSTAmount (stepAmt.out, book_.out), j_); + toSTAmount (ownerGives, book_.out), j_); if (cr != tesSUCCESS) Throw (cr); } @@ -330,7 +379,9 @@ BookStep::revImp ( [&](TOffer& offer, TAmounts const& ofrAmt, TAmounts const& stpAmt, - std::uint32_t transferRateIn) mutable -> bool + TOut const& ownerGives, + std::uint32_t transferRateIn, + std::uint32_t transferRateOut) mutable -> bool { if (remainingOut <= beast::zero) return false; @@ -341,7 +392,7 @@ BookStep::revImp ( savedOuts.insert(stpAmt.out); result = TAmounts(sum (savedIns), sum(savedOuts)); remainingOut = out - result.out; - this->consumeOffer (sb, offer, ofrAmt, stpAmt); + 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; @@ -350,22 +401,24 @@ BookStep::revImp ( { auto ofrAdjAmt = ofrAmt; auto stpAdjAmt = stpAmt; - limitStepOut ( - offer.quality (), ofrAdjAmt, stpAdjAmt, transferRateIn, remainingOut); + auto ownerGivesAdj = ownerGives; + limitStepOut (offer.quality (), ofrAdjAmt, stpAdjAmt, ownerGivesAdj, + transferRateIn, transferRateOut, remainingOut); remainingOut = beast::zero; savedIns.insert (stpAdjAmt.in); savedOuts.insert (remainingOut); result.in = sum(savedIns); result.out = out; - this->consumeOffer (sb, offer, ofrAdjAmt, stpAdjAmt); + this->consumeOffer (sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj); return false; } }; { - auto const r = forEachOffer ( - sb, afView, book_, - strandSrc_, strandDst_, eachOffer, maxOffersToConsume_, j_); + auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, false); + auto const r = forEachOffer (sb, afView, book_, strandSrc_, + strandDst_, prevStepRedeems, ownerPaysTransferFee_, eachOffer, + maxOffersToConsume_, j_); boost::container::flat_set toRm = std::move(std::get<0>(r)); std::uint32_t const offersConsumed = std::get<1>(r); ofrsToRm.insert (boost::container::ordered_unique_range_t{}, @@ -427,42 +480,93 @@ BookStep::fwdImp ( [&](TOffer& offer, TAmounts const& ofrAmt, TAmounts const& stpAmt, - std::uint32_t transferRateIn) mutable -> bool + TOut const& ownerGives, + std::uint32_t transferRateIn, + std::uint32_t transferRateOut) mutable -> bool { + assert(cache_); + if (remainingIn <= beast::zero) return false; + bool processMore = true; + auto ofrAdjAmt = ofrAmt; + auto stpAdjAmt = stpAmt; + auto ownerGivesAdj = ownerGives; + + typename boost::container::flat_multiset::const_iterator lastOut; if (stpAmt.in <= remainingIn) { savedIns.insert(stpAmt.in); - savedOuts.insert(stpAmt.out); + lastOut = savedOuts.insert(stpAmt.out); result = TAmounts(sum (savedIns), sum(savedOuts)); - remainingIn = in - result.in; - this->consumeOffer (sb, offer, ofrAmt, stpAmt); - // return true b/c even if the payment is satisfied, - // we need to consume the offer - return true; + // consume the offer even if stepAmt.in == remainingIn + processMore = true; } else { - auto ofrAdjAmt = ofrAmt; - auto stpAdjAmt = stpAmt; - limitStepIn ( - offer.quality (), ofrAdjAmt, stpAdjAmt, transferRateIn, remainingIn); + limitStepIn (offer.quality (), ofrAdjAmt, stpAdjAmt, ownerGivesAdj, + transferRateIn, transferRateOut, remainingIn); savedIns.insert (remainingIn); - savedOuts.insert (stpAdjAmt.out); - remainingIn = beast::zero; + lastOut = savedOuts.insert (stpAdjAmt.out); result.out = sum (savedOuts); result.in = in; - this->consumeOffer (sb, offer, ofrAdjAmt, stpAdjAmt); - return false; + + processMore = false; } + + if (result.out > cache_->out && result.in <= cache_->in) + { + // The step produced more output in the forward pass than the + // reverse pass while consuming the same input (or less). If we + // compute the input required to produce the cached output + // (produced in the reverse step) and the input is equal to + // the input consumed in the forward step, then consume the + // input provided in the forward step and produce the output + // requested from the reverse step. + auto const lastOutAmt = *lastOut; + savedOuts.erase(lastOut); + auto const remainingOut = cache_->out - sum (savedOuts); + auto ofrAdjAmtRev = ofrAmt; + auto stpAdjAmtRev = stpAmt; + auto ownerGivesAdjRev = ownerGives; + limitStepOut (offer.quality (), ofrAdjAmtRev, stpAdjAmtRev, + ownerGivesAdjRev, transferRateIn, transferRateOut, + remainingOut); + + if (stpAdjAmtRev.in == remainingIn) + { + result.in = in; + result.out = cache_->out; + + savedIns.clear(); + savedIns.insert(result.in); + savedOuts.clear(); + savedOuts.insert(result.out); + + ofrAdjAmt = ofrAdjAmtRev; + stpAdjAmt.in = remainingIn; + stpAdjAmt.out = remainingOut; + ownerGivesAdj = ownerGivesAdjRev; + } + else + { + // This is (likely) a problem case, and wil be caught + // with later checks + savedOuts.insert (lastOutAmt); + } + } + + remainingIn = in - result.in; + this->consumeOffer (sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj); + return processMore; }; { - auto const r = forEachOffer ( - sb, afView, book_, - strandSrc_, strandDst_, eachOffer, maxOffersToConsume_, j_); + auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, true); + auto const r = forEachOffer (sb, afView, book_, strandSrc_, + strandDst_, prevStepRedeems, ownerPaysTransferFee_, eachOffer, + maxOffersToConsume_, j_); boost::container::flat_set toRm = std::move(std::get<0>(r)); std::uint32_t const offersConsumed = std::get<1>(r); ofrsToRm.insert (boost::container::ordered_unique_range_t{}, @@ -607,7 +711,8 @@ make_BookStepHelper ( Issue const& out) { auto r = std::make_unique> ( - in, out, ctx.strandSrc, ctx.strandDst, ctx.j); + in, out, ctx.strandSrc, ctx.strandDst, ctx.prevStep, + ctx.ownerPaysTransferFee, ctx.j); auto ter = r->check (ctx); if (ter != tesSUCCESS) return {ter, nullptr}; diff --git a/src/ripple/app/paths/impl/DirectStep.cpp b/src/ripple/app/paths/impl/DirectStep.cpp index 9a492a34b7..cf59ae9022 100644 --- a/src/ripple/app/paths/impl/DirectStep.cpp +++ b/src/ripple/app/paths/impl/DirectStep.cpp @@ -39,9 +39,10 @@ class DirectStepI : public StepImp AccountID src_; AccountID dst_; Currency currency_; - // Transfer fees are never charged when sending directly to or from an - // issuing account - bool noTransferFee_; + + // Charge transfer fees when the prev step redeems + Step const* const prevStep_ = nullptr; + beast::Journal j_; struct Cache @@ -49,15 +50,17 @@ class DirectStepI : public StepImp IOUAmount in; IOUAmount srcToDst; IOUAmount out; + bool srcRedeems; - Cache () = default; Cache ( IOUAmount const& in_, IOUAmount const& srcToDst_, - IOUAmount const& out_) + IOUAmount const& out_, + bool srcRedeems_) : in(in_) , srcToDst(srcToDst_) , out(out_) + , srcRedeems(srcRedeems_) {} }; @@ -67,18 +70,19 @@ class DirectStepI : public StepImp std::pair qualities ( PaymentSandbox& sb, - bool srcRedeems) const; + bool srcRedeems, + bool fwd) const; public: DirectStepI ( AccountID const& src, AccountID const& dst, Currency const& c, - bool noTransferFee, + Step const* prevStep, beast::Journal j) :src_(src) , dst_(dst) , currency_ (c) - , noTransferFee_ (noTransferFee) + , prevStep_ (prevStep) , j_ (j) {} AccountID const& src () const @@ -115,6 +119,9 @@ class DirectStepI : public StepImp return src_; } + bool + redeems (ReadView const& sb, bool fwd) const override; + std::pair revImp ( PaymentSandbox& sb, @@ -142,14 +149,14 @@ class DirectStepI : public StepImp void setCacheLimiting ( IOUAmount const& fwdIn, IOUAmount const& fwdSrcToDst, - IOUAmount const& fwdOut); + IOUAmount const& fwdOut, + bool srcRedeems); friend bool operator==(DirectStepI const& lhs, DirectStepI const& rhs) { return lhs.src_ == rhs.src_ && lhs.dst_ == rhs.dst_ && - lhs.currency_ == rhs.currency_ && - lhs.noTransferFee_ == rhs.noTransferFee_; + lhs.currency_ == rhs.currency_; } friend bool operator!=(DirectStepI const& lhs, DirectStepI const& rhs) @@ -199,6 +206,25 @@ maxFlow ( return {creditLimit2 (sb, dst, src, cur) + srcOwed, false}; } +bool +DirectStepI::redeems (ReadView const& sb, bool fwd) const +{ + if (!fwd) + { + auto const srcOwed = creditBalance2 (sb, dst_, src_, currency_); + return srcOwed.signum () > 0; + } + else + { + if (!cache_) + { + assert (0); + return false; + } + return cache_->srcRedeems; + } +} + std::pair DirectStepI::revImp ( PaymentSandbox& sb, @@ -214,7 +240,7 @@ DirectStepI::revImp ( maxFlow (sb, src_, dst_, currency_); std::uint32_t srcQOut, dstQIn; - std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems); + std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems, false); Issue const srcToDstIss (currency_, srcRedeems ? dst_ : src_); @@ -232,7 +258,8 @@ DirectStepI::revImp ( cache_.emplace ( IOUAmount (beast::zero), IOUAmount (beast::zero), - IOUAmount (beast::zero)); + IOUAmount (beast::zero), + srcRedeems); return {beast::zero, beast::zero}; } @@ -244,7 +271,7 @@ DirectStepI::revImp ( IOUAmount const in = mulRatio ( srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); - cache_.emplace (in, srcToDst, out); + cache_.emplace (in, srcToDst, out, srcRedeems); rippleCredit (sb, src_, dst_, toSTAmount (srcToDst, srcToDstIss), /*checkIssuer*/ true, j_); @@ -262,7 +289,7 @@ DirectStepI::revImp ( maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); IOUAmount const actualOut = mulRatio ( maxSrcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); - cache_.emplace (in, maxSrcToDst, actualOut); + cache_.emplace (in, maxSrcToDst, actualOut, srcRedeems); rippleCredit (sb, src_, dst_, toSTAmount (maxSrcToDst, srcToDstIss), /*checkIssuer*/ true, j_); @@ -276,14 +303,15 @@ DirectStepI::revImp ( } // The forward pass should never have more liquidity than the reverse -// pass. But sometime rounding differences cause the forward pass to +// pass. But sometimes rounding differences cause the forward pass to // deliver more liquidity. Use the cached values from the reverse pass // to prevent this. void DirectStepI::setCacheLimiting ( IOUAmount const& fwdIn, IOUAmount const& fwdSrcToDst, - IOUAmount const& fwdOut) + IOUAmount const& fwdOut, + bool srcRedeems) { if (cache_->in < fwdIn) { @@ -305,7 +333,7 @@ DirectStepI::setCacheLimiting ( << " cacheSrcToDst: " << to_string (cache_->srcToDst) << " fwdOut: " << to_string (fwdOut) << " cacheOut: " << to_string (cache_->out); - cache_.emplace (fwdIn, fwdSrcToDst, fwdOut); + cache_.emplace (fwdIn, fwdSrcToDst, fwdOut, srcRedeems); return; } } @@ -315,6 +343,7 @@ DirectStepI::setCacheLimiting ( cache_->srcToDst = fwdSrcToDst; if (fwdOut < cache_->out) cache_->out = fwdOut; + cache_->srcRedeems = srcRedeems; }; std::pair @@ -332,7 +361,7 @@ DirectStepI::fwdImp ( maxFlow (sb, src_, dst_, currency_); std::uint32_t srcQOut, dstQIn; - std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems); + std::tie (srcQOut, dstQIn) = qualities (sb, srcRedeems, true); Issue const srcToDstIss (currency_, srcRedeems ? dst_ : src_); @@ -350,7 +379,8 @@ DirectStepI::fwdImp ( cache_.emplace ( IOUAmount (beast::zero), IOUAmount (beast::zero), - IOUAmount (beast::zero)); + IOUAmount (beast::zero), + srcRedeems); return {beast::zero, beast::zero}; } @@ -361,7 +391,7 @@ DirectStepI::fwdImp ( { IOUAmount const out = mulRatio ( srcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); - setCacheLimiting (in, srcToDst, out); + setCacheLimiting (in, srcToDst, out, srcRedeems); rippleCredit (sb, src_, dst_, toSTAmount (cache_->srcToDst, srcToDstIss), /*checkIssuer*/ true, j_); @@ -379,7 +409,7 @@ DirectStepI::fwdImp ( maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true); IOUAmount const out = mulRatio ( maxSrcToDst, dstQIn, QUALITY_ONE, /*roundUp*/ false); - setCacheLimiting (actualIn, maxSrcToDst, out); + setCacheLimiting (actualIn, maxSrcToDst, out, srcRedeems); rippleCredit (sb, src_, dst_, toSTAmount (cache_->srcToDst, srcToDstIss), /*checkIssuer*/ true, j_); @@ -499,7 +529,8 @@ quality ( std::pair DirectStepI::qualities ( PaymentSandbox& sb, - bool srcRedeems) const + bool srcRedeems, + bool fwd) const { if (srcRedeems) { @@ -511,9 +542,10 @@ DirectStepI::qualities ( } else { - // Charge a transfer rate when issuing, unless this is the first step. + // Charge a transfer rate when issuing and previous step redeems + auto const prevStepRedeems = prevStep_ && prevStep_->redeems (sb, fwd); std::uint32_t const srcQOut = - noTransferFee_ ? QUALITY_ONE : rippleTransferRate (sb, src_); + prevStepRedeems ? rippleTransferRate (sb, src_) : QUALITY_ONE; return std::make_pair( srcQOut, quality ( // dst quality in @@ -655,8 +687,9 @@ make_DirectStepI ( AccountID const& dst, Currency const& c) { + // Only charge a transfer fee if the previous step redeems auto r = std::make_unique ( - src, dst, c, /* noTransferFee */ ctx.isFirst, ctx.j); + src, dst, c, ctx.prevStep, ctx.j); auto ter = r->check (ctx); if (ter != tesSUCCESS) return {ter, nullptr}; diff --git a/src/ripple/app/paths/impl/PaySteps.cpp b/src/ripple/app/paths/impl/PaySteps.cpp index 374dec4ed7..e400c7462b 100644 --- a/src/ripple/app/paths/impl/PaySteps.cpp +++ b/src/ripple/app/paths/impl/PaySteps.cpp @@ -139,6 +139,7 @@ toStrand ( Issue const& deliver, boost::optional const& sendMaxIssue, STPath const& path, + bool ownerPaysTransferFee, beast::Journal j) { if (isXRP (src)) @@ -233,7 +234,7 @@ toStrand ( auto ctx = [&](bool isLast = false) { return StrandContext{view, result, strandSrc, strandDst, isLast, - seenDirectIssues, seenBookOuts, j}; + ownerPaysTransferFee, seenDirectIssues, seenBookOuts, j}; }; for (int i = 0; i < pes.size () - 1; ++i) @@ -359,7 +360,7 @@ toStrand ( result.emplace_back (std::move (s.second)); else { - JLOG (j.warn()) << "toStep failed"; + JLOG (j.debug()) << "toStep failed: " << s.first; return {s.first, Strand{}}; } } @@ -376,6 +377,7 @@ toStrands ( boost::optional const& sendMax, STPathSet const& paths, bool addDefaultPath, + bool ownerPaysTransferFee, beast::Journal j) { std::vector result; @@ -391,7 +393,8 @@ toStrands ( if (addDefaultPath) { - auto sp = toStrand (view, src, dst, deliver, sendMax, STPath(), j); + auto sp = toStrand ( + view, src, dst, deliver, sendMax, STPath (), ownerPaysTransferFee, j); auto const ter = sp.first; auto& strand = sp.second; @@ -407,7 +410,10 @@ toStrands ( JLOG (j.trace()) << "toStrand failed"; Throw (tefEXCEPTION, "toStrand returned tes & empty strand"); } - insert(std::move(strand)); + else + { + insert(std::move(strand)); + } } else if (paths.empty ()) { @@ -419,7 +425,8 @@ toStrands ( TER lastFailTer = tesSUCCESS; for (auto const& p : paths) { - auto sp = toStrand (view, src, dst, deliver, sendMax, p, j); + auto sp = toStrand ( + view, src, dst, deliver, sendMax, p, ownerPaysTransferFee, j); auto ter = sp.first; auto& strand = sp.second; @@ -436,9 +443,10 @@ toStrands ( JLOG (j.trace()) << "toStrand failed"; Throw (tefEXCEPTION, "toStrand returned tes & empty strand"); } - - if (ter == tesSUCCESS) + else + { insert(std::move(strand)); + } } if (result.empty ()) @@ -455,6 +463,7 @@ StrandContext::StrandContext ( AccountID strandSrc_, AccountID strandDst_, bool isLast_, + bool ownerPaysTransferFee_, std::array, 2>& seenDirectIssues_, boost::container::flat_set& seenBookOuts_, beast::Journal j_) @@ -463,6 +472,7 @@ StrandContext::StrandContext ( , strandDst (strandDst_) , isFirst (strand_.empty ()) , isLast (isLast_) + , ownerPaysTransferFee (ownerPaysTransferFee_) , strandSize (strand_.size ()) , prevStep (!strand_.empty () ? strand_.back ().get () : nullptr) diff --git a/src/ripple/app/paths/impl/Steps.h b/src/ripple/app/paths/impl/Steps.h index 27c8175983..7bff5a9462 100644 --- a/src/ripple/app/paths/impl/Steps.h +++ b/src/ripple/app/paths/impl/Steps.h @@ -120,6 +120,18 @@ public: return boost::none; } + /** + If this step is a DirectStepI and the src redeems to the dst, return true, + otherwise return false. + If this step is a BookStep, return false if the owner pays the transfer fee, + otherwise return true. + */ + virtual bool + redeems (ReadView const& sb, bool fwd) const + { + return false; + } + /** If this step is a BookStep, return the book. */ @@ -226,6 +238,7 @@ toStrand ( Issue const& deliver, boost::optional const& sendMaxIssue, STPath const& path, + bool ownerPaysTransferFee, beast::Journal j); /** @@ -253,6 +266,7 @@ toStrands (ReadView const& sb, boost::optional const& sendMax, STPathSet const& paths, bool addDefaultPath, + bool ownerPaysTransferFee, beast::Journal j); template @@ -338,6 +352,7 @@ struct StrandContext AccountID const strandDst; bool const isFirst; bool const isLast = false; + bool ownerPaysTransferFee; size_t const strandSize; // The previous step in the strand. Needed to check the no ripple constraint Step const* const prevStep = nullptr; @@ -357,6 +372,7 @@ struct StrandContext AccountID strandSrc_, AccountID strandDst_, bool isLast_, + bool ownerPaysTransferFee_, std::array, 2>& seenDirectIssues_, boost::container::flat_set& seenBookOuts_, beast::Journal j); diff --git a/src/ripple/app/paths/impl/StrandFlow.h b/src/ripple/app/paths/impl/StrandFlow.h index fbdc1a5b62..9b98a28b10 100644 --- a/src/ripple/app/paths/impl/StrandFlow.h +++ b/src/ripple/app/paths/impl/StrandFlow.h @@ -138,7 +138,7 @@ flow ( limitStepOut = r.second; if (strand[i]->dry (r.second) || - get (r.first) != get (*maxIn)) + get (r.first) != *maxIn) { // Something is very wrong // throwing out the sandbox can only increase liquidity diff --git a/src/ripple/app/tests/Flow_test.cpp b/src/ripple/app/tests/Flow_test.cpp index 0da3a81d2c..49887ce7cf 100644 --- a/src/ripple/app/tests/Flow_test.cpp +++ b/src/ripple/app/tests/Flow_test.cpp @@ -182,7 +182,7 @@ struct Flow_test : public beast::unit_test::suite TER expTer, auto&&... expSteps) { auto r = toStrand (*env.current (), alice, bob, - deliver, sendMaxIssue, path, env.app ().logs ().journal ("Flow")); + deliver, sendMaxIssue, path, true, env.app ().logs ().journal ("Flow")); expect (r.first == expTer); if (sizeof...(expSteps)) expect (equal ( @@ -190,7 +190,7 @@ struct Flow_test : public beast::unit_test::suite }; { - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); test (env, USD, boost::none, STPath(), terNO_LINE); @@ -236,20 +236,20 @@ struct Flow_test : public beast::unit_test::suite { // The root account can't be the dst auto r = toStrand (*env.current (), alice, - xrpAccount (), XRP, USD.issue (), STPath (), flowJournal); + xrpAccount (), XRP, USD.issue (), STPath (), true, flowJournal); expect (r.first == temBAD_PATH); } { // The root account can't be the src auto r = toStrand (*env.current (), xrpAccount (), - alice, XRP, boost::none, STPath (), flowJournal); + alice, XRP, boost::none, STPath (), true, flowJournal); expect (r.first == temBAD_PATH); } { // The root account can't be the src auto r = toStrand (*env.current (), - noAccount (), bob, USD, boost::none, STPath (), flowJournal); + noAccount (), bob, USD, boost::none, STPath (), true, flowJournal); expect (r.first == terNO_ACCOUNT); } } @@ -279,7 +279,7 @@ struct Flow_test : public beast::unit_test::suite // cannot have more than one offer with the same output issue using namespace jtx; - Env env (*this, features (featureFlowV2)); + Env env (*this, features (featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (10000), alice, bob, carol); @@ -299,7 +299,7 @@ struct Flow_test : public beast::unit_test::suite } { - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, noripple (gw)); env.trust (USD (1000), alice, bob); env (pay (gw, alice, USD (100))); @@ -308,7 +308,7 @@ struct Flow_test : public beast::unit_test::suite { // check global freeze - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, gw); env.trust (USD (1000), alice, bob); env (pay (gw, alice, USD (100))); @@ -333,7 +333,7 @@ struct Flow_test : public beast::unit_test::suite } { // Freeze between gw and alice - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, gw); env.trust (USD (1000), alice, bob); env (pay (gw, alice, USD (100))); @@ -346,7 +346,7 @@ struct Flow_test : public beast::unit_test::suite // check no auth // An account may require authorization to receive IOUs from an // issuer - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, gw); env (fset (gw, asfRequireAuth)); env.trust (USD (1000), alice, bob); @@ -359,7 +359,7 @@ struct Flow_test : public beast::unit_test::suite // Check pure issue redeem still works auto r = toStrand (*env.current (), alice, gw, USD, - boost::none, STPath (), env.app ().logs ().journal ("Flow")); + boost::none, STPath (), true, env.app ().logs ().journal ("Flow")); expect (r.first == tesSUCCESS); expect (equal (r.second, D{alice, gw, usdC})); } @@ -382,7 +382,7 @@ struct Flow_test : public beast::unit_test::suite auto const USD = gw["USD"]; { // Pay USD, trivial path - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, gw); env.trust (USD (1000), alice, bob); @@ -392,7 +392,7 @@ struct Flow_test : public beast::unit_test::suite } { // XRP transfer - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob); env (pay (alice, bob, XRP (100))); @@ -401,7 +401,7 @@ struct Flow_test : public beast::unit_test::suite } { // Partial payments - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, gw); env.trust (USD (1000), alice, bob); @@ -415,7 +415,7 @@ struct Flow_test : public beast::unit_test::suite } { // Pay by rippling through accounts, use path finder - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, dan); env.trust (USDA (10), bob); @@ -430,32 +430,52 @@ struct Flow_test : public beast::unit_test::suite { // Pay by rippling through accounts, specify path // and charge a transfer fee - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, dan); env.trust (USDA (10), bob); - env.trust (USDB (10), carol); + env.trust (USDB (10), alice, carol); + env.trust (USDC (10), dan); + env (rate (bob, 1.1)); + + // alice will redeem to bob; a transfer fee will be charged + env (pay (bob, alice, USDB(6))); + env (pay (alice, dan, USDC (5)), path (bob, carol), + sendmax (USDA (6)), txflags (tfNoRippleDirect)); + env.require (balance (dan, USDC (5))); + env.require (balance (alice, USDB (0.5))); + } + { + // Pay by rippling through accounts, specify path and transfer fee + // Test that the transfer fee is not charged when alice issues + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); + + env.fund (XRP (10000), alice, bob, carol, dan); + env.trust (USDA (10), bob); + env.trust (USDB (10), alice, carol); env.trust (USDC (10), dan); env (rate (bob, 1.1)); env (pay (alice, dan, USDC (5)), path (bob, carol), - sendmax (USDA (6)), txflags (tfNoRippleDirect)); + sendmax (USDA (6)), txflags (tfNoRippleDirect)); env.require (balance (dan, USDC (5))); - env.require (balance (bob, USDA (5.5))); + env.require (balance (bob, USDA (5))); } { // test best quality path is taken // Paths: A->B->D->E ; A->C->D->E - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, dan, erin); env.trust (USDA (10), bob, carol); env.trust (USDB (10), dan); - env.trust (USDC (10), dan); + env.trust (USDC (10), alice, dan); env.trust (USDD (20), erin); env (rate (bob, 1)); env (rate (carol, 1.1)); + // Pay alice so she redeems to carol and a transfer fee is charged + env (pay (carol, alice, USDC(10))); env (pay (alice, erin, USDD (5)), path (carol, dan), path (bob, dan), txflags (tfNoRippleDirect)); @@ -465,7 +485,7 @@ struct Flow_test : public beast::unit_test::suite } { // Limit quality - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol); env.trust (USDA (10), bob); @@ -497,7 +517,7 @@ struct Flow_test : public beast::unit_test::suite { // simple IOU/IOU offer - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -518,7 +538,7 @@ struct Flow_test : public beast::unit_test::suite } { // simple IOU/XRP XRP/IOU offer - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -542,7 +562,7 @@ struct Flow_test : public beast::unit_test::suite } { // simple XRP -> USD through offer and sendmax - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -563,7 +583,7 @@ struct Flow_test : public beast::unit_test::suite } { // simple USD -> XRP through offer and sendmax - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -584,7 +604,7 @@ struct Flow_test : public beast::unit_test::suite } { // test unfunded offers are removed when payment succeeds - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -630,7 +650,7 @@ struct Flow_test : public beast::unit_test::suite // offer. When the payment fails `flow` should return the unfunded // offer. This test is intentionally similar to the one that removes // unfunded offers when the payment succeeds. - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env.trust (USD (1000), alice, bob, carol); @@ -666,7 +686,7 @@ struct Flow_test : public beast::unit_test::suite paths.push_back (p2); } - return flow (sb, deliver, alice, carol, paths, false, false, + return flow (sb, deliver, alice, carol, paths, false, false, true, boost::none, smax, flowJournal); }(); @@ -689,6 +709,38 @@ struct Flow_test : public beast::unit_test::suite // found unfunded expect (!isOffer (env, bob, BTC (60), EUR (50))); } + { + // Do not produce more in the forward pass than the reverse pass + // This test uses a path that whose reverse pass will compute a + // 0.5 USD input required for a 1 EUR output. It sets a sendmax of + // 0.4 USD, so the payment engine will need to do a forward pass. + // Without limits, the 0.4 USD would produce 1000 EUR in the forward + // pass. This test checks that the payment produces 1 EUR, as expected. + + Env env (*this, features (featureFlowV2), + features (featureOwnerPaysFee)); + + auto const closeTime = STAmountSO::soTime2 + + 100 * env.closed ()->info ().closeTimeResolution; + env.close (closeTime); + + env.fund (XRP (10000), alice, bob, carol, gw); + env.trust (USD (1000), alice, bob, carol); + env.trust (EUR (1000), alice, bob, carol); + + env (pay (gw, alice, USD (1000))); + env (pay (gw, bob, EUR (1000))); + + env (offer (bob, USD (1), drops (2)), txflags (tfPassive)); + env (offer (bob, drops (1), EUR (1000)), txflags (tfPassive)); + + env (pay (alice, carol, EUR (1)), path (~XRP, ~EUR), + sendmax (USD (0.4)), txflags (tfNoRippleDirect|tfPartialPayment)); + + env.require (balance (carol, EUR (1))); + env.require (balance (bob, USD (0.4))); + env.require (balance (bob, EUR (999))); + } } void testTransferRate () @@ -709,7 +761,7 @@ struct Flow_test : public beast::unit_test::suite { // Simple payment through a gateway with a // transfer rate - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env(rate(gw, 1.25)); @@ -721,7 +773,7 @@ struct Flow_test : public beast::unit_test::suite } { // transfer rate is not charged when issuer is src or dst - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env(rate(gw, 1.25)); @@ -733,25 +785,25 @@ struct Flow_test : public beast::unit_test::suite } { // transfer fee on an offer - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env(rate(gw, 1.25)); env.trust (USD (1000), alice, bob, carol); - env (pay (gw, bob, USD (50))); + env (pay (gw, bob, USD (65))); env (offer (bob, XRP (50), USD (50))); - env (pay (alice, carol, USD (40)), path (~USD), sendmax (XRP (50))); + env (pay (alice, carol, USD (50)), path (~USD), sendmax (XRP (50))); env.require ( balance (alice, xrpMinusFee (env, 10000 - 50)), - balance (bob, USD (0)), - balance (carol, USD (40))); + balance (bob, USD (2.5)), // owner pays transfer fee + balance (carol, USD (50))); } { // Transfer fee two consecutive offers - Env env (*this, features(featureFlowV2)); + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); env.fund (XRP (10000), alice, bob, carol, gw); env(rate(gw, 1.25)); @@ -763,12 +815,89 @@ struct Flow_test : public beast::unit_test::suite env (offer (bob, XRP (50), USD (50))); env (offer (bob, USD (50), EUR (50))); - env (pay (alice, carol, EUR (32)), path (~USD, ~EUR), sendmax (XRP (50))); + env (pay (alice, carol, EUR (40)), path (~USD, ~EUR), sendmax (XRP (40))); env.require ( - balance (alice, xrpMinusFee (env, 10000 - 50)), + balance (alice, xrpMinusFee (env, 10000 - 40)), balance (bob, USD (40)), - balance (bob, EUR (50 - 40)), - balance (carol, EUR (32))); + balance (bob, EUR (0)), + balance (carol, EUR (40))); + } + + { + // First pass through a strand redeems, second pass issues, no offers + // limiting step is not an endpoint + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); + auto const USDA = alice["USD"]; + auto const USDB = bob["USD"]; + + env.fund (XRP (10000), alice, bob, carol, gw); + env(rate(gw, 1.25)); + env.trust (USD (1000), alice, bob, carol); + env.trust (USDA (1000), bob); + env.trust (USDB (1000), gw); + env (pay (gw, bob, USD (50))); + // alice -> bob -> gw -> carol. $50 should have transfer fee; $10, no fee + env (pay (alice, carol, USD(50)), path (bob), sendmax (USDA(60))); + env.require ( + balance (bob, USD (-10)), + balance (bob, USDA (60)), + balance (carol, USD (50))); + } + { + // First pass through a strand redeems, second pass issues, through an offer + // limiting step is not an endpoint + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); + auto const USDA = alice["USD"]; + auto const USDB = bob["USD"]; + Account const dan ("dan"); + + env.fund (XRP (10000), alice, bob, carol, dan, gw); + env(rate(gw, 1.25)); + env.trust (USD (1000), alice, bob, carol, dan); + env.trust (EUR (1000), carol, dan); + env.trust (USDA (1000), bob); + env.trust (USDB (1000), gw); + env (pay (gw, bob, USD (50))); + env (pay (gw, dan, EUR (100))); + env (offer (dan, USD (100), EUR (100))); + // alice -> bob -> gw -> carol. $50 should have transfer fee; $10, no fee + env (pay (alice, carol, EUR (50)), path (bob, gw, ~EUR), + sendmax (USDA (60)), txflags (tfNoRippleDirect)); + env.require ( + balance (bob, USD (-10)), + balance (bob, USDA (60)), + balance (dan, USD (50)), + balance (dan, EUR (37.5)), + balance (carol, EUR (50))); + } + + { + // Offer where the owner is also the issuer, owner pays fee + Env env (*this, features(featureFlowV2), features(featureOwnerPaysFee)); + + env.fund (XRP (10000), alice, bob, gw); + env(rate(gw, 1.25)); + env.trust (USD (1000), alice, bob); + env (offer (gw, XRP (100), USD (100))); + env (pay (alice, bob, USD (100)), + sendmax (XRP (100))); + env.require ( + balance (alice, xrpMinusFee(env, 10000-100)), + balance (bob, USD (100))); + } + { + // Offer where the owner is also the issuer, sender pays fee + Env env (*this, features(featureFlowV2)); + + env.fund (XRP (10000), alice, bob, gw); + env(rate(gw, 1.25)); + env.trust (USD (1000), alice, bob); + env (offer (gw, XRP (125), USD (125))); + env (pay (alice, bob, USD (100)), + sendmax (XRP (200))); + env.require ( + balance (alice, xrpMinusFee(env, 10000-125)), + balance (bob, USD (100))); } } diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index c5621d3c0b..d56faf4864 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -34,8 +34,8 @@ namespace ripple { NetClock::time_point const& flowV2SoTime () { using namespace std::chrono_literals; - // Mon March 28, 2016 10:00:00am PST - static NetClock::time_point const soTime{512503200s}; + // Wed May 25, 2016 10:00:00am PDT + static NetClock::time_point const soTime{517510800s}; return soTime; } diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 086851b2f8..7acd6daac8 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -40,6 +40,7 @@ extern uint256 const featureSusPay; extern uint256 const featureTrustSetAuth; extern uint256 const featureFeeEscalation; extern uint256 const featureFlowV2; +extern uint256 const featureOwnerPaysFee; } // ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 0590a90257..dfe15456ba 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -51,5 +51,6 @@ uint256 const featureSusPay = feature("SusPay"); uint256 const featureTrustSetAuth = feature("TrustSetAuth"); uint256 const featureFeeEscalation = feature("FeeEscalation"); uint256 const featureFlowV2 = feature("FlowV2"); +uint256 const featureOwnerPaysFee = feature("OwnerPaysFee"); } // ripple