Improve transaction error condition handling (RIPD-1578, RIPD-1593):

As described in #2314, when an offer executed with `Fill or Kill`
semantics, the server would return `tesSUCCESS` even if the order
couldn't be filled and was aborted. This would require additional
processing of metadata by users to determine the effects of the
transaction.

This commit introduces the `fix1578` amendment which, if enabled,
will cause the server to return the new `tecKILLED` error code
instead of `tesSUCCESS` for `Fill or Kill` orders that could not
be filled.

Additionally, the `fix1578` amendment will prevent the setting of
the `No Ripple` flag on trust lines with negative balance; trying
to set the flag on such a trust line will fail with the new error
code `tecNEGATIVE_BALANCE`.
This commit is contained in:
Scott Schurr
2018-06-15 14:56:13 -07:00
committed by Nik Bougalis
parent 4dcb3c9199
commit 4104778067
9 changed files with 153 additions and 59 deletions

View File

@@ -619,10 +619,16 @@ public:
auto const bob = Account {"bob"};
auto const USD = gw["USD"];
// Fill or Kill - unless we fully cross, just charge
// a fee and not place the offer on the books:
// Fill or Kill - unless we fully cross, just charge a fee and don't
// place the offer on the books. But also clean up expired offers
// that are discovered along the way.
//
// fix1578 changes the return code. Verify expected behavior
// without and with fix1578.
for (auto const tweakedFeatures :
{features - fix1578, features | fix1578})
{
Env env {*this, features};
Env env {*this, tweakedFeatures};
auto const closeTime =
fix1449Time () +
100 * env.closed ()->info ().closeTimeResolution;
@@ -631,20 +637,41 @@ public:
auto const f = env.current ()->fees ().base;
env.fund (startBalance, gw, alice, bob);
// bob creates an offer that expires before the next ledger close.
env (offer (bob, USD (500), XRP (500)),
json (sfExpiration.fieldName, lastClose(env) + 1),
ter(tesSUCCESS));
// The offer expires (it's not removed yet).
env.close ();
env.require (
owners (bob, 1),
offers (bob, 1));
// bob creates the offer that will be crossed.
env (offer (bob, USD (500), XRP (500)), ter(tesSUCCESS));
env.close();
env.require (
owners (bob, 2),
offers (bob, 2));
env (trust (alice, USD (1000)), ter(tesSUCCESS));
env (pay (gw, alice, USD (1000)), ter(tesSUCCESS));
// Order that can't be filled:
env (offer (alice, XRP (1000), USD (1000)),
txflags (tfFillOrKill), ter(tesSUCCESS));
// Order that can't be filled but will remove bob's expired offer:
{
TER const killedCode {tweakedFeatures[fix1578] ?
TER {tecKILLED} : TER {tesSUCCESS}};
env (offer (alice, XRP (1000), USD (1000)),
txflags (tfFillOrKill), ter(killedCode));
}
env.require (
balance (alice, startBalance - f - f),
balance (alice, startBalance - (f * 2)),
balance (alice, USD (1000)),
owners (alice, 1),
offers (alice, 0),
balance (bob, startBalance - f),
balance (bob, startBalance - (f * 2)),
balance (bob, USD (none)),
owners (bob, 1),
offers (bob, 1));
@@ -654,11 +681,11 @@ public:
txflags (tfFillOrKill), ter(tesSUCCESS));
env.require (
balance (alice, startBalance - f - f - f + XRP (500)),
balance (alice, startBalance - (f * 3) + XRP (500)),
balance (alice, USD (500)),
owners (alice, 1),
offers (alice, 0),
balance (bob, startBalance - f - XRP (500)),
balance (bob, startBalance - (f * 2) - XRP (500)),
balance (bob, USD (500)),
owners (bob, 1),
offers (bob, 0));
@@ -682,7 +709,7 @@ public:
// No cross:
env (offer (alice, XRP (1000), USD (1000)),
txflags (tfImmediateOrCancel), ter(tesSUCCESS));
txflags (tfImmediateOrCancel), ter(tesSUCCESS));
env.require (
balance (alice, startBalance - f - f),
@@ -691,9 +718,9 @@ public:
offers (alice, 0));
// Partially cross:
env (offer (bob, USD (50), XRP (50)), ter(tesSUCCESS));
env (offer (bob, USD (50), XRP (50)), ter(tesSUCCESS));
env (offer (alice, XRP (1000), USD (1000)),
txflags (tfImmediateOrCancel), ter(tesSUCCESS));
txflags (tfImmediateOrCancel), ter(tesSUCCESS));
env.require (
balance (alice, startBalance - f - f - f + XRP (50)),
@@ -706,9 +733,9 @@ public:
offers (bob, 0));
// Fully cross:
env (offer (bob, USD (50), XRP (50)), ter(tesSUCCESS));
env (offer (bob, USD (50), XRP (50)), ter(tesSUCCESS));
env (offer (alice, XRP (50), USD (50)),
txflags (tfImmediateOrCancel), ter(tesSUCCESS));
txflags (tfImmediateOrCancel), ter(tesSUCCESS));
env.require (
balance (alice, startBalance - f - f - f - f + XRP (100)),
@@ -2739,6 +2766,10 @@ public:
env.fund (XRP(10000000), gw, alice, bob);
// Code returned if an offer is killed.
TER const killedCode {
features[fix1578] ? TER {tecKILLED} : TER {tesSUCCESS}};
// bob offers XRP for USD.
env (trust(bob, USD(200)));
env.close();
@@ -2748,8 +2779,8 @@ public:
env.close();
{
// alice submits a tfSell | tfFillOrKill offer that does not cross.
// It's still a tesSUCCESS, since the offer was successfully killed.
env (offer(alice, USD(21), XRP(2100), tfSell | tfFillOrKill));
env (offer(alice, USD(21), XRP(2100),
tfSell | tfFillOrKill), ter (killedCode));
env.close();
env.require (balance (alice, USD(none)));
env.require (offers (alice, 0));
@@ -2782,7 +2813,8 @@ public:
// all of the offer is consumed.
// We're using bob's left-over offer for XRP(500), USD(5)
env (offer(alice, USD(1), XRP(501), tfSell | tfFillOrKill));
env (offer(alice, USD(1), XRP(501),
tfSell | tfFillOrKill), ter (killedCode));
env.close();
env.require (balance (alice, USD(35)));
env.require (offers (alice, 0));

View File

@@ -56,7 +56,8 @@ public:
env.close();
// Check no-ripple flag on sender 'gateway'
auto lines = env.rpc("json", "account_lines", to_string(account_gw));
Json::Value lines {env.rpc(
"json", "account_lines", to_string(account_gw))};
auto const& gline0 = lines[jss::result][jss::lines][0u];
BEAST_EXPECT(gline0[jss::no_ripple].asBool() == SetOrClear);
@@ -72,45 +73,92 @@ public:
testcase("Set noripple on a line with negative balance");
using namespace jtx;
Env env(*this, features);
auto const gw = Account("gateway");
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
env.fund(XRP(10000), gw, alice, bob, carol);
// fix1578 changes the return code. Verify expected behavior
// without and with fix1578.
for (auto const tweakedFeatures :
{features - fix1578, features | fix1578})
{
Env env(*this, tweakedFeatures);
env.trust(alice["USD"](100), bob);
env.trust(bob["USD"](100), carol);
env.close();
env.fund(XRP(10000), gw, alice, bob, carol);
env(pay(alice, carol, carol["USD"](50)), path(bob));
env.trust(alice["USD"](100), bob);
env.trust(bob["USD"](100), carol);
env.close();
env(trust(alice, bob["USD"](100), bob, tfSetNoRipple));
env(trust(bob, carol["USD"](100), carol, tfSetNoRipple));
env.close();
// After this payment alice has a -50 USD balance with bob, and
// bob has a -50 USD balance with carol. So neither alice nor
// bob should be able to clear the noRipple flag.
env(pay(alice, carol, carol["USD"](50)), path(bob));
env.close();
Json::Value params;
params[jss::source_account] = alice.human();
params[jss::destination_account] = carol.human();
params[jss::destination_amount] = [] {
Json::Value dest_amt;
dest_amt[jss::currency] = "USD";
dest_amt[jss::value] = "1";
dest_amt[jss::issuer] = Account("carol").human();
return dest_amt;
}();
TER const terNeg {tweakedFeatures[fix1578] ?
TER {tecNO_PERMISSION} : TER {tesSUCCESS}};
auto const resp = env.rpc("json", "ripple_path_find", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::alternatives].size()==1);
env(trust(
alice, bob["USD"](100), bob, tfSetNoRipple), ter(terNeg));
env(trust(
bob, carol["USD"](100), carol, tfSetNoRipple), ter(terNeg));
env.close();
Json::Value account_alice;
account_alice[jss::account] = alice.human();
auto const res = env.rpc("json", "account_lines", to_string(account_alice));
auto const& lines = res[jss::result][jss::lines];
BEAST_EXPECT(lines.size() == 1);
BEAST_EXPECT(!lines[0u].isMember(jss::no_ripple));
Json::Value params;
params[jss::source_account] = alice.human();
params[jss::destination_account] = carol.human();
params[jss::destination_amount] = [] {
Json::Value dest_amt;
dest_amt[jss::currency] = "USD";
dest_amt[jss::value] = "1";
dest_amt[jss::issuer] = Account("carol").human();
return dest_amt;
}();
auto const resp = env.rpc(
"json", "ripple_path_find", to_string(params));
BEAST_EXPECT(resp[jss::result][jss::alternatives].size()==1);
auto getAccountLines = [&env] (Account const& acct)
{
Json::Value jv;
jv[jss::account] = acct.human();
auto const resp =
env.rpc("json", "account_lines", to_string(jv));
return resp[jss::result][jss::lines];
};
{
auto const aliceLines = getAccountLines (alice);
BEAST_EXPECT(aliceLines.size() == 1);
BEAST_EXPECT(!aliceLines[0u].isMember(jss::no_ripple));
auto const bobLines = getAccountLines (bob);
BEAST_EXPECT(bobLines.size() == 2);
BEAST_EXPECT(!bobLines[0u].isMember(jss::no_ripple));
BEAST_EXPECT(!bobLines[1u].isMember(jss::no_ripple));
}
// Now carol sends the 50 USD back to alice. Then alice and
// bob can set the noRipple flag.
env(pay(carol, alice, alice["USD"](50)), path(bob));
env.close();
env(trust(alice, bob["USD"](100), bob, tfSetNoRipple));
env(trust(bob, carol["USD"](100), carol, tfSetNoRipple));
env.close();
{
auto const aliceLines = getAccountLines (alice);
BEAST_EXPECT(aliceLines.size() == 1);
BEAST_EXPECT(aliceLines[0u].isMember(jss::no_ripple));
auto const bobLines = getAccountLines (bob);
BEAST_EXPECT(bobLines.size() == 2);
BEAST_EXPECT(bobLines[0u].isMember(jss::no_ripple_peer));
BEAST_EXPECT(bobLines[1u].isMember(jss::no_ripple));
}
}
}
void testPairwise(FeatureBitset features)
@@ -144,7 +192,8 @@ public:
return dest_amt;
}();
auto const resp = env.rpc("json", "ripple_path_find", to_string(params));
Json::Value const resp {
env.rpc("json", "ripple_path_find", to_string(params))};
BEAST_EXPECT(resp[jss::result][jss::alternatives].size() == 0);
env(pay(alice, carol, bob["USD"](50)), ter(tecPATH_DRY));