-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fixFillOrKill
: fix OfferCreate with tfFillOrKill if offer is better than open offer rate (Amendment)
#4694
fixFillOrKill
: fix OfferCreate with tfFillOrKill if offer is better than open offer rate (Amendment)
#4694
Changes from 2 commits
7e5b31d
a98ebed
c7c6e96
3859760
2834d46
7a7c9cc
404b1b4
0a1aad8
41b5c61
7040e9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -817,6 +817,35 @@ flow( | |
JLOG(j.trace()) << "Total flow: in: " << to_string(actualIn) | ||
<< " out: " << to_string(actualOut); | ||
|
||
/* flowCross doesn't handle offer crossing with tfFillOrKill flag correctly. | ||
* 1. If tfFillOrKill is set then the owner must receive the full | ||
* TakerPays. We reverse pays and gets because during crossing | ||
* we are taking, therefore the owner must deliver the full TakerPays and | ||
* the entire TakerGets doesn't have to be spent. | ||
* Pre-fixFillOrKill amendment code fails if the entire TakerGets | ||
* is not spent. fixFillOrKill addresses this issue. | ||
* 2. If tfSell is also set then the owner must spend the entire TakerGets | ||
* even if it means obtaining more than TakerPays. Since the pays and gets | ||
* are reversed, the owner must send the entire TakerGets. | ||
*/ | ||
bool const fixFillOrKill = | ||
baseView.rules().enabled(fixFillOrKillOnFlowCross); | ||
// tfSell is handled by setting the deliver amount to max | ||
auto const sell = [&]() -> bool { | ||
if constexpr (std::is_same_v<TOutAmt, XRPAmount>) | ||
{ | ||
static auto max = XRPAmount{STAmount::cMaxNative}; | ||
return outReq == max; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can talk about this, but I don't love this solution. It's using a special value of output request to change behavior. Any payment that uses this value will get this special behavior, even payments that aren't offers and don't have the flag set. It also means if the implementation changes so we no long set these max amounts this code breaks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't argue that this is a great solution, but that's a problem with the FlowCross implementation, not with this fix. @gregtatcam is using the only indication that FlowCross sends down through the payment engine that the But, yes, there is also an
That's still not great, but it makes sure that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @scottschurr, it is better but as you said not great. I wanted to minimize the payment engine impact and not change the arguments to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks good. Thanks for making that change, I appreciate it. |
||
} | ||
else if constexpr (std::is_same_v<TOutAmt, IOUAmount>) | ||
{ | ||
static auto max = | ||
IOUAmount{STAmount::cMaxValue / 2, STAmount::cMaxOffset}; | ||
return outReq == max; | ||
} | ||
return false; | ||
}(); | ||
|
||
if (actualOut != outReq) | ||
{ | ||
if (actualOut > outReq) | ||
|
@@ -827,8 +856,13 @@ flow( | |
if (!partialPayment) | ||
{ | ||
// If we're offerCrossing a !partialPayment, then we're | ||
// handling tfFillOrKill. That case is handled below; not here. | ||
if (!offerCrossing) | ||
// handling tfFillOrKill. | ||
// Pre-fixFillOrKill amendment: | ||
// That case is handled below; not here. | ||
// fixFillOrKill amendment: | ||
// That case is handled here if tfSell is also not set; i.e, | ||
// case 1. | ||
if (!offerCrossing || (fixFillOrKill && !sell)) | ||
return { | ||
tecPATH_PARTIAL, | ||
actualIn, | ||
|
@@ -840,11 +874,15 @@ flow( | |
return {tecPATH_DRY, std::move(ofrsToRmOnFail)}; | ||
} | ||
} | ||
if (offerCrossing && !partialPayment) | ||
if (offerCrossing && (!partialPayment && (!fixFillOrKill || sell))) | ||
{ | ||
// If we're offer crossing and partialPayment is *not* true, then | ||
// we're handling a FillOrKill offer. In this case remainingIn must | ||
// be zero (all funds must be consumed) or else we kill the offer. | ||
// Pre-fixFillOrKill amendment: | ||
// Handles both cases 1. and 2. | ||
// fixFillOrKill amendment: | ||
// Handles 2. 1. is handled above and falls through for tfSell. | ||
assert(remainingIn); | ||
if (remainingIn && *remainingIn != beast::zero) | ||
return { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,7 +74,7 @@ namespace detail { | |
// Feature.cpp. Because it's only used to reserve storage, and determine how | ||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than | ||
// the actual number of amendments. A LogicError on startup will verify this. | ||
static constexpr std::size_t numFeatures = 61; | ||
static constexpr std::size_t numFeatures = 62; | ||
|
||
/** Amendments that this server supports and the default voting behavior. | ||
Whether they are enabled depends on the Rules defined in the validated | ||
|
@@ -348,6 +348,7 @@ extern uint256 const fixNonFungibleTokensV1_2; | |
extern uint256 const fixNFTokenRemint; | ||
extern uint256 const fixReducedOffersV1; | ||
extern uint256 const featureClawback; | ||
extern uint256 const fixFillOrKillOnFlowCross; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I probably wouldn't mention "FlowCross". Just |
||
|
||
} // namespace ripple | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5081,6 +5081,196 @@ class Offer_test : public beast::unit_test::suite | |
pass(); | ||
} | ||
|
||
void | ||
testFillOrKill(FeatureBitset features) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks very much for adding this test. I especially like the way it demonstrates identical behavior before flowCross and after flowCross but with your fix. Nicely done! |
||
{ | ||
testcase("fixFillOrKillOnFlowCross"); | ||
using namespace jtx; | ||
Env env(*this, features); | ||
Account const issuer("issuer"); | ||
Account const maker("maker"); | ||
Account const taker("taker"); | ||
auto const USD = issuer["USD"]; | ||
auto const EUR = issuer["EUR"]; | ||
|
||
env.fund(XRP(1'000), issuer); | ||
env.fund(XRP(1'000), maker, taker); | ||
env.close(); | ||
|
||
env.trust(USD(1'000), maker, taker); | ||
env.trust(EUR(1'000), maker, taker); | ||
scottschurr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
env.close(); | ||
|
||
env(pay(issuer, maker, USD(1'000))); | ||
env(pay(issuer, taker, USD(1'000))); | ||
env(pay(issuer, maker, EUR(1'000))); | ||
env.close(); | ||
|
||
auto makerUSDBalance = env.balance(maker, USD).value(); | ||
auto takerUSDBalance = env.balance(taker, USD).value(); | ||
auto makerEURBalance = env.balance(maker, EUR).value(); | ||
auto takerEURBalance = env.balance(taker, EUR).value(); | ||
auto makerXRPBalance = env.balance(maker, XRP).value(); | ||
auto takerXRPBalance = env.balance(taker, XRP).value(); | ||
|
||
// tfFillOrKill, TakerPays must be filled | ||
{ | ||
TER const err = features[fixFillOrKillOnFlowCross] || | ||
!features[featureFlowCross] | ||
? TER(tesSUCCESS) | ||
: tecKILLED; | ||
|
||
env(offer(maker, XRP(100), USD(100))); | ||
env.close(); | ||
|
||
env(offer(taker, USD(100), XRP(101)), | ||
txflags(tfFillOrKill), | ||
ter(err)); | ||
env.close(); | ||
|
||
makerXRPBalance -= txfee(env, 1); | ||
takerXRPBalance -= txfee(env, 1); | ||
if (err == tesSUCCESS) | ||
{ | ||
makerUSDBalance -= USD(100); | ||
takerUSDBalance += USD(100); | ||
makerXRPBalance += XRP(100).value(); | ||
takerXRPBalance -= XRP(100).value(); | ||
} | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
|
||
env(offer(maker, USD(100), XRP(100))); | ||
env.close(); | ||
|
||
env(offer(taker, XRP(100), USD(101)), | ||
txflags(tfFillOrKill), | ||
ter(err)); | ||
env.close(); | ||
|
||
makerXRPBalance -= txfee(env, 1); | ||
takerXRPBalance -= txfee(env, 1); | ||
if (err == tesSUCCESS) | ||
{ | ||
makerUSDBalance += USD(100); | ||
takerUSDBalance -= USD(100); | ||
makerXRPBalance -= XRP(100).value(); | ||
takerXRPBalance += XRP(100).value(); | ||
} | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
|
||
env(offer(maker, USD(100), EUR(100))); | ||
env.close(); | ||
|
||
env(offer(taker, EUR(100), USD(101)), | ||
txflags(tfFillOrKill), | ||
ter(err)); | ||
env.close(); | ||
|
||
makerXRPBalance -= txfee(env, 1); | ||
takerXRPBalance -= txfee(env, 1); | ||
if (err == tesSUCCESS) | ||
{ | ||
makerUSDBalance += USD(100); | ||
takerUSDBalance -= USD(100); | ||
makerEURBalance -= EUR(100); | ||
takerEURBalance += EUR(100); | ||
} | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
} | ||
|
||
// tfFillOrKill + tfSell, TakerGets must be filled | ||
{ | ||
env(offer(maker, XRP(101), USD(101))); | ||
env.close(); | ||
|
||
env(offer(taker, USD(100), XRP(101)), | ||
txflags(tfFillOrKill | tfSell)); | ||
env.close(); | ||
|
||
makerUSDBalance -= USD(101); | ||
takerUSDBalance += USD(101); | ||
makerXRPBalance += XRP(101).value() - txfee(env, 1); | ||
takerXRPBalance -= XRP(101).value() + txfee(env, 1); | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
|
||
env(offer(maker, USD(101), XRP(101))); | ||
env.close(); | ||
|
||
env(offer(taker, XRP(100), USD(101)), | ||
txflags(tfFillOrKill | tfSell)); | ||
env.close(); | ||
|
||
makerUSDBalance += USD(101); | ||
takerUSDBalance -= USD(101); | ||
makerXRPBalance -= XRP(101).value() + txfee(env, 1); | ||
takerXRPBalance += XRP(101).value() - txfee(env, 1); | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
|
||
env(offer(maker, USD(101), EUR(101))); | ||
env.close(); | ||
|
||
env(offer(taker, EUR(100), USD(101)), | ||
txflags(tfFillOrKill | tfSell)); | ||
env.close(); | ||
|
||
makerUSDBalance += USD(101); | ||
takerUSDBalance -= USD(101); | ||
makerEURBalance -= EUR(101); | ||
takerEURBalance += EUR(101); | ||
makerXRPBalance -= txfee(env, 1); | ||
takerXRPBalance -= txfee(env, 1); | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
} | ||
|
||
// Fail regardless of fixFillOrKillOnFlowCross amendment | ||
for (auto const flags : {tfFillOrKill, tfFillOrKill + tfSell}) | ||
{ | ||
env(offer(maker, XRP(100), USD(100))); | ||
env.close(); | ||
|
||
env(offer(taker, USD(100), XRP(99)), | ||
txflags(flags), | ||
ter(tecKILLED)); | ||
env.close(); | ||
|
||
makerXRPBalance -= txfee(env, 1); | ||
takerXRPBalance -= txfee(env, 1); | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
|
||
env(offer(maker, USD(100), XRP(100))); | ||
env.close(); | ||
|
||
env(offer(taker, XRP(100), USD(99)), | ||
txflags(flags), | ||
ter(tecKILLED)); | ||
env.close(); | ||
|
||
makerXRPBalance -= txfee(env, 1); | ||
takerXRPBalance -= txfee(env, 1); | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
|
||
env(offer(maker, USD(100), EUR(100))); | ||
env.close(); | ||
|
||
env(offer(taker, EUR(100), USD(99)), | ||
txflags(flags), | ||
ter(tecKILLED)); | ||
env.close(); | ||
|
||
makerXRPBalance -= txfee(env, 1); | ||
takerXRPBalance -= txfee(env, 1); | ||
BEAST_EXPECT(expectOffers(env, taker, 0)); | ||
} | ||
|
||
BEAST_EXPECT( | ||
env.balance(maker, USD) == makerUSDBalance && | ||
env.balance(taker, USD) == takerUSDBalance && | ||
env.balance(maker, EUR) == makerEURBalance && | ||
env.balance(taker, EUR) == takerEURBalance && | ||
env.balance(maker, XRP) == makerXRPBalance && | ||
env.balance(taker, XRP) == takerXRPBalance); | ||
} | ||
|
||
void | ||
testAll(FeatureBitset features) | ||
{ | ||
|
@@ -5142,6 +5332,7 @@ class Offer_test : public beast::unit_test::suite | |
testTicketCancelOffer(features); | ||
testRmSmallIncreasedQOffersXRP(features); | ||
testRmSmallIncreasedQOffersIOU(features); | ||
testFillOrKill(features); | ||
} | ||
|
||
void | ||
|
@@ -5153,12 +5344,17 @@ class Offer_test : public beast::unit_test::suite | |
FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; | ||
FeatureBitset const rmSmallIncreasedQOffers{fixRmSmallIncreasedQOffers}; | ||
FeatureBitset const immediateOfferKilled{featureImmediateOfferKilled}; | ||
FeatureBitset const fixFillOrKill{fixFillOrKillOnFlowCross}; | ||
|
||
testAll(all - takerDryOffer - immediateOfferKilled); | ||
testAll(all - flowCross - takerDryOffer - immediateOfferKilled); | ||
testAll(all - flowCross - immediateOfferKilled); | ||
testAll(all - rmSmallIncreasedQOffers - immediateOfferKilled); | ||
testAll( | ||
all - rmSmallIncreasedQOffers - immediateOfferKilled - | ||
fixFillOrKill); | ||
testAll(all - fixFillOrKill); | ||
testAll(all); | ||
|
||
testFalseAssert(); | ||
} | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fix is elegant! Thanks for figuring out the problem and fixing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing and the suggestions. I added/pushed your commit.