From 22a375a5f460f3617f9c13a33613ab3978b899b6 Mon Sep 17 00:00:00 2001 From: JoelKatz Date: Sun, 4 Dec 2016 18:27:58 -0800 Subject: [PATCH] Add support for tick sizes (RIPD-1363): Add an amendment to allow gateways to set a "tick size" for assets they issue. There are no changes unless the amendment is enabled (since the tick size option cannot be set). With the amendment enabled: AccountSet transactions may set a "TickSize" parameter. Legal values are 0 and 3-15 inclusive. Zero removes the setting. 3-15 allow that many decimal digits of precision in the pricing of offers for assets issued by this account. For asset pairs with XRP, the tick size imposed, if any, is the tick size of the issuer of the non-XRP asset. For asset pairs without XRP, the tick size imposed, if any, is the smaller of the two issuer's configured tick sizes. The tick size is imposed by rounding the offer quality down to nearest tick and recomputing the non-critical side of the offer. For a buy, the amount offered is rounded down. For a sell, the amount charged is rounded up. Gateways must enable a TickSize on their account for this feature to benefit them. The primary expected benefit is the elimination of bots fighting over the tip of the order book. This means: - Quicker price discovery as outpricing someone by a microscopic amount is made impossible. Currently bots can spend hours outbidding each other with no significant price movement. - A reduction in offer creation and cancellation spam. - More offers left on the books as priority means something when you can't outbid by a microscopic amount. --- src/ripple/app/main/Amendments.cpp | 3 +- src/ripple/app/tx/impl/CreateOffer.cpp | 55 ++++++++- src/ripple/app/tx/impl/SetAccount.cpp | 35 ++++++ src/ripple/protocol/Feature.h | 1 + src/ripple/protocol/Quality.h | 9 ++ src/ripple/protocol/SField.h | 1 + src/ripple/protocol/TER.h | 1 + src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/LedgerFormats.cpp | 1 + src/ripple/protocol/impl/Quality.cpp | 32 ++++++ src/ripple/protocol/impl/SField.cpp | 3 + src/ripple/protocol/impl/TER.cpp | 1 + src/ripple/protocol/impl/TxFormats.cpp | 1 + src/test/app/Offer_test.cpp | 123 +++++++++++++++++++++ src/test/protocol/Quality_test.cpp | 23 ++++ 15 files changed, 288 insertions(+), 2 deletions(-) diff --git a/src/ripple/app/main/Amendments.cpp b/src/ripple/app/main/Amendments.cpp index 20b676e25..fb0f2b071 100644 --- a/src/ripple/app/main/Amendments.cpp +++ b/src/ripple/app/main/Amendments.cpp @@ -48,7 +48,8 @@ supportedAmendments () { "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee" }, { "08DE7D96082187F6E6578530258C77FAABABE4C20474BDB82F04B021F1A68647 PayChan" }, { "740352F2412A9909880C23A559FCECEDA3BE2126FED62FC7660D628A06927F11 Flow" }, - { "1562511F573A19AE9BD103B5D6B9E01B3B46805AEC5D3C4805C902B514399146 CryptoConditions" } + { "1562511F573A19AE9BD103B5D6B9E01B3B46805AEC5D3C4805C902B514399146 CryptoConditions" }, + { "532651B4FD58DF8922A49BA101AB3E996E5BFBF95A913B3E392504863E63B164 TickSize" } }; } diff --git a/src/ripple/app/tx/impl/CreateOffer.cpp b/src/ripple/app/tx/impl/CreateOffer.cpp index 29ab6d6e3..7509d1b2a 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -680,7 +681,7 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel) // This is the original rate of the offer, and is the rate at which // it will be placed, even if crossing offers change the amounts that // end up on the books. - auto const uRate = getRate (saTakerGets, saTakerPays); + auto uRate = getRate (saTakerGets, saTakerPays); auto viewJ = ctx_.app.journal("View"); @@ -722,6 +723,58 @@ CreateOffer::applyGuts (ApplyView& view, ApplyView& view_cancel) if (result == tesSUCCESS) { + // If a tick size applies, round the offer to the tick size + auto const& uPaysIssuerID = saTakerPays.getIssuer (); + auto const& uGetsIssuerID = saTakerGets.getIssuer (); + + std::uint8_t uTickSize = Quality::maxTickSize; + if (!isXRP (uPaysIssuerID)) + { + auto const sle = + view.read(keylet::account(uPaysIssuerID)); + if (sle && sle->isFieldPresent (sfTickSize)) + uTickSize = std::min (uTickSize, + (*sle)[sfTickSize]); + } + if (!isXRP (uGetsIssuerID)) + { + auto const sle = + view.read(keylet::account(uGetsIssuerID)); + if (sle && sle->isFieldPresent (sfTickSize)) + uTickSize = std::min (uTickSize, + (*sle)[sfTickSize]); + } + if (uTickSize < Quality::maxTickSize) + { + auto const rate = + Quality{saTakerGets, saTakerPays}.round + (uTickSize).rate(); + + // We round the side that's not exact, + // just as if the offer happened to execute + // at a slightly better (for the placer) rate + if (bSell) + { + // this is a sell, round taker pays + saTakerPays = multiply ( + saTakerGets, rate, saTakerPays.issue()); + } + else + { + // this is a buy, round taker gets + saTakerGets = divide ( + saTakerPays, rate, saTakerGets.issue()); + } + if (! saTakerGets || ! saTakerPays) + { + JLOG (j_.debug()) << + "Offer rounded to zero"; + return { result, true }; + } + + uRate = getRate (saTakerGets, saTakerPays); + } + // We reverse pays and gets because during crossing we are taking. Amounts const taker_amount (saTakerGets, saTakerPays); diff --git a/src/ripple/app/tx/impl/SetAccount.cpp b/src/ripple/app/tx/impl/SetAccount.cpp index 36d307668..726cab791 100644 --- a/src/ripple/app/tx/impl/SetAccount.cpp +++ b/src/ripple/app/tx/impl/SetAccount.cpp @@ -124,6 +124,23 @@ SetAccount::preflight (PreflightContext const& ctx) } } + // TickSize + if (tx.isFieldPresent (sfTickSize)) + { + if (!ctx.rules.enabled(featureTickSize, + ctx.app.config().features)) + return temDISABLED; + + auto uTickSize = tx[sfTickSize]; + if (uTickSize && + ((uTickSize < Quality::minTickSize) || + (uTickSize > Quality::maxTickSize))) + { + JLOG(j.trace()) << "Malformed transaction: Bad tick size."; + return temBAD_TICK_SIZE; + } + } + if (auto const mk = tx[~sfMessageKey]) { if (mk->size() && ! publicKeyType ({mk->data(), mk->size()})) @@ -445,6 +462,24 @@ SetAccount::doApply () } } + // + // TickSize + // + if (ctx_.tx.isFieldPresent (sfTickSize)) + { + auto uTickSize = ctx_.tx[sfTickSize]; + if ((uTickSize == 0) || (uTickSize == Quality::maxTickSize)) + { + JLOG(j_.trace()) << "unset tick size"; + sle->makeFieldAbsent (sfTickSize); + } + else + { + JLOG(j_.trace()) << "set tick size"; + sle->setFieldU8 (sfTickSize, uTickSize); + } + } + if (uFlagsIn != uFlagsOut) sle->setFieldU32 (sfFlags, uFlagsOut); diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 4d5a4bc1c..06109f39f 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -45,6 +45,7 @@ extern uint256 const featureSHAMapV2; extern uint256 const featurePayChan; extern uint256 const featureFlow; extern uint256 const featureCryptoConditions; +extern uint256 const featureTickSize; } // ripple diff --git a/src/ripple/protocol/Quality.h b/src/ripple/protocol/Quality.h index 7c5c458be..45d6cc901 100644 --- a/src/ripple/protocol/Quality.h +++ b/src/ripple/protocol/Quality.h @@ -124,6 +124,9 @@ public: // have lower unsigned integer representations. using value_type = std::uint64_t; + static const int minTickSize = 3; + static const int maxTickSize = 16; + private: value_type m_value; @@ -170,6 +173,12 @@ public: return amountFromQuality (m_value); } + /** Returns the quality rounded up to the specified number + of decimal digits. + */ + Quality + round (int tickSize) const; + /** Returns the scaled amount with in capped. Math is avoided if the result is exact. The output is clamped to prevent money creation. diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index be2125336..9bfeb33cd 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -330,6 +330,7 @@ extern SField const sfMetadata; extern SF_U8 const sfCloseResolution; extern SF_U8 const sfMethod; extern SF_U8 const sfTransactionResult; +extern SF_U8 const sfTickSize; // 16-bit integers extern SF_U16 const sfLedgerEntryType; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index e41dcaf54..37b893811 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -85,6 +85,7 @@ enum TER temBAD_SIGNER, temBAD_QUORUM, temBAD_WEIGHT, + temBAD_TICK_SIZE, // An intermediate result used internally, should never be returned. temUNCERTAIN, diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index a3d4814dc..cb78ff5a3 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -56,5 +56,6 @@ uint256 const featureSHAMapV2 = feature("SHAMapV2"); uint256 const featurePayChan = feature("PayChan"); uint256 const featureFlow = feature("Flow"); uint256 const featureCryptoConditions = feature("CryptoConditions"); +uint256 const featureTickSize = feature("TickSize"); } // ripple diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 0a83ff7c8..49e15bbab 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -39,6 +39,7 @@ LedgerFormats::LedgerFormats () << SOElement (sfMessageKey, SOE_OPTIONAL) << SOElement (sfTransferRate, SOE_OPTIONAL) << SOElement (sfDomain, SOE_OPTIONAL) + << SOElement (sfTickSize, SOE_OPTIONAL) ; add ("DirectoryNode", ltDIR_NODE) diff --git a/src/ripple/protocol/impl/Quality.cpp b/src/ripple/protocol/impl/Quality.cpp index fefcb7802..b6e7427f0 100644 --- a/src/ripple/protocol/impl/Quality.cpp +++ b/src/ripple/protocol/impl/Quality.cpp @@ -120,4 +120,36 @@ composed_quality (Quality const& lhs, Quality const& rhs) return Quality ((stored_exponent << (64 - 8)) | stored_mantissa); } +Quality +Quality::round (int digits) const +{ + // Modulus for mantissa + static const std::uint64_t mod[17] = { + /* 0 */ 10000000000000000, + /* 1 */ 1000000000000000, + /* 2 */ 100000000000000, + /* 3 */ 10000000000000, + /* 4 */ 1000000000000, + /* 5 */ 100000000000, + /* 6 */ 10000000000, + /* 7 */ 1000000000, + /* 8 */ 100000000, + /* 9 */ 10000000, + /* 10 */ 1000000, + /* 11 */ 100000, + /* 12 */ 10000, + /* 13 */ 1000, + /* 14 */ 100, + /* 15 */ 10, + /* 16 */ 1, + }; + + auto exponent = m_value >> (64 - 8); + auto mantissa = m_value & 0x00ffffffffffffffULL; + mantissa += mod[digits] - 1; + mantissa -= (mantissa % mod[digits]); + + return Quality{(exponent << (64 - 8)) | mantissa}; +} + } diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 98c7cf90c..228d893ef 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -82,6 +82,9 @@ SF_U8 const sfCloseResolution = make::one(&sfCloseResolution, S SF_U8 const sfMethod = make::one(&sfMethod, STI_UINT8, 2, "Method"); SF_U8 const sfTransactionResult = make::one(&sfTransactionResult, STI_UINT8, 3, "TransactionResult"); +// 8-bit integers (uncommon) +SF_U8 const sfTickSize = make::one(&sfTickSize, STI_UINT8, 16, "TickSize"); + // 16-bit integers SF_U16 const sfLedgerEntryType = make::one(&sfLedgerEntryType, STI_UINT16, 1, "LedgerEntryType", SField::sMD_Never); SF_U16 const sfTransactionType = make::one(&sfTransactionType, STI_UINT16, 2, "TransactionType"); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 458b1c7e9..fede93f6e 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -123,6 +123,7 @@ bool transResultInfo (TER code, std::string& token, std::string& text) { temUNCERTAIN, { "temUNCERTAIN", "In process of determining result. Never returned." } }, { temUNKNOWN, { "temUNKNOWN", "The transaction requires logic that is not implemented yet." } }, { temDISABLED, { "temDISABLED", "The transaction requires logic that is currently disabled." } }, + { temBAD_TICK_SIZE, { "temBAD_TICK_SIZE", "Malformed: Tick size out of range." } }, { terRETRY, { "terRETRY", "Retry transaction." } }, { terFUNDS_SPENT, { "terFUNDS_SPENT", "Can't set password, password set funds already spent." } }, diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 982ed1c06..6a0bf18b4 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -33,6 +33,7 @@ TxFormats::TxFormats () << SOElement (sfTransferRate, SOE_OPTIONAL) << SOElement (sfSetFlag, SOE_OPTIONAL) << SOElement (sfClearFlag, SOE_OPTIONAL) + << SOElement (sfTickSize, SOE_OPTIONAL) ; add ("TrustSet", ttTRUST_SET) diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 0dc1800c0..bed353087 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -21,7 +21,9 @@ #include #include #include +#include #include +#include namespace ripple { namespace test { @@ -1762,6 +1764,126 @@ public: BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-101"); } + void testTickSize () + { + testcase ("Tick Size"); + + using namespace jtx; + + // Try to set tick size without enabling feature + { + Env env {*this}; + auto const gw = Account {"gateway"}; + env.fund (XRP(10000), gw); + + auto txn = noop(gw); + txn[sfTickSize.fieldName] = 0; + env(txn, ter(temDISABLED)); + } + + // Try to set tick size out of range + { + Env env {*this, features (featureTickSize)}; + auto const gw = Account {"gateway"}; + env.fund (XRP(10000), gw); + + auto txn = noop(gw); + txn[sfTickSize.fieldName] = Quality::minTickSize - 1; + env(txn, ter (temBAD_TICK_SIZE)); + + txn[sfTickSize.fieldName] = Quality::minTickSize; + env(txn); + BEAST_EXPECT ((*env.le(gw))[sfTickSize] + == Quality::minTickSize); + + txn = noop (gw); + txn[sfTickSize.fieldName] = Quality::maxTickSize; + env(txn); + BEAST_EXPECT (! env.le(gw)->isFieldPresent (sfTickSize)); + + txn = noop (gw); + txn[sfTickSize.fieldName] = Quality::maxTickSize - 1; + env(txn); + BEAST_EXPECT ((*env.le(gw))[sfTickSize] + == Quality::maxTickSize - 1); + + txn = noop (gw); + txn[sfTickSize.fieldName] = Quality::maxTickSize + 1; + env(txn, ter (temBAD_TICK_SIZE)); + + txn[sfTickSize.fieldName] = 0; + env(txn, tesSUCCESS); + BEAST_EXPECT (! env.le(gw)->isFieldPresent (sfTickSize)); + } + + Env env {*this, features (featureTickSize)}; + auto const gw = Account {"gateway"}; + auto const alice = Account {"alice"}; + auto const XTS = gw["XTS"]; + auto const XXX = gw["XXX"]; + + env.fund (XRP (10000), gw, alice); + + { + // Gateway sets its tick size to 5 + auto txn = noop(gw); + txn[sfTickSize.fieldName] = 5; + env(txn); + BEAST_EXPECT ((*env.le(gw))[sfTickSize] == 5); + } + + env (trust (alice, XTS (1000))); + env (trust (alice, XXX (1000))); + + env (pay (gw, alice, alice["XTS"] (100))); + env (pay (gw, alice, alice["XXX"] (100))); + + env (offer (alice, XTS (10), XXX (30))); + env (offer (alice, XTS (30), XXX (10))); + env (offer (alice, XTS (10), XXX (30)), + json(jss::Flags, tfSell)); + env (offer (alice, XTS (30), XXX (10)), + json(jss::Flags, tfSell)); + + std::map > offers; + forEachItem (*env.current(), alice, + [&](std::shared_ptr const& sle) + { + if (sle->getType() == ltOFFER) + offers.emplace((*sle)[sfSequence], + std::make_pair((*sle)[sfTakerPays], + (*sle)[sfTakerGets])); + }); + + // first offer + auto it = offers.begin(); + BEAST_EXPECT (it != offers.end()); + BEAST_EXPECT (it->second.first == XTS(10) && + it->second.second < XXX(30) && + it->second.second > XXX(29.9994)); + + // second offer + ++it; + BEAST_EXPECT (it != offers.end()); + BEAST_EXPECT (it->second.first == XTS(30) && + it->second.second == XXX(10)); + + // third offer + ++it; + BEAST_EXPECT (it != offers.end()); + BEAST_EXPECT (it->second.first == XTS(10.0002) && + it->second.second == XXX(30)); + + // fourth offer + // exact TakerPays is XTS(1/.033333) + ++it; + BEAST_EXPECT (it != offers.end()); + BEAST_EXPECT (it->second.first == XTS(30) && + it->second.second == XXX(10)); + + BEAST_EXPECT (++it == offers.end()); + } + void run () { testCanceledOffer (); @@ -1793,6 +1915,7 @@ public: testSellFlagBasic (); testSellFlagExceedLimit (); testGatewayCrossCurrency (); + testTickSize (); } }; diff --git a/src/test/protocol/Quality_test.cpp b/src/test/protocol/Quality_test.cpp index 9dc033f99..3215f2de9 100644 --- a/src/test/protocol/Quality_test.cpp +++ b/src/test/protocol/Quality_test.cpp @@ -238,6 +238,28 @@ public: } } + void + test_round() + { + testcase ("round"); + + Quality q (0x59148191fb913522ull); // 57719.63525051682 + BEAST_EXPECT(q.round(3).rate().getText() == "57800"); + BEAST_EXPECT(q.round(4).rate().getText() == "57720"); + BEAST_EXPECT(q.round(5).rate().getText() == "57720"); + BEAST_EXPECT(q.round(6).rate().getText() == "57719.7"); + BEAST_EXPECT(q.round(7).rate().getText() == "57719.64"); + BEAST_EXPECT(q.round(8).rate().getText() == "57719.636"); + BEAST_EXPECT(q.round(9).rate().getText() == "57719.6353"); + BEAST_EXPECT(q.round(10).rate().getText() == "57719.63526"); + BEAST_EXPECT(q.round(11).rate().getText() == "57719.635251"); + BEAST_EXPECT(q.round(12).rate().getText() == "57719.6352506"); + BEAST_EXPECT(q.round(13).rate().getText() == "57719.63525052"); + BEAST_EXPECT(q.round(14).rate().getText() == "57719.635250517"); + BEAST_EXPECT(q.round(15).rate().getText() == "57719.6352505169"); + BEAST_EXPECT(q.round(16).rate().getText() == "57719.63525051682"); + } + void test_comparisons() { @@ -320,6 +342,7 @@ public: test_ceil_in (); test_ceil_out (); test_raw (); + test_round (); } };