From 528143e655a6e3b1a04d0b1fb7d44b23d407b8b1 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:28 -0500 Subject: [PATCH 01/13] Updated XDR: added Liabilities, extended AccountEntry and TrustLineEntry --- src/xdr/Stellar-ledger-entries.x | 34 +++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/xdr/Stellar-ledger-entries.x b/src/xdr/Stellar-ledger-entries.x index ddbb8d0124..ed7b09adee 100644 --- a/src/xdr/Stellar-ledger-entries.x +++ b/src/xdr/Stellar-ledger-entries.x @@ -12,7 +12,7 @@ typedef opaque Thresholds[4]; typedef string string32<32>; typedef string string64<64>; typedef int64 SequenceNumber; -typedef opaque DataValue<64>; +typedef opaque DataValue<64>; enum AssetType { @@ -50,6 +50,12 @@ struct Price int32 d; // denominator }; +struct Liabilities +{ + int64 buying; + int64 selling; +}; + // the 'Thresholds' type is packed uint8_t values // defined by these indexes enum ThresholdIndexes @@ -123,6 +129,18 @@ struct AccountEntry { case 0: void; + case 1: + struct + { + Liabilities liabilities; + + union switch (int v) + { + case 0: + void; + } + ext; + } v1; } ext; }; @@ -139,7 +157,6 @@ enum TrustLineFlags AUTHORIZED_FLAG = 1 }; - // mask for all trustline flags const MASK_TRUSTLINE_FLAGS = 1; @@ -158,6 +175,18 @@ struct TrustLineEntry { case 0: void; + case 1: + struct + { + Liabilities liabilities; + + union switch (int v) + { + case 0: + void; + } + ext; + } v1; } ext; }; @@ -221,7 +250,6 @@ struct DataEntry ext; }; - struct LedgerEntry { uint32 lastModifiedLedgerSeq; // ledger the LedgerEntry was last changed From 0e8da7a4e33e5c3cbb6b3249296daa713df29b32 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:30 -0500 Subject: [PATCH 02/13] Updated AccountFrame SQL --- docs/db-schema.md | 2 ++ src/database/Database.cpp | 11 ++++++++++- src/ledger/AccountFrame.cpp | 32 ++++++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/docs/db-schema.md b/docs/db-schema.md index b790a75585..9bf85b9cc2 100644 --- a/docs/db-schema.md +++ b/docs/db-schema.md @@ -51,6 +51,8 @@ homedomain | VARCHAR(32) | thresholds | TEXT | (BASE64) flags | INT NOT NULL | lastmodified | INT NOT NULL | lastModifiedLedgerSeq +buyingliabilities | BIGINT CHECK (buyingliabilities >= 0) +sellingliabilities | BIGINT CHECK (sellingliabilities >= 0) ## offers diff --git a/src/database/Database.cpp b/src/database/Database.cpp index 7d1d40b6ed..af31fff197 100644 --- a/src/database/Database.cpp +++ b/src/database/Database.cpp @@ -53,7 +53,7 @@ using namespace std; bool Database::gDriversRegistered = false; -static unsigned long const SCHEMA_VERSION = 6; +static unsigned long const SCHEMA_VERSION = 7; static void setSerializable(soci::session& sess) @@ -140,9 +140,18 @@ Database::applySchemaUpgrade(unsigned long vers) } } break; + case 6: mSession << "ALTER TABLE peers ADD flags INT NOT NULL DEFAULT 0"; break; + + case 7: + mSession << "ALTER TABLE accounts ADD buyingliabilities BIGINT " + "CHECK (buyingliabilities >= 0)"; + mSession << "ALTER TABLE accounts ADD sellingliabilities BIGINT " + "CHECK (sellingliabilities >= 0)"; + break; + default: throw std::runtime_error("Unknown DB schema version"); break; diff --git a/src/ledger/AccountFrame.cpp b/src/ledger/AccountFrame.cpp index c8cb338e0b..a4dea3412f 100644 --- a/src/ledger/AccountFrame.cpp +++ b/src/ledger/AccountFrame.cpp @@ -222,7 +222,9 @@ AccountFrame::loadAccount(AccountID const& accountID, Database& db) std::string publicKey, inflationDest, creditAuthKey; std::string homeDomain, thresholds; + Liabilities liabilities; soci::indicator inflationDestInd; + soci::indicator buyingLiabilitiesInd, sellingLiabilitiesInd; AccountFrame::pointer res = make_shared(accountID); AccountEntry& account = res->getAccount(); @@ -230,7 +232,8 @@ AccountFrame::loadAccount(AccountID const& accountID, Database& db) auto prep = db.getPreparedStatement("SELECT balance, seqnum, numsubentries, " "inflationdest, homedomain, thresholds, " - "flags, lastmodified " + "flags, lastmodified, buyingliabilities, " + "sellingliabilities " "FROM accounts WHERE accountid=:v1"); auto& st = prep.statement(); st.exchange(into(account.balance)); @@ -241,6 +244,8 @@ AccountFrame::loadAccount(AccountID const& accountID, Database& db) st.exchange(into(thresholds)); st.exchange(into(account.flags)); st.exchange(into(res->getLastModified())); + st.exchange(into(liabilities.buying, buyingLiabilitiesInd)); + st.exchange(into(liabilities.selling, sellingLiabilitiesInd)); st.exchange(use(actIDStrKey)); st.define_and_bind(); { @@ -273,6 +278,13 @@ AccountFrame::loadAccount(AccountID const& accountID, Database& db) signers.end()); } + assert(buyingLiabilitiesInd == sellingLiabilitiesInd); + if (buyingLiabilitiesInd == soci::i_ok) + { + account.ext.v(1); + account.ext.v1().liabilities = liabilities; + } + res->normalize(); res->mUpdateSigners = false; res->mKeyCalculated = false; @@ -430,8 +442,9 @@ AccountFrame::storeUpdate(LedgerDelta& delta, Database& db, bool insert) sql = std::string( "INSERT INTO accounts ( accountid, balance, seqnum, " "numsubentries, inflationdest, homedomain, thresholds, flags, " - "lastmodified ) " - "VALUES ( :id, :v1, :v2, :v3, :v4, :v5, :v6, :v7, :v8 )"); + "lastmodified, buyingliabilities, sellingliabilities ) " + "VALUES ( :id, :v1, :v2, :v3, :v4, :v5, :v6, :v7, :v8, :v9, :v10 " + ")"); } else { @@ -439,7 +452,8 @@ AccountFrame::storeUpdate(LedgerDelta& delta, Database& db, bool insert) "UPDATE accounts SET balance = :v1, seqnum = :v2, " "numsubentries = :v3, " "inflationdest = :v4, homedomain = :v5, thresholds = :v6, " - "flags = :v7, lastmodified = :v8 WHERE accountid = :id"); + "flags = :v7, lastmodified = :v8, buyingliabilities = :v9, " + "sellingliabilities = :v10 WHERE accountid = :id"); } auto prep = db.getPreparedStatement(sql); @@ -453,6 +467,14 @@ AccountFrame::storeUpdate(LedgerDelta& delta, Database& db, bool insert) inflation_ind = soci::i_ok; } + Liabilities liabilities; + soci::indicator liabilitiesInd = soci::i_null; + if (mAccountEntry.ext.v() == 1) + { + liabilities = mAccountEntry.ext.v1().liabilities; + liabilitiesInd = soci::i_ok; + } + string thresholds(decoder::encode_b64(mAccountEntry.thresholds)); { @@ -467,6 +489,8 @@ AccountFrame::storeUpdate(LedgerDelta& delta, Database& db, bool insert) st.exchange(use(thresholds, "v6")); st.exchange(use(mAccountEntry.flags, "v7")); st.exchange(use(getLastModified(), "v8")); + st.exchange(use(liabilities.buying, liabilitiesInd, "v9")); + st.exchange(use(liabilities.selling, liabilitiesInd, "v10")); st.define_and_bind(); { auto timer = insert ? db.getInsertTimer("account") From b5506429a8b247673a51f63f2b499fd4e15cc53b Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:32 -0500 Subject: [PATCH 03/13] Updated TrustFrame SQL --- docs/db-schema.md | 2 ++ src/database/Database.cpp | 4 ++++ src/ledger/TrustFrame.cpp | 43 +++++++++++++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/docs/db-schema.md b/docs/db-schema.md index 9bf85b9cc2..4ef1893adb 100644 --- a/docs/db-schema.md +++ b/docs/db-schema.md @@ -94,6 +94,8 @@ tlimit | BIGINT NOT NULL DEFAULT 0 CHECK (tlimit >= 0) | limit balance | BIGINT NOT NULL DEFAULT 0 CHECK (balance >= 0) | flags | INT NOT NULL | lastmodified | INT NOT NULL | lastModifiedLedgerSeq +buyingliabilities | BIGINT CHECK (buyingliabilities >= 0) +sellingliabilities | BIGINT CHECK (sellingliabilities >= 0) ## txhistory diff --git a/src/database/Database.cpp b/src/database/Database.cpp index af31fff197..1e55168371 100644 --- a/src/database/Database.cpp +++ b/src/database/Database.cpp @@ -150,6 +150,10 @@ Database::applySchemaUpgrade(unsigned long vers) "CHECK (buyingliabilities >= 0)"; mSession << "ALTER TABLE accounts ADD sellingliabilities BIGINT " "CHECK (sellingliabilities >= 0)"; + mSession << "ALTER TABLE trustlines ADD buyingliabilities BIGINT " + "CHECK (buyingliabilities >= 0)"; + mSession << "ALTER TABLE trustlines ADD sellingliabilities BIGINT " + "CHECK (sellingliabilities >= 0)"; break; default: diff --git a/src/ledger/TrustFrame.cpp b/src/ledger/TrustFrame.cpp index 7c216fae19..6872d3e839 100644 --- a/src/ledger/TrustFrame.cpp +++ b/src/ledger/TrustFrame.cpp @@ -237,15 +237,26 @@ TrustFrame::storeChange(LedgerDelta& delta, Database& db) std::string actIDStrKey, issuerStrKey, assetCode; getKeyFields(key, actIDStrKey, issuerStrKey, assetCode); + Liabilities liabilities; + soci::indicator liabilitiesInd = soci::i_null; + if (mTrustLine.ext.v() == 1) + { + liabilities = mTrustLine.ext.v1().liabilities; + liabilitiesInd = soci::i_ok; + } + auto prep = db.getPreparedStatement( "UPDATE trustlines " - "SET balance=:b, tlimit=:tl, flags=:a, lastmodified=:lm " + "SET balance=:b, tlimit=:tl, flags=:a, lastmodified=:lm, " + "buyingliabilities=:bl, sellingliabilities=:sl " "WHERE accountid=:v1 AND issuer=:v2 AND assetcode=:v3"); auto& st = prep.statement(); st.exchange(use(mTrustLine.balance)); st.exchange(use(mTrustLine.limit)); st.exchange(use(mTrustLine.flags)); st.exchange(use(getLastModified())); + st.exchange(use(liabilities.buying, liabilitiesInd)); + st.exchange(use(liabilities.selling, liabilitiesInd)); st.exchange(use(actIDStrKey)); st.exchange(use(issuerStrKey)); st.exchange(use(assetCode)); @@ -277,11 +288,19 @@ TrustFrame::storeAdd(LedgerDelta& delta, Database& db) unsigned int assetType = getKey().trustLine().asset.type(); getKeyFields(getKey(), actIDStrKey, issuerStrKey, assetCode); + Liabilities liabilities; + soci::indicator liabilitiesInd = soci::i_null; + if (mTrustLine.ext.v() == 1) + { + liabilities = mTrustLine.ext.v1().liabilities; + liabilitiesInd = soci::i_ok; + } + auto prep = db.getPreparedStatement( "INSERT INTO trustlines " "(accountid, assettype, issuer, assetcode, balance, tlimit, flags, " - "lastmodified) " - "VALUES (:v1, :v2, :v3, :v4, :v5, :v6, :v7, :v8)"); + "lastmodified, buyingliabilities, sellingliabilities) " + "VALUES (:v1, :v2, :v3, :v4, :v5, :v6, :v7, :v8, :v9, :v10)"); auto& st = prep.statement(); st.exchange(use(actIDStrKey)); st.exchange(use(assetType)); @@ -291,6 +310,8 @@ TrustFrame::storeAdd(LedgerDelta& delta, Database& db) st.exchange(use(mTrustLine.limit)); st.exchange(use(mTrustLine.flags)); st.exchange(use(getLastModified())); + st.exchange(use(liabilities.buying, liabilitiesInd)); + st.exchange(use(liabilities.selling, liabilitiesInd)); st.define_and_bind(); { auto timer = db.getInsertTimer("trust"); @@ -307,7 +328,8 @@ TrustFrame::storeAdd(LedgerDelta& delta, Database& db) static const char* trustLineColumnSelector = "SELECT " - "accountid,assettype,issuer,assetcode,tlimit,balance,flags,lastmodified " + "accountid,assettype,issuer,assetcode,tlimit,balance,flags,lastmodified," + "buyingliabilities,sellingliabilities " "FROM trustlines"; TrustFrame::pointer @@ -425,6 +447,10 @@ TrustFrame::loadLines(StatementContext& prep, std::string issuerStrKey, assetCode; unsigned int assetType; + Liabilities liabilities; + soci::indicator buyingLiabilitiesInd; + soci::indicator sellingLiabilitiesInd; + LedgerEntry le; le.data.type(TRUSTLINE); @@ -439,6 +465,8 @@ TrustFrame::loadLines(StatementContext& prep, st.exchange(into(tl.balance)); st.exchange(into(tl.flags)); st.exchange(into(le.lastModifiedLedgerSeq)); + st.exchange(into(liabilities.buying, buyingLiabilitiesInd)); + st.exchange(into(liabilities.selling, sellingLiabilitiesInd)); st.define_and_bind(); st.execute(true); @@ -459,6 +487,13 @@ TrustFrame::loadLines(StatementContext& prep, strToAssetCode(tl.asset.alphaNum12().assetCode, assetCode); } + assert(buyingLiabilitiesInd == sellingLiabilitiesInd); + if (buyingLiabilitiesInd == soci::i_ok) + { + tl.ext.v(1); + tl.ext.v1().liabilities = liabilities; + } + trustProcessor(le); st.fetch(); From 777bb87b25ea547890e2408407c8dd362af3e018 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:34 -0500 Subject: [PATCH 04/13] Updated OfferFrame SQL --- src/ledger/OfferFrame.cpp | 46 +++++++++++++++++++++++++++++++++++++++ src/ledger/OfferFrame.h | 4 ++++ 2 files changed, 50 insertions(+) diff --git a/src/ledger/OfferFrame.cpp b/src/ledger/OfferFrame.cpp index 4aff644e0d..02f310b114 100644 --- a/src/ledger/OfferFrame.cpp +++ b/src/ledger/OfferFrame.cpp @@ -349,6 +349,52 @@ OfferFrame::loadAllOffers(Database& db) return retOffers; } +// Note: This function is currently only used in AllowTrustOpFrame, which means +// the asset parameter will never satisfy asset.type() == ASSET_TYPE_NATIVE. As +// a consequence, I have not implemented that possibility so this function +// throws in that case. +std::vector +OfferFrame::loadOffersByAccountAndAsset(AccountID const& accountID, + Asset const& asset, Database& db) +{ + std::vector retOffers; + std::string sql = offerColumnSelector; + sql += " WHERE sellerid = :acc" + " AND ((sellingassetcode = :code AND sellingissuer = :iss)" + " OR (buyingassetcode = :code AND buyingissuer = :iss))"; + + std::string accountStr = KeyUtils::toStrKey(accountID); + + std::string assetCode; + std::string assetIssuer; + if (asset.type() == ASSET_TYPE_CREDIT_ALPHANUM4) + { + assetCodeToStr(asset.alphaNum4().assetCode, assetCode); + assetIssuer = KeyUtils::toStrKey(asset.alphaNum4().issuer); + } + else if (asset.type() == ASSET_TYPE_CREDIT_ALPHANUM12) + { + assetCodeToStr(asset.alphaNum12().assetCode, assetCode); + assetIssuer = KeyUtils::toStrKey(asset.alphaNum12().issuer); + } + else + { + throw std::runtime_error("Invalid asset type"); + } + + auto prep = db.getPreparedStatement(sql); + auto& st = prep.statement(); + st.exchange(use(accountStr, "acc")); + st.exchange(use(assetCode, "code")); + st.exchange(use(assetIssuer, "iss")); + + auto timer = db.getSelectTimer("offer"); + loadOffers(prep, [&retOffers](LedgerEntry const& of) { + retOffers.emplace_back(make_shared(of)); + }); + return retOffers; +} + bool OfferFrame::exists(Database& db, LedgerKey const& key) { diff --git a/src/ledger/OfferFrame.h b/src/ledger/OfferFrame.h index 80d8c618ca..15ae2e71a2 100644 --- a/src/ledger/OfferFrame.h +++ b/src/ledger/OfferFrame.h @@ -98,6 +98,10 @@ class OfferFrame : public EntryFrame static std::unordered_map> loadAllOffers(Database& db); + static std::vector + loadOffersByAccountAndAsset(AccountID const& accountID, Asset const& asset, + Database& db); + static void dropAll(Database& db); private: From b21170ef5a5b951e6a0ff3870fd74f46720f0d4b Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:36 -0500 Subject: [PATCH 05/13] Refactored implementation of exchangeV10 into two functions --- src/transactions/OfferExchange.cpp | 28 ++++++++++++++++++++++++++++ src/transactions/OfferExchange.h | 7 +++++++ 2 files changed, 35 insertions(+) diff --git a/src/transactions/OfferExchange.cpp b/src/transactions/OfferExchange.cpp index bc6bc05628..798da1ada9 100644 --- a/src/transactions/OfferExchange.cpp +++ b/src/transactions/OfferExchange.cpp @@ -426,6 +426,22 @@ calculateOfferValue(int32_t priceN, int32_t priceD, int64_t maxSend, ExchangeResultV10 exchangeV10(Price price, int64_t maxWheatSend, int64_t maxWheatReceive, int64_t maxSheepSend, int64_t maxSheepReceive, bool isPathPayment) +{ + auto beforeThresholds = exchangeV10WithoutPriceErrorThresholds( + price, maxWheatSend, maxWheatReceive, maxSheepSend, maxSheepReceive, + isPathPayment); + return applyPriceErrorThresholds( + price, beforeThresholds.numWheatReceived, beforeThresholds.numSheepSend, + beforeThresholds.wheatStays, isPathPayment); +} + +// See comment before exchangeV10. +ExchangeResultV10 +exchangeV10WithoutPriceErrorThresholds(Price price, int64_t maxWheatSend, + int64_t maxWheatReceive, + int64_t maxSheepSend, + int64_t maxSheepReceive, + bool isPathPayment) { uint128_t wheatValue = calculateOfferValue(price.n, price.d, maxWheatSend, maxSheepReceive); @@ -473,6 +489,18 @@ exchangeV10(Price price, int64_t maxWheatSend, int64_t maxWheatReceive, throw std::runtime_error("sheepSend out of bounds"); } + ExchangeResultV10 res; + res.numWheatReceived = wheatReceive; + res.numSheepSend = sheepSend; + res.wheatStays = wheatStays; + return res; +} + +// See comment before exchangeV10. +ExchangeResultV10 +applyPriceErrorThresholds(Price price, int64_t wheatReceive, int64_t sheepSend, + bool wheatStays, bool isPathPayment) +{ if (wheatReceive > 0 && sheepSend > 0) { uint128_t wheatReceiveValue = bigMultiply(wheatReceive, price.n); diff --git a/src/transactions/OfferExchange.h b/src/transactions/OfferExchange.h index 640908167e..0ea4c20798 100644 --- a/src/transactions/OfferExchange.h +++ b/src/transactions/OfferExchange.h @@ -61,6 +61,13 @@ ExchangeResultV10 exchangeV10(Price price, int64_t maxWheatSend, int64_t maxWheatReceive, int64_t maxSheepSend, int64_t maxSheepReceive, bool isPathPayment); +ExchangeResultV10 exchangeV10WithoutPriceErrorThresholds( + Price price, int64_t maxWheatSend, int64_t maxWheatReceive, + int64_t maxSheepSend, int64_t maxSheepReceive, bool isPathPayment); +ExchangeResultV10 applyPriceErrorThresholds(Price price, int64_t wheatReceive, + int64_t sheepSend, bool wheatStays, + bool isPathPayment); + void adjustOffer(OfferFrame& offer, LedgerManager& lm, AccountFrame::pointer account, Asset const& wheat, TrustFrame::pointer wheatLine, Asset const& sheep, From cdbe53977760d9751f2b8f572f9ac12025a43a76 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:38 -0500 Subject: [PATCH 06/13] Updated TxSetFrame, TransactionFrame, and transactions --- src/herder/HerderImpl.cpp | 2 +- src/herder/TxSetFrame.cpp | 6 +- src/ledger/AccountFrame.cpp | 143 +++++++++++++++++++-- src/ledger/AccountFrame.h | 15 ++- src/ledger/OfferFrame.cpp | 144 ++++++++++++++++++++++ src/ledger/OfferFrame.h | 25 ++++ src/ledger/TrustFrame.cpp | 139 ++++++++++++++++++++- src/ledger/TrustFrame.h | 17 ++- src/test/TestExceptions.cpp | 2 + src/test/TestExceptions.h | 1 + src/transactions/AllowTrustOpFrame.cpp | 47 +++++-- src/transactions/ChangeTrustOpFrame.cpp | 2 +- src/transactions/CreateAccountOpFrame.cpp | 15 +-- src/transactions/InflationOpFrame.cpp | 11 +- src/transactions/ManageOfferOpFrame.cpp | 111 +++++++++++++---- src/transactions/MergeOpFrame.cpp | 8 +- src/transactions/OfferExchange.cpp | 64 +++++++--- src/transactions/OfferExchange.h | 7 +- src/transactions/PathPaymentOpFrame.cpp | 12 +- src/transactions/TransactionFrame.cpp | 17 +-- src/xdr/Stellar-transaction.x | 4 +- 21 files changed, 689 insertions(+), 103 deletions(-) diff --git a/src/herder/HerderImpl.cpp b/src/herder/HerderImpl.cpp index 3bcb1ceadd..80607f7924 100644 --- a/src/herder/HerderImpl.cpp +++ b/src/herder/HerderImpl.cpp @@ -341,7 +341,7 @@ HerderImpl::recvTransaction(TransactionFramePtr tx) return TX_STATUS_ERROR; } - if (tx->getSourceAccount().getBalanceAboveReserve(mLedgerManager) < totFee) + if (tx->getSourceAccount().getAvailableBalance(mLedgerManager) < totFee) { tx->getResult().result.code(txINSUFFICIENT_BALANCE); return TX_STATUS_ERROR; diff --git a/src/herder/TxSetFrame.cpp b/src/herder/TxSetFrame.cpp index d5f9935dd3..d4ac490efa 100644 --- a/src/herder/TxSetFrame.cpp +++ b/src/herder/TxSetFrame.cpp @@ -234,10 +234,8 @@ TxSetFrame::checkOrTrim( if (lastTx) { // make sure account can pay the fee for all these tx - int64_t newBalance = - lastTx->getSourceAccount().getBalance() - totFee; - if (newBalance < lastTx->getSourceAccount().getMinimumBalance( - app.getLedgerManager())) + auto const& source = lastTx->getSourceAccount(); + if (source.getAvailableBalance(app.getLedgerManager()) < totFee) { if (!processInsufficientBalance(item.second)) return false; diff --git a/src/ledger/AccountFrame.cpp b/src/ledger/AccountFrame.cpp index a4dea3412f..429685f8d0 100644 --- a/src/ledger/AccountFrame.cpp +++ b/src/ledger/AccountFrame.cpp @@ -118,10 +118,119 @@ AccountFrame::getBalance() const return (mAccountEntry.balance); } +int64_t +getBuyingLiabilities(AccountEntry const& acc, LedgerManager const& lm) +{ + assert(lm.getCurrentLedgerVersion() >= 10); + return (acc.ext.v() == 0) ? 0 : acc.ext.v1().liabilities.buying; +} + +int64_t +getSellingLiabilities(AccountEntry const& acc, LedgerManager const& lm) +{ + assert(lm.getCurrentLedgerVersion() >= 10); + return (acc.ext.v() == 0) ? 0 : acc.ext.v1().liabilities.selling; +} + +int64_t +AccountFrame::getBuyingLiabilities(LedgerManager const& lm) const +{ + return stellar::getBuyingLiabilities(mAccountEntry, lm); +} + +int64_t +AccountFrame::getSellingLiabilities(LedgerManager const& lm) const +{ + return stellar::getSellingLiabilities(mAccountEntry, lm); +} + +bool +AccountFrame::addBuyingLiabilities(int64_t delta, LedgerManager const& lm) +{ + assert(lm.getCurrentLedgerVersion() >= 10); + assert(getBalance() >= 0); + if (delta == 0) + { + return true; + } + int64_t buyingLiab = (mAccountEntry.ext.v() == 0) + ? 0 + : mAccountEntry.ext.v1().liabilities.buying; + + int64_t maxLiabilities = INT64_MAX - getBalance(); + bool res = stellar::addBalance(buyingLiab, delta, maxLiabilities); + if (res) + { + if (mAccountEntry.ext.v() == 0) + { + mAccountEntry.ext.v(1); + mAccountEntry.ext.v1().liabilities = Liabilities{0, 0}; + } + mAccountEntry.ext.v1().liabilities.buying = buyingLiab; + } + return res; +} + +bool +AccountFrame::addSellingLiabilities(int64_t delta, LedgerManager const& lm) +{ + assert(lm.getCurrentLedgerVersion() >= 10); + assert(getBalance() >= 0); + if (delta == 0) + { + return true; + } + int64_t sellingLiab = (mAccountEntry.ext.v() == 0) + ? 0 + : mAccountEntry.ext.v1().liabilities.selling; + + int64_t maxLiabilities = getBalance() - getMinimumBalance(lm); + if (maxLiabilities < 0) + { + return false; + } + + bool res = stellar::addBalance(sellingLiab, delta, maxLiabilities); + if (res) + { + if (mAccountEntry.ext.v() == 0) + { + mAccountEntry.ext.v(1); + mAccountEntry.ext.v1().liabilities = Liabilities{0, 0}; + } + mAccountEntry.ext.v1().liabilities.selling = sellingLiab; + } + return res; +} + bool -AccountFrame::addBalance(int64_t delta) +AccountFrame::addBalance(int64_t delta, LedgerManager const& lm) { - return stellar::addBalance(mAccountEntry.balance, delta); + if (delta == 0) + { + return true; + } + + auto newBalance = mAccountEntry.balance; + if (!stellar::addBalance(newBalance, delta)) + { + return false; + } + if (lm.getCurrentLedgerVersion() >= 10) + { + if (delta < 0 && + newBalance - getMinimumBalance(lm) < getSellingLiabilities(lm)) + { + return false; + } + if (newBalance > INT64_MAX - getBuyingLiabilities(lm)) + { + return false; + } + } + + mAccountEntry.balance = newBalance; + return true; } int64_t @@ -131,19 +240,30 @@ AccountFrame::getMinimumBalance(LedgerManager const& lm) const } int64_t -AccountFrame::getBalanceAboveReserve(LedgerManager const& lm) const +AccountFrame::getAvailableBalance(LedgerManager const& lm) const { int64_t avail = getBalance() - lm.getMinBalance(mAccountEntry.numSubEntries); - if (avail < 0) + if (lm.getCurrentLedgerVersion() >= 10) { - // nothing can leave this account if below the reserve - // (this can happen if the reserve is raised) - avail = 0; + avail -= getSellingLiabilities(lm); } return avail; } +int64_t +AccountFrame::getMaxAmountReceive(LedgerManager const& lm) const +{ + if (lm.getCurrentLedgerVersion() >= 10) + { + return INT64_MAX - getBalance() - getBuyingLiabilities(lm); + } + else + { + return INT64_MAX; + } +} + // returns true if successfully updated, // false if balance is not sufficient bool @@ -154,8 +274,15 @@ AccountFrame::addNumEntries(int count, LedgerManager const& lm) { throw std::runtime_error("invalid account state"); } + + int64_t effMinBalance = lm.getMinBalance(newEntriesCount); + if (lm.getCurrentLedgerVersion() >= 10) + { + effMinBalance += getSellingLiabilities(lm); + } + // only check minBalance when attempting to add subEntries - if (count > 0 && getBalance() < lm.getMinBalance(newEntriesCount)) + if (count > 0 && getBalance() < effMinBalance) { // balance too low return false; diff --git a/src/ledger/AccountFrame.h b/src/ledger/AccountFrame.h index 90d67bc1d1..9ecc988583 100644 --- a/src/ledger/AccountFrame.h +++ b/src/ledger/AccountFrame.h @@ -23,6 +23,9 @@ namespace stellar class LedgerManager; class LedgerRange; +int64_t getBuyingLiabilities(AccountEntry const& acc, LedgerManager const& lm); +int64_t getSellingLiabilities(AccountEntry const& acc, LedgerManager const& lm); + class AccountFrame : public EntryFrame { void storeUpdate(LedgerDelta& delta, Database& db, bool insert); @@ -63,14 +66,22 @@ class AccountFrame : public EntryFrame // actual balance for the account int64_t getBalance() const; + int64_t getBuyingLiabilities(LedgerManager const& lm) const; + int64_t getSellingLiabilities(LedgerManager const& lm) const; + + bool addBuyingLiabilities(int64_t delta, LedgerManager const& lm); + bool addSellingLiabilities(int64_t delta, LedgerManager const& lm); + // update balance for account - bool addBalance(int64_t delta); + bool addBalance(int64_t delta, LedgerManager const& lm); // reserve balance that the account must always hold int64_t getMinimumBalance(LedgerManager const& lm) const; // balance that can be spent (above the limit) - int64_t getBalanceAboveReserve(LedgerManager const& lm) const; + int64_t getAvailableBalance(LedgerManager const& lm) const; + + int64_t getMaxAmountReceive(LedgerManager const& lm) const; // returns true if successfully updated, // false if balance is not sufficient diff --git a/src/ledger/OfferFrame.cpp b/src/ledger/OfferFrame.cpp index 02f310b114..ad6d4ea5f3 100644 --- a/src/ledger/OfferFrame.cpp +++ b/src/ledger/OfferFrame.cpp @@ -9,7 +9,9 @@ #include "crypto/SecretKey.h" #include "database/Database.h" #include "ledger/LedgerRange.h" +#include "ledger/TrustFrame.h" #include "transactions/ManageOfferOpFrame.h" +#include "transactions/OfferExchange.h" #include "util/types.h" using namespace std; @@ -52,6 +54,22 @@ static const char* offerColumnSelector = "flags,lastmodified " "FROM offers"; +int64_t +getSellingLiabilities(OfferEntry const& oe) +{ + auto res = exchangeV10WithoutPriceErrorThresholds( + oe.price, oe.amount, INT64_MAX, INT64_MAX, INT64_MAX, false); + return res.numWheatReceived; +} + +int64_t +getBuyingLiabilities(OfferEntry const& oe) +{ + auto res = exchangeV10WithoutPriceErrorThresholds( + oe.price, oe.amount, INT64_MAX, INT64_MAX, INT64_MAX, false); + return res.numSheepSend; +} + OfferFrame::OfferFrame() : EntryFrame(OFFER), mOffer(mEntry.data.offer()) { } @@ -118,6 +136,18 @@ OfferFrame::getFlags() const return mOffer.flags; } +int64_t +OfferFrame::getSellingLiabilities() const +{ + return stellar::getSellingLiabilities(mOffer); +} + +int64_t +OfferFrame::getBuyingLiabilities() const +{ + return stellar::getBuyingLiabilities(mOffer); +} + OfferFrame::pointer OfferFrame::loadOffer(AccountID const& sellerID, uint64_t offerID, Database& db, LedgerDelta* delta) @@ -600,4 +630,118 @@ OfferFrame::dropAll(Database& db) db.getSession() << kSQLCreateStatement3; db.getSession() << kSQLCreateStatement4; } + +void +OfferFrame::releaseLiabilities(AccountFrame::pointer const& account, + TrustFrame::pointer const& buyingTrust, + TrustFrame::pointer const& sellingTrust, + LedgerDelta& delta, Database& db, + LedgerManager& ledgerManager) +{ + acquireOrReleaseLiabilities(false, account, buyingTrust, sellingTrust, + delta, db, ledgerManager); +} + +void +OfferFrame::acquireLiabilities(AccountFrame::pointer const& account, + TrustFrame::pointer const& buyingTrust, + TrustFrame::pointer const& sellingTrust, + LedgerDelta& delta, Database& db, + LedgerManager& ledgerManager) +{ + acquireOrReleaseLiabilities(true, account, buyingTrust, sellingTrust, delta, + db, ledgerManager); +} + +void +OfferFrame::acquireOrReleaseLiabilities(bool isAcquire, + AccountFrame::pointer const& account, + TrustFrame::pointer const& buyingTrust, + TrustFrame::pointer const& sellingTrust, + LedgerDelta& delta, Database& db, + LedgerManager& ledgerManager) +{ + // This should never happen + if (getBuying() == getSelling()) + { + throw std::runtime_error("buying and selling same asset"); + } + + auto loadAccountIfNecessaryAndValidate = [this, &account, &delta, &db]() { + AccountFrame::pointer acc = account; + if (!acc) + { + acc = AccountFrame::loadAccount(delta, getSellerID(), db); + assert(acc); + } + assert(acc->getID() == getSellerID()); + return acc; + }; + + auto loadTrustIfNecessaryAndValidate = [this, &delta, &db]( + TrustFrame::pointer const& trust, + Asset const& asset) { + TrustFrame::pointer tf = trust; + if (!tf) + { + tf = TrustFrame::loadTrustLine(getSellerID(), asset, db, &delta); + assert(tf); + } + assert(tf->getTrustLine().accountID == getSellerID()); + assert(tf->getTrustLine().asset == asset); + return tf; + }; + + int64_t buyingLiabilities = + isAcquire ? getBuyingLiabilities() : -getBuyingLiabilities(); + Asset const& buyingAsset = getBuying(); + if (buyingAsset.type() == ASSET_TYPE_NATIVE) + { + auto acc = loadAccountIfNecessaryAndValidate(); + bool res = acc->addBuyingLiabilities(buyingLiabilities, ledgerManager); + if (!res) + { + throw std::runtime_error("could not add buying liabilities"); + } + acc->storeChange(delta, db); + } + else + { + auto trust = loadTrustIfNecessaryAndValidate(buyingTrust, buyingAsset); + bool res = + trust->addBuyingLiabilities(buyingLiabilities, ledgerManager); + if (!res) + { + throw std::runtime_error("could not add buying liabilities"); + } + trust->storeChange(delta, db); + } + + int64_t sellingLiabilities = + isAcquire ? getSellingLiabilities() : -getSellingLiabilities(); + Asset const& sellingAsset = getSelling(); + if (sellingAsset.type() == ASSET_TYPE_NATIVE) + { + auto acc = loadAccountIfNecessaryAndValidate(); + bool res = + acc->addSellingLiabilities(sellingLiabilities, ledgerManager); + if (!res) + { + throw std::runtime_error("could not add selling liabilities"); + } + acc->storeChange(delta, db); + } + else + { + auto trust = + loadTrustIfNecessaryAndValidate(sellingTrust, sellingAsset); + bool res = + trust->addSellingLiabilities(sellingLiabilities, ledgerManager); + if (!res) + { + throw std::runtime_error("could not add selling liabilities"); + } + trust->storeChange(delta, db); + } +} } diff --git a/src/ledger/OfferFrame.h b/src/ledger/OfferFrame.h index 15ae2e71a2..1a1c1b4b2d 100644 --- a/src/ledger/OfferFrame.h +++ b/src/ledger/OfferFrame.h @@ -5,6 +5,7 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "ledger/EntryFrame.h" +#include "ledger/TrustFrame.h" #include #include @@ -19,6 +20,9 @@ class LedgerRange; class ManageOfferOpFrame; class StatementContext; +int64_t getSellingLiabilities(OfferEntry const& oe); +int64_t getBuyingLiabilities(OfferEntry const& oe); + class OfferFrame : public EntryFrame { static void @@ -59,6 +63,9 @@ class OfferFrame : public EntryFrame uint64 getOfferID() const; uint32 getFlags() const; + int64_t getSellingLiabilities() const; + int64_t getBuyingLiabilities() const; + OfferEntry const& getOffer() const { @@ -104,7 +111,25 @@ class OfferFrame : public EntryFrame static void dropAll(Database& db); + void releaseLiabilities(AccountFrame::pointer const& account, + TrustFrame::pointer const& buyingTrust, + TrustFrame::pointer const& sellingTrust, + LedgerDelta& delta, Database& db, + LedgerManager& ledgerManager); + void acquireLiabilities(AccountFrame::pointer const& account, + TrustFrame::pointer const& buyingTrust, + TrustFrame::pointer const& sellingTrust, + LedgerDelta& delta, Database& db, + LedgerManager& ledgerManager); + private: + void acquireOrReleaseLiabilities(bool isAcquire, + AccountFrame::pointer const& account, + TrustFrame::pointer const& buyingTrust, + TrustFrame::pointer const& sellingTrust, + LedgerDelta& delta, Database& db, + LedgerManager& ledgerManager); + static const char* kSQLCreateStatement1; static const char* kSQLCreateStatement2; static const char* kSQLCreateStatement3; diff --git a/src/ledger/TrustFrame.cpp b/src/ledger/TrustFrame.cpp index 6872d3e839..018b219fc2 100644 --- a/src/ledger/TrustFrame.cpp +++ b/src/ledger/TrustFrame.cpp @@ -8,6 +8,7 @@ #include "crypto/SHA.h" #include "crypto/SecretKey.h" #include "database/Database.h" +#include "ledger/LedgerManager.h" #include "ledger/LedgerRange.h" #include "util/XDROperators.h" #include "util/types.h" @@ -90,6 +91,115 @@ TrustFrame::getBalance() const return mTrustLine.balance; } +int64_t +TrustFrame::getAvailableBalance(LedgerManager const& lm) const +{ + int64_t availableBalance = getBalance(); + if (lm.getCurrentLedgerVersion() >= 10) + { + availableBalance -= getSellingLiabilities(lm); + } + return availableBalance; +} + +int64_t +TrustFrame::getMinimumLimit(LedgerManager const& lm) const +{ + int64_t minLimit = getBalance(); + if (lm.getCurrentLedgerVersion() >= 10) + { + minLimit += getBuyingLiabilities(lm); + } + return minLimit; +} + +int64_t +getBuyingLiabilities(TrustLineEntry const& tl, LedgerManager const& lm) +{ + assert(lm.getCurrentLedgerVersion() >= 10); + return (tl.ext.v() == 0) ? 0 : tl.ext.v1().liabilities.buying; +} + +int64_t +getSellingLiabilities(TrustLineEntry const& tl, LedgerManager const& lm) +{ + assert(lm.getCurrentLedgerVersion() >= 10); + return (tl.ext.v() == 0) ? 0 : tl.ext.v1().liabilities.selling; +} + +int64_t +TrustFrame::getBuyingLiabilities(LedgerManager const& lm) const +{ + return stellar::getBuyingLiabilities(mTrustLine, lm); +} + +int64_t +TrustFrame::getSellingLiabilities(LedgerManager const& lm) const +{ + return stellar::getSellingLiabilities(mTrustLine, lm); +} + +bool +TrustFrame::addBuyingLiabilities(int64_t delta, LedgerManager const& lm) +{ + assert(lm.getCurrentLedgerVersion() >= 10); + assert(getBalance() >= 0); + assert(mTrustLine.limit >= 0); + if (mIsIssuer || delta == 0) + { + return true; + } + if (!isAuthorized()) + { + return false; + } + int64_t buyingLiab = + (mTrustLine.ext.v() == 0) ? 0 : mTrustLine.ext.v1().liabilities.buying; + + int64_t maxLiabilities = mTrustLine.limit - getBalance(); + bool res = stellar::addBalance(buyingLiab, delta, maxLiabilities); + if (res) + { + if (mTrustLine.ext.v() == 0) + { + mTrustLine.ext.v(1); + mTrustLine.ext.v1().liabilities = Liabilities{0, 0}; + } + mTrustLine.ext.v1().liabilities.buying = buyingLiab; + } + return res; +} + +bool +TrustFrame::addSellingLiabilities(int64_t delta, LedgerManager const& lm) +{ + assert(lm.getCurrentLedgerVersion() >= 10); + assert(getBalance() >= 0); + if (mIsIssuer || delta == 0) + { + return true; + } + if (!isAuthorized()) + { + return false; + } + int64_t sellingLiab = + (mTrustLine.ext.v() == 0) ? 0 : mTrustLine.ext.v1().liabilities.selling; + + int64_t maxLiabilities = mTrustLine.balance; + bool res = stellar::addBalance(sellingLiab, delta, maxLiabilities); + if (res) + { + if (mTrustLine.ext.v() == 0) + { + mTrustLine.ext.v(1); + mTrustLine.ext.v1().liabilities = Liabilities{0, 0}; + } + mTrustLine.ext.v1().liabilities.selling = sellingLiab; + } + return res; +} + bool TrustFrame::isAuthorized() const { @@ -110,7 +220,7 @@ TrustFrame::setAuthorized(bool authorized) } bool -TrustFrame::addBalance(int64_t delta) +TrustFrame::addBalance(int64_t delta, LedgerManager const& lm) { if (mIsIssuer || delta == 0) { @@ -120,11 +230,30 @@ TrustFrame::addBalance(int64_t delta) { return false; } - return stellar::addBalance(mTrustLine.balance, delta, mTrustLine.limit); + + auto newBalance = mTrustLine.balance; + if (!stellar::addBalance(newBalance, delta, mTrustLine.limit)) + { + return false; + } + if (lm.getCurrentLedgerVersion() >= 10) + { + if (newBalance < getSellingLiabilities(lm)) + { + return false; + } + if (newBalance > mTrustLine.limit - getBuyingLiabilities(lm)) + { + return false; + } + } + + mTrustLine.balance = newBalance; + return true; } int64_t -TrustFrame::getMaxAmountReceive() const +TrustFrame::getMaxAmountReceive(LedgerManager& lm) const { int64_t amount = 0; if (mIsIssuer) @@ -134,6 +263,10 @@ TrustFrame::getMaxAmountReceive() const else if (isAuthorized()) { amount = mTrustLine.limit - mTrustLine.balance; + if (lm.getCurrentLedgerVersion() >= 10) + { + amount -= getBuyingLiabilities(lm); + } } return amount; } diff --git a/src/ledger/TrustFrame.h b/src/ledger/TrustFrame.h index 5128e3c41c..d12e6c8c8b 100644 --- a/src/ledger/TrustFrame.h +++ b/src/ledger/TrustFrame.h @@ -25,6 +25,10 @@ class LedgerRange; class TrustSetTx; class StatementContext; +int64_t getBuyingLiabilities(TrustLineEntry const& tl, LedgerManager const& lm); +int64_t getSellingLiabilities(TrustLineEntry const& tl, + LedgerManager const& lm); + class TrustFrame : public EntryFrame { public: @@ -90,13 +94,22 @@ class TrustFrame : public EntryFrame loadAllLines(Database& db); int64_t getBalance() const; - bool addBalance(int64_t delta); + bool addBalance(int64_t delta, LedgerManager const& lm); + + int64_t getAvailableBalance(LedgerManager const& lm) const; + int64_t getMinimumLimit(LedgerManager const& lm) const; + + int64_t getBuyingLiabilities(LedgerManager const& lm) const; + int64_t getSellingLiabilities(LedgerManager const& lm) const; + + bool addBuyingLiabilities(int64_t delta, LedgerManager const& lm); + bool addSellingLiabilities(int64_t delta, LedgerManager const& lm); bool isAuthorized() const; void setAuthorized(bool authorized); // returns the maximum amount that can be added to this trust line - int64_t getMaxAmountReceive() const; + int64_t getMaxAmountReceive(LedgerManager& lm) const; TrustLineEntry const& getTrustLine() const diff --git a/src/test/TestExceptions.cpp b/src/test/TestExceptions.cpp index 1ac7d75101..ef44263b6d 100644 --- a/src/test/TestExceptions.cpp +++ b/src/test/TestExceptions.cpp @@ -221,6 +221,8 @@ throwIf(AccountMergeResult const& result) throw ex_ACCOUNT_MERGE_HAS_SUB_ENTRIES{}; case ACCOUNT_MERGE_SEQNUM_TOO_FAR: throw ex_ACCOUNT_MERGE_SEQNUM_TOO_FAR{}; + case ACCOUNT_MERGE_DEST_FULL: + throw ex_ACCOUNT_MERGE_DEST_FULL{}; case ACCOUNT_MERGE_SUCCESS: break; default: diff --git a/src/test/TestExceptions.h b/src/test/TestExceptions.h index cb44c69b44..b5ae9d31d3 100644 --- a/src/test/TestExceptions.h +++ b/src/test/TestExceptions.h @@ -40,6 +40,7 @@ TEST_EXCEPTION(ex_ACCOUNT_MERGE_NO_ACCOUNT) TEST_EXCEPTION(ex_ACCOUNT_MERGE_IMMUTABLE_SET) TEST_EXCEPTION(ex_ACCOUNT_MERGE_HAS_SUB_ENTRIES) TEST_EXCEPTION(ex_ACCOUNT_MERGE_SEQNUM_TOO_FAR) +TEST_EXCEPTION(ex_ACCOUNT_MERGE_DEST_FULL) TEST_EXCEPTION(ex_ALLOW_TRUST_MALFORMED) TEST_EXCEPTION(ex_ALLOW_TRUST_NO_TRUST_LINE) diff --git a/src/transactions/AllowTrustOpFrame.cpp b/src/transactions/AllowTrustOpFrame.cpp index 16c631940b..2cad1136a2 100644 --- a/src/transactions/AllowTrustOpFrame.cpp +++ b/src/transactions/AllowTrustOpFrame.cpp @@ -4,7 +4,9 @@ #include "transactions/AllowTrustOpFrame.h" #include "database/Database.h" +#include "ledger/LedgerDelta.h" #include "ledger/LedgerManager.h" +#include "ledger/OfferFrame.h" #include "ledger/TrustFrame.h" #include "main/Application.h" #include "medida/meter.h" @@ -79,9 +81,8 @@ AllowTrustOpFrame::doApply(Application& app, LedgerDelta& delta, } Database& db = ledgerManager.getDatabase(); - TrustFrame::pointer trustLine; - trustLine = TrustFrame::loadTrustLine(mAllowTrust.trustor, ci, db, &delta); - + TrustFrame::pointer trustLine = + TrustFrame::loadTrustLine(mAllowTrust.trustor, ci, db, &delta); if (!trustLine) { app.getMetrics() @@ -92,15 +93,45 @@ AllowTrustOpFrame::doApply(Application& app, LedgerDelta& delta, return false; } - app.getMetrics() - .NewMeter({"op-allow-trust", "success", "apply"}, "operation") - .Mark(); - innerResult().code(ALLOW_TRUST_SUCCESS); + bool wasAuthorized = trustLine->isAuthorized(); + bool didRevokeAuth = wasAuthorized && !mAllowTrust.authorize; + if (ledgerManager.getCurrentLedgerVersion() >= 10 && didRevokeAuth) + { + auto trustAcc = + AccountFrame::loadAccount(delta, mAllowTrust.trustor, db); + + // Delete all offers owned by the trustor that are either buying or + // selling the asset which had authorization revoked. + auto offers = OfferFrame::loadOffersByAccountAndAsset( + mAllowTrust.trustor, ci, db); + for (auto& offer : offers) + { + delta.recordEntry(*offer); + + if (offer->getBuying() == ci) + { + offer->releaseLiabilities(trustAcc, trustLine, nullptr, delta, + db, ledgerManager); + } + else if (offer->getSelling() == ci) + { + offer->releaseLiabilities(trustAcc, nullptr, trustLine, delta, + db, ledgerManager); + } + + trustAcc->addNumEntries(-1, ledgerManager); + trustAcc->storeChange(delta, db); + offer->storeDelete(delta, db); + } + } trustLine->setAuthorized(mAllowTrust.authorize); - trustLine->storeChange(delta, db); + app.getMetrics() + .NewMeter({"op-allow-trust", "success", "apply"}, "operation") + .Mark(); + innerResult().code(ALLOW_TRUST_SUCCESS); return true; } diff --git a/src/transactions/ChangeTrustOpFrame.cpp b/src/transactions/ChangeTrustOpFrame.cpp index 2d6e0a85cd..f0df1f66ce 100644 --- a/src/transactions/ChangeTrustOpFrame.cpp +++ b/src/transactions/ChangeTrustOpFrame.cpp @@ -50,7 +50,7 @@ ChangeTrustOpFrame::doApply(Application& app, LedgerDelta& delta, if (trustLine) { // we are modifying an old trustline - if (mChangeTrust.limit < trustLine->getBalance()) + if (mChangeTrust.limit < trustLine->getMinimumLimit(ledgerManager)) { // Can't drop the limit // below the balance you // are holding with them diff --git a/src/transactions/CreateAccountOpFrame.cpp b/src/transactions/CreateAccountOpFrame.cpp index acfe976e2c..dcd75e4e8a 100644 --- a/src/transactions/CreateAccountOpFrame.cpp +++ b/src/transactions/CreateAccountOpFrame.cpp @@ -53,10 +53,7 @@ CreateAccountOpFrame::doApply(Application& app, LedgerDelta& delta, } else { - int64_t minBalance = - mSourceAccount->getMinimumBalance(ledgerManager); - - if ((mSourceAccount->getAccount().balance - minBalance) < + if (mSourceAccount->getAvailableBalance(ledgerManager) < mCreateAccount.startingBalance) { // they don't have enough to send app.getMetrics() @@ -67,16 +64,16 @@ CreateAccountOpFrame::doApply(Application& app, LedgerDelta& delta, return false; } - auto ok = - mSourceAccount->addBalance(-mCreateAccount.startingBalance); + auto ok = mSourceAccount->addBalance( + -mCreateAccount.startingBalance, ledgerManager); assert(ok); mSourceAccount->storeChange(delta, db); destAccount = make_shared(mCreateAccount.destination); - destAccount->getAccount().seqNum = - delta.getHeaderFrame().getStartingSequenceNumber(); - destAccount->getAccount().balance = mCreateAccount.startingBalance; + auto& acc = destAccount->getAccount(); + acc.seqNum = delta.getHeaderFrame().getStartingSequenceNumber(); + acc.balance = mCreateAccount.startingBalance; destAccount->storeAdd(delta, db); diff --git a/src/transactions/InflationOpFrame.cpp b/src/transactions/InflationOpFrame.cpp index e29a037b38..f46ceb88a3 100644 --- a/src/transactions/InflationOpFrame.cpp +++ b/src/transactions/InflationOpFrame.cpp @@ -103,12 +103,21 @@ InflationOpFrame::doApply(Application& app, LedgerDelta& delta, if (winner) { + if (ledgerManager.getCurrentLedgerVersion() >= 10) + { + toDoleThisWinner = + std::min(winner->getMaxAmountReceive(ledgerManager), + toDoleThisWinner); + if (toDoleThisWinner == 0) + continue; + } + leftAfterDole -= toDoleThisWinner; if (ledgerManager.getCurrentLedgerVersion() <= 7) { lcl.totalCoins += toDoleThisWinner; } - if (!winner->addBalance(toDoleThisWinner)) + if (!winner->addBalance(toDoleThisWinner, ledgerManager)) { throw std::runtime_error( "inflation overflowed destination balance"); diff --git a/src/transactions/ManageOfferOpFrame.cpp b/src/transactions/ManageOfferOpFrame.cpp index cf33e16341..33eebfc1a9 100644 --- a/src/transactions/ManageOfferOpFrame.cpp +++ b/src/transactions/ManageOfferOpFrame.cpp @@ -167,6 +167,31 @@ ManageOfferOpFrame::doApply(Application& app, LedgerDelta& delta, return false; } + // We are releasing the liabilites associated with this offer. This is + // required in order to produce available balance for the offer to be + // executed. Both trust lines must be reset since it is possible that + // the assets are updated (including the edge case that the buying and + // selling assets are swapped). + if (ledgerManager.getCurrentLedgerVersion() >= 10) + { + mWheatLineA.reset(); + mSheepLineA.reset(); + + mSellSheepOffer->releaseLiabilities( + mSourceAccount, nullptr, nullptr, tempDelta, db, ledgerManager); + + if (sheep.type() != ASSET_TYPE_NATIVE) + { + mSheepLineA = TrustFrame::loadTrustLine(getSourceID(), sheep, + db, &tempDelta); + } + if (wheat.type() != ASSET_TYPE_NATIVE) + { + mWheatLineA = TrustFrame::loadTrustLine(getSourceID(), wheat, + db, &tempDelta); + } + } + // WARNING: mSellSheepOffer is deleted but mSourceAccount is not updated // to reflect the change in numSubEntries at this point. However, we // can't delete it here since doing so would modify mSourceAccount, @@ -199,34 +224,62 @@ ManageOfferOpFrame::doApply(Application& app, LedgerDelta& delta, } else { - if (sheep.type() == ASSET_TYPE_NATIVE) + auto ledgerVersion = app.getLedgerManager().getCurrentLedgerVersion(); + if (creatingNewOffer && + (ledgerVersion >= 10 || + (sheep.type() == ASSET_TYPE_NATIVE && ledgerVersion > 8))) { - if (creatingNewOffer && - app.getLedgerManager().getCurrentLedgerVersion() > 8) + // we need to compute maxAmountOfSheepCanSell based on the + // updated reserve to avoid selling too many and falling + // below the reserve when we try to create the offer later on + if (!mSourceAccount->addNumEntries(1, ledgerManager)) { - // we need to compute maxAmountOfSheepCanSell based on the - // updated reserve to avoid selling too many and falling - // below the reserve when we try to create the offer later on - if (!mSourceAccount->addNumEntries(1, ledgerManager)) - { - app.getMetrics() - .NewMeter({"op-manage-offer", "invalid", "low reserve"}, - "operation") - .Mark(); - innerResult().code(MANAGE_OFFER_LOW_RESERVE); - return false; - } - adjusted = true; + app.getMetrics() + .NewMeter({"op-manage-offer", "invalid", "low reserve"}, + "operation") + .Mark(); + innerResult().code(MANAGE_OFFER_LOW_RESERVE); + return false; } + adjusted = true; } Price const& sheepPrice = mSellSheepOffer->getPrice(); const Price maxWheatPrice(sheepPrice.d, sheepPrice.n); - int64_t maxWheatReceive = canBuyAtMost(wheat, mWheatLineA); + int64_t maxWheatReceive = + canBuyAtMost(mSourceAccount, wheat, mWheatLineA, ledgerManager); int64_t maxSheepSend; if (app.getLedgerManager().getCurrentLedgerVersion() >= 10) { + int64_t availableLimit = + (wheat.type() == ASSET_TYPE_NATIVE) + ? mSourceAccount->getMaxAmountReceive(ledgerManager) + : mWheatLineA->getMaxAmountReceive(ledgerManager); + if (availableLimit < mSellSheepOffer->getBuyingLiabilities()) + { + app.getMetrics() + .NewMeter({"op-manage-offer", "invalid", "line-full"}, + "operation") + .Mark(); + innerResult().code(MANAGE_OFFER_LINE_FULL); + return false; + } + + int64_t availableBalance = + (sheep.type() == ASSET_TYPE_NATIVE) + ? mSourceAccount->getAvailableBalance(ledgerManager) + : mSheepLineA->getAvailableBalance(ledgerManager); + if (availableBalance < mSellSheepOffer->getSellingLiabilities()) + { + app.getMetrics() + .NewMeter({"op-manage-offer", "invalid", "underfunded"}, + "operation") + .Mark(); + innerResult().code(MANAGE_OFFER_UNDERFUNDED); + return false; + } + maxSheepSend = canSellAtMost(mSourceAccount, sheep, mSheepLineA, ledgerManager); } @@ -318,42 +371,42 @@ ManageOfferOpFrame::doApply(Application& app, LedgerDelta& delta, // here as OfferExchange won't cross offers from source account if (wheat.type() == ASSET_TYPE_NATIVE) { - if (!mSourceAccount->addBalance(wheatReceived)) + if (!mSourceAccount->addBalance(wheatReceived, ledgerManager)) { // this would indicate a bug in OfferExchange throw std::runtime_error("offer claimed over limit"); } - mSourceAccount->storeChange(delta, db); + mSourceAccount->storeChange(tempDelta, db); } else { - if (!mWheatLineA->addBalance(wheatReceived)) + if (!mWheatLineA->addBalance(wheatReceived, ledgerManager)) { // this would indicate a bug in OfferExchange throw std::runtime_error("offer claimed over limit"); } - mWheatLineA->storeChange(delta, db); + mWheatLineA->storeChange(tempDelta, db); } if (sheep.type() == ASSET_TYPE_NATIVE) { - if (!mSourceAccount->addBalance(-sheepSent)) + if (!mSourceAccount->addBalance(-sheepSent, ledgerManager)) { // this would indicate a bug in OfferExchange throw std::runtime_error("offer sold more than balance"); } - mSourceAccount->storeChange(delta, db); + mSourceAccount->storeChange(tempDelta, db); } else { - if (!mSheepLineA->addBalance(-sheepSent)) + if (!mSheepLineA->addBalance(-sheepSent, ledgerManager)) { // this would indicate a bug in OfferExchange throw std::runtime_error("offer sold more than balance"); } - mSheepLineA->storeChange(delta, db); + mSheepLineA->storeChange(tempDelta, db); } } @@ -374,7 +427,6 @@ ManageOfferOpFrame::doApply(Application& app, LedgerDelta& delta, if (mSellSheepOffer->getOffer().amount > 0) { // we still have sheep to sell so leave an offer - if (creatingNewOffer) { // make sure we don't allow us to add offers when we don't have @@ -399,6 +451,13 @@ ManageOfferOpFrame::doApply(Application& app, LedgerDelta& delta, } mSellSheepOffer->storeAdd(tempDelta, db); innerResult().success().offer.offer() = mSellSheepOffer->getOffer(); + + if (ledgerManager.getCurrentLedgerVersion() >= 10) + { + mSellSheepOffer->acquireLiabilities(mSourceAccount, mWheatLineA, + mSheepLineA, tempDelta, db, + ledgerManager); + } } else { diff --git a/src/transactions/MergeOpFrame.cpp b/src/transactions/MergeOpFrame.cpp index dbc73cc1df..a38b2bbff1 100644 --- a/src/transactions/MergeOpFrame.cpp +++ b/src/transactions/MergeOpFrame.cpp @@ -111,9 +111,13 @@ MergeOpFrame::doApply(Application& app, LedgerDelta& delta, } // "success" path starts - if (!otherAccount->addBalance(sourceBalance)) + if (!otherAccount->addBalance(sourceBalance, ledgerManager)) { - throw std::runtime_error("merge overflowed destination balance"); + app.getMetrics() + .NewMeter({"op-merge", "failure", "dest-full"}, "operation") + .Mark(); + innerResult().code(ACCOUNT_MERGE_DEST_FULL); + return false; } otherAccount->storeChange(delta, db); diff --git a/src/transactions/OfferExchange.cpp b/src/transactions/OfferExchange.cpp index 798da1ada9..ca3f2e4616 100644 --- a/src/transactions/OfferExchange.cpp +++ b/src/transactions/OfferExchange.cpp @@ -16,7 +16,7 @@ namespace stellar // while buying as much sheep as possible int64_t canSellAtMostBasedOnSheep(Asset const& sheep, TrustFrame::pointer sheepLine, - Price const& wheatPrice) + Price const& wheatPrice, LedgerManager& ledgerManager) { if (sheep.type() == ASSET_TYPE_NATIVE) { @@ -24,7 +24,8 @@ canSellAtMostBasedOnSheep(Asset const& sheep, TrustFrame::pointer sheepLine, } // compute value based on what the account can receive - auto sellerMaxSheep = sheepLine ? sheepLine->getMaxAmountReceive() : 0; + auto sellerMaxSheep = + sheepLine ? sheepLine->getMaxAmountReceive(ledgerManager) : 0; auto wheatAmount = int64_t{}; if (!bigDivide(wheatAmount, sellerMaxSheep, wheatPrice.d, wheatPrice.n, @@ -43,27 +44,34 @@ canSellAtMost(AccountFrame::pointer account, Asset const& asset, if (asset.type() == ASSET_TYPE_NATIVE) { // can only send above the minimum balance - return account->getBalanceAboveReserve(ledgerManager); + return std::max( + {account->getAvailableBalance(ledgerManager), int64_t(0)}); } if (trustLine && trustLine->isAuthorized()) { - return trustLine->getBalance(); + return std::max( + {trustLine->getAvailableBalance(ledgerManager), int64_t(0)}); } return 0; } int64_t -canBuyAtMost(Asset const& asset, TrustFrame::pointer trustLine) +canBuyAtMost(AccountFrame::pointer account, Asset const& asset, + TrustFrame::pointer trustLine, LedgerManager& ledgerManager) { if (asset.type() == ASSET_TYPE_NATIVE) { - return INT64_MAX; + return std::max( + {account->getMaxAmountReceive(ledgerManager), int64_t(0)}); } else { - return trustLine ? trustLine->getMaxAmountReceive() : 0; + return trustLine + ? std::max({trustLine->getMaxAmountReceive(ledgerManager), + int64_t(0)}) + : 0; } } @@ -634,7 +642,8 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer, numWheatReceived = std::min( {canSellAtMostBasedOnSheep(sheep, sheepLineAccountB, - sellingWheatOffer.getOffer().price), + sellingWheatOffer.getOffer().price, + mLedgerManager), canSellAtMost(accountB, wheat, wheatLineAccountB, mLedgerManager), sellingWheatOffer.getOffer().amount}); assert(numWheatReceived >= 0); @@ -686,7 +695,7 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer, { if (sheep.type() == ASSET_TYPE_NATIVE) { - if (!accountB->addBalance(numSheepSend)) + if (!accountB->addBalance(numSheepSend, mLedgerManager)) { return eOfferCantConvert; } @@ -694,7 +703,7 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer, } else { - if (!sheepLineAccountB->addBalance(numSheepSend)) + if (!sheepLineAccountB->addBalance(numSheepSend, mLedgerManager)) { return eOfferCantConvert; } @@ -706,7 +715,7 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer, { if (wheat.type() == ASSET_TYPE_NATIVE) { - if (!accountB->addBalance(-numWheatReceived)) + if (!accountB->addBalance(-numWheatReceived, mLedgerManager)) { return eOfferCantConvert; } @@ -714,7 +723,8 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer, } else { - if (!wheatLineAccountB->addBalance(-numWheatReceived)) + if (!wheatLineAccountB->addBalance(-numWheatReceived, + mLedgerManager)) { return eOfferCantConvert; } @@ -737,7 +747,7 @@ adjustOffer(OfferFrame& offer, LedgerManager& lm, AccountFrame::pointer account, OfferEntry& oe = offer.getOffer(); int64_t maxWheatSend = std::min({oe.amount, canSellAtMost(account, wheat, wheatLine, lm)}); - int64_t maxSheepReceive = canBuyAtMost(sheep, sheepLine); + int64_t maxSheepReceive = canBuyAtMost(account, sheep, sheepLine, lm); oe.amount = adjustOffer(oe.price, maxWheatSend, maxSheepReceive); } @@ -915,6 +925,16 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer, TrustFrame::loadTrustLine(accountBID, sheep, db, &mDelta); } + // Remove liabilities associated with the offer being crossed. + if (mLedgerManager.getCurrentLedgerVersion() >= 10) + { + sellingWheatOffer.releaseLiabilities(accountB, sheepLineAccountB, + wheatLineAccountB, mDelta, db, + mLedgerManager); + } + + // Note: As of the asset-backed offers proposal, this call to adjustOffer + // should only effect offers that remained after the version upgrade. adjustOffer(sellingWheatOffer, mLedgerManager, accountB, wheat, wheatLineAccountB, sheep, sheepLineAccountB); @@ -922,7 +942,8 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer, canSellAtMost(accountB, wheat, wheatLineAccountB, mLedgerManager); maxWheatSend = std::min({sellingWheatOffer.getOffer().amount, maxWheatSend}); - int64_t maxSheepReceive = canBuyAtMost(sheep, sheepLineAccountB); + int64_t maxSheepReceive = + canBuyAtMost(accountB, sheep, sheepLineAccountB, mLedgerManager); auto exchangeResult = exchangeV10( sellingWheatOffer.getOffer().price, maxWheatSend, maxWheatReceived, maxSheepSend, maxSheepReceive, isPathPayment); @@ -936,7 +957,7 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer, { if (sheep.type() == ASSET_TYPE_NATIVE) { - if (!accountB->addBalance(numSheepSend)) + if (!accountB->addBalance(numSheepSend, mLedgerManager)) { throw std::runtime_error("overflowed sheep balance"); } @@ -944,7 +965,7 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer, } else { - if (!sheepLineAccountB->addBalance(numSheepSend)) + if (!sheepLineAccountB->addBalance(numSheepSend, mLedgerManager)) { throw std::runtime_error("overflowed sheep balance"); } @@ -956,7 +977,7 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer, { if (wheat.type() == ASSET_TYPE_NATIVE) { - if (!accountB->addBalance(-numWheatReceived)) + if (!accountB->addBalance(-numWheatReceived, mLedgerManager)) { throw std::runtime_error("overflowed wheat balance"); } @@ -964,7 +985,8 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer, } else { - if (!wheatLineAccountB->addBalance(-numWheatReceived)) + if (!wheatLineAccountB->addBalance(-numWheatReceived, + mLedgerManager)) { throw std::runtime_error("overflowed wheat balance"); } @@ -992,6 +1014,12 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer, } else { + if (mLedgerManager.getCurrentLedgerVersion() >= 10) + { + sellingWheatOffer.acquireLiabilities(accountB, sheepLineAccountB, + wheatLineAccountB, mDelta, db, + mLedgerManager); + } sellingWheatOffer.storeChange(mDelta, db); } diff --git a/src/transactions/OfferExchange.h b/src/transactions/OfferExchange.h index 0ea4c20798..e5b9352c1a 100644 --- a/src/transactions/OfferExchange.h +++ b/src/transactions/OfferExchange.h @@ -46,12 +46,15 @@ struct ExchangeResultV10 int64_t canSellAtMostBasedOnSheep(Asset const& sheep, TrustFrame::pointer sheepLine, - Price const& wheatPrice); + Price const& wheatPrice, + LedgerManager& ledgerManager); int64_t canSellAtMost(AccountFrame::pointer account, Asset const& asset, TrustFrame::pointer trustLine, LedgerManager& ledgerManager); -int64_t canBuyAtMost(Asset const& asset, TrustFrame::pointer trustLine); +int64_t canBuyAtMost(AccountFrame::pointer account, Asset const& asset, + TrustFrame::pointer trustLine, + LedgerManager& ledgerManager); ExchangeResult exchangeV2(int64_t wheatReceived, Price price, int64_t maxWheatReceive, int64_t maxSheepSend); diff --git a/src/transactions/PathPaymentOpFrame.cpp b/src/transactions/PathPaymentOpFrame.cpp index 5cd648b92e..2128631122 100644 --- a/src/transactions/PathPaymentOpFrame.cpp +++ b/src/transactions/PathPaymentOpFrame.cpp @@ -82,7 +82,7 @@ PathPaymentOpFrame::doApply(Application& app, LedgerDelta& delta, // update last balance in the chain if (curB.type() == ASSET_TYPE_NATIVE) { - if (!destination->addBalance(curBReceived)) + if (!destination->addBalance(curBReceived, ledgerManager)) { app.getMetrics() .NewMeter({"op-path-payment", "invalid", "balance-overflow"}, @@ -139,7 +139,7 @@ PathPaymentOpFrame::doApply(Application& app, LedgerDelta& delta, return false; } - if (!destLine->addBalance(curBReceived)) + if (!destLine->addBalance(curBReceived, ledgerManager)) { app.getMetrics() .NewMeter({"op-path-payment", "failure", "line-full"}, @@ -270,9 +270,7 @@ PathPaymentOpFrame::doApply(Application& app, LedgerDelta& delta, } } - int64_t minBalance = sourceAccount->getMinimumBalance(ledgerManager); - - if ((sourceAccount->getAccount().balance - curBSent) < minBalance) + if (curBSent > sourceAccount->getAvailableBalance(ledgerManager)) { // they don't have enough to send app.getMetrics() .NewMeter({"op-path-payment", "failure", "underfunded"}, @@ -282,7 +280,7 @@ PathPaymentOpFrame::doApply(Application& app, LedgerDelta& delta, return false; } - auto ok = sourceAccount->addBalance(-curBSent); + auto ok = sourceAccount->addBalance(-curBSent, ledgerManager); assert(ok); sourceAccount->storeChange(delta, db); } @@ -332,7 +330,7 @@ PathPaymentOpFrame::doApply(Application& app, LedgerDelta& delta, return false; } - if (!sourceLineFrame->addBalance(-curBSent)) + if (!sourceLineFrame->addBalance(-curBSent, ledgerManager)) { app.getMetrics() .NewMeter({"op-path-payment", "failure", "underfunded"}, diff --git a/src/transactions/TransactionFrame.cpp b/src/transactions/TransactionFrame.cpp index d9eb5e0bdd..456574813a 100644 --- a/src/transactions/TransactionFrame.cpp +++ b/src/transactions/TransactionFrame.cpp @@ -380,13 +380,11 @@ TransactionFrame::commonValid(SignatureChecker& signatureChecker, // if we are in applying mode fee was already deduced from signing account // balance, if not, we need to check if after that deduction this account // will still have minimum balance - auto balanceAfter = - (applying && (lm.getCurrentLedgerVersion() > 8)) - ? mSigningAccount->getAccount().balance - : mSigningAccount->getAccount().balance - mEnvelope.tx.fee; - - // don't let the account go below the reserve - if (balanceAfter < mSigningAccount->getMinimumBalance(lm)) + uint32_t feeToPay = + (applying && (lm.getCurrentLedgerVersion() > 8)) ? 0 : mEnvelope.tx.fee; + // don't let the account go below the reserve after accounting for + // liabilities + if (mSigningAccount->getAvailableBalance(lm) < feeToPay) { app.getMetrics() .NewMeter({"transaction", "invalid", "insufficient-balance"}, @@ -418,7 +416,10 @@ TransactionFrame::processFeeSeqNum(LedgerDelta& delta, if (fee > 0) { fee = std::min(mSigningAccount->getAccount().balance, fee); - mSigningAccount->addBalance(-fee); + // Note: AccountFrame::addBalance checks that reserve plus liabilities + // are respected. In this case, we allow it to fall below that since it + // will be caught later in commonValid. + stellar::addBalance(mSigningAccount->getAccount().balance, -fee); delta.getHeader().feePool += fee; } // in v10 we update sequence numbers during apply diff --git a/src/xdr/Stellar-transaction.x b/src/xdr/Stellar-transaction.x index ff5176c72a..22c3a6139e 100644 --- a/src/xdr/Stellar-transaction.x +++ b/src/xdr/Stellar-transaction.x @@ -608,7 +608,9 @@ enum AccountMergeResultCode ACCOUNT_MERGE_NO_ACCOUNT = -2, // destination does not exist ACCOUNT_MERGE_IMMUTABLE_SET = -3, // source account has AUTH_IMMUTABLE set ACCOUNT_MERGE_HAS_SUB_ENTRIES = -4, // account has trust lines/offers - ACCOUNT_MERGE_SEQNUM_TOO_FAR = -5 // sequence number is over max allowed + ACCOUNT_MERGE_SEQNUM_TOO_FAR = -5, // sequence number is over max allowed + ACCOUNT_MERGE_DEST_FULL = -6 // can't add source balance to + // destination balance }; union AccountMergeResult switch (AccountMergeResultCode code) From 1d77de83481a6c359515bc2553975afa1f4f2c91 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:40 -0500 Subject: [PATCH 07/13] Added upgradehistory table to support upgrade-level history --- docs/db-schema.md | 12 +++++++ src/database/Database.cpp | 2 ++ src/herder/Upgrades.cpp | 62 ++++++++++++++++++++++++++++++++ src/herder/Upgrades.h | 11 ++++++ src/ledger/LedgerManagerImpl.cpp | 1 + 5 files changed, 88 insertions(+) diff --git a/docs/db-schema.md b/docs/db-schema.md index 4ef1893adb..d7cf620a8f 100644 --- a/docs/db-schema.md +++ b/docs/db-schema.md @@ -158,3 +158,15 @@ port | INT DEFAULT 0 CHECK (port > 0 AND port <= 65535) NOT NULL | nextattempt | TIMESTAMP NOT NULL | numfailures | INT DEFAULT 0 CHECK (numfailures >= 0) NOT NULL | + +## upgradehistory + +Defined in [`src/herder/Upgrades.cpp`](/src/herder/Upgrades.cpp) + +Field | Type | Description +------|------|--------------- +ledgerseq | INT NOT NULL CHECK (ledgerseq >= 0) | Ledger this upgrade got applied +upgradeindex | INT NOT NULL | Apply order (per ledger, 1) +upgrade | TEXT NOT NULL | The upgrade (XDR) +changes | TEXT NOT NULL | LedgerEntryChanges (XDR) + diff --git a/src/database/Database.cpp b/src/database/Database.cpp index 1e55168371..b3988a4284 100644 --- a/src/database/Database.cpp +++ b/src/database/Database.cpp @@ -15,6 +15,7 @@ #include "bucket/BucketManager.h" #include "herder/HerderPersistence.h" +#include "herder/Upgrades.h" #include "history/HistoryManager.h" #include "ledger/AccountFrame.h" #include "ledger/DataFrame.h" @@ -146,6 +147,7 @@ Database::applySchemaUpgrade(unsigned long vers) break; case 7: + Upgrades::dropAll(*this); mSession << "ALTER TABLE accounts ADD buyingliabilities BIGINT " "CHECK (buyingliabilities >= 0)"; mSession << "ALTER TABLE accounts ADD sellingliabilities BIGINT " diff --git a/src/herder/Upgrades.cpp b/src/herder/Upgrades.cpp index 4a971cbdf6..7a4e5dd87d 100644 --- a/src/herder/Upgrades.cpp +++ b/src/herder/Upgrades.cpp @@ -3,7 +3,11 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "herder/Upgrades.h" +#include "database/Database.h" +#include "database/DatabaseUtils.h" +#include "ledger/LedgerManager.h" #include "main/Config.h" +#include "util/Decoder.h" #include "util/Logging.h" #include "util/Timer.h" #include @@ -318,4 +322,62 @@ Upgrades::timeForUpgrade(uint64_t time) const { return mParams.mUpgradeTime <= VirtualClock::from_time_t(time); } + +void +Upgrades::dropAll(Database& db) +{ + db.getSession() << "DROP TABLE IF EXISTS upgradehistory"; + db.getSession() << "CREATE TABLE upgradehistory (" + "ledgerseq INT NOT NULL CHECK (ledgerseq >= 0), " + "upgradeindex INT NOT NULL, " + "upgrade TEXT NOT NULL, " + "changes TEXT NOT NULL, " + "PRIMARY KEY (ledgerseq, upgradeindex)" + ")"; + db.getSession() + << "CREATE INDEX upgradehistbyseq ON upgradehistory (ledgerseq);"; +} + +void +Upgrades::storeUpgradeHistory(LedgerManager& ledgerManager, + LedgerUpgrade const& upgrade, + LedgerEntryChanges const& changes, int index) +{ + uint32_t ledgerSeq = ledgerManager.getCurrentLedgerHeader().ledgerSeq; + + xdr::opaque_vec<> upgradeContent(xdr::xdr_to_opaque(upgrade)); + std::string upgradeContent64 = decoder::encode_b64(upgradeContent); + + xdr::opaque_vec<> upgradeChanges(xdr::xdr_to_opaque(changes)); + std::string upgradeChanges64 = decoder::encode_b64(upgradeChanges); + + auto& db = ledgerManager.getDatabase(); + auto prep = db.getPreparedStatement( + "INSERT INTO upgradehistory " + "(ledgerseq, upgradeindex, upgrade, changes) VALUES " + "(:seq, :upgradeindex, :upgrade, :changes)"); + + auto& st = prep.statement(); + st.exchange(soci::use(ledgerSeq)); + st.exchange(soci::use(index)); + st.exchange(soci::use(upgradeContent64)); + st.exchange(soci::use(upgradeChanges64)); + st.define_and_bind(); + { + auto timer = db.getInsertTimer("upgradehistory"); + st.execute(true); + } + + if (st.get_affected_rows() != 1) + { + throw std::runtime_error("Could not update data in SQL"); + } +} + +void +Upgrades::deleteOldEntries(Database& db, uint32_t ledgerSeq, uint32_t count) +{ + DatabaseUtils::deleteOldEntriesHelper(db.getSession(), ledgerSeq, count, + "upgradehistory", "ledgerseq"); +} } diff --git a/src/herder/Upgrades.h b/src/herder/Upgrades.h index c4c928908c..21bfb0638b 100644 --- a/src/herder/Upgrades.h +++ b/src/herder/Upgrades.h @@ -15,6 +15,8 @@ namespace stellar { class Config; +class Database; +class LedgerManager; struct LedgerHeader; struct LedgerUpgrade; @@ -81,6 +83,15 @@ class Upgrades std::vector::const_iterator endUpdates, bool& updated); + static void dropAll(Database& db); + + static void storeUpgradeHistory(LedgerManager& ledgerManager, + LedgerUpgrade const& upgrade, + LedgerEntryChanges const& changes, + int index); + static void deleteOldEntries(Database& db, uint32_t ledgerSeq, + uint32_t count); + private: UpgradeParameters mParams; diff --git a/src/ledger/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp index a8d88907d3..12113fb1e7 100644 --- a/src/ledger/LedgerManagerImpl.cpp +++ b/src/ledger/LedgerManagerImpl.cpp @@ -908,6 +908,7 @@ LedgerManagerImpl::deleteOldEntries(Database& db, uint32_t ledgerSeq, LedgerHeaderFrame::deleteOldEntries(db, ledgerSeq, count); TransactionFrame::deleteOldEntries(db, ledgerSeq, count); HerderPersistence::deleteOldEntries(db, ledgerSeq, count); + Upgrades::deleteOldEntries(db, ledgerSeq, count); db.clearPreparedStatementCache(); txscope.commit(); } From 92d2f48be1f5b1e6f492c7ceb7811c271d010c9d Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:42 -0500 Subject: [PATCH 08/13] Upgrade protocol version and base reserve maintain liabilities --- src/herder/Upgrades.cpp | 310 ++++++++++++++++++++++++++++- src/herder/Upgrades.h | 10 +- src/ledger/LedgerManagerImpl.cpp | 29 ++- src/transactions/OfferExchange.cpp | 4 +- 4 files changed, 346 insertions(+), 7 deletions(-) diff --git a/src/herder/Upgrades.cpp b/src/herder/Upgrades.cpp index 7a4e5dd87d..7e1436d7b7 100644 --- a/src/herder/Upgrades.cpp +++ b/src/herder/Upgrades.cpp @@ -5,11 +5,17 @@ #include "herder/Upgrades.h" #include "database/Database.h" #include "database/DatabaseUtils.h" +#include "ledger/AccountFrame.h" +#include "ledger/LedgerDelta.h" #include "ledger/LedgerManager.h" +#include "ledger/OfferFrame.h" +#include "ledger/TrustFrame.h" #include "main/Config.h" +#include "transactions/OfferExchange.h" #include "util/Decoder.h" #include "util/Logging.h" #include "util/Timer.h" +#include "util/types.h" #include #include #include @@ -123,12 +129,14 @@ Upgrades::createUpgradesFor(LedgerHeader const& header) const } void -Upgrades::applyTo(LedgerUpgrade const& upgrade, LedgerHeader& header) +Upgrades::applyTo(LedgerUpgrade const& upgrade, LedgerManager& ledgerManager, + LedgerDelta& ld) { + LedgerHeader& header = ld.getHeader(); switch (upgrade.type()) { case LEDGER_UPGRADE_VERSION: - header.ledgerVersion = upgrade.newLedgerVersion(); + applyVersionUpgrade(ledgerManager, ld, upgrade.newLedgerVersion()); break; case LEDGER_UPGRADE_BASE_FEE: header.baseFee = upgrade.newBaseFee(); @@ -137,7 +145,7 @@ Upgrades::applyTo(LedgerUpgrade const& upgrade, LedgerHeader& header) header.maxTxSetSize = upgrade.newMaxTxSetSize(); break; case LEDGER_UPGRADE_BASE_RESERVE: - header.baseReserve = upgrade.newBaseReserve(); + applyReserveUpgrade(ledgerManager, ld, upgrade.newBaseReserve()); break; default: { @@ -380,4 +388,300 @@ Upgrades::deleteOldEntries(Database& db, uint32_t ledgerSeq, uint32_t count) DatabaseUtils::deleteOldEntriesHelper(db.getSession(), ledgerSeq, count, "upgradehistory", "ledgerseq"); } + +static void +addLiabilities(std::map>& liabilities, + AccountID const& accountID, Asset const& asset, int64_t delta) +{ + auto iter = + liabilities.insert(std::make_pair(asset, std::make_unique(0))) + .first; + if (asset.type() != ASSET_TYPE_NATIVE && accountID == getIssuer(asset)) + { + return; + } + if (iter->second) + { + if (!stellar::addBalance(*iter->second, delta)) + { + iter->second.reset(); + } + } +} + +static int64_t +getAvailableBalance(AccountID const& accountID, Asset const& asset, + int64_t balanceAboveReserve, LedgerDelta& ld, Database& db) +{ + if (asset.type() == ASSET_TYPE_NATIVE) + { + return balanceAboveReserve; + } + + if (accountID == getIssuer(asset)) + { + return INT64_MAX; + } + else + { + auto trust = TrustFrame::loadTrustLine(accountID, asset, db, &ld); + if (trust && trust->isAuthorized()) + { + return trust->getBalance(); + } + else + { + return 0; + } + } +} + +static int64_t +getAvailableLimit(AccountID const& accountID, Asset const& asset, + int64_t balance, LedgerDelta& ld, Database& db) +{ + if (asset.type() == ASSET_TYPE_NATIVE) + { + return INT64_MAX - balance; + } + + if (accountID == getIssuer(asset)) + { + return INT64_MAX; + } + else + { + auto trust = TrustFrame::loadTrustLine(accountID, asset, db, &ld); + if (trust && trust->isAuthorized()) + { + return trust->getTrustLine().limit - trust->getBalance(); + } + else + { + return 0; + } + } +} + +static bool +shouldDeleteOffer(Asset const& asset, int64_t effectiveBalance, + std::map> const& liabilities, + std::function getCap) +{ + auto iter = liabilities.find(asset); + if (iter == liabilities.end()) + { + throw std::runtime_error("liabilities were not calculated"); + } + // Offers should be deleted if liabilities exceed INT64_MAX (nullptr) or if + // there are excess liabilities. + return iter->second ? *iter->second > getCap(asset, effectiveBalance) + : true; +} + +// This function is used to bring offers and liabilities into a valid state. +// For every account that has offers, +// 1. Calculate total liabilities for each asset +// 2. For every asset with excess buying liabilities according to (1), erase +// all offers buying that asset. For every asset with excess selling +// liabilities according to (1), erase all offers selling that asset. +// 3. Update liabilities to reflect offers remaining in the book. +// It is essential to note that the excess liabilities are determined only +// using the initial result of step (1), so it does not matter what order the +// offers are processed. +static void +prepareLiabilities(LedgerManager& ledgerManager, LedgerDelta& ld) +{ + using namespace std::placeholders; + + auto& db = ledgerManager.getDatabase(); + db.getEntryCache().clear(); + auto offersByAccount = OfferFrame::loadAllOffers(db); + + for (auto& accountOffers : offersByAccount) + { + // The purpose of std::unique_ptr here is to have a special value + // (nullptr) to indicate that an integer overflow would have occured. + // Overflow is possible here because existing offers were not + // constrainted to have int64_t liabilities. This must be carefully + // handled in what follows. + std::map> initialBuyingLiabilities; + std::map> initialSellingLiabilities; + for (auto const& offerFrame : accountOffers.second) + { + auto const& offer = offerFrame->getOffer(); + addLiabilities(initialBuyingLiabilities, offer.sellerID, + offer.buying, offerFrame->getBuyingLiabilities()); + addLiabilities(initialSellingLiabilities, offer.sellerID, + offer.selling, offerFrame->getSellingLiabilities()); + } + + auto accountFrame = + AccountFrame::loadAccount(ld, accountOffers.first, db); + if (!accountFrame) + { + throw std::runtime_error("account does not exist"); + } + + // balanceAboveReserve must exclude native selling liabilities, since + // these are in the process of being recalculated from scratch. + int64_t balance = accountFrame->getBalance(); + int64_t minBalance = accountFrame->getMinimumBalance(ledgerManager); + int64_t balanceAboveReserve = balance - minBalance; + + std::map liabilities; + for (auto const& offerFrame : accountOffers.second) + { + auto& offer = offerFrame->getOffer(); + + auto availableBalanceBind = + std::bind(getAvailableBalance, offer.sellerID, _1, _2, + std::ref(ld), std::ref(db)); + auto availableLimitBind = + std::bind(getAvailableLimit, offer.sellerID, _1, _2, + std::ref(ld), std::ref(db)); + + bool erase = shouldDeleteOffer(offer.selling, balanceAboveReserve, + initialSellingLiabilities, + availableBalanceBind); + erase = erase || shouldDeleteOffer(offer.buying, balance, + initialBuyingLiabilities, + availableLimitBind); + + // If erase == false then we know that the total buying liabilities + // of the buying asset do not exceed its available limit, and the + // total selling liabilities of the selling asset do not exceed its + // available balance. This implies that there are no excess + // liabilities for this offer, so the only applicable limit is the + // offer amount. We then use adjustOffer to check that it will + // satisfy thresholds. + erase = + erase || (adjustOffer(offerFrame->getPrice(), + offerFrame->getAmount(), INT64_MAX) == 0); + + if (erase) + { + accountFrame->addNumEntries(-1, ledgerManager); + offerFrame->storeDelete(ld, db); + } + else + { + // The same logic for adjustOffer discussed above applies here, + // except that we now actually update the offer to reflect the + // adjustment. + offer.amount = adjustOffer(offerFrame->getPrice(), + offerFrame->getAmount(), INT64_MAX); + offerFrame->storeChange(ld, db); + + if (offer.buying.type() == ASSET_TYPE_NATIVE || + !(offer.sellerID == getIssuer(offer.buying))) + { + if (!stellar::addBalance( + liabilities[offer.buying].buying, + offerFrame->getBuyingLiabilities())) + { + throw std::runtime_error("could not add buying " + "liabilities"); + } + } + if (offer.selling.type() == ASSET_TYPE_NATIVE || + !(offer.sellerID == getIssuer(offer.selling))) + { + if (!stellar::addBalance( + liabilities[offer.selling].selling, + offerFrame->getSellingLiabilities())) + { + throw std::runtime_error("could not add selling " + "liabilities"); + } + } + } + } + + for (auto const& assetLiabilities : liabilities) + { + Asset const& asset = assetLiabilities.first; + Liabilities const& liab = assetLiabilities.second; + if (asset.type() == ASSET_TYPE_NATIVE) + { + int64_t deltaSelling = + liab.selling - + accountFrame->getSellingLiabilities(ledgerManager); + int64_t deltaBuying = + liab.buying - + accountFrame->getBuyingLiabilities(ledgerManager); + if (!accountFrame->addSellingLiabilities(deltaSelling, + ledgerManager)) + { + throw std::runtime_error("invalid selling liabilities " + "during upgrade"); + } + if (!accountFrame->addBuyingLiabilities(deltaBuying, + ledgerManager)) + { + throw std::runtime_error("invalid buying liabilities " + "during upgrade"); + } + } + else + { + auto trustFrame = TrustFrame::loadTrustLine(accountOffers.first, + asset, db, &ld); + int64_t deltaSelling = + liab.selling - + trustFrame->getSellingLiabilities(ledgerManager); + int64_t deltaBuying = + liab.buying - + trustFrame->getBuyingLiabilities(ledgerManager); + if (!trustFrame->addSellingLiabilities(deltaSelling, + ledgerManager)) + { + throw std::runtime_error("invalid selling liabilities " + "during upgrade"); + } + if (!trustFrame->addBuyingLiabilities(deltaBuying, + ledgerManager)) + { + throw std::runtime_error("invalid buying liabilities " + "during upgrade"); + } + trustFrame->storeChange(ld, db); + } + } + + accountFrame->storeChange(ld, db); + } + + db.getEntryCache().clear(); +} + +void +Upgrades::applyVersionUpgrade(LedgerManager& ledgerManager, LedgerDelta& ld, + uint32_t newVersion) +{ + LedgerHeader& header = ld.getHeader(); + uint32_t prevVersion = header.ledgerVersion; + + header.ledgerVersion = newVersion; + ledgerManager.getCurrentLedgerHeader().ledgerVersion = newVersion; + if (header.ledgerVersion >= 10 && prevVersion < 10) + { + prepareLiabilities(ledgerManager, ld); + } +} + +void +Upgrades::applyReserveUpgrade(LedgerManager& ledgerManager, LedgerDelta& ld, + uint32_t newReserve) +{ + LedgerHeader& header = ld.getHeader(); + bool didReserveIncrease = newReserve > header.baseReserve; + + header.baseReserve = newReserve; + ledgerManager.getCurrentLedgerHeader().baseReserve = newReserve; + if (header.ledgerVersion >= 10 && didReserveIncrease) + { + prepareLiabilities(ledgerManager, ld); + } +} } diff --git a/src/herder/Upgrades.h b/src/herder/Upgrades.h index 21bfb0638b..dc48cdee41 100644 --- a/src/herder/Upgrades.h +++ b/src/herder/Upgrades.h @@ -16,6 +16,7 @@ namespace stellar { class Config; class Database; +class LedgerDelta; class LedgerManager; struct LedgerHeader; struct LedgerUpgrade; @@ -62,7 +63,8 @@ class Upgrades createUpgradesFor(LedgerHeader const& header) const; // apply upgrade to ledger header - static void applyTo(LedgerUpgrade const& upgrade, LedgerHeader& header); + static void applyTo(LedgerUpgrade const& upgrade, + LedgerManager& ledgerManager, LedgerDelta& ld); // convert upgrade value to string static std::string toString(LedgerUpgrade const& upgrade); @@ -96,5 +98,11 @@ class Upgrades UpgradeParameters mParams; bool timeForUpgrade(uint64_t time) const; + + static void applyVersionUpgrade(LedgerManager& lm, LedgerDelta& ld, + uint32_t newVersion); + + static void applyReserveUpgrade(LedgerManager& lm, LedgerDelta& ld, + uint32_t newReserve); }; } diff --git a/src/ledger/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp index 12113fb1e7..2c7b74447a 100644 --- a/src/ledger/LedgerManagerImpl.cpp +++ b/src/ledger/LedgerManagerImpl.cpp @@ -848,20 +848,47 @@ LedgerManagerImpl::closeLedger(LedgerCloseData const& ledgerData) // apply any upgrades that were decided during consensus // this must be done after applying transactions as the txset // was validated before upgrades + LedgerHeader headerBeforeUpgrades = getCurrentLedgerHeader(); for (size_t i = 0; i < sv.upgrades.size(); i++) { LedgerUpgrade lupgrade; try { xdr::xdr_from_opaque(sv.upgrades[i], lupgrade); - Upgrades::applyTo(lupgrade, ledgerDelta.getHeader()); } catch (xdr::xdr_runtime_error) { CLOG(FATAL, "Ledger") << "Unknown upgrade step at index " << i; throw; } + + LedgerHeader previousHeader = getCurrentLedgerHeader(); + try + { + soci::transaction upgradeScope(getDatabase().getSession()); + LedgerDelta upgradeDelta(ledgerDelta); + Upgrades::applyTo(lupgrade, *this, upgradeDelta); + // Note: Index from 1 rather than 0 to match the behavior of + // storeTransaction and storeTransactionFee. + Upgrades::storeUpgradeHistory(*this, lupgrade, + upgradeDelta.getChanges(), i + 1); + upgradeDelta.commit(); + upgradeScope.commit(); + } + catch (std::runtime_error& e) + { + CLOG(ERROR, "Ledger") << "Exception during upgrade: " << e.what(); + getCurrentLedgerHeader() = previousHeader; + } + catch (...) + { + CLOG(ERROR, "Ledger") << "Unknown exception during upgrade"; + getCurrentLedgerHeader() = previousHeader; + } } + // It is required to rollback the current LedgerHeader in order to satisfy + // the consistency checks enforced by LedgerDelta::commit. + getCurrentLedgerHeader() = headerBeforeUpgrades; ledgerDelta.commit(); ledgerClosed(ledgerDelta); diff --git a/src/transactions/OfferExchange.cpp b/src/transactions/OfferExchange.cpp index ca3f2e4616..78bb5d2fea 100644 --- a/src/transactions/OfferExchange.cpp +++ b/src/transactions/OfferExchange.cpp @@ -933,8 +933,8 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer, mLedgerManager); } - // Note: As of the asset-backed offers proposal, this call to adjustOffer - // should only effect offers that remained after the version upgrade. + // As of the protocol version 10, this call to adjustOffer should have no + // effect. We leave it here only as a preventative measure. adjustOffer(sellingWheatOffer, mLedgerManager, accountB, wheat, wheatLineAccountB, sheep, sheepLineAccountB); From 8b67e6bb1533137e36c1b657d8172a5267a8488a Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:44 -0500 Subject: [PATCH 09/13] Added LiabilitiesMatchOffers invariant which encompasses the functionality of the (now-removed) MinimumAccountBalance invariant --- Builds/VisualStudio/stellar-core.vcxproj | 6 +- docs/stellar-core_example.cfg | 8 +- src/invariant/LiabilitiesMatchOffers.cpp | 313 +++++++++++++++++++++++ src/invariant/LiabilitiesMatchOffers.h | 63 +++++ src/invariant/MinimumAccountBalance.cpp | 100 -------- src/invariant/MinimumAccountBalance.h | 47 ---- src/main/ApplicationImpl.cpp | 4 +- 7 files changed, 385 insertions(+), 156 deletions(-) create mode 100644 src/invariant/LiabilitiesMatchOffers.cpp create mode 100644 src/invariant/LiabilitiesMatchOffers.h delete mode 100644 src/invariant/MinimumAccountBalance.cpp delete mode 100644 src/invariant/MinimumAccountBalance.h diff --git a/Builds/VisualStudio/stellar-core.vcxproj b/Builds/VisualStudio/stellar-core.vcxproj index 3ced56c337..2916fdd77e 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj +++ b/Builds/VisualStudio/stellar-core.vcxproj @@ -331,8 +331,8 @@ exit /b 0 - + @@ -601,7 +601,7 @@ exit /b 0 - + @@ -927,4 +927,4 @@ exit /b 0 - \ No newline at end of file + diff --git a/docs/stellar-core_example.cfg b/docs/stellar-core_example.cfg index 46cbc2de42..b70704fc75 100644 --- a/docs/stellar-core_example.cfg +++ b/docs/stellar-core_example.cfg @@ -298,11 +298,11 @@ RUN_STANDALONE=false # valid. # The overhead may cause slower systems to not perform as fast as the rest # of the network, caution is advised when using this. -# - "MinimumAccountBalance" +# - "LiabilitiesMatchOffers" # Setting this will cause additional work on each operation apply - it -# checks that accounts that have had their balance decrease satisfy the -# minimum balance requirement. For additional information see the comment -# in the header invariant/MinimumAccountBalance.h. +# checks that accounts, trust lines, and offers satisfy all constraints +# associated with liabilities. For additional information, see the comment +# in the header invariant/LiabilitiesMatchOffers.h. # The overhead may cause slower systems to not perform as fast as the rest # of the network, caution is advised when using this. INVARIANT_CHECKS = [] diff --git a/src/invariant/LiabilitiesMatchOffers.cpp b/src/invariant/LiabilitiesMatchOffers.cpp new file mode 100644 index 0000000000..8905cd0c89 --- /dev/null +++ b/src/invariant/LiabilitiesMatchOffers.cpp @@ -0,0 +1,313 @@ +// Copyright 2018 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "invariant/LiabilitiesMatchOffers.h" +#include "invariant/InvariantManager.h" +#include "ledger/LedgerManager.h" +#include "ledger/OfferFrame.h" +#include "lib/util/format.h" +#include "main/Application.h" +#include "util/types.h" +#include "xdrpp/printer.h" + +namespace stellar +{ + +std::shared_ptr +LiabilitiesMatchOffers::registerInvariant(Application& app) +{ + return app.getInvariantManager().registerInvariant( + app.getLedgerManager()); +} + +LiabilitiesMatchOffers::LiabilitiesMatchOffers(LedgerManager& lm) + : Invariant(false), mLedgerManager(lm) +{ +} + +std::string +LiabilitiesMatchOffers::getName() const +{ + // NOTE: In order for the acceptance tests to run correctly, this will + // currently need to read "MinimumAccountBalance". We will update this to + // "LiabilitiesMatchOffers" after. + return "MinimumAccountBalance"; +} + +std::string +LiabilitiesMatchOffers::checkOnOperationApply(Operation const& operation, + OperationResult const& result, + LedgerDelta const& delta) +{ + if (delta.getHeader().ledgerVersion >= 10) + { + std::map> deltaLiabilities; + for (auto iter = delta.added().begin(); iter != delta.added().end(); + ++iter) + { + auto checkAuthStr = checkAuthorized(iter); + if (!checkAuthStr.empty()) + { + return checkAuthStr; + } + addCurrentLiabilities(deltaLiabilities, iter); + } + for (auto iter = delta.modified().begin(); + iter != delta.modified().end(); ++iter) + { + auto checkAuthStr = checkAuthorized(iter); + if (!checkAuthStr.empty()) + { + return checkAuthStr; + } + addCurrentLiabilities(deltaLiabilities, iter); + subtractPreviousLiabilities(deltaLiabilities, iter); + } + for (auto iter = delta.deleted().begin(); iter != delta.deleted().end(); + ++iter) + { + subtractPreviousLiabilities(deltaLiabilities, iter); + } + + for (auto const& accLiabilities : deltaLiabilities) + { + for (auto const& assetLiabilities : accLiabilities.second) + { + if (assetLiabilities.second.buying != 0) + { + return fmt::format( + "Change in buying liabilities differed from " + "change in total buying liabilities of " + "offers by {} for account {} in asset {}", + assetLiabilities.second.buying, + xdr::xdr_to_string(accLiabilities.first), + xdr::xdr_to_string(assetLiabilities.first)); + } + else if (assetLiabilities.second.selling != 0) + { + return fmt::format( + "Change in selling liabilities differed from " + "change in total selling liabilities of " + "offers by {} for account {} in asset {}", + assetLiabilities.second.selling, + xdr::xdr_to_string(accLiabilities.first), + xdr::xdr_to_string(assetLiabilities.first)); + } + } + } + } + + auto ledgerVersion = mLedgerManager.getCurrentLedgerVersion(); + for (auto iter = delta.added().begin(); iter != delta.added().end(); ++iter) + { + auto msg = checkBalanceAndLimit(iter, ledgerVersion); + if (!msg.empty()) + { + return msg; + } + } + for (auto iter = delta.modified().begin(); iter != delta.modified().end(); + ++iter) + { + auto msg = checkBalanceAndLimit(iter, ledgerVersion); + if (!msg.empty()) + { + return msg; + } + } + return {}; +} + +template +void +LiabilitiesMatchOffers::addCurrentLiabilities( + std::map>& deltaLiabilities, + IterType const& iter) const +{ + auto const& current = iter->current->mEntry; + if (current.data.type() == ACCOUNT) + { + auto const& account = current.data.account(); + Asset native(ASSET_TYPE_NATIVE); + deltaLiabilities[account.accountID][native].selling -= + getSellingLiabilities(account, mLedgerManager); + deltaLiabilities[account.accountID][native].buying -= + getBuyingLiabilities(account, mLedgerManager); + } + else if (current.data.type() == TRUSTLINE) + { + auto const& trust = current.data.trustLine(); + deltaLiabilities[trust.accountID][trust.asset].selling -= + getSellingLiabilities(trust, mLedgerManager); + deltaLiabilities[trust.accountID][trust.asset].buying -= + getBuyingLiabilities(trust, mLedgerManager); + } + else if (current.data.type() == OFFER) + { + auto const& offer = current.data.offer(); + if (offer.selling.type() == ASSET_TYPE_NATIVE || + !(getIssuer(offer.selling) == offer.sellerID)) + { + deltaLiabilities[offer.sellerID][offer.selling].selling += + getSellingLiabilities(offer); + } + if (offer.buying.type() == ASSET_TYPE_NATIVE || + !(getIssuer(offer.buying) == offer.sellerID)) + { + deltaLiabilities[offer.sellerID][offer.buying].buying += + getBuyingLiabilities(offer); + } + } +} + +template +void +LiabilitiesMatchOffers::subtractPreviousLiabilities( + std::map>& deltaLiabilities, + IterType const& iter) const +{ + auto const& previous = iter->previous->mEntry; + if (previous.data.type() == ACCOUNT) + { + auto const& account = previous.data.account(); + Asset native(ASSET_TYPE_NATIVE); + deltaLiabilities[account.accountID][native].selling += + getSellingLiabilities(account, mLedgerManager); + deltaLiabilities[account.accountID][native].buying += + getBuyingLiabilities(account, mLedgerManager); + } + else if (previous.data.type() == TRUSTLINE) + { + auto const& trust = previous.data.trustLine(); + deltaLiabilities[trust.accountID][trust.asset].selling += + getSellingLiabilities(trust, mLedgerManager); + deltaLiabilities[trust.accountID][trust.asset].buying += + getBuyingLiabilities(trust, mLedgerManager); + } + else if (previous.data.type() == OFFER) + { + auto const& offer = previous.data.offer(); + if (offer.selling.type() == ASSET_TYPE_NATIVE || + !(getIssuer(offer.selling) == offer.sellerID)) + { + deltaLiabilities[offer.sellerID][offer.selling].selling -= + getSellingLiabilities(offer); + } + if (offer.buying.type() == ASSET_TYPE_NATIVE || + !(getIssuer(offer.buying) == offer.sellerID)) + { + deltaLiabilities[offer.sellerID][offer.buying].buying -= + getBuyingLiabilities(offer); + } + } +} + +bool +LiabilitiesMatchOffers::shouldCheckAccount( + LedgerDelta::AddedLedgerEntry const& ale, uint32_t ledgerVersion) const +{ + return true; +} + +bool +LiabilitiesMatchOffers::shouldCheckAccount( + LedgerDelta::ModifiedLedgerEntry const& mle, uint32_t ledgerVersion) const +{ + auto const& current = mle.current->mEntry; + auto const& previous = mle.previous->mEntry; + assert(previous.data.type() == ACCOUNT); + + auto const& currAcc = current.data.account(); + auto const& prevAcc = previous.data.account(); + + bool didBalanceDecrease = currAcc.balance < prevAcc.balance; + if (ledgerVersion >= 10) + { + bool sellingLiabilitiesInc = + getSellingLiabilities(currAcc, mLedgerManager) > + getSellingLiabilities(prevAcc, mLedgerManager); + bool buyingLiabilitiesInc = + getBuyingLiabilities(currAcc, mLedgerManager) > + getBuyingLiabilities(prevAcc, mLedgerManager); + bool didLiabilitiesIncrease = + sellingLiabilitiesInc || buyingLiabilitiesInc; + return didBalanceDecrease || didLiabilitiesIncrease; + } + else + { + return didBalanceDecrease; + } +} + +template +std::string +LiabilitiesMatchOffers::checkAuthorized(IterType const& iter) const +{ + auto const& current = iter->current->mEntry; + if (current.data.type() == TRUSTLINE) + { + auto const& trust = current.data.trustLine(); + if (!(trust.flags & AUTHORIZED_FLAG)) + { + if (getSellingLiabilities(trust, mLedgerManager) > 0 || + getBuyingLiabilities(trust, mLedgerManager) > 0) + { + return fmt::format("Unauthorized trust line has liabilities {}", + xdr::xdr_to_string(trust)); + } + } + } + return ""; +} + +template +std::string +LiabilitiesMatchOffers::checkBalanceAndLimit(IterType const& iter, + uint32_t ledgerVersion) const +{ + auto const& current = iter->current->mEntry; + if (current.data.type() == ACCOUNT) + { + if (shouldCheckAccount(*iter, ledgerVersion)) + { + auto const& account = current.data.account(); + Liabilities liabilities; + if (ledgerVersion >= 10) + { + liabilities.selling = + getSellingLiabilities(account, mLedgerManager); + liabilities.buying = + getBuyingLiabilities(account, mLedgerManager); + } + int64_t minBalance = + mLedgerManager.getMinBalance(account.numSubEntries); + if ((account.balance < minBalance + liabilities.selling) || + (INT64_MAX - account.balance < liabilities.buying)) + { + return fmt::format( + "Balance not compatible with liabilities for account {}", + xdr::xdr_to_string(account)); + } + } + } + else if (current.data.type() == TRUSTLINE) + { + auto const& trust = current.data.trustLine(); + Liabilities liabilities; + if (ledgerVersion >= 10) + { + liabilities.selling = getSellingLiabilities(trust, mLedgerManager); + liabilities.buying = getBuyingLiabilities(trust, mLedgerManager); + } + if ((trust.balance < liabilities.selling) || + (trust.limit - trust.balance < liabilities.buying)) + { + return fmt::format( + "Balance not compatible with liabilities for trustline {}", + xdr::xdr_to_string(trust)); + } + } + return {}; +} +} diff --git a/src/invariant/LiabilitiesMatchOffers.h b/src/invariant/LiabilitiesMatchOffers.h new file mode 100644 index 0000000000..dd14ca29bf --- /dev/null +++ b/src/invariant/LiabilitiesMatchOffers.h @@ -0,0 +1,63 @@ +#pragma once + +// Copyright 2018 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "invariant/Invariant.h" +#include "ledger/LedgerDelta.h" +#include + +namespace stellar +{ + +class Application; +class LedgerManager; + +// This Invariant has two purposes: to ensure that liabilities remain in sync +// with the offer book, and to ensure that the balance of accounts and +// trustlines respect the liabilities (and reserve). It is important to note +// that accounts can be below the minimum balance if the minimum balance +// increased since the last time the balance of those accounts decreased. +// Therefore, the Invariant only checks accounts that have had their balance +// decrease or their liabilities increase in the operation. +class LiabilitiesMatchOffers : public Invariant +{ + public: + explicit LiabilitiesMatchOffers(LedgerManager& lm); + + static std::shared_ptr registerInvariant(Application& app); + + virtual std::string getName() const override; + + virtual std::string + checkOnOperationApply(Operation const& operation, + OperationResult const& result, + LedgerDelta const& delta) override; + + private: + template + void addCurrentLiabilities( + std::map>& deltaLiabilities, + IterType const& iter) const; + + template + void subtractPreviousLiabilities( + std::map>& deltaLiabilities, + IterType const& iter) const; + + bool shouldCheckAccount(LedgerDelta::AddedLedgerEntry const& ale, + uint32_t ledgerVersion) const; + bool shouldCheckAccount(LedgerDelta::ModifiedLedgerEntry const& ale, + uint32_t ledgerVersion) const; + + template + std::string checkAuthorized(IterType const& iter) const; + + template + std::string checkBalanceAndLimit(IterType const& iter, + uint32_t ledgerVersion) const; + + LedgerManager& mLedgerManager; +}; +} diff --git a/src/invariant/MinimumAccountBalance.cpp b/src/invariant/MinimumAccountBalance.cpp deleted file mode 100644 index d55193c9f7..0000000000 --- a/src/invariant/MinimumAccountBalance.cpp +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2017 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#include "invariant/MinimumAccountBalance.h" -#include "invariant/InvariantManager.h" -#include "ledger/LedgerDelta.h" -#include "ledger/LedgerManager.h" -#include "lib/util/format.h" -#include "main/Application.h" -#include "xdrpp/printer.h" - -namespace stellar -{ - -std::shared_ptr -MinimumAccountBalance::registerInvariant(Application& app) -{ - return app.getInvariantManager().registerInvariant( - app.getLedgerManager()); -} - -MinimumAccountBalance::MinimumAccountBalance(LedgerManager const& lm) - : Invariant(false), mLedgerManager{lm} -{ -} - -std::string -MinimumAccountBalance::getName() const -{ - return "MinimumAccountBalance"; -} - -std::string -MinimumAccountBalance::checkOnOperationApply(Operation const& operation, - OperationResult const& result, - LedgerDelta const& delta) -{ - auto msg = checkAccountBalance(delta.added().begin(), delta.added().end()); - if (!msg.empty()) - { - return msg; - } - - msg = checkAccountBalance(delta.modified().begin(), delta.modified().end()); - if (!msg.empty()) - { - return msg; - } - return {}; -} - -bool -MinimumAccountBalance::shouldCheckBalance( - LedgerDelta::AddedLedgerEntry const& ale) const -{ - return ale.current->mEntry.data.type() == ACCOUNT; -} - -bool -MinimumAccountBalance::shouldCheckBalance( - LedgerDelta::ModifiedLedgerEntry const& mle) const -{ - auto const& current = mle.current->mEntry; - if (current.data.type() == ACCOUNT) - { - auto const& previous = mle.previous->mEntry; - assert(previous.data.type() == ACCOUNT); - return current.data.account().balance < previous.data.account().balance; - } - return false; -} - -template -std::string -MinimumAccountBalance::checkAccountBalance(IterType iter, - IterType const& end) const -{ - for (; iter != end; ++iter) - { - auto const& current = iter->current->mEntry; - if (current.data.type() == ACCOUNT) - { - if (shouldCheckBalance(*iter)) - { - auto const& account = current.data.account(); - auto minBalance = - mLedgerManager.getMinBalance(account.numSubEntries); - if (account.balance < minBalance) - { - return fmt::format("Account does not meet the minimum " - "balance requirement: {}", - xdr::xdr_to_string(current)); - } - } - } - } - return {}; -} -} diff --git a/src/invariant/MinimumAccountBalance.h b/src/invariant/MinimumAccountBalance.h deleted file mode 100644 index 8e71e1c0bb..0000000000 --- a/src/invariant/MinimumAccountBalance.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -// Copyright 2017 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#include "invariant/Invariant.h" -#include "ledger/LedgerDelta.h" -#include - -namespace stellar -{ - -class Application; -class LedgerManager; - -// This Invariant is used to validate that accounts have the minimum balance. -// It is important to note that accounts can be below the minimum balance if -// the minimum balance increased since the last time the balance of those -// accounts decreased. Therefore, the Invariant only checks accounts that have -// had their balance decrease in the operation. -class MinimumAccountBalance : public Invariant -{ - public: - MinimumAccountBalance(); - - static std::shared_ptr registerInvariant(Application& app); - - explicit MinimumAccountBalance(LedgerManager const& lm); - - virtual std::string getName() const override; - - virtual std::string - checkOnOperationApply(Operation const& operation, - OperationResult const& result, - LedgerDelta const& delta) override; - - private: - bool shouldCheckBalance(LedgerDelta::AddedLedgerEntry const& ale) const; - bool shouldCheckBalance(LedgerDelta::ModifiedLedgerEntry const& mle) const; - - template - std::string checkAccountBalance(IterType iter, IterType const& end) const; - - LedgerManager const& mLedgerManager; -}; -} diff --git a/src/main/ApplicationImpl.cpp b/src/main/ApplicationImpl.cpp index 41207d106a..13a896458e 100644 --- a/src/main/ApplicationImpl.cpp +++ b/src/main/ApplicationImpl.cpp @@ -24,7 +24,7 @@ #include "invariant/ConservationOfLumens.h" #include "invariant/InvariantManager.h" #include "invariant/LedgerEntryIsValid.h" -#include "invariant/MinimumAccountBalance.h" +#include "invariant/LiabilitiesMatchOffers.h" #include "ledger/LedgerManager.h" #include "main/CommandHandler.h" #include "main/ExternalQueue.h" @@ -127,7 +127,7 @@ ApplicationImpl::initialize() CacheIsConsistentWithDatabase::registerInvariant(*this); ConservationOfLumens::registerInvariant(*this); LedgerEntryIsValid::registerInvariant(*this); - MinimumAccountBalance::registerInvariant(*this); + LiabilitiesMatchOffers::registerInvariant(*this); enableInvariantsFromConfig(); if (!mConfig.NTP_SERVER.empty()) From 97e522816ebce7a8c1261db517a75837ea15fa53 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:46 -0500 Subject: [PATCH 10/13] Added new transactions tests --- src/transactions/AllowTrustTests.cpp | 79 +- src/transactions/ChangeTrustTests.cpp | 59 + src/transactions/InflationTests.cpp | 88 +- src/transactions/ManageDataTests.cpp | 41 + src/transactions/MergeTests.cpp | 47 +- src/transactions/OfferTests.cpp | 1657 +++++++++++++++++++++++-- src/transactions/PathPaymentTests.cpp | 429 ++++--- src/transactions/PaymentTests.cpp | 81 ++ src/transactions/SetOptionsTests.cpp | 41 + src/transactions/TxResultsTests.cpp | 37 + 10 files changed, 2287 insertions(+), 272 deletions(-) diff --git a/src/transactions/AllowTrustTests.cpp b/src/transactions/AllowTrustTests.cpp index 202d909ecc..bd56422d95 100644 --- a/src/transactions/AllowTrustTests.cpp +++ b/src/transactions/AllowTrustTests.cpp @@ -6,6 +6,7 @@ #include "main/Application.h" #include "test/TestAccount.h" #include "test/TestExceptions.h" +#include "test/TestMarket.h" #include "test/TestUtils.h" #include "test/TxTests.h" #include "test/test.h" @@ -26,13 +27,13 @@ TEST_CASE("allow trust", "[tx][allowtrust]") const int64_t trustLineLimit = INT64_MAX; const int64_t trustLineStartingBalance = 20000; - auto const minBalance2 = app->getLedgerManager().getMinBalance(2); + auto const minBalance4 = app->getLedgerManager().getMinBalance(4); // set up world auto root = TestAccount::createRoot(*app); - auto gateway = root.create("gw", minBalance2); - auto a1 = root.create("A1", minBalance2); - auto a2 = root.create("A2", minBalance2); + auto gateway = root.create("gw", minBalance4); + auto a1 = root.create("A1", minBalance4 + 10000); + auto a2 = root.create("A2", minBalance4); auto idr = makeAsset(gateway, "IDR"); @@ -175,4 +176,74 @@ TEST_CASE("allow trust", "[tx][allowtrust]") } } } + + SECTION("allow trust with offers") + { + SECTION("an asset matches") + { + for_versions_from(10, *app, [&] { + auto native = makeNativeAsset(); + + auto toSet = static_cast(AUTH_REQUIRED_FLAG) | + static_cast(AUTH_REVOCABLE_FLAG); + gateway.setOptions(setFlags(toSet)); + + a1.changeTrust(idr, trustLineLimit); + gateway.allowTrust(idr, a1); + + auto market = TestMarket{*app}; + SECTION("buying asset matches") + { + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer( + a1, {native, idr, Price{1, 1}, 1000}); + }); + market.requireChanges({{offer.key, OfferState::DELETED}}, + [&] { gateway.denyTrust(idr, a1); }); + } + SECTION("selling asset matches") + { + gateway.pay(a1, idr, trustLineStartingBalance); + + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer( + a1, {idr, native, Price{1, 1}, 1000}); + }); + market.requireChanges({{offer.key, OfferState::DELETED}}, + [&] { gateway.denyTrust(idr, a1); }); + } + }); + } + + SECTION("neither asset matches") + { + for_versions_from(10, *app, [&] { + auto toSet = static_cast(AUTH_REQUIRED_FLAG) | + static_cast(AUTH_REVOCABLE_FLAG); + gateway.setOptions(setFlags(toSet)); + + auto cur1 = makeAsset(gateway, "CUR1"); + auto cur2 = makeAsset(gateway, "CUR2"); + + a1.changeTrust(idr, trustLineLimit); + gateway.allowTrust(idr, a1); + + a1.changeTrust(cur1, trustLineLimit); + gateway.allowTrust(cur1, a1); + + a1.changeTrust(cur2, trustLineLimit); + gateway.allowTrust(cur2, a1); + + gateway.pay(a1, cur1, trustLineStartingBalance); + + auto market = TestMarket{*app}; + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(a1, {cur1, cur2, Price{1, 1}, 1000}); + }); + market.requireChanges( + {{offer.key, {cur1, cur2, Price{1, 1}, 1000}}}, + [&] { gateway.denyTrust(idr, a1); }); + }); + } + } } diff --git a/src/transactions/ChangeTrustTests.cpp b/src/transactions/ChangeTrustTests.cpp index e8b0d42582..8420557dfb 100644 --- a/src/transactions/ChangeTrustTests.cpp +++ b/src/transactions/ChangeTrustTests.cpp @@ -7,6 +7,7 @@ #include "main/Application.h" #include "test/TestAccount.h" #include "test/TestExceptions.h" +#include "test/TestMarket.h" #include "test/TestUtils.h" #include "test/TxTests.h" #include "test/test.h" @@ -171,4 +172,62 @@ TEST_CASE("change trust", "[tx][changetrust]") ex_CHANGE_TRUST_MALFORMED); }); } + + SECTION("create trust line with native selling liabilities") + { + auto const minBal2 = app->getLedgerManager().getMinBalance(2); + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBal2 + 2 * txfee + 500 - 1); + TestMarket market(*app); + + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {native, cur1, Price{1, 1}, 500}); + }); + + for_versions_to(9, *app, [&] { acc1.changeTrust(idr, 1000); }); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(acc1.changeTrust(idr, 1000), + ex_CHANGE_TRUST_LOW_RESERVE); + root.pay(acc1, txfee + 1); + acc1.changeTrust(idr, 1000); + }); + } + + SECTION("create trust line with native buying liabilities") + { + auto const minBal2 = app->getLedgerManager().getMinBalance(2); + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBal2 + 2 * txfee + 500 - 1); + TestMarket market(*app); + + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {cur1, native, Price{1, 1}, 500}); + }); + + for_all_versions(*app, [&] { acc1.changeTrust(idr, 1000); }); + } + + SECTION("cannot reduce limit below buying liabilities or delete") + { + for_versions_from(10, *app, [&] { + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBalance2 + 10 * txfee); + TestMarket market(*app); + + acc1.changeTrust(idr, 1000); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {native, idr, Price{1, 1}, 500}); + }); + acc1.changeTrust(idr, 500); + REQUIRE_THROWS_AS(acc1.changeTrust(idr, 499), + ex_CHANGE_TRUST_INVALID_LIMIT); + REQUIRE_THROWS_AS(acc1.changeTrust(idr, 0), + ex_CHANGE_TRUST_INVALID_LIMIT); + }); + } } diff --git a/src/transactions/InflationTests.cpp b/src/transactions/InflationTests.cpp index ce7c1e3794..892be0cfb2 100644 --- a/src/transactions/InflationTests.cpp +++ b/src/transactions/InflationTests.cpp @@ -10,6 +10,7 @@ #include "main/Config.h" #include "test/TestAccount.h" #include "test/TestExceptions.h" +#include "test/TestMarket.h" #include "test/TestUtils.h" #include "test/TxTests.h" #include "test/test.h" @@ -65,7 +66,8 @@ createTestAccounts(Application& app, int nbAccounts, static std::vector simulateInflation(int ledgerVersion, int nbAccounts, int64& totCoins, int64& totFees, std::function getBalance, - std::function getVote) + std::function getVote, LedgerManager& ledgerManager, + Database& db) { std::map balances; std::map votes; @@ -135,6 +137,13 @@ simulateInflation(int ledgerVersion, int nbAccounts, int64& totCoins, // computes the share of this guy int64 toDoleToThis = bigDivide(coinsToDole, votes.at(w), totVotes, ROUND_DOWN); + if (ledgerVersion >= 10) + { + auto winner = + AccountFrame::loadAccount(getTestAccount(w).getPublicKey(), db); + toDoleToThis = std::min(winner->getMaxAmountReceive(ledgerManager), + toDoleToThis); + } if (balances[w] >= 0) { balances[w] += toDoleToThis; @@ -206,9 +215,10 @@ doInflation(Application& app, int ledgerVersion, int nbAccounts, auto txFrame = root.tx({inflation()}); expectedFees += txFrame->getFee(); - expectedBalances = simulateInflation( - ledgerVersion, nbAccounts, expectedTotcoins, expectedFees, - [&](int i) { return balances[i]; }, getVote); + expectedBalances = + simulateInflation(ledgerVersion, nbAccounts, expectedTotcoins, + expectedFees, [&](int i) { return balances[i]; }, + getVote, app.getLedgerManager(), app.getDatabase()); // perform actual inflation applyTx(txFrame, app); @@ -551,4 +561,74 @@ TEST_CASE("inflation", "[tx][inflation]") } }); } + + SECTION("inflation with liabilities") + { + auto inflationWithLiabilities = [&](int64_t available, + int64_t expectedPayout) { + int64_t txfee = app->getLedgerManager().getTxFee(); + + auto balanceFunc = [&](int n) { return 1 + winnerVote / 2; }; + auto voteFunc = [&](int n) { return 0; }; + createTestAccounts(*app, 2, balanceFunc, voteFunc); + auto a0 = TestAccount(*app, getTestAccount(0)); + auto native = makeNativeAsset(); + auto cur1 = a0.asset("CUR1"); + auto offerAmount = INT64_MAX - a0.getBalance() - available; + + TestMarket market(*app); + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer( + a0, {cur1, native, Price{1, 1}, offerAmount}); + }); + root.pay(a0, txfee); + + closeLedgerOn(*app, 2, 21, 7, 2014); + + int expectedWinners = (expectedPayout > 0); + doInflation(*app, app->getLedgerManager().getCurrentLedgerVersion(), + 2, balanceFunc, voteFunc, expectedWinners); + }; + + auto header = app->getLedgerManager().getCurrentLedgerHeader(); + int64_t maxPayout = + bigDivide(header.totalCoins, 190721, 1000000000, ROUND_DOWN) + + header.feePool; + + SECTION("no available balance") + { + for_versions_to(9, *app, + [&] { inflationWithLiabilities(0, maxPayout); }); + for_versions_from(10, *app, + [&] { inflationWithLiabilities(0, 0); }); + } + + SECTION("small available balance less than payout") + { + for_versions_to(9, *app, + [&] { inflationWithLiabilities(1, maxPayout); }); + for_versions_from(10, *app, + [&] { inflationWithLiabilities(1, 1); }); + } + + SECTION("large available balance less than payout") + { + for_versions_to(9, *app, [&] { + inflationWithLiabilities(maxPayout - 1, maxPayout); + }); + for_versions_from(10, *app, [&] { + inflationWithLiabilities(maxPayout - 1, maxPayout - 1); + }); + } + + SECTION("available balance greater than payout") + { + for_versions_to(9, *app, [&] { + inflationWithLiabilities(maxPayout + 1, maxPayout); + }); + for_versions_from(10, *app, [&] { + inflationWithLiabilities(maxPayout + 1, maxPayout); + }); + } + } } diff --git a/src/transactions/ManageDataTests.cpp b/src/transactions/ManageDataTests.cpp index 4657f98921..98168557ad 100644 --- a/src/transactions/ManageDataTests.cpp +++ b/src/transactions/ManageDataTests.cpp @@ -7,6 +7,7 @@ #include "main/Application.h" #include "test/TestAccount.h" #include "test/TestExceptions.h" +#include "test/TestMarket.h" #include "test/TestUtils.h" #include "test/TxTests.h" #include "test/test.h" @@ -116,4 +117,44 @@ TEST_CASE("manage data", "[tx][managedata]") // fail to remove data entry that isn't present REQUIRE_THROWS_AS(gateway.manageData(t4, nullptr), ex_txINTERNAL_ERROR); }); + + SECTION("create data with native selling liabilities") + { + auto const minBal2 = app->getLedgerManager().getMinBalance(2); + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBal2 + 2 * txfee + 500 - 1); + TestMarket market(*app); + + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {native, cur1, Price{1, 1}, 500}); + }); + + for_versions({2}, *app, [&] { acc1.manageData(t1, &value); }); + for_versions(4, 9, *app, [&] { acc1.manageData(t1, &value); }); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(acc1.manageData(t1, &value), + ex_MANAGE_DATA_LOW_RESERVE); + root.pay(acc1, txfee + 1); + acc1.manageData(t1, &value); + }); + } + + SECTION("create data with native buying liabilities") + { + auto const minBal2 = app->getLedgerManager().getMinBalance(2); + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBal2 + 2 * txfee + 500 - 1); + TestMarket market(*app); + + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {cur1, native, Price{1, 1}, 500}); + }); + + for_versions({2}, *app, [&] { acc1.manageData(t1, &value); }); + for_versions_from(4, *app, [&] { acc1.manageData(t1, &value); }); + } } diff --git a/src/transactions/MergeTests.cpp b/src/transactions/MergeTests.cpp index 8c251d8118..07c64b13aa 100644 --- a/src/transactions/MergeTests.cpp +++ b/src/transactions/MergeTests.cpp @@ -9,6 +9,7 @@ #include "main/Config.h" #include "test/TestAccount.h" #include "test/TestExceptions.h" +#include "test/TestMarket.h" #include "test/TestUtils.h" #include "test/TxTests.h" #include "test/test.h" @@ -469,17 +470,13 @@ TEST_CASE("merge", "[tx][merge]") for_all_versions(*app, [&] { gateway.pay(a1, usd, trustLineBalance); auto xlm = makeNativeAsset(); + auto curIssued = a1.asset("CUR1"); const Price somePrice(3, 2); for (int i = 0; i < 4; i++) { - a1.manageOffer(0, xlm, usd, somePrice, 100); + a1.manageOffer(0, xlm, curIssued, somePrice, 100); } - // empty out balance - a1.pay(gateway, usd, trustLineBalance); - // delete the trust line - a1.changeTrust(usd, 0); - REQUIRE_THROWS_AS(a1.merge(b1), ex_ACCOUNT_MERGE_HAS_SUB_ENTRIES); }); @@ -648,4 +645,42 @@ TEST_CASE("merge", "[tx][merge]") } }); } + + SECTION("destination with native buying liabilities") + { + auto& lm = app->getLedgerManager(); + auto txfee = lm.getTxFee(); + auto minBal = lm.getMinBalance(1); + auto acc1 = root.create("acc1", minBal + txfee); + auto acc2 = root.create("acc2", minBal + txfee + 1); + + auto const native = makeNativeAsset(); + auto cur1 = acc1.asset("CUR1"); + + TestMarket market(*app); + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {cur1, native, Price{1, 1}, INT64_MAX - 2 * minBal}); + }); + + for_versions_to(9, *app, [&] { + acc2.merge(acc1); + + auto af = AccountFrame::loadAccount(acc1.getPublicKey(), + app->getDatabase()); + REQUIRE(af->getBalance() == 2 * minBal + 1); + }); + for_versions_from(10, *app, [&] { + closeLedgerOn(*app, 3, 1, 1, 2017); + REQUIRE_THROWS_AS(acc2.merge(acc1), ex_ACCOUNT_MERGE_DEST_FULL); + root.pay(acc2, txfee - 1); + acc2.merge(acc1); + + auto af = AccountFrame::loadAccount(acc1.getPublicKey(), + app->getDatabase()); + REQUIRE(af->getBalance() == 2 * minBal); + REQUIRE(af->getBalance() + af->getBuyingLiabilities(lm) == + INT64_MAX); + }); + } } diff --git a/src/transactions/OfferTests.cpp b/src/transactions/OfferTests.cpp index e30fdd24fa..ab8a4c7ae7 100644 --- a/src/transactions/OfferTests.cpp +++ b/src/transactions/OfferTests.cpp @@ -32,7 +32,7 @@ using namespace stellar::txtest; TEST_CASE("create offer", "[tx][offers]") { - Config const& cfg = getTestConfig(); + Config const& cfg = getTestConfig(0); VirtualClock clock; auto app = createTestApplication(clock, cfg); @@ -331,6 +331,7 @@ TEST_CASE("create offer", "[tx][offers]") auto offer = market.requireChangesWithOffer({}, [&] { return market.addOffer(a1, {idr, usd, oneone, 100}); }); + auto cancelCheck = [&]() { market.requireChangesWithOffer({}, [&] { return market.updateOffer(a1, offer.key.offerID, @@ -346,35 +347,88 @@ TEST_CASE("create offer", "[tx][offers]") SECTION("cancel offer with empty selling trust line") { - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { a1.pay(issuer, idr, trustLineBalance); cancelCheck(); }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer selling asset X with no balance of + // asset X. This can be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to reduce balance + // below selling liabilities (tested in "payment"/"pathpayment" + // section "liabilities" subsection "cannot pay balance below + // selling liabilities") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to reduce + // balance below selling liabilities (tested in "create offer" + // section "cannot create offer that would lead to excess + // liabilities" subsection "* selling liabilities") + // - Cannot use ManageOfferOp to create an offer with no selling + // liabilities (tested in "create offer" section "new offer is + // not created if it does not satisfy thresholds") } SECTION("cancel offer with deleted selling trust line") { - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { a1.pay(issuer, idr, trustLineBalance); a1.changeTrust(idr, 0); cancelCheck(); }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer selling asset X with no trust line + // for asset X. This can be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to reduce balance + // below selling liabilities (tested in "payment"/"pathpayment" + // section "liabilities" subsection "cannot pay balance below + // selling liabilities") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to reduce + // balance below selling liabilities (tested in "create offer" + // section "cannot create offer that would lead to excess + // liabilities" subsection "non-native selling liabilities") + // - Cannot use ManageOfferOp to create an offer with no selling + // liabilities (tested in "create offer" section "new offer is + // not created if it does not satisfy thresholds") + // - Upgrade to version 10 or increased base reserve removes + // offers with no selling liabilities (tested in "upgrade to + // version 10" section "adjust offers" subsection "thresholds") + // - Cannot use ChangeTrustOp to delete a trust line with non-zero + // balance (tested in "change trust" section "basic tests") } SECTION("cancel offer with full buying trust line") { - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { issuer.pay(a1, usd, trustLineLimit); cancelCheck(); }); + + for_versions_from(10, *app, [&] { + auto usdBuyingLiabilities = getBuyingLiabilities( + a1.loadTrustLine(usd), app->getLedgerManager()); + issuer.pay(a1, usd, trustLineLimit - usdBuyingLiabilities); + REQUIRE_THROWS_AS(issuer.pay(a1, usd, 1), ex_PAYMENT_LINE_FULL); + cancelCheck(); + }); } SECTION("cancel offer with deleted buying trust line") { - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { a1.changeTrust(usd, 0); cancelCheck(); }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer buying asset X with no trust line for + // asset X. This can be verified from the following tests: + // - Cannot use ManageOfferOp to create an offer with no buying + // liabilities (tested in "create offer" section "new offer is + // not created if it does not satisfy thresholds") + // - Upgrade to version 10 or increased base reserve removes + // offers with no buying liabilities (tested in "upgrade to + // version 10" section "adjust offers" subsection "thresholds") + // - Cannot use ChangeTrustOp to delete a trust line with non-zero + // buying liabilities (tested in "change trust" section "cannot + // reduce limit below buying liabilities or delete") } SECTION("update price") @@ -562,7 +616,7 @@ TEST_CASE("create offer", "[tx][offers]") ex_MANAGE_OFFER_LOW_RESERVE); }); - for_versions_from(9, *app, [&]() { + for_versions({9}, *app, [&]() { // in v9, we sell as much as possible above base1 // endBalance = base1 // actualPayment = start - 2*txfee - endBalance @@ -572,6 +626,15 @@ TEST_CASE("create offer", "[tx][offers]") auto actualPayment = a1IDrs - delta; checkCrossed(b1, actualPayment, offerAmount); }); + + for_versions_from(10, *app, [&]() { + auto actualPayment = a1IDrs - delta; + REQUIRE_THROWS_AS( + checkCrossed(b1, actualPayment, actualPayment + 1), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(b1, txfee); + checkCrossed(b1, actualPayment, actualPayment); + }); } } } @@ -608,7 +671,7 @@ TEST_CASE("create offer", "[tx][offers]") issuer.pay(a1, usd, 20000); // ensure we could receive proceeds from the offer - a1.pay(issuer, idr, 100000); + a1.pay(issuer, idr, 50000); // offer is sell 150 USD for 100 IDR; sell USD @ 1.5 / // buy IRD @ 0.66 @@ -662,6 +725,86 @@ TEST_CASE("create offer", "[tx][offers]") // the USDs were sold at the (better) rate found in the // original offers + // offer is sell 1010 USD for 505 IDR; sell USD @ 0.5 + market.requireChangesWithOffer( + {{offers[0].key, OfferState::DELETED}, + {offers[1].key, OfferState::DELETED}, + {offers[2].key, OfferState::DELETED}, + {offers[3].key, OfferState::DELETED}, + {offers[4].key, OfferState::DELETED}, + {offers[5].key, OfferState::DELETED}, + {offers[6].key, {idr, usd, price, 28}}}, + [&] { + return market.addOffer( + b1, {usd, idr, Price{1, 2}, 1009}, + OfferState::DELETED); + }); + + market.requireBalances({{a1, {{usd, 1009}, {idr, 99328}}}, + {b1, {{usd, 18991}, {idr, 672}}}}); + }); + + for_versions_from(10, *app, [&] { + issuer.pay(b1, usd, 20000); + + market.requireBalances({{a1, {{usd, 0}, {idr, 100000}}}, + {b1, {{usd, 20000}, {idr, 0}}}}); + + // Offers are: sell 100 IDR for 150 USD; sell IRD @ 0.66 + // -> buy USD @ 1.5 + // first 6 offers get taken for 6*150=900 USD, gets 600 + // IDR in return + + // For versions < 10: + // offer #7 : has 110 USD available + // -> can claim partial offer 100*110/150 = 73.333 ; + // -> 26.66666 left + // 8 .. untouched + // the USDs were sold at the (better) rate found in the + // original offers + + // offer is sell 1010 USD for 505 IDR; sell USD @ 0.5 + market.requireChangesWithOffer( + {{offers[0].key, OfferState::DELETED}, + {offers[1].key, OfferState::DELETED}, + {offers[2].key, OfferState::DELETED}, + {offers[3].key, OfferState::DELETED}, + {offers[4].key, OfferState::DELETED}, + {offers[5].key, OfferState::DELETED}, + {offers[6].key, {idr, usd, price, 28}}}, + [&] { + return market.addOffer( + b1, {usd, idr, Price{1, 2}, 1009}, + OfferState::DELETED); + }); + + market.requireBalances({{a1, {{usd, 1008}, {idr, 99328}}}, + {b1, {{usd, 18992}, {idr, 672}}}}); + }); + } + + SECTION("offer crosses, removes first six and removes seventh by " + "adjustment") + { + for_versions_to(9, *app, [&] { + issuer.pay(b1, usd, 20000); + + market.requireBalances({{a1, {{usd, 0}, {idr, 100000}}}, + {b1, {{usd, 20000}, {idr, 0}}}}); + + // Offers are: sell 100 IDR for 150 USD; sell IRD @ 0.66 + // -> buy USD @ 1.5 + // first 6 offers get taken for 6*150=900 USD, gets 600 + // IDR in return + + // For versions < 10: + // offer #7 : has 110 USD available + // -> can claim partial offer 100*110/150 = 73.333 ; + // -> 26.66666 left + // 8 .. untouched + // the USDs were sold at the (better) rate found in the + // original offers + // offer is sell 1010 USD for 505 IDR; sell USD @ 0.5 market.requireChangesWithOffer( {{offers[0].key, OfferState::DELETED}, @@ -729,10 +872,9 @@ TEST_CASE("create offer", "[tx][offers]") }); } - SECTION("offer crosses, removes first six and changes seventh and " - "then remains") + SECTION("offer crosses, removes all offers, and remains") { - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { issuer.pay(b1, usd, 20000); market.requireBalances({{a1, {{usd, 0}, {idr, 100000}}}, @@ -755,7 +897,6 @@ TEST_CASE("create offer", "[tx][offers]") market.checkCurrentOffers(); // offer is sell 10000 USD for 5000 IDR; sell USD @ 0.5 - auto usdBalanceForSale = 10000; auto usdBalanceRemaining = 6700; auto offerPosted = @@ -776,6 +917,34 @@ TEST_CASE("create offer", "[tx][offers]") market.requireBalances({{a1, {{usd, 3300}, {idr, 97800}}}, {b1, {{usd, 16700}, {idr, 2200}}}}); }); + + for_versions_from(10, *app, [&] { + issuer.pay(b1, usd, 20000); + + market.requireBalances({{a1, {{usd, 0}, {idr, 100000}}}, + {b1, {{usd, 20000}, {idr, 0}}}}); + + // Cannot add invalid offer as in versions less than 10 + + // offer is sell 10000 USD for 5000 IDR; sell USD @ 0.5 + auto usdBalanceForSale = 10000; + auto usdBalanceRemaining = 6700; + auto offerPosted = + OfferState{usd, idr, Price{1, 2}, usdBalanceForSale}; + auto offerRemaining = + OfferState{usd, idr, Price{1, 2}, usdBalanceRemaining}; + auto removed = std::vector{}; + for (auto o : offers) + { + removed.push_back({o.key, OfferState::DELETED}); + } + auto offer = market.requireChangesWithOffer(removed, [&] { + return market.addOffer(b1, offerPosted, offerRemaining); + }); + + market.requireBalances({{a1, {{usd, 3300}, {idr, 97800}}}, + {b1, {{usd, 16700}, {idr, 2200}}}}); + }); } SECTION("multiple offers with small amount crosses") @@ -858,7 +1027,7 @@ TEST_CASE("create offer", "[tx][offers]") SECTION("creates an offer but reaches limit while selling") { - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { // fund C such that it's 150 IDR below its limit issuer.pay(c1, idr, trustLineLimit - 150); @@ -899,11 +1068,20 @@ TEST_CASE("create offer", "[tx][offers]") {b1, {{usd, 75}, {idr, 99950}}}, {c1, {{usd, 99775}, {idr, 1000000}}}}); }); + // This is no longer possible starting in version 10, as it is + // impossible to create an offer that can take a trust line + // above its limit. This can be verified from the following + // tests: + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess buying liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "non-native buying + // liabilities") } SECTION("creates an offer but top seller is not authorized") { - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { // sets up the secure issuer account for USD auto issuerAuth = root.create("issuerAuth", minBalance2); @@ -997,11 +1175,20 @@ TEST_CASE("create offer", "[tx][offers]") {e1, {{usdAuth, 150}, {idrAuth, 99900}}}, {f1, {{usdAuth, 99850}, {idrAuth, 100}}}}); }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer selling asset X without a trust + // line authorized to hold X . This can be verified from the + // following tests: + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer without an authorized trustline + // - AllowTrustOp deletes all offers that would no longer be + // authorized (tested in "allow trust" section "allow trust + // with offers") } SECTION("creates an offer but top seller reaches limit") { - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { // makes "A" only capable of holding 75 "USD" issuer.pay(a1, usd, trustLineLimit - 75); @@ -1037,6 +1224,22 @@ TEST_CASE("create offer", "[tx][offers]") {b1, {{usd, 150}, {idr, trustLineBalance - 100}}}, {c1, {{usd, trustLineBalance - 225}, {idr, 150}}}}); }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer that can take a trust line above + // its limit. This can be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to increase + // balance + buying liabilities above limit (tested in + // "payment"/"pathpayment" section "liabilities" subsection + // "cannot receive such that balance + buying liabilities + // exceeds limit") + // - Cannot use ChangeTrustOp to reduce limit below balance + + // buying liabilities (tested in "change trust" section + // "cannot reduce limit below buying liabilities or delete") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess buying liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "non-native buying + // liabilities") } } @@ -1094,38 +1297,48 @@ TEST_CASE("create offer", "[tx][offers]") }); for_versions_from(10, *app, [&]() { - // as b1 cannot buy any more IDRs the offer should be - // deleted - auto offerB1 = market.requireChangesWithOffer( - {{offerC1.key, offerC1Changed}}, [&] { - return market.addOffer(b1, offerB1Params, - OfferState::DELETED); - }); + // as b1 cannot buy any more IDRs the operation should fail + REQUIRE_THROWS_AS( + market.addOffer(b1, offerB1Params, OfferState::DELETED), + ex_MANAGE_OFFER_LINE_FULL); }); } SECTION("Offer reaches limit") { - // make it that c1 can only receive 9 USD (for 1 IDR) - c1.changeTrust(usd, 9 * assetMultiplier); - // make it that b1 can receive more than 1 IDR - b1.changeTrust(idr, 1000 * assetMultiplier); - - // as c1 cannot buy any more USDs, c1's offer should be - // deleted - // b1's offer is created to sell the remainder - // 200-9 = 191 USD - - auto offerRemaining = - OfferState{usd, idr, p, 191 * assetMultiplier}; - - for_all_versions(*app, [&]() { + for_versions_to(9, *app, [&]() { + // make it that c1 can only receive 9 USD (for 1 IDR) + c1.changeTrust(usd, 9 * assetMultiplier); + // make it that b1 can receive more than 1 IDR + b1.changeTrust(idr, 1000 * assetMultiplier); + // as c1 cannot buy any more USDs, c1's offer should be + // deleted + // b1's offer is created to sell the remainder + // 200-9 = 191 USD + auto offerRemaining = + OfferState{usd, idr, p, 191 * assetMultiplier}; auto offerB1 = market.requireChangesWithOffer( {{offerC1.key, OfferState::DELETED}}, [&] { return market.addOffer(b1, offerB1Params, offerRemaining); }); }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer that can take a trust line above + // its limit. This can be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to increase + // balance + buying liabilities above limit (tested in + // "payment"/"pathpayment" section "liabilities" subsection + // "cannot receive such that balance + buying liabilities + // exceeds limit") + // - Cannot use ChangeTrustOp to reduce limit below balance + + // buying liabilities (tested in "change trust" section + // "cannot reduce limit below buying liabilities or delete") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess buying liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "non-native buying + // liabilities") } } @@ -1278,11 +1491,11 @@ TEST_CASE("create offer", "[tx][offers]") { auto market = TestMarket{*app}; auto a1 = root.create("A", app->getLedgerManager().getMinBalance(2) + - 4 * txfee + 10); + 3 * txfee + 110); a1.changeTrust(usd, trustLineLimit); - for_all_versions(*app, [&] { + for_versions_to(9, *app, [&] { auto offer = market.requireChangesWithOffer({}, [&] { - return market.addOffer(a1, {xlm, usd, oneone, 9}); + return market.addOffer(a1, {xlm, usd, oneone, 110}); }); market.requireChangesWithOffer({}, [&] { return market.updateOffer(a1, offer.key.offerID, @@ -1290,6 +1503,14 @@ TEST_CASE("create offer", "[tx][offers]") {xlm, usd, oneone, 110}); }); }); + for_versions_from(10, *app, [&] { + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(a1, {xlm, usd, oneone, 110}); + }); + REQUIRE_THROWS_AS(market.updateOffer(a1, offer.key.offerID, + {xlm, usd, oneone, 111}), + ex_MANAGE_OFFER_UNDERFUNDED); + }); } SECTION("wheat stays or sheep stays") @@ -1417,94 +1638,1320 @@ TEST_CASE("create offer", "[tx][offers]") }); } - SECTION("available balance non-native can cause an offer to adjust to 0") + SECTION("max liabilities") { - for_versions_from(10, *app, [&] { - auto const minBalance3 = app->getLedgerManager().getMinBalance(3); - auto wheatSeller = root.create("wheat", minBalance3 + 10000); - auto sheepSeller = root.create("sheep", minBalance3 + 10000); + SECTION("buying non-native") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(2); + auto acc1 = root.create("acc1", minBalance + 10000); + auto acc2 = root.create("acc2", minBalance + 10000); + + acc1.changeTrust(usd, 1000); + acc2.changeTrust(usd, 1000); + issuer.pay(acc2, usd, 1000); + + auto market = TestMarket{*app}; + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 1000}); + }); + market.requireChangesWithOffer( + {{offer.key, OfferState::DELETED}}, [&] { + return market.addOffer(acc2, + {usd, xlm, Price{1, 1}, 1000}, + OfferState::DELETED); + }); + }); + } + SECTION("selling non-native") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(2); + auto acc1 = root.create("acc1", minBalance + 10000); + auto acc2 = root.create("acc2", minBalance + 10000); + + acc1.changeTrust(usd, 1000); + acc2.changeTrust(usd, 1000); + issuer.pay(acc2, usd, 1000); + + auto market = TestMarket{*app}; + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc2, {usd, xlm, Price{1, 1}, 1000}); + }); + market.requireChangesWithOffer( + {{offer.key, OfferState::DELETED}}, [&] { + return market.addOffer(acc1, + {xlm, usd, Price{1, 1}, 1000}, + OfferState::DELETED); + }); + }); + } + } - wheatSeller.changeTrust(idr, INT64_MAX); - wheatSeller.changeTrust(usd, INT64_MAX); - sheepSeller.changeTrust(idr, INT64_MAX); - sheepSeller.changeTrust(usd, INT64_MAX); + SECTION("cannot create offer that would lead to excess liabilities") + { + SECTION("native selling liabilities") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(2) + txfee; + auto acc1 = root.create("acc1", minBalance); + auto market = TestMarket{*app}; - auto market = TestMarket{*app}; + acc1.changeTrust(usd, 2000); - issuer.pay(wheatSeller, idr, 28); - auto wheatOffer = market.requireChangesWithOffer({}, [&] { - return market.addOffer(wheatSeller, {idr, usd, Price{3, 2}, 28}, - {idr, usd, Price{3, 2}, 28}); + // Test when no existing offers + root.pay(acc1, xlm, 500 + txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {xlm, usd, Price{1, 1}, 501}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, xlm, txfee); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 500}); + }); + + // Test when existing offers + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + root.pay(acc1, xlm, 500 + txfee + reserve); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {xlm, usd, Price{1, 1}, 501}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, xlm, txfee); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 500}); + }); }); - wheatSeller.pay(issuer, idr, 1); + } - issuer.pay(sheepSeller, usd, 1000); - auto sheepOffer = market.requireChangesWithOffer( - {{wheatOffer.key, OfferState::DELETED}}, [&] { - return market.addOffer(sheepSeller, - {usd, idr, Price{2, 3}, 999}, - {usd, idr, Price{2, 3}, 999}); + SECTION("native buying liabilities") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + txfee); + auto market = TestMarket{*app}; + + acc1.changeTrust(usd, INT64_MAX); + issuer.pay(acc1, usd, INT64_MAX); + + // Test when no existing offers + root.pay(acc1, xlm, 2 * txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {usd, xlm, Price{1, 1}, + INT64_MAX - minBalance - txfee + 1}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, xlm, txfee); + auto o1 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {usd, xlm, Price{1, 1}, + INT64_MAX - minBalance - txfee}); }); - }); - } - SECTION("available balance native can cause an offer to adjust to 0") - { - for_versions_from(10, *app, [&] { - auto const baseMinBalance2 = - app->getLedgerManager().getMinBalance(2); - auto wheatSeller = root.create("wheat", baseMinBalance2 + 10000); - auto sheepSeller = root.create("sheep", baseMinBalance2 + 10000); + // Free some available limit + market.requireChangesWithOffer({}, [&] { + return market.updateOffer( + acc1, o1.key.offerID, + {usd, xlm, Price{1, 1}, INT64_MAX - minBalance - 500}); + }); - wheatSeller.changeTrust(usd, INT64_MAX); - sheepSeller.changeTrust(usd, INT64_MAX); + // Test when existing offers + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + root.pay(acc1, xlm, txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {usd, xlm, Price{1, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, xlm, txfee); + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {usd, xlm, Price{1, 1}, 500}); + }); + }); + } - auto market = TestMarket{*app}; + SECTION("non-native selling liabilities") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + auto market = TestMarket{*app}; - auto wheatOffer = market.requireChangesWithOffer({}, [&] { - return market.addOffer(wheatSeller, {xlm, usd, Price{3, 2}, 28}, - {xlm, usd, Price{3, 2}, 28}); + acc1.changeTrust(usd, 2000); + + // Test when no existing offers + issuer.pay(acc1, usd, 500); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {usd, xlm, Price{1, 1}, 501}), + ex_MANAGE_OFFER_UNDERFUNDED); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {usd, xlm, Price{1, 1}, 500}); + }); + + // Test when existing offers + issuer.pay(acc1, usd, 500); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {usd, xlm, Price{1, 1}, 501}), + ex_MANAGE_OFFER_UNDERFUNDED); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {usd, xlm, Price{1, 1}, 500}); + }); }); - wheatSeller.pay(issuer, xlm, 10000 - 3 * txfee - 27); + } - issuer.pay(sheepSeller, usd, 1000); - auto sheepOffer = market.requireChangesWithOffer( - {{wheatOffer.key, OfferState::DELETED}}, [&] { - return market.addOffer(sheepSeller, - {usd, xlm, Price{2, 3}, 999}, - {usd, xlm, Price{2, 3}, 999}); + SECTION("non-native buying liabilities") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + auto market = TestMarket{*app}; + + // Test when no existing offers + acc1.changeTrust(usd, 1000); + issuer.pay(acc1, usd, 500); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {xlm, usd, Price{1, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 500}); }); - }); + + // Test when existing offers + acc1.changeTrust(usd, 1500); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {xlm, usd, Price{1, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 500}); + }); + }); + } } - SECTION("trust line limit can cause an offer to adjust to 0") + SECTION("cannot modify offer that would lead to excess liabilities") { - for_versions_from(10, *app, [&] { - auto const minBalance3 = app->getLedgerManager().getMinBalance(3); - auto wheatSeller = root.create("wheat", minBalance3 + 10000); - auto sheepSeller = root.create("sheep", minBalance3 + 10000); + SECTION("native selling liabilities") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(2); + auto acc1 = root.create("acc1", minBalance + txfee); + auto market = TestMarket{*app}; - wheatSeller.changeTrust(idr, INT64_MAX); - wheatSeller.changeTrust(usd, INT64_MAX); - sheepSeller.changeTrust(idr, INT64_MAX); - sheepSeller.changeTrust(usd, INT64_MAX); + acc1.changeTrust(usd, 2000); - auto market = TestMarket{*app}; + // Test when no existing offers + root.pay(acc1, xlm, 500 + txfee); + auto o1 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 250}); + }); + root.pay(acc1, xlm, txfee); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o1.key.offerID, + {xlm, usd, Price{1, 1}, 501}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, xlm, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {xlm, usd, Price{1, 1}, 500}); + }); - issuer.pay(wheatSeller, idr, 28); - auto wheatOffer = market.requireChangesWithOffer({}, [&] { - return market.addOffer(wheatSeller, {idr, usd, Price{3, 2}, 28}, - {idr, usd, Price{3, 2}, 28}); + // Test when existing offers + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + root.pay(acc1, xlm, 500 + txfee + reserve); + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 250}); + }); + root.pay(acc1, xlm, txfee); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o2.key.offerID, + {xlm, usd, Price{1, 1}, 501}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, xlm, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o2.key.offerID, + {xlm, usd, Price{1, 1}, 500}); + }); }); - wheatSeller.changeTrust(usd, 41); + } - issuer.pay(sheepSeller, usd, 1000); - auto sheepOffer = market.requireChangesWithOffer( - {{wheatOffer.key, OfferState::DELETED}}, [&] { - return market.addOffer(sheepSeller, - {usd, idr, Price{2, 3}, 999}, - {usd, idr, Price{2, 3}, 999}); + SECTION("native buying liabilities") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + txfee); + auto market = TestMarket{*app}; + + acc1.changeTrust(usd, INT64_MAX); + issuer.pay(acc1, usd, INT64_MAX); + + // Test when no existing offers + root.pay(acc1, xlm, txfee); + auto o1 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {usd, xlm, Price{1, 1}, + INT64_MAX - minBalance - txfee}); }); - }); - } + root.pay(acc1, xlm, txfee); + REQUIRE_THROWS_AS( + market.updateOffer( + acc1, o1.key.offerID, + {usd, xlm, Price{1, 1}, INT64_MAX - minBalance + 1}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, xlm, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer( + acc1, o1.key.offerID, + {usd, xlm, Price{1, 1}, INT64_MAX - minBalance}); + }); + + // Free some available limit + market.requireChangesWithOffer({}, [&] { + return market.updateOffer( + acc1, o1.key.offerID, + {usd, xlm, Price{1, 1}, INT64_MAX - minBalance - 500}); + }); + + // Test when existing offers + root.pay(acc1, xlm, 2 * txfee); + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {usd, xlm, Price{1, 1}, 250}); + }); + root.pay(acc1, xlm, txfee); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o2.key.offerID, + {usd, xlm, Price{1, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, xlm, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o2.key.offerID, + {usd, xlm, Price{1, 1}, 500}); + }); + }); + } + + SECTION("non-native selling liabilities") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + auto market = TestMarket{*app}; + + acc1.changeTrust(usd, 2000); + + // Test when no existing offers + issuer.pay(acc1, usd, 500); + auto o1 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {usd, xlm, Price{1, 1}, 250}); + }); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o1.key.offerID, + {usd, xlm, Price{1, 1}, 501}), + ex_MANAGE_OFFER_UNDERFUNDED); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {usd, xlm, Price{1, 1}, 500}); + }); + + // Test when existing offers + issuer.pay(acc1, usd, 500); + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {usd, xlm, Price{1, 1}, 250}); + }); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o2.key.offerID, + {usd, xlm, Price{1, 1}, 501}), + ex_MANAGE_OFFER_UNDERFUNDED); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o2.key.offerID, + {usd, xlm, Price{1, 1}, 500}); + }); + }); + } + + SECTION("non-native buying liabilities") + { + for_versions_from(10, *app, [&] { + auto const minBalance = + app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + auto market = TestMarket{*app}; + + // Test when no existing offers + acc1.changeTrust(usd, 1000); + issuer.pay(acc1, usd, 500); + auto o1 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 250}); + }); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o1.key.offerID, + {xlm, usd, Price{1, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {xlm, usd, Price{1, 1}, 500}); + }); + + // Test when existing offers + acc1.changeTrust(usd, 1500); + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 250}); + }); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o2.key.offerID, + {xlm, usd, Price{1, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o2.key.offerID, + {xlm, usd, Price{1, 1}, 500}); + }); + }); + } + } + + SECTION("cannot create unauthorized offer") + { + for_all_versions(*app, [&] { + auto const minBalance = app->getLedgerManager().getMinBalance(2); + auto acc1 = root.create("acc1", minBalance + 10000); + + auto toSet = static_cast(AUTH_REQUIRED_FLAG) | + static_cast(AUTH_REVOCABLE_FLAG); + issuer.setOptions(txtest::setFlags(toSet)); + + acc1.changeTrust(idr, trustLineLimit); + issuer.allowTrust(idr, acc1); + issuer.pay(acc1, idr, 1); + issuer.denyTrust(idr, acc1); + + TestMarket market(*app); + REQUIRE_THROWS_AS(market.addOffer(acc1, {idr, xlm, Price{1, 1}, 1}), + ex_MANAGE_OFFER_SELL_NOT_AUTHORIZED); + REQUIRE_THROWS_AS(market.addOffer(acc1, {xlm, idr, Price{1, 1}, 1}), + ex_MANAGE_OFFER_BUY_NOT_AUTHORIZED); + }); + } + + SECTION("offer with excess liabilities that does not meet thresholds") + { + SECTION("create fails") + { + auto const minBalance = app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + + acc1.changeTrust(usd, 1); + acc1.changeTrust(idr, 1); + issuer.pay(acc1, usd, 1); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS( + market.addOffer(acc1, {usd, idr, Price{3, 2}, 27}), + ex_MANAGE_OFFER_LINE_FULL); + }); + } + + SECTION("modify fails") + { + auto const minBalance = app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + + acc1.changeTrust(usd, 2); + acc1.changeTrust(idr, 3); + issuer.pay(acc1, usd, 2); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {usd, idr, Price{3, 2}, 2}); + }); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, offer.key.offerID, + {usd, idr, Price{3, 2}, 27}), + ex_MANAGE_OFFER_LINE_FULL); + }); + } + } + + SECTION("modify offer price with liabilities") + { + SECTION("selling native") + { + auto const minBalance = app->getLedgerManager().getMinBalance(2); + auto acc1 = root.create("acc1", minBalance + 10000); + + acc1.changeTrust(usd, 500); + + TestMarket market(*app); + auto o1 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {xlm, usd, Price{1, 1}, 500}); + }); + + for_versions_from(10, *app, [&] { + SECTION("increase price") + { + acc1.changeTrust(usd, 999); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o1.key.offerID, + {xlm, usd, Price{2, 1}, 500}), + ex_MANAGE_OFFER_LINE_FULL); + acc1.changeTrust(usd, 1000); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {xlm, usd, Price{2, 1}, 500}); + }); + } + SECTION("decrease price") + { + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {xlm, usd, Price{1, 2}, 500}); + }); + } + }); + } + + SECTION("buying native") + { + auto const minBalance = app->getLedgerManager().getMinBalance(4); + auto acc1 = root.create("acc1", minBalance + 4 * txfee); + + acc1.changeTrust(idr, 500); + acc1.changeTrust(usd, INT64_MAX); + issuer.pay(acc1, idr, 500); + issuer.pay(acc1, usd, INT64_MAX); + + TestMarket market(*app); + auto o1 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {idr, xlm, Price{1, 1}, 500}); + }); + + for_versions_from(10, *app, [&] { + SECTION("increase price") + { + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {usd, xlm, Price{1, 1}, + INT64_MAX - minBalance - 999}); + }); + + root.pay(acc1, txfee); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o1.key.offerID, + {idr, xlm, Price{2, 1}, 500}), + ex_MANAGE_OFFER_LINE_FULL); + + root.pay(acc1, 2 * txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer( + acc1, o2.key.offerID, + {usd, xlm, Price{1, 1}, + INT64_MAX - minBalance - 1000}); + }); + + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {idr, xlm, Price{2, 1}, 500}); + }); + } + SECTION("decrease price") + { + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {usd, xlm, Price{1, 1}, + INT64_MAX - minBalance - 500 - txfee}); + }); + root.pay(acc1, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {idr, xlm, Price{1, 2}, 500}); + }); + } + }); + } + + SECTION("non-native") + { + auto const minBalance = app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + + acc1.changeTrust(idr, 500); + acc1.changeTrust(usd, 500); + issuer.pay(acc1, idr, 500); + + TestMarket market(*app); + auto o1 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {idr, usd, Price{1, 1}, 500}); + }); + + for_versions_from(10, *app, [&] { + SECTION("increase price") + { + acc1.changeTrust(usd, 999); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, o1.key.offerID, + {idr, usd, Price{2, 1}, 500}), + ex_MANAGE_OFFER_LINE_FULL); + acc1.changeTrust(usd, 1000); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {idr, usd, Price{2, 1}, 500}); + }); + } + SECTION("decrease price") + { + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, o1.key.offerID, + {idr, usd, Price{1, 2}, 500}); + }); + } + }); + } + } + + SECTION("modify offer assets with liabilities") + { + auto getLiabilities = [&](TestAccount& acc) { + auto& lm = app->getLedgerManager(); + Liabilities res; + auto account = txtest::loadAccount(acc.getPublicKey(), *app); + res.selling = account->getSellingLiabilities(lm); + res.buying = account->getBuyingLiabilities(lm); + return res; + }; + auto getAssetLiabilities = [&](TestAccount& acc, Asset const& asset) { + auto& lm = app->getLedgerManager(); + Liabilities res; + if (acc.hasTrustLine(asset)) + { + auto trust = acc.loadTrustLine(asset); + res.selling = getSellingLiabilities(trust, lm); + res.buying = getBuyingLiabilities(trust, lm); + } + return res; + }; + auto checkLiabilities = [&](TestAccount& acc, Asset const& asset) { + if (asset.type() == ASSET_TYPE_NATIVE) + { + return getLiabilities(acc); + } + else + { + return getAssetLiabilities(acc, asset); + } + }; + + auto checkModifyAssets = [&](Asset initialSelling, Asset initialBuying, + Asset finalSelling, Asset finalBuying) { + auto reserve = + app->getLedgerManager().getCurrentLedgerHeader().baseReserve; + auto const minBalance = app->getLedgerManager().getMinBalance(0); + auto acc1 = root.create("acc1", minBalance); + TestMarket market(*app); + + REQUIRE(!(initialSelling == initialBuying)); + REQUIRE(!(finalSelling == finalBuying)); + + if (initialSelling.type() == ASSET_TYPE_NATIVE) + { + root.pay(acc1, 500); + } + else + { + root.pay(acc1, reserve + txfee); + acc1.changeTrust(initialSelling, 500); + issuer.pay(acc1, initialSelling, 500); + } + + if (initialBuying.type() != ASSET_TYPE_NATIVE) + { + root.pay(acc1, reserve + txfee); + acc1.changeTrust(initialBuying, 1000); + } + + if (!(finalSelling == initialSelling)) + { + if (finalSelling.type() == ASSET_TYPE_NATIVE) + { + root.pay(acc1, 499); + } + else + { + if (finalSelling == initialBuying) + { + root.pay(acc1, txfee); + acc1.changeTrust(finalSelling, 1500); + } + else + { + root.pay(acc1, reserve + txfee); + acc1.changeTrust(finalSelling, 500); + } + issuer.pay(acc1, finalSelling, 499); + } + } + + if (!(finalBuying == initialBuying)) + { + if (finalBuying.type() == ASSET_TYPE_NATIVE) + { + root.pay(acc1, reserve + txfee); + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, + {cur1, xlm, Price{1, 1}, + INT64_MAX - acc1.getBalance() - reserve - 999}); + }); + root.pay(acc1, txfee); + } + else + { + if (finalBuying == initialSelling) + { + root.pay(acc1, txfee); + acc1.changeTrust(finalBuying, 1499); + } + else + { + root.pay(acc1, reserve + txfee); + acc1.changeTrust(finalBuying, 999); + } + } + } + + root.pay(acc1, reserve + txfee); + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {initialSelling, initialBuying, Price{2, 1}, 500}); + }); + auto offerID = offer.key.offerID; + + if (!(finalBuying == initialBuying)) + { + root.pay(acc1, txfee); + REQUIRE_THROWS_AS(market.updateOffer(acc1, offerID, + {finalSelling, finalBuying, + Price{2, 1}, 500}), + ex_MANAGE_OFFER_LINE_FULL); + if (finalBuying.type() == ASSET_TYPE_NATIVE) + { + root.pay(acc1, txfee); + acc1.pay(root, 1); + } + else + { + root.pay(acc1, txfee); + acc1.changeTrust(finalBuying, 1500); + } + } + + if (!(finalSelling == initialSelling)) + { + root.pay(acc1, txfee); + REQUIRE_THROWS_AS(market.updateOffer(acc1, offerID, + {finalSelling, finalBuying, + Price{2, 1}, 500}), + ex_MANAGE_OFFER_UNDERFUNDED); + if (finalSelling.type() == ASSET_TYPE_NATIVE) + { + root.pay(acc1, 1); + } + else + { + issuer.pay(acc1, finalSelling, 1); + } + } + + root.pay(acc1, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer( + acc1, offerID, + {finalSelling, finalBuying, Price{2, 1}, 500}); + }); + + if (!(initialSelling == finalSelling) && + !(initialSelling == finalBuying)) + { + REQUIRE(checkLiabilities(acc1, initialSelling) == + Liabilities{0, 0}); + } + if (!(initialBuying == finalSelling) && + !(initialBuying == finalBuying)) + { + REQUIRE(checkLiabilities(acc1, initialBuying) == + Liabilities{0, 0}); + } + + REQUIRE(checkLiabilities(acc1, finalSelling) == + Liabilities{0, 500}); + if (!(finalBuying == initialBuying) && + finalBuying.type() == ASSET_TYPE_NATIVE) + { + REQUIRE(checkLiabilities(acc1, finalBuying) == + Liabilities{INT64_MAX - acc1.getBalance(), 0}); + } + else + { + REQUIRE(checkLiabilities(acc1, finalBuying) == + Liabilities{1000, 0}); + } + }; + + auto cur1 = issuer.asset("CUR1"); + auto cur2 = issuer.asset("CUR2"); + + for_versions_from(10, *app, [&] { + SECTION("selling native swap assets") + { + checkModifyAssets(xlm, usd, usd, xlm); + } + SECTION("buying native swap assets") + { + checkModifyAssets(usd, xlm, xlm, usd); + } + SECTION("non-native swap assets") + { + checkModifyAssets(idr, usd, usd, idr); + } + + SECTION("selling native change selling asset") + { + checkModifyAssets(xlm, usd, cur1, usd); + } + SECTION("selling native change buying asset") + { + checkModifyAssets(xlm, usd, xlm, cur1); + } + SECTION("selling native change both assets") + { + checkModifyAssets(xlm, usd, cur1, cur2); + } + + SECTION("buying native change buying asset") + { + checkModifyAssets(usd, xlm, usd, cur1); + } + SECTION("buying native change selling asset") + { + checkModifyAssets(usd, xlm, cur1, xlm); + } + SECTION("buying native change both assets") + { + checkModifyAssets(usd, xlm, cur1, cur2); + } + + SECTION("non-native change selling asset non-native") + { + checkModifyAssets(idr, usd, cur1, usd); + } + SECTION("non-native change buying asset non-native") + { + checkModifyAssets(idr, usd, idr, cur1); + } + SECTION("non-native change both assets non-native") + { + checkModifyAssets(idr, usd, cur1, cur2); + } + + SECTION("non-native change selling asset native") + { + checkModifyAssets(idr, usd, xlm, usd); + } + SECTION("non-native change buying asset native") + { + checkModifyAssets(idr, usd, idr, xlm); + } + }); + } + + SECTION("reserve and liabilities checks") + { + SECTION("when creating an offer") + { + SECTION("selling native") + { + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + auto const minBalance = + app->getLedgerManager().getMinBalance(1); + auto acc1 = root.create("acc1", minBalance + 2 * txfee + 499); + + acc1.changeTrust(idr, 1000); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS( + market.addOffer(acc1, {xlm, idr, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LOW_RESERVE); + root.pay(acc1, reserve + txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {xlm, idr, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {xlm, idr, Price{2, 1}, 500}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, txfee); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {xlm, idr, Price{2, 1}, 499}); + }); + }); + } + + SECTION("buying native") + { + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + auto const minBalance = + app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 4 * txfee); + + acc1.changeTrust(idr, 499); + acc1.changeTrust(usd, INT64_MAX); + issuer.pay(acc1, idr, 499); + issuer.pay(acc1, usd, INT64_MAX); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {usd, xlm, Price{1, 1}, + INT64_MAX - minBalance - reserve - 1000}); + }); + + REQUIRE_THROWS_AS( + market.addOffer(acc1, {idr, xlm, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LOW_RESERVE); + root.pay(acc1, reserve + txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {idr, xlm, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {idr, xlm, Price{2, 1}, 500}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, txfee); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {idr, xlm, Price{2, 1}, 499}); + }); + }); + } + + SECTION("non-native") + { + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + auto const minBalance = + app->getLedgerManager().getMinBalance(2); + auto acc1 = root.create("acc1", minBalance + 3 * txfee); + + acc1.changeTrust(usd, 499); + acc1.changeTrust(idr, 1000); + issuer.pay(acc1, usd, 499); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS( + market.addOffer(acc1, {usd, idr, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LOW_RESERVE); + root.pay(acc1, reserve + txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {usd, idr, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, txfee); + REQUIRE_THROWS_AS( + market.addOffer(acc1, {usd, idr, Price{2, 1}, 500}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, txfee); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {usd, idr, Price{2, 1}, 499}); + }); + }); + } + } + + SECTION("when modifying an offer") + { + SECTION("selling native") + { + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + auto const minBalance = + app->getLedgerManager().getMinBalance(2); + auto acc1 = root.create("acc1", minBalance + 3 * txfee + 499); + + acc1.changeTrust(idr, 1000); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {xlm, idr, Price{2, 1}, 250}); + }); + auto offerID = offer.key.offerID; + + REQUIRE_THROWS_AS( + market.updateOffer(acc1, offerID, + {xlm, idr, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, txfee); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, offerID, + {xlm, idr, Price{2, 1}, 500}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, offerID, + {xlm, idr, Price{2, 1}, 499}); + }); + }); + } + + SECTION("buying native") + { + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + auto const minBalance = + app->getLedgerManager().getMinBalance(4); + auto acc1 = root.create("acc1", minBalance + 5 * txfee); + + acc1.changeTrust(idr, 499); + acc1.changeTrust(usd, INT64_MAX); + issuer.pay(acc1, idr, 499); + issuer.pay(acc1, usd, INT64_MAX); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {usd, xlm, Price{1, 1}, + INT64_MAX - minBalance - 1000}); + }); + + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {idr, xlm, Price{2, 1}, 250}); + }); + auto offerID = offer.key.offerID; + + REQUIRE_THROWS_AS( + market.updateOffer(acc1, offerID, + {idr, xlm, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, txfee); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, offerID, + {idr, xlm, Price{2, 1}, 500}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, offerID, + {idr, xlm, Price{2, 1}, 499}); + }); + }); + } + + SECTION("non-native") + { + auto reserve = app->getLedgerManager() + .getCurrentLedgerHeader() + .baseReserve; + auto const minBalance = + app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 4 * txfee); + + acc1.changeTrust(usd, 499); + acc1.changeTrust(idr, 1000); + issuer.pay(acc1, usd, 499); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, + {usd, idr, Price{2, 1}, 250}); + }); + auto offerID = offer.key.offerID; + + REQUIRE_THROWS_AS( + market.updateOffer(acc1, offerID, + {usd, idr, Price{2, 1}, 501}), + ex_MANAGE_OFFER_LINE_FULL); + root.pay(acc1, txfee); + REQUIRE_THROWS_AS( + market.updateOffer(acc1, offerID, + {usd, idr, Price{2, 1}, 500}), + ex_MANAGE_OFFER_UNDERFUNDED); + root.pay(acc1, txfee); + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(acc1, offerID, + {usd, idr, Price{2, 1}, 499}); + }); + }); + } + } + } + + SECTION("issuer offers") + { + SECTION("issuer offers do not overflow selling liabilities") + { + auto reserve = + app->getLedgerManager().getCurrentLedgerHeader().baseReserve; + auto const minBalance = app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + auto cur1 = acc1.asset("CUR1"); + + acc1.changeTrust(usd, INT64_MAX); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {cur1, usd, Price{1, 2}, (INT64_MAX / 3) * 2}); + }); + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {cur1, usd, Price{1, 2}, (INT64_MAX / 3) * 2}); + }); + }); + } + + SECTION("issuer offers do not overflow buying liabilities") + { + auto reserve = + app->getLedgerManager().getCurrentLedgerHeader().baseReserve; + auto const minBalance = app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + auto cur1 = acc1.asset("CUR1"); + + acc1.changeTrust(usd, INT64_MAX); + issuer.pay(acc1, usd, INT64_MAX); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {usd, cur1, Price{2, 1}, INT64_MAX / 3}); + }); + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {usd, cur1, Price{2, 1}, INT64_MAX / 3}); + }); + }); + } + + SECTION("issuer offers contribute buying liabilities to other assets") + { + auto reserve = + app->getLedgerManager().getCurrentLedgerHeader().baseReserve; + auto const minBalance = app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + auto cur1 = acc1.asset("CUR1"); + + acc1.changeTrust(usd, INT64_MAX); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {cur1, usd, Price{1, 1}, (INT64_MAX / 3) * 2}); + }); + REQUIRE_THROWS_AS(market.addOffer(acc1, {cur1, usd, Price{1, 1}, + (INT64_MAX / 3) * 2}), + ex_MANAGE_OFFER_LINE_FULL); + }); + } + + SECTION("issuer offers contribute selling liabilities to other assets") + { + auto reserve = + app->getLedgerManager().getCurrentLedgerHeader().baseReserve; + auto const minBalance = app->getLedgerManager().getMinBalance(3); + auto acc1 = root.create("acc1", minBalance + 10000); + auto cur1 = acc1.asset("CUR1"); + + acc1.changeTrust(usd, INT64_MAX); + issuer.pay(acc1, usd, INT64_MAX); + + TestMarket market(*app); + for_versions_from(10, *app, [&] { + market.requireChangesWithOffer({}, [&] { + return market.addOffer( + acc1, {usd, cur1, Price{1, 1}, (INT64_MAX / 3) * 2}); + }); + REQUIRE_THROWS_AS(market.addOffer(acc1, {usd, cur1, Price{1, 1}, + (INT64_MAX / 3) * 2}), + ex_MANAGE_OFFER_UNDERFUNDED); + }); + } + } +} + +TEST_CASE("liabilities match created offer", "[tx][offers]") +{ + VirtualClock clock; + auto app = createTestApplication(clock, getTestConfig()); + auto& lm = app->getLedgerManager(); + app->start(); + + int64_t txfee = lm.getTxFee(); + + auto root = TestAccount::createRoot(*app); + auto issuer = root.create("issuer", lm.getMinBalance(0) + 1000 * txfee); + auto cur1 = issuer.asset("CUR1"); + auto cur2 = issuer.asset("CUR2"); + + TestMarket market(*app); + + auto checkLiabilities = [&](int64_t sellingBalance, int64_t buyingLimit, + int64_t amount, Price price) { + auto a1 = root.create("a1", lm.getMinBalance(3) + 1000 * txfee); + a1.changeTrust(cur1, INT64_MAX); + a1.changeTrust(cur2, buyingLimit); + issuer.pay(a1, cur1, sellingBalance); + + int64_t remainAmount = adjustOffer(price, amount, buyingLimit); + REQUIRE(remainAmount > 0); + OfferState expected = {cur1, cur2, price, remainAmount}; + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(a1, {cur1, cur2, price, amount}, expected); + }); + + auto oe = a1.loadOffer(offer.key.offerID); + Liabilities liabilities{getBuyingLiabilities(oe), + getSellingLiabilities(oe)}; + REQUIRE(liabilities.selling == oe.amount); + + auto a2 = root.create("a2", lm.getMinBalance(3) + 1000 * txfee); + a2.changeTrust(cur1, INT64_MAX); + a2.changeTrust(cur2, INT64_MAX); + issuer.pay(a2, cur2, INT64_MAX); + + // wheatValue = min(price.n * oe.amount, price.d * INT64_MAX) + // = price.n * oe.amount + // sheepValue = min(price.d * X, price.n * INT64_MAX) + // = price.d * X + // sheepValue > wheatValue + // -> price.d * X > price.n * oe.amount + // X > oe.amount * price.n / price.d + // X = ceil(oe.amount * price.n / price.d) + 1 + int64_t crossAmount = bigDivide(oe.amount, price.n, price.d, ROUND_UP); + if (crossAmount < INT64_MAX) + { + ++crossAmount; + } + Price crossPrice{price.d, price.n}; + int64_t crossRemainAmount = + adjustOffer(crossPrice, crossAmount - liabilities.buying, + INT64_MAX - liabilities.selling); + OfferState expectedCross = + (crossRemainAmount > 0) + ? OfferState{cur2, cur1, crossPrice, crossRemainAmount} + : OfferState::DELETED; + + auto crossOffer = market.requireChangesWithOffer( + {{offer.key, OfferState::DELETED}}, [&] { + return market.addOffer( + a2, {cur2, cur1, crossPrice, crossAmount}, expectedCross); + }); + REQUIRE(a1.loadTrustLine(cur1).balance == + sellingBalance - liabilities.selling); + REQUIRE(a1.loadTrustLine(cur2).balance == liabilities.buying); + REQUIRE(a2.loadTrustLine(cur1).balance == liabilities.selling); + REQUIRE(a2.loadTrustLine(cur2).balance == + INT64_MAX - liabilities.buying); + + auto mergeAccount = [&](TestAccount& acc) { + if (acc.loadTrustLine(cur1).balance > 0) + { + acc.pay(issuer, cur1, acc.loadTrustLine(cur1).balance); + } + if (acc.loadTrustLine(cur2).balance > 0) + { + acc.pay(issuer, cur2, acc.loadTrustLine(cur2).balance); + } + acc.changeTrust(cur1, 0); + acc.changeTrust(cur2, 0); + acc.merge(root); + }; + + if (crossRemainAmount > 0) + { + market.requireChangesWithOffer({}, [&] { + return market.updateOffer(a2, crossOffer.key.offerID, + {cur2, cur1, price, 0}, + OfferState::DELETED); + }); + } + ++lm.getCurrentLedgerHeader().ledgerSeq; + mergeAccount(a1); + mergeAccount(a2); + }; + + SECTION("maximum limits") + { + for_versions_from(10, *app, [&] { + // price < 1, no rounding + checkLiabilities(INT64_MAX, INT64_MAX, 2, Price{1, 2}); + checkLiabilities(INT64_MAX, INT64_MAX, INT64_MAX - 1, Price{1, 2}); + + // price < 1, rounding + checkLiabilities(INT64_MAX, INT64_MAX, 101, Price{3, 7}); + + // price = 1, no rounding + checkLiabilities(INT64_MAX, INT64_MAX, 1, Price{1, 1}); + checkLiabilities(INT64_MAX, INT64_MAX, INT64_MAX, Price{1, 1}); + + // price > 1, no rounding + checkLiabilities(INT64_MAX, INT64_MAX, 1, Price{2, 1}); + checkLiabilities(INT64_MAX, INT64_MAX, INT64_MAX / 2, Price{2, 1}); + + // price > 1, rounding + checkLiabilities(INT64_MAX, INT64_MAX, 101, Price{7, 3}); + }); + } + + SECTION("minimum limits") + { + for_versions_from(10, *app, [&] { + // price < 1, no rounding + checkLiabilities(2, 1, 2, Price{1, 2}); + + // price < 1, rounding + checkLiabilities(101, 44, 101, Price{3, 7}); + + // price = 1, no rounding + checkLiabilities(1, 1, 1, Price{1, 1}); + checkLiabilities(INT64_MAX, INT64_MAX, INT64_MAX, Price{1, 1}); + + // price > 1, no rounding + checkLiabilities(1, 2, 1, Price{2, 1}); + checkLiabilities(INT64_MAX / 2, INT64_MAX - 1, INT64_MAX / 2, + Price{2, 1}); + + // price > 1, rounding + checkLiabilities(101, 236, 101, Price{7, 3}); + }); + } + + // NOTE: Starting in version 10, it is not possible to create an offer that + // initially exceeds limits. } diff --git a/src/transactions/PathPaymentTests.cpp b/src/transactions/PathPaymentTests.cpp index 9e8f82e551..7cfa6148b8 100644 --- a/src/transactions/PathPaymentTests.cpp +++ b/src/transactions/PathPaymentTests.cpp @@ -1679,9 +1679,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - mm12a.changeTrust(cur1, 5); + for_versions_to(9, *app, [&] { + mm12a.changeTrust(cur1, 5); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1a.key, OfferState::DELETED}, {o1b.key, {cur2, cur1, Price{2, 1}, 2}}, @@ -1709,6 +1709,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm12a.changeTrust(cur1, 5), + ex_CHANGE_TRUST_INVALID_LIMIT); + }); } SECTION("path payment reaches limit for offer for second exchange") @@ -1751,9 +1756,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - mm23a.changeTrust(cur2, 5); + for_versions_to(9, *app, [&] { + mm23a.changeTrust(cur2, 5); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2a.key, OfferState::DELETED}, @@ -1781,6 +1786,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm23a.changeTrust(cur2, 5), + ex_CHANGE_TRUST_INVALID_LIMIT); + }); } SECTION("path payment reaches limit for offer for last exchange") @@ -1823,9 +1833,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34b, {cur4, cur3, Price{2, 1}, 10}); }); - mm34a.changeTrust(cur3, 2); + for_versions_to(9, *app, [&] { + mm34a.changeTrust(cur3, 2); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2.key, OfferState::DELETED}, @@ -1853,6 +1863,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm34a.changeTrust(cur3, 2), + ex_CHANGE_TRUST_INVALID_LIMIT); + }); } SECTION("path payment missing trust line for offer for first exchange") @@ -1897,10 +1912,10 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") SECTION("missing selling line") { - mm12a.pay(gateway, cur2, 40); - mm12a.changeTrust(cur2, 0); + for_versions_to(9, *app, [&] { + mm12a.pay(gateway, cur2, 40); + mm12a.changeTrust(cur2, 0); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges( {{o1a.key, OfferState::DELETED}, @@ -1928,13 +1943,18 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm12a.pay(gateway, cur2, 40), + ex_PAYMENT_UNDERFUNDED); + }); } SECTION("missing buying line") { - mm12a.changeTrust(cur1, 0); + for_versions_to(9, *app, [&] { + mm12a.changeTrust(cur1, 0); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges( {{o1a.key, OfferState::DELETED}, @@ -1962,6 +1982,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm12a.changeTrust(cur1, 0), + ex_CHANGE_TRUST_INVALID_LIMIT); + }); } } @@ -2007,10 +2032,10 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") SECTION("missing selling line") { - mm23a.pay(gateway2, cur3, 20); - mm23a.changeTrust(cur3, 0); + for_versions_to(9, *app, [&] { + mm23a.pay(gateway2, cur3, 20); + mm23a.changeTrust(cur3, 0); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges( {{o1.key, OfferState::DELETED}, @@ -2038,13 +2063,18 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm23a.pay(gateway2, cur3, 20), + ex_PAYMENT_UNDERFUNDED); + }); } SECTION("missing buying line") { - mm23a.changeTrust(cur2, 0); + for_versions_to(9, *app, [&] { + mm23a.changeTrust(cur2, 0); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges( {{o1.key, OfferState::DELETED}, @@ -2072,6 +2102,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm23a.changeTrust(cur2, 0), + ex_CHANGE_TRUST_INVALID_LIMIT); + }); } } @@ -2117,10 +2152,10 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") SECTION("missing selling line") { - mm34a.pay(gateway2, cur4, 10); - mm34a.changeTrust(cur4, 0); + for_versions_to(9, *app, [&] { + mm34a.pay(gateway2, cur4, 10); + mm34a.changeTrust(cur4, 0); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges( {{o1.key, OfferState::DELETED}, @@ -2148,13 +2183,18 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm34a.pay(gateway2, cur4, 10), + ex_PAYMENT_UNDERFUNDED); + }); } SECTION("missing buying line") { - mm34a.changeTrust(cur3, 0); + for_versions_to(9, *app, [&] { + mm34a.changeTrust(cur3, 0); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges( {{o1.key, OfferState::DELETED}, @@ -2182,6 +2222,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm34a.changeTrust(cur3, 0), + ex_CHANGE_TRUST_INVALID_LIMIT); + }); } } @@ -2226,9 +2271,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - mm12a.pay(gateway, cur2, 40); + for_versions_to(9, *app, [&] { + mm12a.pay(gateway, cur2, 40); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1a.key, OfferState::DELETED}, {o1b.key, OfferState::DELETED}, @@ -2256,6 +2301,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm12a.pay(gateway, cur2, 40), + ex_PAYMENT_UNDERFUNDED); + }); } SECTION("path payment empty trust line for selling asset for offer for " @@ -2299,9 +2349,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - mm23a.pay(gateway2, cur3, 20); + for_versions_to(9, *app, [&] { + mm23a.pay(gateway2, cur3, 20); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2a.key, OfferState::DELETED}, @@ -2329,6 +2379,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm23a.pay(gateway2, cur3, 20), + ex_PAYMENT_UNDERFUNDED); + }); } SECTION("path payment empty trust line for selling asset for offer for " @@ -2372,9 +2427,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34b, {cur4, cur3, Price{2, 1}, 10}); }); - mm34a.pay(gateway2, cur4, 10); + for_versions_to(9, *app, [&] { + mm34a.pay(gateway2, cur4, 10); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2.key, OfferState::DELETED}, @@ -2402,6 +2457,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(mm34a.pay(gateway2, cur4, 10), + ex_PAYMENT_UNDERFUNDED); + }); } SECTION("path payment full trust line for buying asset for offer for " @@ -2445,9 +2505,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - gateway.pay(mm12a, cur1, 200); + for_versions_to(9, *app, [&] { + gateway.pay(mm12a, cur1, 200); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1a.key, OfferState::DELETED}, {o1b.key, OfferState::DELETED}, @@ -2475,6 +2535,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(gateway.pay(mm12a, cur1, 200), + ex_PAYMENT_LINE_FULL); + }); } SECTION("path payment full trust line for buying asset for offer for " @@ -2518,9 +2583,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - gateway.pay(mm23a, cur2, 200); + for_versions_to(9, *app, [&] { + gateway.pay(mm23a, cur2, 200); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2a.key, OfferState::DELETED}, @@ -2548,6 +2613,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(gateway.pay(mm23a, cur2, 200), + ex_PAYMENT_LINE_FULL); + }); } SECTION("path payment full trust line for buying asset for offer for last " @@ -2591,9 +2661,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34b, {cur4, cur3, Price{2, 1}, 10}); }); - gateway2.pay(mm34a, cur3, 200); + for_versions_to(9, *app, [&] { + gateway2.pay(mm34a, cur3, 200); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2.key, OfferState::DELETED}, @@ -2621,6 +2691,11 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(gateway2.pay(mm34a, cur3, 200), + ex_PAYMENT_LINE_FULL); + }); } SECTION("path payment 1 in trust line for selling asset for offer for " @@ -2664,9 +2739,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - mm12a.pay(gateway, cur2, 39); - for_versions_to(2, *app, [&] { + mm12a.pay(gateway, cur2, 39); + auto actual = std::vector{}; market.requireChanges({{o1a.key, OfferState::DELETED}, {o1b.key, OfferState::DELETED}, @@ -2695,6 +2770,8 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") // clang-format on }); for_versions(3, 9, *app, [&] { + mm12a.pay(gateway, cur2, 39); + auto actual = std::vector{}; market.requireChanges({{o1a.key, OfferState::DELETED}, {o1b.key, {cur2, cur1, Price{2, 1}, 1}}, @@ -2722,42 +2799,17 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); - - // For o1a: - // wheatValue = 1 * 1 = 1 - // sheepValue = 40 * 1 = 40 - // !wheatStays - // price.n < price.d - // sheepSend = floor(1 / 2) = 0 - // wheatReceive = ceil(0 * 2 / 1) = 0 - for_versions_from(10, *app, [&] { - auto actual = std::vector{}; - market.requireChanges({{o1a.key, OfferState::DELETED}, - {o1b.key, OfferState::DELETED}, - {o2.key, OfferState::DELETED}, - {o3.key, OfferState::DELETED}}, - [&] { - actual = - source - .pay(destination, cur1, 80, cur4, - 10, {cur1, cur2, cur3, cur4}) - .success() - .offers; - }); - auto expected = std::vector{ - o1a.exchanged(0, 0), o1b.exchanged(40, 80), - o2.exchanged(20, 40), o3.exchanged(10, 20)}; - REQUIRE(actual == expected); - // clang-format off - market.requireBalances( - {{source, {{xlm, minBalance4 - 2 * txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 0}}}, - {mm12a, {{xlm, minBalance3 - 4 * txfee}, {cur1, 0}, {cur2, 1}, {cur3, 0}, {cur4, 0}}}, - {mm12b, {{xlm, minBalance3 - 3 * txfee}, {cur1, 80}, {cur2, 0}, {cur3, 0}, {cur4, 0}}}, - {mm23, {{xlm, minBalance3 - 3 * txfee}, {cur1, 0}, {cur2, 40}, {cur3, 0}, {cur4, 0}}}, - {mm34, {{xlm, minBalance3 - 3 * txfee}, {cur1, 0}, {cur2, 0}, {cur3, 20}, {cur4, 0}}}, - {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); - // clang-format on - }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer with excess selling liabilities. This can + // be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to reduce balance below + // selling liabilities (tested in "payment"/"pathpayment" section + // "liabilities" subsection "cannot pay balance below selling + // liabilities") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess selling liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "* selling liabilities") } SECTION("path payment 1 in trust line for selling asset for offer for " @@ -2801,9 +2853,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - mm23a.pay(gateway2, cur3, 19); - for_versions_to(2, *app, [&] { + mm23a.pay(gateway2, cur3, 19); + auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2a.key, OfferState::DELETED}, @@ -2832,6 +2884,8 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") // clang-format on }); for_versions(3, 9, *app, [&] { + mm23a.pay(gateway2, cur3, 19); + auto actual = std::vector{}; market.requireChanges({{o1.key, {cur2, cur1, Price{2, 1}, 1}}, {o2a.key, OfferState::DELETED}, @@ -2859,42 +2913,17 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); - - // For o2a: - // wheatValue = 1 * 1 = 1 - // sheepValue = 20 * 1 = 20 - // !wheatStays - // price.n < price.d - // sheepSend = floor(1 / 2) = 0 - // wheatReceive = ceil(0 * 2 / 1) = 0 - for_versions_from(10, *app, [&] { - auto actual = std::vector{}; - market.requireChanges({{o1.key, OfferState::DELETED}, - {o2a.key, OfferState::DELETED}, - {o2b.key, OfferState::DELETED}, - {o3.key, OfferState::DELETED}}, - [&] { - actual = - source - .pay(destination, cur1, 80, cur4, - 10, {cur1, cur2, cur3, cur4}) - .success() - .offers; - }); - auto expected = std::vector{ - o1.exchanged(40, 80), o2a.exchanged(0, 0), - o2b.exchanged(20, 40), o3.exchanged(10, 20)}; - REQUIRE(actual == expected); - // clang-format off - market.requireBalances( - {{source, {{xlm, minBalance4 - 2 * txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 0}}}, - {mm12, {{xlm, minBalance3 - 3 * txfee}, {cur1, 80}, {cur2, 0}, {cur3, 0}, {cur4, 0}}}, - {mm23a, {{xlm, minBalance3 - 4 * txfee}, {cur1, 0}, {cur2, 0}, {cur3, 1}, {cur4, 0}}}, - {mm23b, {{xlm, minBalance3 - 3 * txfee}, {cur1, 0}, {cur2, 40}, {cur3, 0}, {cur4, 0}}}, - {mm34, {{xlm, minBalance3 - 3 * txfee}, {cur1, 0}, {cur2, 0}, {cur3, 20}, {cur4, 0}}}, - {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); - // clang-format on - }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer with excess selling liabilities. This can + // be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to reduce balance below + // selling liabilities (tested in "payment"/"pathpayment" section + // "liabilities" subsection "cannot pay balance below selling + // liabilities") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess selling liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "* selling liabilities") } SECTION("path payment 1 in trust line for selling asset for offer for last " @@ -2938,9 +2967,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34b, {cur4, cur3, Price{2, 1}, 10}); }); - mm34a.pay(gateway2, cur4, 9); - for_versions_to(2, *app, [&] { + mm34a.pay(gateway2, cur4, 9); + auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2.key, OfferState::DELETED}, @@ -2969,6 +2998,8 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") // clang-format on }); for_versions(3, 9, *app, [&] { + mm34a.pay(gateway2, cur4, 9); + auto actual = std::vector{}; market.requireChanges({{o1.key, {cur2, cur1, Price{2, 1}, 2}}, {o2.key, {cur3, cur2, Price{2, 1}, 1}}, @@ -2996,42 +3027,17 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); - - // For o3a: - // wheatValue = 1 * 1 = 1 - // sheepValue = 10 * 1 = 10 - // !wheatStays - // price.n < price.d - // sheepSend = floor(1 / 2) = 0 - // wheatReceive = ceil(0 * 2 / 1) = 0 - for_versions_from(10, *app, [&] { - auto actual = std::vector{}; - market.requireChanges({{o1.key, OfferState::DELETED}, - {o2.key, OfferState::DELETED}, - {o3a.key, OfferState::DELETED}, - {o3b.key, OfferState::DELETED}}, - [&] { - actual = - source - .pay(destination, cur1, 80, cur4, - 10, {cur1, cur2, cur3, cur4}) - .success() - .offers; - }); - auto expected = std::vector{ - o1.exchanged(40, 80), o2.exchanged(20, 40), o3a.exchanged(0, 0), - o3b.exchanged(10, 20)}; - REQUIRE(actual == expected); - // clang-format off - market.requireBalances( - {{source, {{xlm, minBalance4 - 2 * txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 0}}}, - {mm12, {{xlm, minBalance3 - 3 * txfee}, {cur1, 80}, {cur2, 0}, {cur3, 0}, {cur4, 0}}}, - {mm23, {{xlm, minBalance3 - 3 * txfee}, {cur1, 0}, {cur2, 40}, {cur3, 0}, {cur4, 0}}}, - {mm34a, {{xlm, minBalance3 - 4 * txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 1}}}, - {mm34b, {{xlm, minBalance3 - 3 * txfee}, {cur1, 0}, {cur2, 0}, {cur3, 20}, {cur4, 0}}}, - {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); - // clang-format on - }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer with excess selling liabilities. This can + // be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to reduce balance below + // selling liabilities (tested in "payment"/"pathpayment" section + // "liabilities" subsection "cannot pay balance below selling + // liabilities") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess selling liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "* selling liabilities") } SECTION("path payment 1 left in trust line for buying asset for offer for " @@ -3075,9 +3081,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - gateway.pay(mm12a, cur1, 199); + for_versions_to(9, *app, [&] { + gateway.pay(mm12a, cur1, 199); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1a.key, OfferState::DELETED}, {o1b.key, OfferState::DELETED}, @@ -3105,6 +3111,21 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer that can take a trust line above + // its limit. This can be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to increase balance + + // buying liabilities above limit (tested in "payment"/"pathpayment" + // section "liabilities" subsection "cannot receive such that + // balance + buying liabilities exceeds limit") + // - Cannot use ChangeTrustOp to reduce limit below balance + + // buying liabilities (tested in "change trust" section + // "cannot reduce limit below buying liabilities or delete") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess buying liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "non-native buying + // liabilities") } SECTION("path payment 1 left in trust line for buying asset for offer for " @@ -3148,9 +3169,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34, {cur4, cur3, Price{2, 1}, 10}); }); - gateway.pay(mm23a, cur2, 199); + for_versions_to(9, *app, [&] { + gateway.pay(mm23a, cur2, 199); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2a.key, OfferState::DELETED}, @@ -3178,6 +3199,21 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer that can take a trust line above + // its limit. This can be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to increase balance + + // buying liabilities above limit (tested in "payment"/"pathpayment" + // section "liabilities" subsection "cannot receive such that + // balance + buying liabilities exceeds limit") + // - Cannot use ChangeTrustOp to reduce limit below balance + + // buying liabilities (tested in "change trust" section + // "cannot reduce limit below buying liabilities or delete") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess buying liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "non-native buying + // liabilities") } SECTION("path payment 1 left in trust line for buying asset for offer for " @@ -3221,9 +3257,9 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") return market.addOffer(mm34b, {cur4, cur3, Price{2, 1}, 10}); }); - gateway2.pay(mm34a, cur3, 199); + for_versions_to(9, *app, [&] { + gateway2.pay(mm34a, cur3, 199); - for_all_versions(*app, [&] { auto actual = std::vector{}; market.requireChanges({{o1.key, OfferState::DELETED}, {o2.key, OfferState::DELETED}, @@ -3251,6 +3287,21 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") {destination, {{xlm, minBalance1 - txfee}, {cur1, 0}, {cur2, 0}, {cur3, 0}, {cur4, 10}}}}); // clang-format on }); + // This is no longer possible starting in version 10, as it is + // impossible to have an offer that can take a trust line above + // its limit. This can be verified from the following tests: + // - Cannot use PaymentOp or PathPaymentOp to increase balance + + // buying liabilities above limit (tested in "payment"/"pathpayment" + // section "liabilities" subsection "cannot receive such that + // balance + buying liabilities exceeds limit") + // - Cannot use ChangeTrustOp to reduce limit below balance + + // buying liabilities (tested in "change trust" section + // "cannot reduce limit below buying liabilities or delete") + // - Cannot use ManageOfferOp (or CreatePassiveOfferOp) to + // create an offer with excess buying liabilities (tested in + // "create offer" section "cannot create offer that would + // lead to excess liabilities" subsection "non-native buying + // liabilities") } SECTION("path payment takes all offers, one offer per exchange") @@ -4061,4 +4112,76 @@ TEST_CASE("pathpayment", "[tx][pathpayment]") }); } } + + SECTION("liabilities") + { + SECTION("cannot pay balance below selling liabilities") + { + TestMarket market(*app); + auto source = root.create("source", minBalance2); + auto destination = root.create("destination", minBalance2); + auto mm12 = root.create("mm12", minBalance3); + + source.changeTrust(cur1, 200); + mm12.changeTrust(cur1, 200); + mm12.changeTrust(cur2, 200); + destination.changeTrust(cur2, 200); + + gateway.pay(source, cur1, 100); + gateway.pay(mm12, cur2, 100); + + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(source, {cur1, xlm, Price{1, 1}, 50}); + }); + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(mm12, {cur2, cur1, Price{1, 1}, 100}); + }); + + for_versions_to(9, *app, [&] { + source.pay(destination, cur1, 51, cur2, 51, {cur1, cur2}); + }); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS( + source.pay(destination, cur1, 51, cur2, 51, {cur1, cur2}), + ex_PATH_PAYMENT_UNDERFUNDED); + source.pay(destination, cur1, 50, cur2, 50, {cur1, cur2}); + }); + } + + SECTION("cannot receive such that balance + buying liabilities exceeds" + " limit") + { + TestMarket market(*app); + auto source = root.create("source", minBalance2); + auto destination = root.create("destination", minBalance2); + auto mm12 = root.create("mm12", minBalance3); + + source.changeTrust(cur1, 200); + mm12.changeTrust(cur1, 200); + mm12.changeTrust(cur2, 200); + destination.changeTrust(cur2, 200); + + gateway.pay(source, cur1, 100); + gateway.pay(mm12, cur2, 100); + gateway.pay(destination, cur2, 100); + + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(destination, + {xlm, cur2, Price{1, 1}, 50}); + }); + auto o2 = market.requireChangesWithOffer({}, [&] { + return market.addOffer(mm12, {cur2, cur1, Price{1, 1}, 100}); + }); + + for_versions_to(9, *app, [&] { + source.pay(destination, cur1, 51, cur2, 51, {cur1, cur2}); + }); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS( + source.pay(destination, cur1, 51, cur2, 51, {cur1, cur2}), + ex_PATH_PAYMENT_LINE_FULL); + source.pay(destination, cur1, 50, cur2, 50, {cur1, cur2}); + }); + } + } } diff --git a/src/transactions/PaymentTests.cpp b/src/transactions/PaymentTests.cpp index 930c27dfdf..81ce7210fb 100644 --- a/src/transactions/PaymentTests.cpp +++ b/src/transactions/PaymentTests.cpp @@ -124,6 +124,48 @@ TEST_CASE("payment", "[tx][payment]") ex_CREATE_ACCOUNT_LOW_RESERVE); }); } + + SECTION("with native selling liabilities") + { + auto const minBal0 = app->getLedgerManager().getMinBalance(0); + auto const minBal3 = app->getLedgerManager().getMinBalance(3); + + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBal3 + 2 * txfee + 500); + TestMarket market(*app); + + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {native, cur1, Price{1, 1}, 500}); + }); + + for_versions_to(9, *app, [&] { acc1.create("acc2", minBal0 + 1); }); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(acc1.create("acc2", minBal0 + 1), + ex_CREATE_ACCOUNT_UNDERFUNDED); + root.pay(acc1, txfee); + acc1.create("acc2", minBal0); + }); + } + + SECTION("with native buying liabilities") + { + auto const minBal0 = app->getLedgerManager().getMinBalance(0); + auto const minBal3 = app->getLedgerManager().getMinBalance(3); + + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBal3 + 2 * txfee + 500); + TestMarket market(*app); + + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {cur1, native, Price{1, 1}, 500}); + }); + + for_all_versions(*app, [&] { acc1.create("acc2", minBal0 + 500); }); + } } SECTION("a pays b, then a merge into b") @@ -1738,6 +1780,45 @@ TEST_CASE("payment", "[tx][payment]") [&] { REQUIRE_NOTHROW(payFrom.pay(root, 1)); }); } } + + SECTION("liabilities") + { + SECTION("cannot pay balance below selling liabilities") + { + a1.changeTrust(idr, 200); + gateway.pay(a1, idr, 100); + + TestMarket market(*app); + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(a1, {idr, xlm, Price{1, 1}, 50}); + }); + + for_versions_to(9, *app, [&] { a1.pay(gateway, idr, 51); }); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(a1.pay(gateway, idr, 51), + ex_PAYMENT_UNDERFUNDED); + a1.pay(gateway, idr, 50); + }); + } + + SECTION("cannot receive such that balance + buying liabilities exceeds" + " limit") + { + a1.changeTrust(idr, 100); + + TestMarket market(*app); + auto offer = market.requireChangesWithOffer({}, [&] { + return market.addOffer(a1, {xlm, idr, Price{1, 1}, 50}); + }); + + for_versions_to(9, *app, [&] { gateway.pay(a1, idr, 51); }); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(gateway.pay(a1, idr, 51), + ex_PAYMENT_LINE_FULL); + gateway.pay(a1, idr, 50); + }); + } + } } TEST_CASE("payment fees", "[tx][payment]") diff --git a/src/transactions/SetOptionsTests.cpp b/src/transactions/SetOptionsTests.cpp index b2ea77bead..f9cad549c8 100644 --- a/src/transactions/SetOptionsTests.cpp +++ b/src/transactions/SetOptionsTests.cpp @@ -8,6 +8,7 @@ #include "main/Config.h" #include "test/TestAccount.h" #include "test/TestExceptions.h" +#include "test/TestMarket.h" #include "test/TestUtils.h" #include "test/TxTests.h" #include "test/test.h" @@ -53,6 +54,46 @@ TEST_CASE("set options", "[tx][setoptions]") }); } + SECTION("add signer with native selling liabilities") + { + auto const minBal2 = app->getLedgerManager().getMinBalance(2); + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBal2 + 2 * txfee + 500 - 1); + TestMarket market(*app); + + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {native, cur1, Price{1, 1}, 500}); + }); + + for_versions_to(9, *app, + [&] { acc1.setOptions(th | setSigner(sk1)); }); + for_versions_from(10, *app, [&] { + REQUIRE_THROWS_AS(acc1.setOptions(th | setSigner(sk1)), + ex_SET_OPTIONS_LOW_RESERVE); + root.pay(acc1, txfee + 1); + acc1.setOptions(th | setSigner(sk1)); + }); + } + + SECTION("add signer with native buying liabilities") + { + auto const minBal2 = app->getLedgerManager().getMinBalance(2); + auto txfee = app->getLedgerManager().getTxFee(); + auto const native = makeNativeAsset(); + auto acc1 = root.create("acc1", minBal2 + 2 * txfee + 500 - 1); + TestMarket market(*app); + + auto cur1 = acc1.asset("CUR1"); + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc1, {cur1, native, Price{1, 1}, 500}); + }); + + for_all_versions(*app, + [&] { acc1.setOptions(th | setSigner(sk1)); }); + } + SECTION("can't use master key as alternate signer") { auto sk = makeSigner(a1, 100); diff --git a/src/transactions/TxResultsTests.cpp b/src/transactions/TxResultsTests.cpp index 895494c77f..71b5a454df 100644 --- a/src/transactions/TxResultsTests.cpp +++ b/src/transactions/TxResultsTests.cpp @@ -6,6 +6,7 @@ #include "crypto/SignerKey.h" #include "ledger/LedgerDelta.h" #include "test/TestAccount.h" +#include "test/TestMarket.h" #include "test/TestUtils.h" #include "test/TxTests.h" #include "test/test.h" @@ -679,4 +680,40 @@ TEST_CASE("txresults", "[tx][txresults]") }); } } + + SECTION("fees with liabilities") + { + auto acc = root.create("acc", lm.getMinBalance(1) + baseFee + 1000); + auto native = makeNativeAsset(); + auto cur1 = acc.asset("CUR1"); + + TestMarket market(*app); + SECTION("selling liabilities") + { + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc, {native, cur1, Price{1, 1}, 1000}); + }); + auto tx = acc.tx({payment(root, 1)}); + for_versions_to(9, *app, [&] { + auto res = + expectedResult(baseFee, 1, txSUCCESS, {PAYMENT_SUCCESS}); + validateTxResults(tx, *app, {baseFee, txSUCCESS}, res); + }); + for_versions_from(10, *app, [&] { + validateTxResults(tx, *app, {baseFee, txINSUFFICIENT_BALANCE}); + }); + } + SECTION("buying liabilities") + { + market.requireChangesWithOffer({}, [&] { + return market.addOffer(acc, {cur1, native, Price{1, 1}, 1000}); + }); + auto tx = acc.tx({payment(root, 1)}); + for_all_versions(*app, [&] { + auto res = + expectedResult(baseFee, 1, txSUCCESS, {PAYMENT_SUCCESS}); + validateTxResults(tx, *app, {baseFee, txSUCCESS}, res); + }); + } + } } From 93b16f95c796667de05964b6ec40d7ebc612a0b4 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:48 -0500 Subject: [PATCH 11/13] Added UpgradesTests for protocol version upgrade and base reserve upgrade --- src/herder/UpgradesTests.cpp | 1048 +++++++++++++++++++++++++++++++++- 1 file changed, 1047 insertions(+), 1 deletion(-) diff --git a/src/herder/UpgradesTests.cpp b/src/herder/UpgradesTests.cpp index 049fb81240..197442dd4b 100644 --- a/src/herder/UpgradesTests.cpp +++ b/src/herder/UpgradesTests.cpp @@ -9,6 +9,7 @@ #include "history/HistoryTestsUtils.h" #include "lib/catch.hpp" #include "simulation/Simulation.h" +#include "test/TestMarket.h" #include "test/TestUtils.h" #include "test/test.h" #include "util/StatusManager.h" @@ -489,7 +490,7 @@ TEST_CASE("Ledger Manager applies upgrades properly", "[upgrades]") VirtualClock clock; auto cfg = getTestConfig(0); cfg.USE_CONFIG_FOR_GENESIS = false; - auto app = Application::create(clock, cfg); + auto app = createTestApplication(clock, cfg); app->start(); auto const& lcl = app->getLedgerManager().getLastClosedLedgerHeader(); @@ -557,6 +558,1051 @@ TEST_CASE("Ledger Manager applies upgrades properly", "[upgrades]") } } +TEST_CASE("upgrade to version 10", "[upgrades]") +{ + VirtualClock clock; + auto cfg = getTestConfig(0); + cfg.LEDGER_PROTOCOL_VERSION = 9; + auto app = createTestApplication(clock, cfg); + app->start(); + + auto& lm = app->getLedgerManager(); + auto txFee = lm.getTxFee(); + + auto const& lcl = lm.getLastClosedLedgerHeader(); + auto txSet = std::make_shared(lcl.hash); + + auto root = TestAccount::createRoot(*app); + auto issuer = root.create("issuer", lm.getMinBalance(0) + 100 * txFee); + auto native = txtest::makeNativeAsset(); + auto cur1 = issuer.asset("CUR1"); + auto cur2 = issuer.asset("CUR2"); + + auto market = TestMarket{*app}; + + auto executeUpgrade = [&] { + auto upgrades = xdr::xvector{}; + upgrades.push_back(toUpgradeType(makeProtocolVersionUpgrade(10))); + + StellarValue sv{txSet->getContentsHash(), 2, upgrades, 0}; + LedgerCloseData ledgerData(lcl.header.ledgerSeq + 1, txSet, sv); + app->getLedgerManager().closeLedger(ledgerData); + + auto const& lhhe = app->getLedgerManager().getLastClosedLedgerHeader(); + REQUIRE(lhhe.header.ledgerVersion == 10); + }; + + auto getLiabilities = [&](TestAccount& acc) { + Liabilities res; + auto account = txtest::loadAccount(acc.getPublicKey(), *app); + res.selling = account->getSellingLiabilities(lm); + res.buying = account->getBuyingLiabilities(lm); + return res; + }; + auto getAssetLiabilities = [&](TestAccount& acc, Asset const& asset) { + Liabilities res; + if (acc.hasTrustLine(asset)) + { + auto trust = acc.loadTrustLine(asset); + res.selling = getSellingLiabilities(trust, lm); + res.buying = getBuyingLiabilities(trust, lm); + } + return res; + }; + + auto createOffer = [&](TestAccount& acc, Asset const& selling, + Asset const& buying, + std::vector& offers, + OfferState const& afterUpgrade = OfferState::SAME) { + OfferState state = {selling, buying, Price{2, 1}, 1000}; + auto offer = market.requireChangesWithOffer( + {}, [&] { return market.addOffer(acc, state); }); + if (afterUpgrade == OfferState::SAME) + { + offers.push_back({offer.key, offer.state}); + } + else + { + offers.push_back({offer.key, afterUpgrade}); + } + }; + + SECTION("one account, multiple offers, one asset pair") + { + SECTION("valid native") + { + auto a1 = root.create("A", lm.getMinBalance(5) + 2000 + 5 * txFee); + a1.changeTrust(cur1, 6000); + issuer.pay(a1, cur1, 2000); + + std::vector offers; + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{4000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 2000}); + } + + SECTION("invalid selling native") + { + auto a1 = root.create("A", lm.getMinBalance(5) + 1000 + 5 * txFee); + a1.changeTrust(cur1, 6000); + issuer.pay(a1, cur1, 2000); + + std::vector offers; + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{4000, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 2000}); + } + + SECTION("invalid buying native") + { + auto createOfferQuantity = + [&](TestAccount& acc, Asset const& selling, Asset const& buying, + int64_t quantity, std::vector& offers, + OfferState const& afterUpgrade = OfferState::SAME) { + OfferState state = {selling, buying, Price{2, 1}, quantity}; + auto offer = market.requireChangesWithOffer( + {}, [&] { return market.addOffer(acc, state); }); + if (afterUpgrade == OfferState::SAME) + { + offers.push_back({offer.key, offer.state}); + } + else + { + offers.push_back({offer.key, afterUpgrade}); + } + }; + + auto a1 = root.create("A", lm.getMinBalance(5) + 2000 + 5 * txFee); + a1.changeTrust(cur1, INT64_MAX); + issuer.pay(a1, cur1, INT64_MAX - 4000); + + std::vector offers; + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + createOfferQuantity(a1, cur1, native, INT64_MAX / 4 - 2000, offers, + OfferState::DELETED); + createOfferQuantity(a1, cur1, native, INT64_MAX / 4 - 2000, offers, + OfferState::DELETED); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{0, 2000}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 0}); + } + + SECTION("valid non-native") + { + auto a1 = root.create("A", lm.getMinBalance(6) + 6 * txFee); + a1.changeTrust(cur1, 6000); + a1.changeTrust(cur2, 6000); + issuer.pay(a1, cur1, 2000); + issuer.pay(a1, cur2, 2000); + + std::vector offers; + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{4000, 2000}); + } + + SECTION("invalid non-native") + { + auto a1 = root.create("A", lm.getMinBalance(6) + 6 * txFee); + a1.changeTrust(cur1, 6000); + a1.changeTrust(cur2, 6000); + issuer.pay(a1, cur1, 1000); + issuer.pay(a1, cur2, 2000); + + std::vector offers; + createOffer(a1, cur1, cur2, offers, OfferState::DELETED); + createOffer(a1, cur1, cur2, offers, OfferState::DELETED); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + market.requireChanges(offers, executeUpgrade); + + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 2000}); + } + + SECTION("valid non-native issued by account") + { + auto a1 = root.create("A", lm.getMinBalance(4) + 4 * txFee); + auto issuedCur1 = a1.asset("CUR1"); + auto issuedCur2 = a1.asset("CUR2"); + + std::vector offers; + createOffer(a1, issuedCur1, issuedCur2, offers); + createOffer(a1, issuedCur1, issuedCur2, offers); + createOffer(a1, issuedCur2, issuedCur1, offers); + createOffer(a1, issuedCur2, issuedCur1, offers); + + market.requireChanges(offers, executeUpgrade); + } + } + + SECTION("one account, multiple offers, multiple asset pairs") + { + SECTION("all valid") + { + auto a1 = + root.create("A", lm.getMinBalance(14) + 4000 + 14 * txFee); + a1.changeTrust(cur1, 12000); + a1.changeTrust(cur2, 12000); + issuer.pay(a1, cur1, 4000); + issuer.pay(a1, cur2, 4000); + + std::vector offers; + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{8000, 4000}); + } + + SECTION("one invalid native") + { + auto a1 = + root.create("A", lm.getMinBalance(14) + 2000 + 14 * txFee); + a1.changeTrust(cur1, 12000); + a1.changeTrust(cur2, 12000); + issuer.pay(a1, cur1, 4000); + issuer.pay(a1, cur2, 4000); + + std::vector offers; + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur2, offers, OfferState::DELETED); + createOffer(a1, native, cur2, offers, OfferState::DELETED); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{8000, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{4000, 4000}); + } + + SECTION("one invalid non-native") + { + auto a1 = + root.create("A", lm.getMinBalance(14) + 4000 + 14 * txFee); + a1.changeTrust(cur1, 12000); + a1.changeTrust(cur2, 12000); + issuer.pay(a1, cur1, 4000); + issuer.pay(a1, cur2, 1000); + + std::vector offers; + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, cur2, native, offers, OfferState::DELETED); + createOffer(a1, cur2, native, offers, OfferState::DELETED); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers, OfferState::DELETED); + createOffer(a1, cur2, cur1, offers, OfferState::DELETED); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{4000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{8000, 0}); + } + } + + SECTION("multiple accounts, multiple offers, multiple asset pairs") + { + SECTION("all valid") + { + auto a1 = + root.create("A", lm.getMinBalance(14) + 4000 + 14 * txFee); + a1.changeTrust(cur1, 12000); + a1.changeTrust(cur2, 12000); + issuer.pay(a1, cur1, 4000); + issuer.pay(a1, cur2, 4000); + + auto a2 = + root.create("B", lm.getMinBalance(14) + 4000 + 14 * txFee); + a2.changeTrust(cur1, 12000); + a2.changeTrust(cur2, 12000); + issuer.pay(a2, cur1, 4000); + issuer.pay(a2, cur2, 4000); + + std::vector offers; + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + createOffer(a2, native, cur1, offers); + createOffer(a2, native, cur1, offers); + createOffer(a2, cur1, native, offers); + createOffer(a2, cur1, native, offers); + createOffer(a2, native, cur2, offers); + createOffer(a2, native, cur2, offers); + createOffer(a2, cur2, native, offers); + createOffer(a2, cur2, native, offers); + createOffer(a2, cur1, cur2, offers); + createOffer(a2, cur1, cur2, offers); + createOffer(a2, cur2, cur1, offers); + createOffer(a2, cur2, cur1, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{8000, 4000}); + REQUIRE(getLiabilities(a2) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a2, cur1) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a2, cur2) == Liabilities{8000, 4000}); + } + + SECTION("one invalid per account") + { + auto a1 = + root.create("A", lm.getMinBalance(14) + 2000 + 14 * txFee); + a1.changeTrust(cur1, 12000); + a1.changeTrust(cur2, 12000); + issuer.pay(a1, cur1, 4000); + issuer.pay(a1, cur2, 4000); + + auto a2 = + root.create("B", lm.getMinBalance(14) + 4000 + 14 * txFee); + a2.changeTrust(cur1, 12000); + a2.changeTrust(cur2, 12000); + issuer.pay(a2, cur1, 4000); + issuer.pay(a2, cur2, 2000); + + std::vector offers; + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur2, offers, OfferState::DELETED); + createOffer(a1, native, cur2, offers, OfferState::DELETED); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + createOffer(a2, native, cur1, offers); + createOffer(a2, native, cur1, offers); + createOffer(a2, cur1, native, offers); + createOffer(a2, cur1, native, offers); + createOffer(a2, native, cur2, offers); + createOffer(a2, native, cur2, offers); + createOffer(a2, cur2, native, offers, OfferState::DELETED); + createOffer(a2, cur2, native, offers, OfferState::DELETED); + createOffer(a2, cur1, cur2, offers); + createOffer(a2, cur1, cur2, offers); + createOffer(a2, cur2, cur1, offers, OfferState::DELETED); + createOffer(a2, cur2, cur1, offers, OfferState::DELETED); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{8000, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{4000, 4000}); + REQUIRE(getLiabilities(a2) == Liabilities{4000, 4000}); + REQUIRE(getAssetLiabilities(a2, cur1) == Liabilities{4000, 4000}); + REQUIRE(getAssetLiabilities(a2, cur2) == Liabilities{8000, 0}); + } + } + + SECTION("liabilities overflow") + { + auto createOfferLarge = [&](TestAccount& acc, Asset const& selling, + Asset const& buying, + std::vector& offers, + OfferState const& afterUpgrade = + OfferState::SAME) { + OfferState state = {selling, buying, Price{2, 1}, INT64_MAX / 3}; + auto offer = market.requireChangesWithOffer( + {}, [&] { return market.addOffer(acc, state); }); + if (afterUpgrade == OfferState::SAME) + { + offers.push_back({offer.key, offer.state}); + } + else + { + offers.push_back({offer.key, afterUpgrade}); + } + }; + + SECTION("non-native for non-native, all invalid") + { + auto a1 = root.create("A", lm.getMinBalance(6) + 6 * txFee); + a1.changeTrust(cur1, INT64_MAX); + a1.changeTrust(cur2, INT64_MAX); + issuer.pay(a1, cur1, INT64_MAX / 3); + issuer.pay(a1, cur2, INT64_MAX / 3); + + std::vector offers; + createOfferLarge(a1, cur1, cur2, offers, OfferState::DELETED); + createOfferLarge(a1, cur1, cur2, offers, OfferState::DELETED); + createOfferLarge(a1, cur2, cur1, offers, OfferState::DELETED); + createOfferLarge(a1, cur2, cur1, offers, OfferState::DELETED); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + + SECTION("non-native for non-native, half invalid") + { + auto a1 = root.create("A", lm.getMinBalance(6) + 6 * txFee); + a1.changeTrust(cur1, INT64_MAX); + a1.changeTrust(cur2, INT64_MAX); + issuer.pay(a1, cur1, INT64_MAX / 3); + issuer.pay(a1, cur2, INT64_MAX / 3); + + std::vector offers; + createOfferLarge(a1, cur1, cur2, offers, OfferState::DELETED); + createOfferLarge(a1, cur1, cur2, offers, OfferState::DELETED); + createOfferLarge(a1, cur2, cur1, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getAssetLiabilities(a1, cur1) == + Liabilities{INT64_MAX / 3 * 2, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == + Liabilities{0, INT64_MAX / 3}); + } + + SECTION("issued asset for issued asset") + { + auto a1 = root.create("A", lm.getMinBalance(4) + 4 * txFee); + auto issuedCur1 = a1.asset("CUR1"); + auto issuedCur2 = a1.asset("CUR2"); + + std::vector offers; + createOfferLarge(a1, issuedCur1, issuedCur2, offers); + createOfferLarge(a1, issuedCur1, issuedCur2, offers); + createOfferLarge(a1, issuedCur2, issuedCur1, offers); + createOfferLarge(a1, issuedCur2, issuedCur1, offers); + + market.requireChanges(offers, executeUpgrade); + } + } + + SECTION("adjust offers") + { + SECTION("offers that do not satisfy thresholds are deleted") + { + auto createOfferQuantity = + [&](TestAccount& acc, Asset const& selling, Asset const& buying, + int64_t quantity, std::vector& offers, + OfferState const& afterUpgrade = OfferState::SAME) { + OfferState state = {selling, buying, Price{3, 2}, quantity}; + auto offer = market.requireChangesWithOffer( + {}, [&] { return market.addOffer(acc, state); }); + if (afterUpgrade == OfferState::SAME) + { + offers.push_back({offer.key, offer.state}); + } + else + { + offers.push_back({offer.key, afterUpgrade}); + } + }; + + auto a1 = root.create("A", lm.getMinBalance(6) + 6 * txFee); + a1.changeTrust(cur1, 1000); + a1.changeTrust(cur2, 1000); + issuer.pay(a1, cur1, 500); + issuer.pay(a1, cur2, 500); + + std::vector offers; + createOfferQuantity(a1, cur1, cur2, 27, offers, + OfferState::DELETED); + createOfferQuantity(a1, cur1, cur2, 28, offers); + createOfferQuantity(a1, cur2, cur1, 27, offers, + OfferState::DELETED); + createOfferQuantity(a1, cur2, cur1, 28, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{42, 28}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{42, 28}); + } + + SECTION("offers that need rounding are rounded") + { + auto createOfferQuantity = + [&](TestAccount& acc, Asset const& selling, Asset const& buying, + int64_t quantity, std::vector& offers, + OfferState const& afterUpgrade = OfferState::SAME) { + OfferState state = {selling, buying, Price{2, 3}, quantity}; + auto offer = market.requireChangesWithOffer( + {}, [&] { return market.addOffer(acc, state); }); + if (afterUpgrade == OfferState::SAME) + { + offers.push_back({offer.key, offer.state}); + } + else + { + offers.push_back({offer.key, afterUpgrade}); + } + }; + + auto a1 = root.create("A", lm.getMinBalance(4) + 4 * txFee); + a1.changeTrust(cur1, 1000); + a1.changeTrust(cur2, 1000); + issuer.pay(a1, cur1, 500); + + std::vector offers; + createOfferQuantity(a1, cur1, cur2, 201, offers); + createOfferQuantity(a1, cur1, cur2, 202, offers, + {cur1, cur2, Price{2, 3}, 201}); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 402}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{268, 0}); + } + + SECTION("offers that do not satisfy thresholds still contribute " + "liabilities") + { + auto createOfferQuantity = + [&](TestAccount& acc, Asset const& selling, Asset const& buying, + int64_t quantity, std::vector& offers, + OfferState const& afterUpgrade = OfferState::SAME) { + OfferState state = {selling, buying, Price{3, 2}, quantity}; + auto offer = market.requireChangesWithOffer( + {}, [&] { return market.addOffer(acc, state); }); + if (afterUpgrade == OfferState::SAME) + { + offers.push_back({offer.key, offer.state}); + } + else + { + offers.push_back({offer.key, afterUpgrade}); + } + }; + + auto a1 = + root.create("A", lm.getMinBalance(10) + 2000 + 12 * txFee); + a1.changeTrust(cur1, 5125); + a1.changeTrust(cur2, 5125); + issuer.pay(a1, cur1, 2050); + issuer.pay(a1, cur2, 2050); + + SECTION("normal offers remain without liabilities from" + " offers that do not satisfy thresholds") + { + // Pay txFee to send 4*baseReserve + 3*txFee for net balance + // decrease of 4*baseReserve + 4*txFee. This matches the balance + // decrease from creating 4 offers as in the next test section. + a1.pay(root, + 4 * lm.getCurrentLedgerHeader().baseReserve + 3 * txFee); + + std::vector offers; + createOfferQuantity(a1, cur1, native, 1000, offers); + createOfferQuantity(a1, cur1, native, 1000, offers); + createOfferQuantity(a1, native, cur1, 1000, offers); + createOfferQuantity(a1, native, cur1, 1000, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{3000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur1) == + Liabilities{3000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + + SECTION("normal offers deleted with liabilities from" + " offers that do not satisfy thresholds") + { + std::vector offers; + createOfferQuantity(a1, cur1, cur2, 27, offers, + OfferState::DELETED); + createOfferQuantity(a1, cur1, cur2, 27, offers, + OfferState::DELETED); + createOfferQuantity(a1, cur1, native, 1000, offers, + OfferState::DELETED); + createOfferQuantity(a1, cur1, native, 1000, offers, + OfferState::DELETED); + createOfferQuantity(a1, cur2, cur1, 27, offers, + OfferState::DELETED); + createOfferQuantity(a1, cur2, cur1, 27, offers, + OfferState::DELETED); + createOfferQuantity(a1, native, cur1, 1000, offers, + OfferState::DELETED); + createOfferQuantity(a1, native, cur1, 1000, offers, + OfferState::DELETED); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + } + } + + SECTION("unauthorized offers") + { + auto toSet = static_cast(AUTH_REQUIRED_FLAG) | + static_cast(AUTH_REVOCABLE_FLAG); + issuer.setOptions(txtest::setFlags(toSet)); + + SECTION("both assets require authorization and authorized") + { + auto a1 = root.create("A", lm.getMinBalance(6) + 6 * txFee); + a1.changeTrust(cur1, 6000); + a1.changeTrust(cur2, 6000); + issuer.allowTrust(cur1, a1); + issuer.allowTrust(cur2, a1); + issuer.pay(a1, cur1, 2000); + issuer.pay(a1, cur2, 2000); + + std::vector offers; + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{4000, 2000}); + } + + SECTION("selling asset not authorized") + { + auto a1 = root.create("A", lm.getMinBalance(6) + 4000 + 6 * txFee); + a1.changeTrust(cur1, 6000); + a1.changeTrust(cur2, 6000); + issuer.allowTrust(cur1, a1); + issuer.allowTrust(cur2, a1); + issuer.pay(a1, cur1, 2000); + issuer.pay(a1, cur2, 2000); + + std::vector offers; + createOffer(a1, cur1, native, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers, OfferState::DELETED); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur2, native, offers); + + issuer.denyTrust(cur1, a1); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{4000, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 2000}); + } + + SECTION("buying asset not authorized") + { + auto a1 = root.create("A", lm.getMinBalance(6) + 4000 + 6 * txFee); + a1.changeTrust(cur1, 6000); + a1.changeTrust(cur2, 6000); + issuer.allowTrust(cur1, a1); + issuer.allowTrust(cur2, a1); + issuer.pay(a1, cur1, 2000); + issuer.pay(a1, cur2, 2000); + + std::vector offers; + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur2, offers); + createOffer(a1, native, cur2, offers); + + issuer.denyTrust(cur1, a1); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{0, 2000}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{4000, 0}); + } + + SECTION("unauthorized offers still contribute liabilities") + { + auto a1 = + root.create("A", lm.getMinBalance(10) + 2000 + 10 * txFee); + a1.changeTrust(cur1, 6000); + a1.changeTrust(cur2, 6000); + issuer.allowTrust(cur1, a1); + issuer.allowTrust(cur2, a1); + issuer.pay(a1, cur1, 2000); + issuer.pay(a1, cur2, 2000); + + SECTION("authorized offers remain without liabilities from" + " unauthorized offers") + { + // Pay txFee to send 4*baseReserve + 3*txFee for net balance + // decrease of 4*baseReserve + 4*txFee. This matches the balance + // decrease from creating 4 offers as in the next test section. + a1.pay(root, + 4 * lm.getCurrentLedgerHeader().baseReserve + 3 * txFee); + + std::vector offers; + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + + issuer.denyTrust(cur2, a1); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{4000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur1) == + Liabilities{4000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + + SECTION("authorized offers deleted with liabilities from" + " unauthorized offers") + { + std::vector offers; + createOffer(a1, cur1, cur2, offers, OfferState::DELETED); + createOffer(a1, cur1, cur2, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers, OfferState::DELETED); + createOffer(a1, cur2, cur1, offers, OfferState::DELETED); + createOffer(a1, cur2, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + + issuer.denyTrust(cur2, a1); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + } + } + + SECTION("deleted trust lines") + { + auto a1 = root.create("A", lm.getMinBalance(4) + 6 * txFee); + a1.changeTrust(cur1, 6000); + a1.changeTrust(cur2, 6000); + issuer.pay(a1, cur1, 2000); + + std::vector offers; + createOffer(a1, cur1, cur2, offers, OfferState::DELETED); + createOffer(a1, cur1, cur2, offers, OfferState::DELETED); + + SECTION("deleted selling trust line") + { + a1.pay(issuer, cur1, 2000); + a1.changeTrust(cur1, 0); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + SECTION("deleted buying trust line") + { + a1.changeTrust(cur2, 0); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + } + + SECTION("offers with deleted trust lines still contribute liabilities") + { + auto a1 = root.create("A", lm.getMinBalance(10) + 2000 + 12 * txFee); + a1.changeTrust(cur1, 6000); + a1.changeTrust(cur2, 6000); + issuer.pay(a1, cur1, 2000); + issuer.pay(a1, cur2, 2000); + + SECTION("normal offers remain without liabilities from" + " offers with deleted trust lines") + { + // Pay txFee to send 4*baseReserve + 3*txFee for net balance + // decrease of 4*baseReserve + 4*txFee. This matches the balance + // decrease from creating 4 offers as in the next test section. + a1.pay(root, + 4 * lm.getCurrentLedgerHeader().baseReserve + 3 * txFee); + + std::vector offers; + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + + a1.pay(issuer, cur2, 2000); + a1.changeTrust(cur2, 0); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{4000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 2000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + + SECTION("normal offers deleted with liabilities from" + " offers with deleted trust lines") + { + std::vector offers; + createOffer(a1, cur1, cur2, offers, OfferState::DELETED); + createOffer(a1, cur1, cur2, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers, OfferState::DELETED); + createOffer(a1, cur2, cur1, offers, OfferState::DELETED); + createOffer(a1, cur2, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + + a1.pay(issuer, cur2, 2000); + a1.changeTrust(cur2, 0); + + market.requireChanges(offers, executeUpgrade); + REQUIRE(getLiabilities(a1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{0, 0}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{0, 0}); + } + } +} + +TEST_CASE("upgrade base reserve", "[upgrades]") +{ + VirtualClock clock; + auto cfg = getTestConfig(0); + auto app = createTestApplication(clock, cfg); + app->start(); + + auto& lm = app->getLedgerManager(); + auto txFee = lm.getTxFee(); + + auto const& lcl = lm.getLastClosedLedgerHeader(); + auto txSet = std::make_shared(lcl.hash); + + auto root = TestAccount::createRoot(*app); + auto issuer = root.create("issuer", lm.getMinBalance(0) + 100 * txFee); + auto native = txtest::makeNativeAsset(); + auto cur1 = issuer.asset("CUR1"); + auto cur2 = issuer.asset("CUR2"); + + auto market = TestMarket{*app}; + + auto executeUpgrade = [&](uint32_t newReserve) { + auto upgrades = xdr::xvector{}; + upgrades.push_back(toUpgradeType(makeBaseReserveUpgrade(newReserve))); + + StellarValue sv{txSet->getContentsHash(), 2, upgrades, 0}; + LedgerCloseData ledgerData(lcl.header.ledgerSeq + 1, txSet, sv); + app->getLedgerManager().closeLedger(ledgerData); + + auto const& lhhe = app->getLedgerManager().getLastClosedLedgerHeader(); + REQUIRE(lhhe.header.baseReserve == newReserve); + }; + + auto getLiabilities = [&](TestAccount& acc) { + Liabilities res; + auto account = txtest::loadAccount(acc.getPublicKey(), *app); + res.selling = account->getSellingLiabilities(lm); + res.buying = account->getBuyingLiabilities(lm); + return res; + }; + auto getAssetLiabilities = [&](TestAccount& acc, Asset const& asset) { + Liabilities res; + auto trust = acc.loadTrustLine(asset); + res.selling = getSellingLiabilities(trust, lm); + res.buying = getBuyingLiabilities(trust, lm); + return res; + }; + + auto createOffer = [&](TestAccount& acc, Asset const& selling, + Asset const& buying, + std::vector& offers, + OfferState const& afterUpgrade = OfferState::SAME) { + OfferState state = {selling, buying, Price{2, 1}, 1000}; + auto offer = market.requireChangesWithOffer( + {}, [&] { return market.addOffer(acc, state); }); + if (afterUpgrade == OfferState::SAME) + { + offers.push_back({offer.key, offer.state}); + } + else + { + offers.push_back({offer.key, afterUpgrade}); + } + }; + + SECTION("decrease reserve") + { + auto a1 = root.create("A", lm.getMinBalance(14) + 4000 + 14 * txFee); + a1.changeTrust(cur1, 12000); + a1.changeTrust(cur2, 12000); + issuer.pay(a1, cur1, 4000); + issuer.pay(a1, cur2, 4000); + + std::vector offers; + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + for_versions_to(9, *app, [&] { + uint32_t baseReserve = lm.getCurrentLedgerHeader().baseReserve; + market.requireChanges(offers, + std::bind(executeUpgrade, baseReserve / 2)); + }); + for_versions_from(10, *app, [&] { + uint32_t baseReserve = lm.getCurrentLedgerHeader().baseReserve; + market.requireChanges(offers, + std::bind(executeUpgrade, baseReserve / 2)); + REQUIRE(getLiabilities(a1) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{8000, 4000}); + }); + } + + SECTION("increase reserve") + { + for_versions_to(9, *app, [&] { + auto a1 = + root.create("A", 2 * lm.getMinBalance(14) + 3999 + 14 * txFee); + a1.changeTrust(cur1, 12000); + a1.changeTrust(cur2, 12000); + issuer.pay(a1, cur1, 4000); + issuer.pay(a1, cur2, 4000); + + auto a2 = + root.create("B", 2 * lm.getMinBalance(14) + 4000 + 14 * txFee); + a2.changeTrust(cur1, 12000); + a2.changeTrust(cur2, 12000); + issuer.pay(a2, cur1, 4000); + issuer.pay(a2, cur2, 4000); + + std::vector offers; + createOffer(a1, native, cur1, offers); + createOffer(a1, native, cur1, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, native, cur2, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + createOffer(a2, native, cur1, offers); + createOffer(a2, native, cur1, offers); + createOffer(a2, cur1, native, offers); + createOffer(a2, cur1, native, offers); + createOffer(a2, native, cur2, offers); + createOffer(a2, native, cur2, offers); + createOffer(a2, cur2, native, offers); + createOffer(a2, cur2, native, offers); + createOffer(a2, cur1, cur2, offers); + createOffer(a2, cur1, cur2, offers); + createOffer(a2, cur2, cur1, offers); + createOffer(a2, cur2, cur1, offers); + + uint32_t baseReserve = lm.getCurrentLedgerHeader().baseReserve; + market.requireChanges(offers, + std::bind(executeUpgrade, 2 * baseReserve)); + }); + for_versions_from(10, *app, [&] { + auto a1 = + root.create("A", 2 * lm.getMinBalance(14) + 3999 + 14 * txFee); + a1.changeTrust(cur1, 12000); + a1.changeTrust(cur2, 12000); + issuer.pay(a1, cur1, 4000); + issuer.pay(a1, cur2, 4000); + + auto a2 = + root.create("B", 2 * lm.getMinBalance(14) + 4000 + 14 * txFee); + a2.changeTrust(cur1, 12000); + a2.changeTrust(cur2, 12000); + issuer.pay(a2, cur1, 4000); + issuer.pay(a2, cur2, 4000); + + std::vector offers; + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, native, cur1, offers, OfferState::DELETED); + createOffer(a1, cur1, native, offers); + createOffer(a1, cur1, native, offers); + createOffer(a1, native, cur2, offers, OfferState::DELETED); + createOffer(a1, native, cur2, offers, OfferState::DELETED); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur2, native, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur1, cur2, offers); + createOffer(a1, cur2, cur1, offers); + createOffer(a1, cur2, cur1, offers); + + createOffer(a2, native, cur1, offers); + createOffer(a2, native, cur1, offers); + createOffer(a2, cur1, native, offers); + createOffer(a2, cur1, native, offers); + createOffer(a2, native, cur2, offers); + createOffer(a2, native, cur2, offers); + createOffer(a2, cur2, native, offers); + createOffer(a2, cur2, native, offers); + createOffer(a2, cur1, cur2, offers); + createOffer(a2, cur1, cur2, offers); + createOffer(a2, cur2, cur1, offers); + createOffer(a2, cur2, cur1, offers); + + uint32_t baseReserve = lm.getCurrentLedgerHeader().baseReserve; + market.requireChanges(offers, + std::bind(executeUpgrade, 2 * baseReserve)); + REQUIRE(getLiabilities(a1) == Liabilities{8000, 0}); + REQUIRE(getAssetLiabilities(a1, cur1) == Liabilities{4000, 4000}); + REQUIRE(getAssetLiabilities(a1, cur2) == Liabilities{4000, 4000}); + REQUIRE(getLiabilities(a2) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a2, cur1) == Liabilities{8000, 4000}); + REQUIRE(getAssetLiabilities(a2, cur2) == Liabilities{8000, 4000}); + }); + } +} + TEST_CASE("simulate upgrades", "[herder][upgrades]") { // no upgrade is done From bf5ee31699bbff47102eb91a2f28bd891c7df047 Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:51 -0500 Subject: [PATCH 12/13] Added LiabilitiesTests for balance and liability adding/getting functions --- Builds/VisualStudio/stellar-core.vcxproj | 1 + src/ledger/LiabilitiesTests.cpp | 1185 ++++++++++++++++++++++ 2 files changed, 1186 insertions(+) create mode 100644 src/ledger/LiabilitiesTests.cpp diff --git a/Builds/VisualStudio/stellar-core.vcxproj b/Builds/VisualStudio/stellar-core.vcxproj index 2916fdd77e..186d00709e 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj +++ b/Builds/VisualStudio/stellar-core.vcxproj @@ -346,6 +346,7 @@ exit /b 0 + diff --git a/src/ledger/LiabilitiesTests.cpp b/src/ledger/LiabilitiesTests.cpp new file mode 100644 index 0000000000..5cbbc35942 --- /dev/null +++ b/src/ledger/LiabilitiesTests.cpp @@ -0,0 +1,1185 @@ +// Copyright 2018 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "ledger/AccountFrame.h" +#include "ledger/LedgerTestUtils.h" +#include "ledger/TrustFrame.h" +#include "lib/catch.hpp" +#include "main/Application.h" +#include "test/TestUtils.h" +#include "test/test.h" +#include "util/Timer.h" + +using namespace stellar; + +TEST_CASE("liabilities", "[ledger][liabilities]") +{ + VirtualClock clock; + auto app = createTestApplication(clock, getTestConfig()); + auto& lm = app->getLedgerManager(); + app->start(); + + SECTION("add account selling liabilities") + { + auto addSellingLiabilities = + [&](int64_t initNumSubEntries, int64_t initBalance, + int64_t initSellingLiabilities, int64_t deltaLiabilities) { + AccountEntry ae = LedgerTestUtils::generateValidAccountEntry(); + ae.balance = initBalance; + ae.numSubEntries = initNumSubEntries; + if (ae.ext.v() < 1) + { + ae.ext.v(1); + } + ae.ext.v1().liabilities.selling = initSellingLiabilities; + int64_t initBuyingLiabilities = ae.ext.v1().liabilities.buying; + + LedgerEntry le; + le.data.type(ACCOUNT); + le.data.account() = ae; + + auto af = std::make_shared(le); + bool res = af->addSellingLiabilities(deltaLiabilities, lm); + REQUIRE(af->getBalance() == initBalance); + REQUIRE(af->getBuyingLiabilities(lm) == initBuyingLiabilities); + if (res) + { + REQUIRE(af->getSellingLiabilities(lm) == + initSellingLiabilities + deltaLiabilities); + } + else + { + REQUIRE(af->getSellingLiabilities(lm) == + initSellingLiabilities); + REQUIRE(af->getAccount() == ae); + } + return res; + }; + auto addSellingLiabilitiesUninitialized = + [&](int64_t initNumSubEntries, int64_t initBalance, + int64_t deltaLiabilities) { + AccountEntry ae = LedgerTestUtils::generateValidAccountEntry(); + ae.balance = initBalance; + ae.numSubEntries = initNumSubEntries; + ae.ext.v(0); + + LedgerEntry le; + le.data.type(ACCOUNT); + le.data.account() = ae; + + auto af = std::make_shared(le); + bool res = af->addSellingLiabilities(deltaLiabilities, lm); + REQUIRE(af->getBalance() == initBalance); + REQUIRE(af->getBuyingLiabilities(lm) == 0); + if (res) + { + REQUIRE(af->getAccount().ext.v() == + (deltaLiabilities != 0)); + REQUIRE(af->getSellingLiabilities(lm) == deltaLiabilities); + } + else + { + REQUIRE(af->getSellingLiabilities(lm) == 0); + REQUIRE(af->getAccount() == ae); + } + return res; + }; + + for_versions_from(10, *app, [&] { + SECTION("uninitialized liabilities") + { + // Uninitialized remains uninitialized after failure + REQUIRE(!addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), 1)); + + // Uninitialized remains unitialized after success of delta 0 + REQUIRE(addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), 0)); + + // Uninitialized is initialized after success of delta != 0 + REQUIRE(addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) + 1, 1)); + } + + SECTION("below reserve") + { + // Can leave unchanged when account is below reserve + REQUIRE( + addSellingLiabilities(0, lm.getMinBalance(0) - 1, 0, 0)); + + // Cannot increase when account is below reserve + REQUIRE( + !addSellingLiabilities(0, lm.getMinBalance(0) - 1, 0, 1)); + + // No need to test decrease below reserve since that would imply + // the previous state had excess liabilities + } + + SECTION("cannot make liabilities negative") + { + // No initial liabilities and at maximum + REQUIRE(addSellingLiabilities(0, lm.getMinBalance(0), 0, 0)); + REQUIRE(addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), 0)); + REQUIRE(!addSellingLiabilities(0, lm.getMinBalance(0), 0, -1)); + REQUIRE(!addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), -1)); + + // No initial liabilities and below maximum + REQUIRE( + addSellingLiabilities(0, lm.getMinBalance(0) + 1, 0, 0)); + REQUIRE(addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) + 1, 0)); + REQUIRE( + !addSellingLiabilities(0, lm.getMinBalance(0) + 1, 0, -1)); + REQUIRE(!addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) + 1, -1)); + + // Initial liabilities and at maximum + REQUIRE( + addSellingLiabilities(0, lm.getMinBalance(0) + 1, 1, -1)); + REQUIRE( + !addSellingLiabilities(0, lm.getMinBalance(0) + 1, 1, -2)); + + // Initial liabilities and below maximum + REQUIRE( + addSellingLiabilities(0, lm.getMinBalance(0) + 2, 1, -1)); + REQUIRE( + !addSellingLiabilities(0, lm.getMinBalance(0) + 2, 1, -2)); + } + + SECTION("cannot increase liabilities above balance minus reserve") + { + // No initial liabilities and at maximum + REQUIRE(addSellingLiabilities(0, lm.getMinBalance(0), 0, 0)); + REQUIRE(addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), 0)); + REQUIRE(!addSellingLiabilities(0, lm.getMinBalance(0), 0, 1)); + REQUIRE(!addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), 1)); + + // No initial liabilities and below maximum + REQUIRE( + addSellingLiabilities(0, lm.getMinBalance(0) + 1, 0, 1)); + REQUIRE(addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) + 1, 1)); + REQUIRE( + !addSellingLiabilities(0, lm.getMinBalance(0) + 1, 0, 2)); + REQUIRE(!addSellingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) + 1, 2)); + + // Initial liabilities and at maximum + REQUIRE( + addSellingLiabilities(0, lm.getMinBalance(0) + 1, 1, 0)); + REQUIRE( + !addSellingLiabilities(0, lm.getMinBalance(0) + 1, 1, 1)); + + // Initial liabilities and below maximum + REQUIRE( + addSellingLiabilities(0, lm.getMinBalance(0) + 2, 1, 1)); + REQUIRE( + !addSellingLiabilities(0, lm.getMinBalance(0) + 2, 1, 2)); + } + + SECTION("limiting values") + { + // Can increase to limit but no higher + REQUIRE(addSellingLiabilities(0, INT64_MAX, 0, + INT64_MAX - lm.getMinBalance(0))); + REQUIRE(!addSellingLiabilities( + 0, INT64_MAX, 0, INT64_MAX - lm.getMinBalance(0) + 1)); + REQUIRE(!addSellingLiabilities( + 0, INT64_MAX, 1, INT64_MAX - lm.getMinBalance(0))); + + // Can decrease from limit + REQUIRE(addSellingLiabilities( + 0, INT64_MAX, INT64_MAX - lm.getMinBalance(0), -1)); + REQUIRE(addSellingLiabilities(0, INT64_MAX, + INT64_MAX - lm.getMinBalance(0), + lm.getMinBalance(0) - INT64_MAX)); + } + }); + } + + SECTION("add account buying liabilities") + { + auto addBuyingLiabilities = [&](int64_t initNumSubEntries, + int64_t initBalance, + int64_t initBuyingLiabilities, + int64_t deltaLiabilities) { + AccountEntry ae = LedgerTestUtils::generateValidAccountEntry(); + ae.balance = initBalance; + ae.numSubEntries = initNumSubEntries; + if (ae.ext.v() < 1) + { + ae.ext.v(1); + } + ae.ext.v1().liabilities.buying = initBuyingLiabilities; + int64_t initSellingLiabilities = ae.ext.v1().liabilities.selling; + + LedgerEntry le; + le.data.type(ACCOUNT); + le.data.account() = ae; + + auto af = std::make_shared(le); + bool res = af->addBuyingLiabilities(deltaLiabilities, lm); + REQUIRE(af->getBalance() == initBalance); + REQUIRE(af->getSellingLiabilities(lm) == initSellingLiabilities); + if (res) + { + REQUIRE(af->getBuyingLiabilities(lm) == + initBuyingLiabilities + deltaLiabilities); + } + else + { + REQUIRE(af->getBuyingLiabilities(lm) == initBuyingLiabilities); + REQUIRE(af->getAccount() == ae); + } + return res; + }; + auto addBuyingLiabilitiesUninitialized = [&](int64_t initNumSubEntries, + int64_t initBalance, + int64_t deltaLiabilities) { + AccountEntry ae = LedgerTestUtils::generateValidAccountEntry(); + ae.balance = initBalance; + ae.numSubEntries = initNumSubEntries; + ae.ext.v(0); + + LedgerEntry le; + le.data.type(ACCOUNT); + le.data.account() = ae; + + auto af = std::make_shared(le); + bool res = af->addBuyingLiabilities(deltaLiabilities, lm); + REQUIRE(af->getBalance() == initBalance); + REQUIRE(af->getSellingLiabilities(lm) == 0); + if (res) + { + REQUIRE(af->getAccount().ext.v() == (deltaLiabilities != 0)); + REQUIRE(af->getBuyingLiabilities(lm) == deltaLiabilities); + } + else + { + REQUIRE(af->getBuyingLiabilities(lm) == 0); + REQUIRE(af->getAccount() == ae); + } + return res; + }; + + for_versions_from(10, *app, [&] { + SECTION("uninitialized liabilities") + { + // Uninitialized remains uninitialized after failure + REQUIRE(!addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), + INT64_MAX - (lm.getMinBalance(0) - 1))); + + // Uninitialized remains uninitialized after success of delta 0 + REQUIRE(addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), 0)); + + // Uninitialized is initialized after success of delta != 0 + REQUIRE(addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), 1)); + } + + SECTION("below reserve") + { + // Can decrease when account is below reserve + REQUIRE( + addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 1, -1)); + + // Can leave unchanged when account is below reserve + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 0, 0)); + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 1, 0)); + + // Can increase when account is below reserve + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 0, 1)); + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 1, 1)); + } + + SECTION("cannot make liabilities negative") + { + // No initial liabilities + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0), 0, 0)); + REQUIRE(addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), 0)); + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0), 0, -1)); + REQUIRE(!addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), -1)); + + // Initial liabilities + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0), 1, -1)); + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0), 1, -2)); + } + + SECTION("cannot increase liabilities above INT64_MAX minus balance") + { + // No initial liabilities, account below reserve + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 0, + INT64_MAX - + (lm.getMinBalance(0) - 1))); + REQUIRE(addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) - 1, + INT64_MAX - (lm.getMinBalance(0) - 1))); + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 0, + INT64_MAX - + (lm.getMinBalance(0) - 2))); + REQUIRE(!addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) - 1, + INT64_MAX - (lm.getMinBalance(0) - 2))); + + // Initial liabilities, account below reserve + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 1, + INT64_MAX - lm.getMinBalance(0))); + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0) - 1, 1, + INT64_MAX - + (lm.getMinBalance(0) - 1))); + + // No initial liabilities, account at reserve + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0), 0, + INT64_MAX - lm.getMinBalance(0))); + REQUIRE(addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), INT64_MAX - lm.getMinBalance(0))); + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0), 0, + INT64_MAX - + (lm.getMinBalance(0) - 1))); + REQUIRE(!addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0), + INT64_MAX - (lm.getMinBalance(0) - 1))); + + // Initial liabilities, account at reserve + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0), 1, + INT64_MAX - + (lm.getMinBalance(0) + 1))); + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0), 1, + INT64_MAX - lm.getMinBalance(0))); + + // No initial liabilities, account above reserve + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0) + 1, 0, + INT64_MAX - + (lm.getMinBalance(0) + 1))); + REQUIRE(addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) + 1, + INT64_MAX - (lm.getMinBalance(0) + 1))); + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0) + 1, 0, + INT64_MAX - lm.getMinBalance(0))); + REQUIRE(!addBuyingLiabilitiesUninitialized( + 0, lm.getMinBalance(0) + 1, + INT64_MAX - lm.getMinBalance(0))); + + // Initial liabilities, account above reserve + REQUIRE(addBuyingLiabilities(0, lm.getMinBalance(0) + 1, 1, + INT64_MAX - + (lm.getMinBalance(0) + 2))); + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0) + 1, 1, + INT64_MAX - + (lm.getMinBalance(0) + 1))); + } + + SECTION("limiting values") + { + REQUIRE(!addBuyingLiabilities(0, INT64_MAX, 0, 1)); + REQUIRE(addBuyingLiabilities(0, INT64_MAX - 1, 0, 1)); + + REQUIRE(!addBuyingLiabilities(0, lm.getMinBalance(0), + INT64_MAX - lm.getMinBalance(0), + 1)); + REQUIRE(addBuyingLiabilities( + 0, lm.getMinBalance(0), INT64_MAX - lm.getMinBalance(0) - 1, + 1)); + + REQUIRE(!addBuyingLiabilities(INT64_MAX, INT64_MAX / 2 + 1, + INT64_MAX / 2, 1)); + REQUIRE(!addBuyingLiabilities(INT64_MAX, INT64_MAX / 2, + INT64_MAX / 2 + 1, 1)); + REQUIRE(addBuyingLiabilities(INT64_MAX, INT64_MAX / 2, + INT64_MAX / 2, 1)); + } + }); + } + + SECTION("add trustline selling liabilities") + { + auto addSellingLiabilities = [&](int64_t initLimit, int64_t initBalance, + int64_t initSellingLiabilities, + int64_t deltaLiabilities) { + TrustLineEntry tl = LedgerTestUtils::generateValidTrustLineEntry(); + tl.flags = AUTHORIZED_FLAG; + tl.balance = initBalance; + tl.limit = initLimit; + if (tl.ext.v() < 1) + { + tl.ext.v(1); + } + tl.ext.v1().liabilities.selling = initSellingLiabilities; + int64_t initBuyingLiabilities = tl.ext.v1().liabilities.buying; + + LedgerEntry le; + le.data.type(TRUSTLINE); + le.data.trustLine() = tl; + + auto tf = std::make_shared(le); + bool res = tf->addSellingLiabilities(deltaLiabilities, lm); + REQUIRE(tf->getTrustLine().limit == initLimit); + REQUIRE(tf->getBalance() == initBalance); + REQUIRE(tf->getBuyingLiabilities(lm) == initBuyingLiabilities); + if (res) + { + REQUIRE(tf->getSellingLiabilities(lm) == + initSellingLiabilities + deltaLiabilities); + } + else + { + REQUIRE(tf->getSellingLiabilities(lm) == + initSellingLiabilities); + REQUIRE(tf->getTrustLine() == tl); + } + return res; + }; + auto addSellingLiabilitiesUninitialized = + [&](int64_t initLimit, int64_t initBalance, + int64_t deltaLiabilities) { + TrustLineEntry tl = + LedgerTestUtils::generateValidTrustLineEntry(); + tl.flags = AUTHORIZED_FLAG; + tl.balance = initBalance; + tl.limit = initLimit; + tl.ext.v(0); + + LedgerEntry le; + le.data.type(TRUSTLINE); + le.data.trustLine() = tl; + + auto tf = std::make_shared(le); + bool res = tf->addSellingLiabilities(deltaLiabilities, lm); + REQUIRE(tf->getTrustLine().limit == initLimit); + REQUIRE(tf->getBalance() == initBalance); + REQUIRE(tf->getBuyingLiabilities(lm) == 0); + if (res) + { + REQUIRE(tf->getTrustLine().ext.v() == + (deltaLiabilities != 0)); + REQUIRE(tf->getSellingLiabilities(lm) == deltaLiabilities); + } + else + { + REQUIRE(tf->getSellingLiabilities(lm) == 0); + REQUIRE(tf->getTrustLine() == tl); + } + return res; + }; + + for_versions_from(10, *app, [&] { + SECTION("uninitialized liabilities") + { + // Uninitialized remains uninitialized after failure + REQUIRE(!addSellingLiabilitiesUninitialized(1, 0, 1)); + + // Uninitialized remains unitialized after success of delta 0 + REQUIRE(addSellingLiabilitiesUninitialized(1, 1, 0)); + + // Uninitialized is initialized after success of delta != 0 + REQUIRE(addSellingLiabilitiesUninitialized(1, 1, 1)); + } + + SECTION("cannot make liabilities negative") + { + // No initial liabilities + REQUIRE(addSellingLiabilities(1, 0, 0, 0)); + REQUIRE(addSellingLiabilitiesUninitialized(1, 0, 0)); + REQUIRE(!addSellingLiabilities(1, 0, 0, -1)); + REQUIRE(!addSellingLiabilitiesUninitialized(1, 1, -1)); + + // Initial liabilities + REQUIRE(addSellingLiabilities(1, 0, 1, -1)); + REQUIRE(!addSellingLiabilities(1, 0, 1, -2)); + } + + SECTION("cannot increase liabilities above balance") + { + // No initial liabilities, below maximum + REQUIRE(addSellingLiabilities(2, 1, 0, 1)); + REQUIRE(addSellingLiabilitiesUninitialized(2, 1, 1)); + REQUIRE(!addSellingLiabilities(2, 1, 0, 2)); + REQUIRE(!addSellingLiabilitiesUninitialized(2, 1, 2)); + + // Initial liabilities, below maximum + REQUIRE(addSellingLiabilities(2, 2, 1, 1)); + REQUIRE(!addSellingLiabilities(2, 2, 1, 2)); + + // No initial liabilities, at maximum + REQUIRE(addSellingLiabilities(2, 0, 0, 0)); + REQUIRE(addSellingLiabilitiesUninitialized(2, 0, 0)); + REQUIRE(!addSellingLiabilities(2, 0, 0, 1)); + REQUIRE(!addSellingLiabilitiesUninitialized(2, 0, 1)); + + // Initial liabilities, at maximum + REQUIRE(addSellingLiabilities(2, 2, 2, 0)); + REQUIRE(!addSellingLiabilities(2, 2, 2, 1)); + } + + SECTION("limiting values") + { + REQUIRE( + addSellingLiabilities(INT64_MAX, INT64_MAX, 0, INT64_MAX)); + REQUIRE(!addSellingLiabilities(INT64_MAX, INT64_MAX - 1, 0, + INT64_MAX)); + REQUIRE(addSellingLiabilities(INT64_MAX, INT64_MAX - 1, 0, + INT64_MAX - 1)); + + REQUIRE( + !addSellingLiabilities(INT64_MAX, INT64_MAX, INT64_MAX, 1)); + REQUIRE(addSellingLiabilities(INT64_MAX, INT64_MAX, + INT64_MAX - 1, 1)); + } + }); + } + + SECTION("add trustline buying liabilities") + { + auto addBuyingLiabilities = [&](int64_t initLimit, int64_t initBalance, + int64_t initBuyingLiabilities, + int64_t deltaLiabilities) { + TrustLineEntry tl = LedgerTestUtils::generateValidTrustLineEntry(); + tl.flags = AUTHORIZED_FLAG; + tl.balance = initBalance; + tl.limit = initLimit; + if (tl.ext.v() < 1) + { + tl.ext.v(1); + } + tl.ext.v1().liabilities.buying = initBuyingLiabilities; + int64_t initSellingLiabilities = tl.ext.v1().liabilities.selling; + + LedgerEntry le; + le.data.type(TRUSTLINE); + le.data.trustLine() = tl; + + auto tf = std::make_shared(le); + bool res = tf->addBuyingLiabilities(deltaLiabilities, lm); + REQUIRE(tf->getTrustLine().limit == initLimit); + REQUIRE(tf->getBalance() == initBalance); + REQUIRE(tf->getSellingLiabilities(lm) == initSellingLiabilities); + if (res) + { + REQUIRE(tf->getBuyingLiabilities(lm) == + initBuyingLiabilities + deltaLiabilities); + } + else + { + REQUIRE(tf->getBuyingLiabilities(lm) == initBuyingLiabilities); + REQUIRE(tf->getTrustLine() == tl); + } + return res; + }; + auto addBuyingLiabilitiesUninitialized = [&](int64_t initLimit, + int64_t initBalance, + int64_t deltaLiabilities) { + TrustLineEntry tl = LedgerTestUtils::generateValidTrustLineEntry(); + tl.flags = AUTHORIZED_FLAG; + tl.balance = initBalance; + tl.limit = initLimit; + tl.ext.v(0); + + LedgerEntry le; + le.data.type(TRUSTLINE); + le.data.trustLine() = tl; + + auto tf = std::make_shared(le); + bool res = tf->addBuyingLiabilities(deltaLiabilities, lm); + REQUIRE(tf->getTrustLine().limit == initLimit); + REQUIRE(tf->getBalance() == initBalance); + REQUIRE(tf->getSellingLiabilities(lm) == 0); + if (res) + { + REQUIRE(tf->getTrustLine().ext.v() == (deltaLiabilities != 0)); + REQUIRE(tf->getBuyingLiabilities(lm) == deltaLiabilities); + } + else + { + REQUIRE(tf->getBuyingLiabilities(lm) == 0); + REQUIRE(tf->getTrustLine() == tl); + } + return res; + }; + + for_versions_from(10, *app, [&] { + SECTION("uninitialized liabilities") + { + // Uninitialized remains uninitialized after failure + REQUIRE(!addBuyingLiabilitiesUninitialized(1, 0, 2)); + + // Uninitialized remains unitialized after success of delta 0 + REQUIRE(addBuyingLiabilitiesUninitialized(1, 0, 0)); + + // Uninitialized is initialized after success of delta != 0 + REQUIRE(addBuyingLiabilitiesUninitialized(1, 0, 1)); + } + + SECTION("cannot make liabilities negative") + { + // No initial liabilities + REQUIRE(addBuyingLiabilities(1, 0, 0, 0)); + REQUIRE(addBuyingLiabilitiesUninitialized(1, 0, 0)); + REQUIRE(!addBuyingLiabilities(1, 0, 0, -1)); + REQUIRE(!addBuyingLiabilitiesUninitialized(1, 1, -1)); + + // Initial liabilities + REQUIRE(addBuyingLiabilities(1, 0, 1, -1)); + REQUIRE(!addBuyingLiabilities(1, 0, 1, -2)); + } + + SECTION("cannot increase liabilities above limit minus balance") + { + // No initial liabilities, below maximum + REQUIRE(addBuyingLiabilities(2, 1, 0, 1)); + REQUIRE(addBuyingLiabilitiesUninitialized(2, 1, 1)); + REQUIRE(!addBuyingLiabilities(2, 1, 0, 2)); + REQUIRE(!addBuyingLiabilitiesUninitialized(2, 1, 2)); + + // Initial liabilities, below maximum + REQUIRE(addBuyingLiabilities(3, 1, 1, 1)); + REQUIRE(!addBuyingLiabilities(3, 1, 1, 2)); + + // No initial liabilities, at maximum + REQUIRE(addBuyingLiabilities(2, 2, 0, 0)); + REQUIRE(addBuyingLiabilitiesUninitialized(2, 2, 0)); + REQUIRE(!addBuyingLiabilities(2, 2, 0, 1)); + REQUIRE(!addBuyingLiabilitiesUninitialized(2, 2, 1)); + + // Initial liabilities, at maximum + REQUIRE(addBuyingLiabilities(3, 2, 1, 0)); + REQUIRE(!addBuyingLiabilities(3, 2, 1, 1)); + } + + SECTION("limiting values") + { + REQUIRE(!addBuyingLiabilities(INT64_MAX, INT64_MAX, 0, 1)); + REQUIRE(addBuyingLiabilities(INT64_MAX, INT64_MAX - 1, 0, 1)); + + REQUIRE(!addBuyingLiabilities(INT64_MAX, 0, INT64_MAX, 1)); + REQUIRE(addBuyingLiabilities(INT64_MAX, 0, INT64_MAX - 1, 1)); + + REQUIRE(!addBuyingLiabilities(INT64_MAX, INT64_MAX / 2 + 1, + INT64_MAX / 2, 1)); + REQUIRE(!addBuyingLiabilities(INT64_MAX, INT64_MAX / 2, + INT64_MAX / 2 + 1, 1)); + REQUIRE(addBuyingLiabilities(INT64_MAX, INT64_MAX / 2, + INT64_MAX / 2, 1)); + } + }); + } +} + +TEST_CASE("balance with liabilities", "[ledger][liabilities]") +{ + VirtualClock clock; + auto app = createTestApplication(clock, getTestConfig()); + auto& lm = app->getLedgerManager(); + app->start(); + + SECTION("account add balance") + { + auto addBalance = [&](int64_t initNumSubEntries, int64_t initBalance, + Liabilities initLiabilities, + int64_t deltaBalance) { + AccountEntry ae = LedgerTestUtils::generateValidAccountEntry(); + ae.balance = initBalance; + ae.numSubEntries = initNumSubEntries; + ae.ext.v(1); + ae.ext.v1().liabilities = initLiabilities; + + LedgerEntry le; + le.data.type(ACCOUNT); + le.data.account() = ae; + + auto af = std::make_shared(le); + bool res = af->addBalance(deltaBalance, lm); + REQUIRE(af->getSellingLiabilities(lm) == initLiabilities.selling); + REQUIRE(af->getBuyingLiabilities(lm) == initLiabilities.buying); + if (res) + { + REQUIRE(af->getBalance() == initBalance + deltaBalance); + } + else + { + REQUIRE(af->getBalance() == initBalance); + REQUIRE(af->getAccount() == ae); + } + return res; + }; + + for_versions_from(10, *app, [&] { + SECTION("can increase balance from below minimum") + { + // Balance can remain unchanged below reserve + REQUIRE(addBalance(0, lm.getMinBalance(0) - 1, + Liabilities{0, 0}, 0)); + + // Balance starts and ends below reserve + REQUIRE(addBalance(0, lm.getMinBalance(0) - 2, + Liabilities{0, 0}, 1)); + + // Balance starts below reserve and ends at reserve + REQUIRE(addBalance(0, lm.getMinBalance(0) - 1, + Liabilities{0, 0}, 1)); + + // Balance starts below reserve and ends above reserve + REQUIRE(addBalance(0, lm.getMinBalance(0) - 1, + Liabilities{0, 0}, 2)); + } + + SECTION("cannot decrease balance below reserve plus selling " + "liabilities") + { + // Below minimum balance, no liabilities + REQUIRE(addBalance(0, lm.getMinBalance(0) - 1, + Liabilities{0, 0}, 0)); + REQUIRE(!addBalance(0, lm.getMinBalance(0) - 1, + Liabilities{0, 0}, -1)); + + // Minimum balance, no liabilities + REQUIRE( + addBalance(0, lm.getMinBalance(0), Liabilities{0, 0}, 0)); + REQUIRE( + !addBalance(0, lm.getMinBalance(0), Liabilities{0, 0}, -1)); + + // Above minimum balance, no liabilities + REQUIRE(addBalance(0, lm.getMinBalance(0) + 1, + Liabilities{0, 0}, -1)); + REQUIRE(!addBalance(0, lm.getMinBalance(0) + 1, + Liabilities{0, 0}, -2)); + + // Above minimum balance, with liabilities + REQUIRE(addBalance(0, lm.getMinBalance(0) + 1, + Liabilities{0, 1}, 0)); + REQUIRE(!addBalance(0, lm.getMinBalance(0) + 1, + Liabilities{0, 1}, -1)); + } + + SECTION("cannot increase balance above INT64_MAX minus buying " + "liabilities") + { + // Maximum balance, no liabilities + REQUIRE(addBalance(0, INT64_MAX, Liabilities{0, 0}, 0)); + REQUIRE(!addBalance(0, INT64_MAX, Liabilities{0, 0}, 1)); + + // Below maximum balance, no liabilities + REQUIRE(addBalance(0, INT64_MAX - 1, Liabilities{0, 0}, 1)); + REQUIRE(!addBalance(0, INT64_MAX - 1, Liabilities{0, 0}, 2)); + + // Below maximum balance, with liabilities + REQUIRE(addBalance(0, INT64_MAX - 1, Liabilities{1, 0}, 0)); + REQUIRE(!addBalance(0, INT64_MAX - 1, Liabilities{1, 0}, 1)); + } + }); + } + + SECTION("account add subentries") + { + auto addSubEntries = [&](int64_t initNumSubEntries, int64_t initBalance, + int64_t initSellingLiabilities, + int64_t deltaNumSubEntries) { + AccountEntry ae = LedgerTestUtils::generateValidAccountEntry(); + ae.balance = initBalance; + ae.numSubEntries = initNumSubEntries; + if (ae.ext.v() == 0) + { + ae.ext.v(1); + } + ae.ext.v1().liabilities.selling = initSellingLiabilities; + int64_t initBuyingLiabilities = ae.ext.v1().liabilities.buying; + + LedgerEntry le; + le.data.type(ACCOUNT); + le.data.account() = ae; + + auto af = std::make_shared(le); + bool res = af->addNumEntries(deltaNumSubEntries, lm); + REQUIRE(af->getSellingLiabilities(lm) == initSellingLiabilities); + REQUIRE(af->getBuyingLiabilities(lm) == initBuyingLiabilities); + REQUIRE(af->getBalance() == initBalance); + if (res) + { + if (deltaNumSubEntries > 0) + { + REQUIRE(af->getAvailableBalance(lm) >= 0); + } + } + else + { + REQUIRE(af->getAccount() == ae); + } + return res; + }; + + for_versions_from(10, *app, [&] { + SECTION("can decrease sub entries when below min balance") + { + // Below reserve and below new reserve + REQUIRE(addSubEntries(1, 0, 0, -1)); + REQUIRE(addSubEntries(1, lm.getMinBalance(0) - 1, 0, -1)); + + // Below reserve but at new reserve + REQUIRE(addSubEntries(1, lm.getMinBalance(0), 0, -1)); + + // Below reserve but above new reserve + REQUIRE(addSubEntries(1, lm.getMinBalance(1) - 1, 0, -1)); + REQUIRE(addSubEntries(1, lm.getMinBalance(0) + 1, 0, -1)); + } + + SECTION("cannot add sub entry without sufficient balance") + { + // Below reserve, no liabilities + REQUIRE(!addSubEntries(0, lm.getMinBalance(0) - 1, 0, 1)); + + // At reserve, no liabilities + REQUIRE(!addSubEntries(0, lm.getMinBalance(0), 0, 1)); + + // Above reserve but below new reserve, no liabilities + REQUIRE(!addSubEntries(0, lm.getMinBalance(0) + 1, 0, 1)); + REQUIRE(!addSubEntries(0, lm.getMinBalance(1) - 1, 0, 1)); + + // Above reserve but below new reserve, with liabilities + REQUIRE(!addSubEntries(0, lm.getMinBalance(0) + 2, 1, 1)); + REQUIRE(!addSubEntries(0, lm.getMinBalance(1), 1, 1)); + + // Above reserve but at new reserve, no liabilities + REQUIRE(addSubEntries(0, lm.getMinBalance(1), 0, 1)); + + // Above reserve but at new reserve, with liabilities + REQUIRE(addSubEntries(0, lm.getMinBalance(1) + 1, 1, 1)); + + // Above reserve and above new reserve, no liabilities + REQUIRE(addSubEntries(0, lm.getMinBalance(1) + 1, 0, 1)); + + // Above reserve and above new reserve, with liabilities + REQUIRE(addSubEntries(0, lm.getMinBalance(1) + 1, 1, 1)); + REQUIRE(!addSubEntries(0, lm.getMinBalance(1) + 1, 2, 1)); + } + }); + } + + SECTION("trustline add balance") + { + auto addBalance = [&](int64_t initLimit, int64_t initBalance, + Liabilities initLiabilities, + int64_t deltaBalance) { + TrustLineEntry tl = LedgerTestUtils::generateValidTrustLineEntry(); + tl.balance = initBalance; + tl.limit = initLimit; + tl.flags = AUTHORIZED_FLAG; + tl.ext.v(1); + tl.ext.v1().liabilities = initLiabilities; + + LedgerEntry le; + le.data.type(TRUSTLINE); + le.data.trustLine() = tl; + + auto tf = std::make_shared(le); + bool res = tf->addBalance(deltaBalance, lm); + REQUIRE(tf->getSellingLiabilities(lm) == initLiabilities.selling); + REQUIRE(tf->getBuyingLiabilities(lm) == initLiabilities.buying); + if (res) + { + REQUIRE(tf->getBalance() == initBalance + deltaBalance); + } + else + { + REQUIRE(tf->getBalance() == initBalance); + REQUIRE(tf->getTrustLine() == tl); + } + return res; + }; + + for_versions_from(10, *app, [&] { + SECTION("cannot decrease balance below selling liabilities") + { + // No balance, no liabilities + REQUIRE(addBalance(2, 0, Liabilities{0, 0}, 0)); + REQUIRE(!addBalance(2, 0, Liabilities{0, 0}, -1)); + + // Balance, no liabilities + REQUIRE(addBalance(2, 1, Liabilities{0, 0}, -1)); + REQUIRE(!addBalance(2, 1, Liabilities{0, 0}, -2)); + + // Balance, liabilities + REQUIRE(addBalance(2, 2, Liabilities{0, 1}, -1)); + REQUIRE(!addBalance(2, 2, Liabilities{0, 1}, -2)); + } + + SECTION( + "cannot increase balance above limit minus buying liabilities") + { + // Maximum balance, no liabilities + REQUIRE(addBalance(2, 2, Liabilities{0, 0}, 0)); + REQUIRE(!addBalance(2, 2, Liabilities{0, 0}, 1)); + + // Below maximum balance, no liabilities + REQUIRE(addBalance(2, 1, Liabilities{0, 0}, 1)); + REQUIRE(!addBalance(2, 1, Liabilities{0, 0}, 2)); + + // Below maximum balance, liabilities + REQUIRE(addBalance(3, 1, Liabilities{1, 0}, 1)); + REQUIRE(!addBalance(3, 1, Liabilities{1, 0}, 2)); + } + }); + } +} + +TEST_CASE("available balance and limit", "[ledger][liabilities]") +{ + VirtualClock clock; + auto app = createTestApplication(clock, getTestConfig()); + auto& lm = app->getLedgerManager(); + app->start(); + + SECTION("account available balance") + { + auto checkAvailableBalance = [&](int64_t initNumSubEntries, + int64_t initBalance, + int64_t initSellingLiabilities) { + AccountEntry ae = LedgerTestUtils::generateValidAccountEntry(); + ae.balance = initBalance; + ae.numSubEntries = initNumSubEntries; + if (ae.ext.v() < 1) + { + ae.ext.v(1); + } + ae.ext.v1().liabilities = Liabilities{0, initSellingLiabilities}; + + LedgerEntry le; + le.data.type(ACCOUNT); + le.data.account() = ae; + + auto af = std::make_shared(le); + auto availableBalance = + std::max({int64_t(0), af->getAvailableBalance(lm)}); + REQUIRE(!af->addBalance(-availableBalance - 1, lm)); + REQUIRE(af->addBalance(-availableBalance, lm)); + }; + + for_versions_from(10, *app, [&] { + // Below reserve, no liabilities + checkAvailableBalance(0, 0, 0); + checkAvailableBalance(0, lm.getMinBalance(0) - 1, 0); + + // At reserve, no liabilities + checkAvailableBalance(0, lm.getMinBalance(0), 0); + + // Above reserve, no liabilities + checkAvailableBalance(0, lm.getMinBalance(0) + 1, 0); + checkAvailableBalance(0, INT64_MAX, 0); + + // Above reserve, with maximum liabilities + checkAvailableBalance(0, lm.getMinBalance(0) + 1, 1); + checkAvailableBalance(0, INT64_MAX, + INT64_MAX - lm.getMinBalance(0)); + + // Above reserve, with non-maximum liabilities + checkAvailableBalance(0, lm.getMinBalance(0) + 2, 1); + checkAvailableBalance(0, INT64_MAX, + INT64_MAX - lm.getMinBalance(0) - 1); + }); + } + + SECTION("account available limit") + { + auto checkAvailableLimit = [&](int64_t initNumSubEntries, + int64_t initBalance, + int64_t initBuyingLiabilities) { + AccountEntry ae = LedgerTestUtils::generateValidAccountEntry(); + ae.balance = initBalance; + ae.numSubEntries = initNumSubEntries; + if (ae.ext.v() < 1) + { + ae.ext.v(1); + } + ae.ext.v1().liabilities = Liabilities{initBuyingLiabilities, 0}; + + LedgerEntry le; + le.data.type(ACCOUNT); + le.data.account() = ae; + + auto af = std::make_shared(le); + auto availableLimit = + std::max({int64_t(0), af->getMaxAmountReceive(lm)}); + if (availableLimit < INT64_MAX) + { + REQUIRE(!af->addBalance(availableLimit + 1, lm)); + } + REQUIRE(af->addBalance(availableLimit, lm)); + }; + + for_versions_from(10, *app, [&] { + // Below reserve, no liabilities + checkAvailableLimit(0, 0, 0); + checkAvailableLimit(0, lm.getMinBalance(0) - 1, 0); + + // Below reserve, with maximum liabilities + checkAvailableLimit(0, 0, INT64_MAX); + checkAvailableLimit(0, lm.getMinBalance(0) - 1, + INT64_MAX - lm.getMinBalance(0) + 1); + + // Below reserve, with non-maximum liabilities + checkAvailableLimit(0, 0, INT64_MAX - 1); + checkAvailableLimit(0, lm.getMinBalance(0) - 1, + INT64_MAX - lm.getMinBalance(0)); + + // At reserve, no liabilities + checkAvailableLimit(0, lm.getMinBalance(0), 0); + + // At reserve, with maximum liabilities + checkAvailableLimit(0, lm.getMinBalance(0), + INT64_MAX - lm.getMinBalance(0)); + + // At reserve, with non-maximum liabilities + checkAvailableLimit(0, lm.getMinBalance(0), + INT64_MAX - lm.getMinBalance(0) - 1); + + // Above reserve, no liabilities + checkAvailableLimit(0, lm.getMinBalance(0) + 1, 0); + checkAvailableLimit(0, INT64_MAX, 0); + + // Above reserve, with maximum liabilities + checkAvailableLimit(0, lm.getMinBalance(0) + 1, + INT64_MAX - lm.getMinBalance(0) - 1); + checkAvailableLimit(0, INT64_MAX - 1, 1); + + // Above reserve, with non-maximum liabilities + checkAvailableLimit(0, lm.getMinBalance(0) + 1, + INT64_MAX - lm.getMinBalance(0) - 2); + checkAvailableLimit(0, INT64_MAX - 2, 1); + }); + } + + SECTION("trustline available balance") + { + auto checkAvailableBalance = [&](int64_t initLimit, int64_t initBalance, + int64_t initSellingLiabilities) { + TrustLineEntry tl = LedgerTestUtils::generateValidTrustLineEntry(); + tl.flags = AUTHORIZED_FLAG; + tl.balance = initBalance; + tl.limit = initLimit; + if (tl.ext.v() < 1) + { + tl.ext.v(1); + } + tl.ext.v1().liabilities = Liabilities{0, initSellingLiabilities}; + + LedgerEntry le; + le.data.type(TRUSTLINE); + le.data.trustLine() = tl; + + auto tf = std::make_shared(le); + auto availableBalance = + std::max({int64_t(0), tf->getAvailableBalance(lm)}); + REQUIRE(!tf->addBalance(-availableBalance - 1, lm)); + REQUIRE(tf->addBalance(-availableBalance, lm)); + }; + + for_versions_from(10, *app, [&] { + // No liabilities + checkAvailableBalance(1, 0, 0); + checkAvailableBalance(1, 1, 0); + checkAvailableBalance(INT64_MAX, INT64_MAX, 0); + + // With maximum liabilities + checkAvailableBalance(1, 1, 1); + checkAvailableBalance(2, 2, 2); + checkAvailableBalance(INT64_MAX, INT64_MAX, INT64_MAX); + + // With non-maximum liabilities + checkAvailableBalance(2, 2, 1); + checkAvailableBalance(INT64_MAX, INT64_MAX, 1); + checkAvailableBalance(INT64_MAX, INT64_MAX, INT64_MAX - 1); + }); + } + + SECTION("trustline available limit") + { + auto checkAvailableLimit = [&](int64_t initLimit, int64_t initBalance, + int64_t initBuyingLiabilities) { + TrustLineEntry tl = LedgerTestUtils::generateValidTrustLineEntry(); + tl.flags = AUTHORIZED_FLAG; + tl.balance = initBalance; + tl.limit = initLimit; + if (tl.ext.v() < 1) + { + tl.ext.v(1); + } + tl.ext.v1().liabilities = Liabilities{initBuyingLiabilities, 0}; + + LedgerEntry le; + le.data.type(TRUSTLINE); + le.data.trustLine() = tl; + + auto tf = std::make_shared(le); + auto availableLimit = + std::max({int64_t(0), tf->getMaxAmountReceive(lm)}); + REQUIRE(!tf->addBalance(availableLimit + 1, lm)); + REQUIRE(tf->addBalance(availableLimit, lm)); + }; + + for_versions_from(10, *app, [&] { + // No liabilities + checkAvailableLimit(1, 0, 0); + checkAvailableLimit(1, 1, 0); + checkAvailableLimit(INT64_MAX, INT64_MAX, 0); + + // With maximum liabilities + checkAvailableLimit(1, 1, INT64_MAX - 1); + checkAvailableLimit(INT64_MAX - 1, INT64_MAX - 1, 1); + + // With non-maximum liabilities + checkAvailableLimit(1, 1, 1); + checkAvailableLimit(1, 1, INT64_MAX - 2); + checkAvailableLimit(INT64_MAX - 2, INT64_MAX - 2, 1); + }); + } + + SECTION("trustline minimum limit") + { + auto checkMinimumLimit = [&](int64_t initBalance, + int64_t initBuyingLiabilities) { + TrustLineEntry tl = LedgerTestUtils::generateValidTrustLineEntry(); + tl.flags = AUTHORIZED_FLAG; + tl.balance = initBalance; + tl.limit = INT64_MAX; + if (tl.ext.v() < 1) + { + tl.ext.v(1); + } + tl.ext.v1().liabilities = Liabilities{initBuyingLiabilities, 0}; + + LedgerEntry le; + le.data.type(TRUSTLINE); + le.data.trustLine() = tl; + + auto tf = std::make_shared(le); + auto minimumLimit = tf->getMinimumLimit(lm); + tf->getTrustLine().limit = minimumLimit; + REQUIRE(tf->getMaxAmountReceive(lm) == 0); + }; + + for_versions_from(10, *app, [&] { + // No liabilities + checkMinimumLimit(0, 0); + checkMinimumLimit(1, 0); + checkMinimumLimit(10, 0); + checkMinimumLimit(INT64_MAX, 0); + + // With maximum liabilities + checkMinimumLimit(1, INT64_MAX - 1); + checkMinimumLimit(10, INT64_MAX - 10); + checkMinimumLimit(INT64_MAX - 1, 1); + + // With non-maximum liabilities + checkMinimumLimit(1, 1); + checkMinimumLimit(1, INT64_MAX - 2); + checkMinimumLimit(INT64_MAX - 2, 1); + }); + } +} From 3c9dfc0f57058586d7e4c638921bab7ad5dd6f1c Mon Sep 17 00:00:00 2001 From: Jon Jove Date: Thu, 26 Jul 2018 09:49:53 -0500 Subject: [PATCH 13/13] Added tests for LiabilitiesMatchOffers invariant --- Builds/VisualStudio/stellar-core.vcxproj | 2 +- src/invariant/ConservationOfLumensTests.cpp | 25 - src/invariant/InvariantTestUtils.cpp | 29 ++ src/invariant/InvariantTestUtils.h | 6 + src/invariant/LiabilitiesMatchOffersTests.cpp | 426 ++++++++++++++++++ src/invariant/MinimumAccountBalanceTests.cpp | 151 ------- src/ledger/LedgerTestUtils.cpp | 15 + 7 files changed, 477 insertions(+), 177 deletions(-) create mode 100644 src/invariant/LiabilitiesMatchOffersTests.cpp delete mode 100644 src/invariant/MinimumAccountBalanceTests.cpp diff --git a/Builds/VisualStudio/stellar-core.vcxproj b/Builds/VisualStudio/stellar-core.vcxproj index 186d00709e..befa464489 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj +++ b/Builds/VisualStudio/stellar-core.vcxproj @@ -331,8 +331,8 @@ exit /b 0 - + diff --git a/src/invariant/ConservationOfLumensTests.cpp b/src/invariant/ConservationOfLumensTests.cpp index f5f1649d78..c9a77be491 100644 --- a/src/invariant/ConservationOfLumensTests.cpp +++ b/src/invariant/ConservationOfLumensTests.cpp @@ -96,31 +96,6 @@ updateBalances(std::vector const& entries, Application& app, return updateBalances(entries, app, gen, newTotalCoins - lh.totalCoins); } -std::vector -generateEntryFrames(std::vector const& entries) -{ - std::vector result; - std::transform( - entries.begin(), entries.end(), std::back_inserter(result), - [](LedgerEntry const& le) { return EntryFrame::FromXDR(le); }); - return result; -} - -UpdateList -generateUpdateList(std::vector const& current, - std::vector const& previous) -{ - assert(current.size() == previous.size()); - UpdateList updates; - std::transform( - current.begin(), current.end(), previous.begin(), - std::back_inserter(updates), - [](EntryFrame::pointer const& curr, EntryFrame::pointer const& prev) { - return UpdateList::value_type{curr, prev}; - }); - return updates; -} - TEST_CASE("Total coins change without inflation", "[invariant][conservationoflumens]") { diff --git a/src/invariant/InvariantTestUtils.cpp b/src/invariant/InvariantTestUtils.cpp index ba5580abb2..0d52568db6 100644 --- a/src/invariant/InvariantTestUtils.cpp +++ b/src/invariant/InvariantTestUtils.cpp @@ -25,6 +25,10 @@ generateRandomAccount(uint32_t ledgerSeq) le.data.type(ACCOUNT); le.data.account() = LedgerTestUtils::generateValidAccountEntry(5); le.data.account().balance = 0; + if (le.data.account().ext.v() > 0) + { + le.data.account().ext.v1().liabilities = Liabilities{0, 0}; + } return le; } @@ -86,5 +90,30 @@ makeUpdateList(EntryFrame::pointer left, EntryFrame::pointer right) ul.push_back(std::make_tuple(left, right)); return ul; } + +std::vector +generateEntryFrames(std::vector const& entries) +{ + std::vector result; + std::transform( + entries.begin(), entries.end(), std::back_inserter(result), + [](LedgerEntry const& le) { return EntryFrame::FromXDR(le); }); + return result; +} + +UpdateList +generateUpdateList(std::vector const& current, + std::vector const& previous) +{ + assert(current.size() == previous.size()); + UpdateList updates; + std::transform( + current.begin(), current.end(), previous.begin(), + std::back_inserter(updates), + [](EntryFrame::pointer const& curr, EntryFrame::pointer const& prev) { + return UpdateList::value_type{curr, prev}; + }); + return updates; +} } } diff --git a/src/invariant/InvariantTestUtils.h b/src/invariant/InvariantTestUtils.h index 34f8d5efa1..c690e75770 100644 --- a/src/invariant/InvariantTestUtils.h +++ b/src/invariant/InvariantTestUtils.h @@ -25,5 +25,11 @@ bool store(Application& app, UpdateList const& apply, OperationResult const* resPtr = nullptr); UpdateList makeUpdateList(EntryFrame::pointer left, EntryFrame::pointer right); + +std::vector +generateEntryFrames(std::vector const& entries); + +UpdateList generateUpdateList(std::vector const& current, + std::vector const& previous); } } diff --git a/src/invariant/LiabilitiesMatchOffersTests.cpp b/src/invariant/LiabilitiesMatchOffersTests.cpp new file mode 100644 index 0000000000..f141f022c8 --- /dev/null +++ b/src/invariant/LiabilitiesMatchOffersTests.cpp @@ -0,0 +1,426 @@ +// Copyright 2017 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "invariant/InvariantDoesNotHold.h" +#include "invariant/InvariantManager.h" +#include "invariant/InvariantTestUtils.h" +#include "invariant/LiabilitiesMatchOffers.h" +#include "ledger/AccountFrame.h" +#include "ledger/LedgerTestUtils.h" +#include "lib/catch.hpp" +#include "main/Application.h" +#include "test/TestUtils.h" +#include "test/test.h" +#include + +using namespace stellar; +using namespace stellar::InvariantTestUtils; + +LedgerEntry +updateAccountWithRandomBalance(LedgerEntry le, Application& app, + std::default_random_engine& gen, + bool exceedsMinimum, int32_t direction) +{ + auto& account = le.data.account(); + + auto minBalance = + app.getLedgerManager().getMinBalance(account.numSubEntries); + + int64_t lbound = 0; + int64_t ubound = std::numeric_limits::max(); + if (direction > 0) + { + lbound = account.balance + 1; + } + else if (direction < 0) + { + ubound = account.balance - 1; + } + if (exceedsMinimum) + { + lbound = std::max(lbound, minBalance); + } + else + { + ubound = std::min(ubound, minBalance - 1); + } + REQUIRE(lbound <= ubound); + + std::uniform_int_distribution dist(lbound, ubound); + account.balance = dist(gen); + return le; +} + +TEST_CASE("Create account above minimum balance", + "[invariant][liabilitiesmatchoffers]") +{ + std::default_random_engine gen; + Config cfg = getTestConfig(0); + cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; + + for (uint32_t i = 0; i < 10; ++i) + { + VirtualClock clock; + Application::pointer app = createTestApplication(clock, cfg); + + auto le = generateRandomAccount(2); + le = updateAccountWithRandomBalance(le, *app, gen, true, 0); + REQUIRE(store(*app, makeUpdateList(EntryFrame::FromXDR(le), nullptr))); + } +} + +TEST_CASE("Create account below minimum balance", + "[invariant][liabilitiesmatchoffers]") +{ + std::default_random_engine gen; + Config cfg = getTestConfig(0); + cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; + + for (uint32_t i = 0; i < 10; ++i) + { + VirtualClock clock; + Application::pointer app = createTestApplication(clock, cfg); + + auto le = generateRandomAccount(2); + le = updateAccountWithRandomBalance(le, *app, gen, false, 0); + REQUIRE(!store(*app, makeUpdateList(EntryFrame::FromXDR(le), nullptr))); + } +} + +TEST_CASE("Create account then decrease balance below minimum", + "[invariant][liabilitiesmatchoffers]") +{ + std::default_random_engine gen; + Config cfg = getTestConfig(0); + cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; + + for (uint32_t i = 0; i < 10; ++i) + { + VirtualClock clock; + Application::pointer app = createTestApplication(clock, cfg); + + auto le1 = generateRandomAccount(2); + le1 = updateAccountWithRandomBalance(le1, *app, gen, true, 0); + auto ef = EntryFrame::FromXDR(le1); + REQUIRE(store(*app, makeUpdateList(ef, nullptr))); + auto le2 = updateAccountWithRandomBalance(le1, *app, gen, false, 0); + REQUIRE(!store(*app, makeUpdateList(EntryFrame::FromXDR(le2), ef))); + } +} + +TEST_CASE("Account below minimum balance increases but stays below minimum", + "[invariant][liabilitiesmatchoffers]") +{ + std::default_random_engine gen; + Config cfg = getTestConfig(0); + cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; + + for (uint32_t i = 0; i < 10; ++i) + { + VirtualClock clock; + Application::pointer app = createTestApplication(clock, cfg); + + auto le1 = generateRandomAccount(2); + le1 = updateAccountWithRandomBalance(le1, *app, gen, false, 0); + auto ef = EntryFrame::FromXDR(le1); + REQUIRE(!store(*app, makeUpdateList(ef, nullptr))); + auto le2 = updateAccountWithRandomBalance(le1, *app, gen, false, 1); + REQUIRE(store(*app, makeUpdateList(EntryFrame::FromXDR(le2), ef))); + } +} + +TEST_CASE("Account below minimum balance decreases", + "[invariant][liabilitiesmatchoffers]") +{ + std::default_random_engine gen; + Config cfg = getTestConfig(0); + cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; + + for (uint32_t i = 0; i < 10; ++i) + { + VirtualClock clock; + Application::pointer app = createTestApplication(clock, cfg); + + auto le1 = generateRandomAccount(2); + le1 = updateAccountWithRandomBalance(le1, *app, gen, false, 0); + auto ef = EntryFrame::FromXDR(le1); + REQUIRE(!store(*app, makeUpdateList(ef, nullptr))); + auto le2 = updateAccountWithRandomBalance(le1, *app, gen, false, -1); + REQUIRE(!store(*app, makeUpdateList(EntryFrame::FromXDR(le2), ef))); + } +} + +static LedgerEntry +generateOffer(Asset const& selling, Asset const& buying, int64_t amount, + Price price) +{ + REQUIRE(!(selling == buying)); + REQUIRE(amount >= 1); + + LedgerEntry le; + le.lastModifiedLedgerSeq = 2; + le.data.type(OFFER); + + auto offer = LedgerTestUtils::generateValidOfferEntry(); + offer.amount = amount; + offer.price = price; + offer.selling = selling; + offer.buying = buying; + + le.data.offer() = offer; + return le; +} + +static LedgerEntry +generateSellingLiabilities(LedgerManager& lm, LedgerEntry offer, bool excess, + bool authorized) +{ + auto const& oe = offer.data.offer(); + + LedgerEntry le; + le.lastModifiedLedgerSeq = 2; + + if (oe.selling.type() == ASSET_TYPE_NATIVE) + { + auto account = LedgerTestUtils::generateValidAccountEntry(); + account.accountID = oe.sellerID; + auto minBalance = lm.getMinBalance(account.numSubEntries) + oe.amount; + account.balance = excess ? std::min(account.balance, minBalance - 1) + : std::max(account.balance, minBalance); + + account.ext.v(1); + account.ext.v1().liabilities = Liabilities{0, oe.amount}; + + le.data.type(ACCOUNT); + le.data.account() = account; + } + else + { + auto trust = LedgerTestUtils::generateValidTrustLineEntry(); + trust.accountID = oe.sellerID; + if (authorized) + { + trust.flags |= AUTHORIZED_FLAG; + } + else + { + trust.flags &= ~AUTHORIZED_FLAG; + } + trust.asset = oe.selling; + trust.balance = excess ? std::min(trust.balance, oe.amount - 1) + : std::max(trust.balance, oe.amount); + trust.limit = std::max({trust.balance, trust.limit}); + + trust.ext.v(1); + trust.ext.v1().liabilities = Liabilities{0, oe.amount}; + + le.data.type(TRUSTLINE); + le.data.trustLine() = trust; + } + return le; +} + +static LedgerEntry +generateBuyingLiabilities(LedgerManager& lm, LedgerEntry offer, bool excess, + bool authorized) +{ + auto const& oe = offer.data.offer(); + + LedgerEntry le; + le.lastModifiedLedgerSeq = 2; + + if (oe.buying.type() == ASSET_TYPE_NATIVE) + { + auto account = LedgerTestUtils::generateValidAccountEntry(); + account.accountID = oe.sellerID; + auto maxBalance = INT64_MAX - oe.amount; + account.balance = excess ? std::max(account.balance, maxBalance + 1) + : std::min(account.balance, maxBalance); + + account.ext.v(1); + account.ext.v1().liabilities = Liabilities{oe.amount, 0}; + + le.data.type(ACCOUNT); + le.data.account() = account; + } + else + { + auto trust = LedgerTestUtils::generateValidTrustLineEntry(); + trust.accountID = oe.sellerID; + if (authorized) + { + trust.flags |= AUTHORIZED_FLAG; + } + else + { + trust.flags &= ~AUTHORIZED_FLAG; + } + trust.asset = oe.buying; + + trust.limit = std::max({trust.limit, oe.amount}); + auto maxBalance = trust.limit - oe.amount; + trust.balance = excess ? std::max(trust.balance, maxBalance + 1) + : std::min(trust.balance, maxBalance); + + trust.ext.v(1); + trust.ext.v1().liabilities = Liabilities{oe.amount, 0}; + + le.data.type(TRUSTLINE); + le.data.trustLine() = trust; + } + return le; +} + +TEST_CASE("Invariant for liabilities", "[invariant][liabilitiesmatchoffers]") +{ + std::default_random_engine gen; + Config cfg = getTestConfig(0); + cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; + + VirtualClock clock; + Application::pointer app = createTestApplication(clock, cfg); + LedgerManager& lm = app->getLedgerManager(); + + Asset native; + + Asset cur1; + cur1.type(ASSET_TYPE_CREDIT_ALPHANUM4); + strToAssetCode(cur1.alphaNum4().assetCode, "CUR1"); + + Asset cur2; + cur2.type(ASSET_TYPE_CREDIT_ALPHANUM4); + strToAssetCode(cur2.alphaNum4().assetCode, "CUR2"); + + SECTION("create then modify then delete offer") + { + auto offer = generateOffer(cur1, cur2, 100, Price{1, 1}); + auto selling = generateSellingLiabilities(lm, offer, false, true); + auto buying = generateBuyingLiabilities(lm, offer, false, true); + std::vector entries{offer, selling, buying}; + auto updates = generateUpdateList( + generateEntryFrames(entries), + std::vector(entries.size())); + REQUIRE(store(*app, updates)); + + auto offer2 = generateOffer(cur1, cur2, 200, Price{1, 1}); + offer2.data.offer().sellerID = offer.data.offer().sellerID; + offer2.data.offer().offerID = offer.data.offer().offerID; + auto selling2 = generateSellingLiabilities(lm, offer2, false, true); + auto buying2 = generateBuyingLiabilities(lm, offer2, false, true); + std::vector entries2{offer2, selling2, buying2}; + auto updates2 = generateUpdateList(generateEntryFrames(entries2), + generateEntryFrames(entries)); + REQUIRE(store(*app, updates2)); + + auto updates3 = generateUpdateList( + std::vector(entries2.size()), + generateEntryFrames(entries2)); + REQUIRE(store(*app, updates3)); + } + + SECTION("create offer with excess liabilities") + { + auto verify = [&](bool excessSelling, bool authorizedSelling, + bool excessBuying, bool authorizedBuying) { + auto offer = generateOffer(cur1, cur2, 100, Price{1, 1}); + auto selling = generateSellingLiabilities(lm, offer, excessSelling, + authorizedSelling); + auto buying = generateBuyingLiabilities(lm, offer, excessBuying, + authorizedBuying); + std::vector entries{offer, selling, buying}; + auto updates = generateUpdateList( + generateEntryFrames(entries), + std::vector(entries.size())); + REQUIRE(!store(*app, updates)); + }; + + SECTION("excess selling") + { + verify(true, true, false, true); + } + SECTION("unauthorized selling") + { + verify(false, false, false, true); + } + SECTION("excess buying") + { + verify(false, true, true, true); + } + SECTION("unauthorized buying") + { + verify(false, true, false, false); + } + } + + SECTION("modify offer to have excess liabilities") + { + auto offer = generateOffer(cur1, cur2, 100, Price{1, 1}); + auto selling = generateSellingLiabilities(lm, offer, false, true); + auto buying = generateBuyingLiabilities(lm, offer, false, true); + std::vector entries{offer, selling, buying}; + auto updates = generateUpdateList( + generateEntryFrames(entries), + std::vector(entries.size())); + REQUIRE(store(*app, updates)); + + auto verify = [&](bool excessSelling, bool authorizedSelling, + bool excessBuying, bool authorizedBuying) { + auto offer2 = generateOffer(cur1, cur2, 200, Price{1, 1}); + offer2.data.offer().sellerID = offer.data.offer().sellerID; + offer2.data.offer().offerID = offer.data.offer().offerID; + auto selling2 = generateSellingLiabilities(lm, offer, excessSelling, + authorizedSelling); + auto buying2 = generateBuyingLiabilities(lm, offer, excessBuying, + authorizedBuying); + std::vector entries2{offer2, selling2, buying2}; + auto updates2 = generateUpdateList(generateEntryFrames(entries2), + generateEntryFrames(entries)); + REQUIRE(!store(*app, updates2)); + }; + + SECTION("excess selling") + { + verify(true, true, false, true); + } + SECTION("unauthorized selling") + { + verify(false, false, false, true); + } + SECTION("excess buying") + { + verify(false, true, true, true); + } + SECTION("unauthorized buying") + { + verify(false, true, false, false); + } + } + + SECTION("revoke authorization") + { + auto offer = generateOffer(cur1, cur2, 100, Price{1, 1}); + auto selling = generateSellingLiabilities(lm, offer, false, true); + auto buying = generateBuyingLiabilities(lm, offer, false, true); + std::vector entries{offer, selling, buying}; + auto updates = generateUpdateList( + generateEntryFrames(entries), + std::vector(entries.size())); + REQUIRE(store(*app, updates)); + + SECTION("selling auth") + { + auto efSelling = EntryFrame::FromXDR(selling); + auto selling2 = generateSellingLiabilities(lm, offer, false, false); + REQUIRE(!store(*app, makeUpdateList(EntryFrame::FromXDR(selling2), + efSelling))); + } + SECTION("buying auth") + { + auto efBuying = EntryFrame::FromXDR(buying); + auto buying2 = generateBuyingLiabilities(lm, offer, false, false); + REQUIRE(!store( + *app, makeUpdateList(EntryFrame::FromXDR(buying2), efBuying))); + } + } +} diff --git a/src/invariant/MinimumAccountBalanceTests.cpp b/src/invariant/MinimumAccountBalanceTests.cpp deleted file mode 100644 index 6bf9538e44..0000000000 --- a/src/invariant/MinimumAccountBalanceTests.cpp +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2017 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#include "invariant/InvariantDoesNotHold.h" -#include "invariant/InvariantManager.h" -#include "invariant/InvariantTestUtils.h" -#include "invariant/MinimumAccountBalance.h" -#include "ledger/AccountFrame.h" -#include "lib/catch.hpp" -#include "main/Application.h" -#include "test/TestUtils.h" -#include "test/test.h" -#include - -using namespace stellar; -using namespace stellar::InvariantTestUtils; - -LedgerEntry -updateAccountWithRandomBalance(LedgerEntry le, Application& app, - std::default_random_engine& gen, - bool exceedsMinimum, int32_t direction) -{ - auto& account = le.data.account(); - - auto minBalance = - app.getLedgerManager().getMinBalance(account.numSubEntries); - - int64_t lbound = 0; - int64_t ubound = std::numeric_limits::max(); - if (direction > 0) - { - lbound = account.balance + 1; - } - else if (direction < 0) - { - ubound = account.balance - 1; - } - if (exceedsMinimum) - { - lbound = std::max(lbound, minBalance); - } - else - { - ubound = std::min(ubound, minBalance - 1); - } - REQUIRE(lbound <= ubound); - - std::uniform_int_distribution dist(lbound, ubound); - account.balance = dist(gen); - return le; -} - -TEST_CASE("Create account above minimum balance", - "[invariant][minimumaccountbalance]") -{ - std::default_random_engine gen; - Config cfg = getTestConfig(0); - cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; - - for (uint32_t i = 0; i < 10; ++i) - { - VirtualClock clock; - Application::pointer app = createTestApplication(clock, cfg); - - auto le = generateRandomAccount(2); - le = updateAccountWithRandomBalance(le, *app, gen, true, 0); - REQUIRE(store(*app, makeUpdateList(EntryFrame::FromXDR(le), nullptr))); - } -} - -TEST_CASE("Create account below minimum balance", - "[invariant][minimumaccountbalance]") -{ - std::default_random_engine gen; - Config cfg = getTestConfig(0); - cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; - - for (uint32_t i = 0; i < 10; ++i) - { - VirtualClock clock; - Application::pointer app = createTestApplication(clock, cfg); - - auto le = generateRandomAccount(2); - le = updateAccountWithRandomBalance(le, *app, gen, false, 0); - REQUIRE(!store(*app, makeUpdateList(EntryFrame::FromXDR(le), nullptr))); - } -} - -TEST_CASE("Create account then decrease balance below minimum", - "[invariant][minimumaccountbalance]") -{ - std::default_random_engine gen; - Config cfg = getTestConfig(0); - cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; - - for (uint32_t i = 0; i < 10; ++i) - { - VirtualClock clock; - Application::pointer app = createTestApplication(clock, cfg); - - auto le1 = generateRandomAccount(2); - le1 = updateAccountWithRandomBalance(le1, *app, gen, true, 0); - auto ef = EntryFrame::FromXDR(le1); - REQUIRE(store(*app, makeUpdateList(ef, nullptr))); - auto le2 = updateAccountWithRandomBalance(le1, *app, gen, false, 0); - REQUIRE(!store(*app, makeUpdateList(EntryFrame::FromXDR(le2), ef))); - } -} - -TEST_CASE("Account below minimum balance increases but stays below minimum", - "[invariant][minimumaccountbalance]") -{ - std::default_random_engine gen; - Config cfg = getTestConfig(0); - cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; - - for (uint32_t i = 0; i < 10; ++i) - { - VirtualClock clock; - Application::pointer app = createTestApplication(clock, cfg); - - auto le1 = generateRandomAccount(2); - le1 = updateAccountWithRandomBalance(le1, *app, gen, false, 0); - auto ef = EntryFrame::FromXDR(le1); - REQUIRE(!store(*app, makeUpdateList(ef, nullptr))); - auto le2 = updateAccountWithRandomBalance(le1, *app, gen, false, 1); - REQUIRE(store(*app, makeUpdateList(EntryFrame::FromXDR(le2), ef))); - } -} - -TEST_CASE("Account below minimum balance decreases", - "[invariant][minimumaccountbalance]") -{ - std::default_random_engine gen; - Config cfg = getTestConfig(0); - cfg.INVARIANT_CHECKS = {"MinimumAccountBalance"}; - - for (uint32_t i = 0; i < 10; ++i) - { - VirtualClock clock; - Application::pointer app = createTestApplication(clock, cfg); - - auto le1 = generateRandomAccount(2); - le1 = updateAccountWithRandomBalance(le1, *app, gen, false, 0); - auto ef = EntryFrame::FromXDR(le1); - REQUIRE(!store(*app, makeUpdateList(ef, nullptr))); - auto le2 = updateAccountWithRandomBalance(le1, *app, gen, false, -1); - REQUIRE(!store(*app, makeUpdateList(EntryFrame::FromXDR(le2), ef))); - } -} diff --git a/src/ledger/LedgerTestUtils.cpp b/src/ledger/LedgerTestUtils.cpp index e8b26cc836..d6a8a4d5a8 100644 --- a/src/ledger/LedgerTestUtils.cpp +++ b/src/ledger/LedgerTestUtils.cpp @@ -96,6 +96,13 @@ makeValid(AccountEntry& a) a.seqNum = -a.seqNum; } a.flags = a.flags & MASK_ACCOUNT_FLAGS; + + if (a.ext.v() == 1) + { + a.ext.v1().liabilities.buying = std::abs(a.ext.v1().liabilities.buying); + a.ext.v1().liabilities.selling = + std::abs(a.ext.v1().liabilities.selling); + } } void @@ -111,6 +118,14 @@ makeValid(TrustLineEntry& tl) strToAssetCode(tl.asset.alphaNum4().assetCode, "USD"); clampHigh(tl.limit, tl.balance); tl.flags = tl.flags & MASK_TRUSTLINE_FLAGS; + + if (tl.ext.v() == 1) + { + tl.ext.v1().liabilities.buying = + std::abs(tl.ext.v1().liabilities.buying); + tl.ext.v1().liabilities.selling = + std::abs(tl.ext.v1().liabilities.selling); + } } void makeValid(OfferEntry& o)