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 20b676e25bd..fb0f2b071c5 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 29ab6d6e328..7509d1b2a03 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 36d3076687a..726cab7911c 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 4d5a4bc1cbf..06109f39f43 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 7c5c458be5b..45d6cc90106 100644 --- a/src/ripple/protocol/Quality.h +++ b/src/ripple/protocol/Quality.h @@ -124,6 +124,9 @@ class Quality // 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 @@ class Quality 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 be212533653..9bfeb33cd12 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 e41dcaf5446..37b89381131 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 a3d4814dc7e..cb78ff5a305 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 0a83ff7c8c1..49e15bbabc4 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 fefcb7802e4..b6e7427f09f 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 98c7cf90c21..228d893ef2d 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 458b1c7e98e..fede93f6e72 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 982ed1c06ab..6a0bf18b4e9 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 0dc1800c076..bed3530872d 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 @@ class Offer_test : public beast::unit_test::suite 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 @@ class Offer_test : public beast::unit_test::suite testSellFlagBasic (); testSellFlagExceedLimit (); testGatewayCrossCurrency (); + testTickSize (); } }; diff --git a/src/test/protocol/Quality_test.cpp b/src/test/protocol/Quality_test.cpp index 9dc033f990c..3215f2de9d4 100644 --- a/src/test/protocol/Quality_test.cpp +++ b/src/test/protocol/Quality_test.cpp @@ -238,6 +238,28 @@ class Quality_test : public beast::unit_test::suite } } + 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 @@ class Quality_test : public beast::unit_test::suite test_ceil_in (); test_ceil_out (); test_raw (); + test_round (); } };