diff --git a/Builds/VisualStudio/stellar-core.vcxproj b/Builds/VisualStudio/stellar-core.vcxproj
index 3ced56c337..befa464489 100644
--- a/Builds/VisualStudio/stellar-core.vcxproj
+++ b/Builds/VisualStudio/stellar-core.vcxproj
@@ -331,8 +331,8 @@ exit /b 0
-
-
+
+
@@ -346,6 +346,7 @@ exit /b 0
+
@@ -601,7 +602,7 @@ exit /b 0
-
+
@@ -927,4 +928,4 @@ exit /b 0
-
\ No newline at end of file
+
diff --git a/docs/db-schema.md b/docs/db-schema.md
index b790a75585..d7cf620a8f 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
@@ -92,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
@@ -154,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/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/database/Database.cpp b/src/database/Database.cpp
index 7d1d40b6ed..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"
@@ -53,7 +54,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 +141,23 @@ Database::applySchemaUpgrade(unsigned long vers)
}
}
break;
+
case 6:
mSession << "ALTER TABLE peers ADD flags INT NOT NULL DEFAULT 0";
break;
+
+ case 7:
+ Upgrades::dropAll(*this);
+ mSession << "ALTER TABLE accounts ADD buyingliabilities BIGINT "
+ "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:
throw std::runtime_error("Unknown DB schema version");
break;
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/herder/Upgrades.cpp b/src/herder/Upgrades.cpp
index 4a971cbdf6..7e1436d7b7 100644
--- a/src/herder/Upgrades.cpp
+++ b/src/herder/Upgrades.cpp
@@ -3,9 +3,19 @@
// 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/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
@@ -119,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();
@@ -133,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:
{
@@ -318,4 +330,358 @@ 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");
+}
+
+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 c4c928908c..dc48cdee41 100644
--- a/src/herder/Upgrades.h
+++ b/src/herder/Upgrades.h
@@ -15,6 +15,9 @@
namespace stellar
{
class Config;
+class Database;
+class LedgerDelta;
+class LedgerManager;
struct LedgerHeader;
struct LedgerUpgrade;
@@ -60,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);
@@ -81,9 +85,24 @@ 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;
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/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
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/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/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/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/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/AccountFrame.cpp b/src/ledger/AccountFrame.cpp
index c8cb338e0b..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;
@@ -222,7 +349,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 +359,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 +371,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 +405,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 +569,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 +579,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 +594,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 +616,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")
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/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp
index a8d88907d3..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);
@@ -908,6 +935,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();
}
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)
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);
+ });
+ }
+}
diff --git a/src/ledger/OfferFrame.cpp b/src/ledger/OfferFrame.cpp
index 4aff644e0d..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)
@@ -349,6 +379,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)
{
@@ -554,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 80d8c618ca..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
{
@@ -98,9 +105,31 @@ 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);
+ 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 7c216fae19..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;
}
@@ -237,15 +370,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 +421,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 +443,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 +461,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 +580,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 +598,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 +620,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();
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/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())
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/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/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/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/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/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/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/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/OfferExchange.cpp b/src/transactions/OfferExchange.cpp
index bc6bc05628..78bb5d2fea 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;
}
}
@@ -426,6 +434,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 +497,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);
@@ -606,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);
@@ -658,7 +695,7 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer,
{
if (sheep.type() == ASSET_TYPE_NATIVE)
{
- if (!accountB->addBalance(numSheepSend))
+ if (!accountB->addBalance(numSheepSend, mLedgerManager))
{
return eOfferCantConvert;
}
@@ -666,7 +703,7 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer,
}
else
{
- if (!sheepLineAccountB->addBalance(numSheepSend))
+ if (!sheepLineAccountB->addBalance(numSheepSend, mLedgerManager))
{
return eOfferCantConvert;
}
@@ -678,7 +715,7 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer,
{
if (wheat.type() == ASSET_TYPE_NATIVE)
{
- if (!accountB->addBalance(-numWheatReceived))
+ if (!accountB->addBalance(-numWheatReceived, mLedgerManager))
{
return eOfferCantConvert;
}
@@ -686,7 +723,8 @@ OfferExchange::crossOffer(OfferFrame& sellingWheatOffer,
}
else
{
- if (!wheatLineAccountB->addBalance(-numWheatReceived))
+ if (!wheatLineAccountB->addBalance(-numWheatReceived,
+ mLedgerManager))
{
return eOfferCantConvert;
}
@@ -709,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);
}
@@ -887,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);
+ }
+
+ // 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);
@@ -894,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);
@@ -908,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");
}
@@ -916,7 +965,7 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer,
}
else
{
- if (!sheepLineAccountB->addBalance(numSheepSend))
+ if (!sheepLineAccountB->addBalance(numSheepSend, mLedgerManager))
{
throw std::runtime_error("overflowed sheep balance");
}
@@ -928,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");
}
@@ -936,7 +985,8 @@ OfferExchange::crossOfferV10(OfferFrame& sellingWheatOffer,
}
else
{
- if (!wheatLineAccountB->addBalance(-numWheatReceived))
+ if (!wheatLineAccountB->addBalance(-numWheatReceived,
+ mLedgerManager))
{
throw std::runtime_error("overflowed wheat balance");
}
@@ -964,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 640908167e..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);
@@ -61,6 +64,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,
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/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/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/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/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);
+ });
+ }
+ }
}
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
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)