mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-05 16:57:56 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beba87129e | ||
|
|
b7e902dccc | ||
|
|
9eb30d4316 | ||
|
|
8fdad0d7fd | ||
|
|
0b812cdece | ||
|
|
724a301599 | ||
|
|
71d7d67fa3 |
53
.github/workflows/nix.yml
vendored
53
.github/workflows/nix.yml
vendored
@@ -1,6 +1,25 @@
|
||||
name: nix
|
||||
on: [push, pull_request]
|
||||
|
||||
# This workflow has two job matrixes.
|
||||
# They can be considered phases because the second matrix ("test")
|
||||
# depends on the first ("dependencies").
|
||||
#
|
||||
# The first phase has a job in the matrix for each combination of
|
||||
# variables that affects dependency ABI:
|
||||
# platform, compiler, and configuration.
|
||||
# It creates a GitHub artifact holding the Conan profile,
|
||||
# and builds and caches binaries for all the dependencies.
|
||||
# If an Artifactory remote is configured, they are cached there.
|
||||
# If not, they are added to the GitHub artifact.
|
||||
# GitHub's "cache" action has a size limit (10 GB) that is too small
|
||||
# to hold the binaries if they are built locally.
|
||||
# We must use the "{upload,download}-artifact" actions instead.
|
||||
#
|
||||
# The second phase has a job in the matrix for each test configuration.
|
||||
# It installs dependency binaries from the cache, whichever was used,
|
||||
# and builds and tests rippled.
|
||||
|
||||
jobs:
|
||||
|
||||
dependencies:
|
||||
@@ -31,6 +50,8 @@ jobs:
|
||||
env:
|
||||
build_dir: .build
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: check environment
|
||||
run: |
|
||||
echo ${PATH} | tr ':' '\n'
|
||||
@@ -38,6 +59,8 @@ jobs:
|
||||
cmake --version
|
||||
env
|
||||
- name: configure Conan
|
||||
env:
|
||||
CONAN_URL: http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod
|
||||
run: |
|
||||
conan profile new default --detect
|
||||
conan profile update settings.compiler.cppstd=20 default
|
||||
@@ -47,19 +70,39 @@ jobs:
|
||||
conan profile update env.CC=${{ matrix.profile.cc }} default
|
||||
conan profile update env.CXX=${{ matrix.profile.cxx }} default
|
||||
conan profile update conf.tools.build:compiler_executables='{"c": "${{ matrix.profile.cc }}", "cpp": "${{ matrix.profile.cxx }}"}' default
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: dependencies
|
||||
# Do not quote the URL. An empty string will be accepted (with
|
||||
# a non-fatal warning), but a missing argument will not.
|
||||
conan remote add ripple ${{ env.CONAN_URL }} --insert 0
|
||||
- name: try to authenticate to ripple Conan remote
|
||||
id: remote
|
||||
run: |
|
||||
echo outcome=$(conan user --remote ripple ${{ secrets.CONAN_USERNAME }} --password ${{ secrets.CONAN_TOKEN }} && echo success || echo failure) | tee ${GITHUB_OUTPUT}
|
||||
- name: archive profile
|
||||
# Create this archive before dependencies are added to the local cache.
|
||||
run: tar -czf conan.tar -C ~/.conan .
|
||||
- name: list missing binaries
|
||||
id: binaries
|
||||
# Print the list of dependencies that would need to be built locally.
|
||||
# A non-empty list means we have "failed" to cache binaries remotely.
|
||||
run: |
|
||||
echo missing=$(conan info . --build missing --json 2>/dev/null | grep '^\[') | tee ${GITHUB_OUTPUT}
|
||||
- name: build dependencies
|
||||
if: (steps.binaries.outputs.missing != '[]')
|
||||
uses: ./.github/actions/dependencies
|
||||
with:
|
||||
configuration: ${{ matrix.configuration }}
|
||||
- name: archive cache
|
||||
- name: upload dependencies to remote
|
||||
if: (steps.binaries.outputs.missing != '[]') && (steps.remote.outputs.outcome == 'success')
|
||||
run: conan upload --remote ripple '*' --all --parallel --confirm
|
||||
- name: recreate archive with dependencies
|
||||
if: (steps.binaries.outputs.missing != '[]') && (steps.remote.outputs.outcome == 'failure')
|
||||
run: tar -czf conan.tar -C ~/.conan .
|
||||
- name: upload cache
|
||||
- name: upload archive
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.platform }}-${{ matrix.compiler }}-${{ matrix.configuration }}
|
||||
path: conan.tar
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
test:
|
||||
|
||||
@@ -427,6 +427,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/CancelOffer.cpp
|
||||
src/ripple/app/tx/impl/CashCheck.cpp
|
||||
src/ripple/app/tx/impl/Change.cpp
|
||||
src/ripple/app/tx/impl/Clawback.cpp
|
||||
src/ripple/app/tx/impl/CreateCheck.cpp
|
||||
src/ripple/app/tx/impl/CreateOffer.cpp
|
||||
src/ripple/app/tx/impl/CreateTicket.cpp
|
||||
@@ -692,6 +693,7 @@ if (tests)
|
||||
src/test/app/AccountTxPaging_test.cpp
|
||||
src/test/app/AmendmentTable_test.cpp
|
||||
src/test/app/Check_test.cpp
|
||||
src/test/app/Clawback_test.cpp
|
||||
src/test/app/CrossingLimits_test.cpp
|
||||
src/test/app/DeliverMin_test.cpp
|
||||
src/test/app/DepositAuth_test.cpp
|
||||
@@ -721,6 +723,7 @@ if (tests)
|
||||
src/test/app/PseudoTx_test.cpp
|
||||
src/test/app/RCLCensorshipDetector_test.cpp
|
||||
src/test/app/RCLValidations_test.cpp
|
||||
src/test/app/ReducedOffer_test.cpp
|
||||
src/test/app/Regression_test.cpp
|
||||
src/test/app/SHAMapStore_test.cpp
|
||||
src/test/app/SetAuth_test.cpp
|
||||
|
||||
@@ -602,6 +602,13 @@ public:
|
||||
return *m_networkOPs;
|
||||
}
|
||||
|
||||
virtual ServerHandlerImp&
|
||||
getServerHandler() override
|
||||
{
|
||||
assert(serverHandler_);
|
||||
return *serverHandler_;
|
||||
}
|
||||
|
||||
boost::asio::io_service&
|
||||
getIOService() override
|
||||
{
|
||||
|
||||
@@ -89,6 +89,7 @@ class Overlay;
|
||||
class PathRequests;
|
||||
class PendingSaves;
|
||||
class PublicKey;
|
||||
class ServerHandlerImp;
|
||||
class SecretKey;
|
||||
class STLedgerEntry;
|
||||
class TimeKeeper;
|
||||
@@ -231,6 +232,8 @@ public:
|
||||
getOPs() = 0;
|
||||
virtual OrderBookDB&
|
||||
getOrderBookDB() = 0;
|
||||
virtual ServerHandlerImp&
|
||||
getServerHandler() = 0;
|
||||
virtual TransactionMaster&
|
||||
getMasterTransaction() = 0;
|
||||
virtual perf::PerfLog&
|
||||
|
||||
@@ -429,7 +429,7 @@ GRPCServerImpl::GRPCServerImpl(Application& app)
|
||||
// if present, get endpoint from config
|
||||
if (app_.config().exists("port_grpc"))
|
||||
{
|
||||
Section section = app_.config().section("port_grpc");
|
||||
const auto& section = app_.config().section("port_grpc");
|
||||
|
||||
auto const optIp = section.get("ip");
|
||||
if (!optIp)
|
||||
|
||||
@@ -65,9 +65,11 @@
|
||||
#include <ripple/rpc/BookChanges.h>
|
||||
#include <ripple/rpc/DeliveredAmount.h>
|
||||
#include <ripple/rpc/impl/RPCHelpers.h>
|
||||
#include <ripple/rpc/impl/ServerHandlerImp.h>
|
||||
#include <boost/asio/ip/host_name.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -2661,6 +2663,51 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
|
||||
info["reporting"] = app_.getReportingETL().getInfo();
|
||||
}
|
||||
|
||||
// This array must be sorted in increasing order.
|
||||
static constexpr std::array<std::string_view, 7> protocols{
|
||||
"http", "https", "peer", "ws", "ws2", "wss", "wss2"};
|
||||
static_assert(std::is_sorted(std::begin(protocols), std::end(protocols)));
|
||||
{
|
||||
Json::Value ports{Json::arrayValue};
|
||||
for (auto const& port : app_.getServerHandler().setup().ports)
|
||||
{
|
||||
// Don't publish admin ports for non-admin users
|
||||
if (!admin &&
|
||||
!(port.admin_nets_v4.empty() && port.admin_nets_v6.empty() &&
|
||||
port.admin_user.empty() && port.admin_password.empty()))
|
||||
continue;
|
||||
std::vector<std::string> proto;
|
||||
std::set_intersection(
|
||||
std::begin(port.protocol),
|
||||
std::end(port.protocol),
|
||||
std::begin(protocols),
|
||||
std::end(protocols),
|
||||
std::back_inserter(proto));
|
||||
if (!proto.empty())
|
||||
{
|
||||
auto& jv = ports.append(Json::Value(Json::objectValue));
|
||||
jv[jss::port] = std::to_string(port.port);
|
||||
jv[jss::protocol] = Json::Value{Json::arrayValue};
|
||||
for (auto const& p : proto)
|
||||
jv[jss::protocol].append(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (app_.config().exists("port_grpc"))
|
||||
{
|
||||
auto const& grpcSection = app_.config().section("port_grpc");
|
||||
auto const optPort = grpcSection.get("port");
|
||||
if (optPort && grpcSection.get("ip"))
|
||||
{
|
||||
auto& jv = ports.append(Json::Value(Json::objectValue));
|
||||
jv[jss::port] = *optPort;
|
||||
jv[jss::protocol] = Json::Value{Json::arrayValue};
|
||||
jv[jss::protocol].append("grpc");
|
||||
}
|
||||
}
|
||||
info[jss::ports] = std::move(ports);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
@@ -531,14 +531,22 @@ limitStepOut(
|
||||
TOut& ownerGives,
|
||||
std::uint32_t transferRateIn,
|
||||
std::uint32_t transferRateOut,
|
||||
TOut const& limit)
|
||||
TOut const& limit,
|
||||
Rules const& rules)
|
||||
{
|
||||
if (limit < stpAmt.out)
|
||||
{
|
||||
stpAmt.out = limit;
|
||||
ownerGives = mulRatio(
|
||||
stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false);
|
||||
ofrAmt = ofrQ.ceil_out(ofrAmt, stpAmt.out);
|
||||
if (rules.enabled(fixReducedOffersV1))
|
||||
// It turns out that the ceil_out implementation has some slop in
|
||||
// it. ceil_out_strict removes that slop. But removing that slop
|
||||
// affects transaction outcomes, so the change must be made using
|
||||
// an amendment.
|
||||
ofrAmt = ofrQ.ceil_out_strict(ofrAmt, stpAmt.out, /*roundUp*/ true);
|
||||
else
|
||||
ofrAmt = ofrQ.ceil_out(ofrAmt, stpAmt.out);
|
||||
stpAmt.in =
|
||||
mulRatio(ofrAmt.in, transferRateIn, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
@@ -577,6 +585,7 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
sb, afView, book_, sb.parentCloseTime(), counter, j_);
|
||||
|
||||
bool const flowCross = afView.rules().enabled(featureFlowCross);
|
||||
bool const fixReduced = afView.rules().enabled(fixReducedOffersV1);
|
||||
bool offerAttempted = false;
|
||||
std::optional<Quality> ofrQ;
|
||||
while (offers.step())
|
||||
@@ -654,7 +663,16 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
ownerGives = funds;
|
||||
stpAmt.out = mulRatio(
|
||||
ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
|
||||
ofrAmt = ofrQ->ceil_out(ofrAmt, stpAmt.out);
|
||||
|
||||
// It turns out we can prevent order book blocking by (strictly)
|
||||
// rounding down the ceil_out() result. This adjustment changes
|
||||
// transaction outcomes, so it must be made under an amendment.
|
||||
if (fixReduced)
|
||||
ofrAmt = ofrQ->ceil_out_strict(
|
||||
ofrAmt, stpAmt.out, /* roundUp */ false);
|
||||
else
|
||||
ofrAmt = ofrQ->ceil_out(ofrAmt, stpAmt.out);
|
||||
|
||||
stpAmt.in =
|
||||
mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
}
|
||||
@@ -770,7 +788,8 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
ownerGivesAdj,
|
||||
transferRateIn,
|
||||
transferRateOut,
|
||||
remainingOut);
|
||||
remainingOut,
|
||||
afView.rules());
|
||||
remainingOut = beast::zero;
|
||||
savedIns.insert(stpAdjAmt.in);
|
||||
savedOuts.insert(remainingOut);
|
||||
@@ -922,7 +941,8 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
ownerGivesAdjRev,
|
||||
transferRateIn,
|
||||
transferRateOut,
|
||||
remainingOut);
|
||||
remainingOut,
|
||||
afView.rules());
|
||||
|
||||
if (stpAdjAmtRev.in == remainingIn)
|
||||
{
|
||||
|
||||
138
src/ripple/app/tx/impl/Clawback.cpp
Normal file
138
src/ripple/app/tx/impl/Clawback.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/tx/impl/Clawback.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/Protocol.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
NotTEC
|
||||
Clawback::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureClawback))
|
||||
return temDISABLED;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (ctx.tx.getFlags() & tfClawbackMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
AccountID const issuer = ctx.tx[sfAccount];
|
||||
STAmount const clawAmount = ctx.tx[sfAmount];
|
||||
|
||||
// The issuer field is used for the token holder instead
|
||||
AccountID const& holder = clawAmount.getIssuer();
|
||||
|
||||
if (issuer == holder || isXRP(clawAmount) || clawAmount <= beast::zero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
Clawback::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
AccountID const issuer = ctx.tx[sfAccount];
|
||||
STAmount const clawAmount = ctx.tx[sfAmount];
|
||||
AccountID const& holder = clawAmount.getIssuer();
|
||||
|
||||
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
|
||||
auto const sleHolder = ctx.view.read(keylet::account(holder));
|
||||
if (!sleIssuer || !sleHolder)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
std::uint32_t const issuerFlagsIn = sleIssuer->getFieldU32(sfFlags);
|
||||
|
||||
// If AllowClawback is not set or NoFreeze is set, return no permission
|
||||
if (!(issuerFlagsIn & lsfAllowClawback) || (issuerFlagsIn & lsfNoFreeze))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const sleRippleState =
|
||||
ctx.view.read(keylet::line(holder, issuer, clawAmount.getCurrency()));
|
||||
if (!sleRippleState)
|
||||
return tecNO_LINE;
|
||||
|
||||
STAmount const balance = (*sleRippleState)[sfBalance];
|
||||
|
||||
// If balance is positive, issuer must have higher address than holder
|
||||
if (balance > beast::zero && issuer < holder)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If balance is negative, issuer must have lower address than holder
|
||||
if (balance < beast::zero && issuer > holder)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// At this point, we know that issuer and holder accounts
|
||||
// are correct and a trustline exists between them.
|
||||
//
|
||||
// Must now explicitly check the balance to make sure
|
||||
// available balance is non-zero.
|
||||
//
|
||||
// We can't directly check the balance of trustline because
|
||||
// the available balance of a trustline is prone to new changes (eg.
|
||||
// XLS-34). So we must use `accountHolds`.
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
holder,
|
||||
clawAmount.getCurrency(),
|
||||
issuer,
|
||||
fhIGNORE_FREEZE,
|
||||
ctx.j) <= beast::zero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Clawback::doApply()
|
||||
{
|
||||
AccountID const& issuer = account_;
|
||||
STAmount clawAmount = ctx_.tx[sfAmount];
|
||||
AccountID const holder = clawAmount.getIssuer(); // cannot be reference
|
||||
|
||||
// Replace the `issuer` field with issuer's account
|
||||
clawAmount.setIssuer(issuer);
|
||||
if (holder == issuer)
|
||||
return tecINTERNAL;
|
||||
|
||||
// Get the spendable balance. Must use `accountHolds`.
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
view(),
|
||||
holder,
|
||||
clawAmount.getCurrency(),
|
||||
clawAmount.getIssuer(),
|
||||
fhIGNORE_FREEZE,
|
||||
j_);
|
||||
|
||||
return rippleCredit(
|
||||
view(),
|
||||
holder,
|
||||
issuer,
|
||||
std::min(spendableAmount, clawAmount),
|
||||
true,
|
||||
j_);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
48
src/ripple/app/tx/impl/Clawback.h
Normal file
48
src/ripple/app/tx/impl/Clawback.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_CLAWBACK_H_INCLUDED
|
||||
#define RIPPLE_TX_CLAWBACK_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Clawback : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit Clawback(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -824,8 +824,22 @@ CreateOffer::flowCross(
|
||||
// what is a good threshold to check?
|
||||
afterCross.in.clear();
|
||||
|
||||
afterCross.out = divRound(
|
||||
afterCross.in, rate, takerAmount.out.issue(), true);
|
||||
afterCross.out = [&]() {
|
||||
// Careful analysis showed that rounding up this
|
||||
// divRound result could lead to placing a reduced
|
||||
// offer in the ledger that blocks order books. So
|
||||
// the fixReducedOffersV1 amendment changes the
|
||||
// behavior to round down instead.
|
||||
if (psb.rules().enabled(fixReducedOffersV1))
|
||||
return divRoundStrict(
|
||||
afterCross.in,
|
||||
rate,
|
||||
takerAmount.out.issue(),
|
||||
false);
|
||||
|
||||
return divRound(
|
||||
afterCross.in, rate, takerAmount.out.issue(), true);
|
||||
}();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <ripple/basics/FeeUnits.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/ledger/ReadView.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/STArray.h>
|
||||
#include <ripple/protocol/SystemParameters.h>
|
||||
@@ -717,4 +718,62 @@ NFTokenCountTracking::finalize(
|
||||
return true;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
ValidClawback::visitEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const&)
|
||||
{
|
||||
if (before && before->getType() == ltRIPPLE_STATE)
|
||||
trustlinesChanged++;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidClawback::finalize(
|
||||
STTx const& tx,
|
||||
TER const result,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
if (tx.getTxnType() != ttCLAWBACK)
|
||||
return true;
|
||||
|
||||
if (result == tesSUCCESS)
|
||||
{
|
||||
if (trustlinesChanged > 1)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: more than one trustline changed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
AccountID const issuer = tx.getAccountID(sfAccount);
|
||||
STAmount const amount = tx.getFieldAmount(sfAmount);
|
||||
AccountID const& holder = amount.getIssuer();
|
||||
STAmount const holderBalance = accountHolds(
|
||||
view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);
|
||||
|
||||
if (holderBalance.signum() < 0)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: trustline balance is negative";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (trustlinesChanged != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
|
||||
"despite failure of the transaction.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -389,6 +389,34 @@ public:
|
||||
beast::Journal const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariant: Token holder's trustline balance cannot be negative after
|
||||
* Clawback.
|
||||
*
|
||||
* We iterate all the trust lines affected by this transaction and ensure
|
||||
* that no more than one trustline is modified, and also holder's balance is
|
||||
* non-negative.
|
||||
*/
|
||||
class ValidClawback
|
||||
{
|
||||
std::uint32_t trustlinesChanged = 0;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(
|
||||
STTx const&,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const&,
|
||||
beast::Journal const&);
|
||||
};
|
||||
|
||||
// additional invariant checks can be declared above and then added to this
|
||||
// tuple
|
||||
using InvariantChecks = std::tuple<
|
||||
@@ -402,7 +430,8 @@ using InvariantChecks = std::tuple<
|
||||
NoZeroEscrow,
|
||||
ValidNewAccountRoot,
|
||||
ValidNFTokenPage,
|
||||
NFTokenCountTracking>;
|
||||
NFTokenCountTracking,
|
||||
ValidClawback>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
@@ -182,17 +182,33 @@ TOfferStreamBase<TIn, TOut>::shouldRmSmallIncreasedQOffer() const
|
||||
}
|
||||
|
||||
TTakerGets const ownerFunds = toAmount<TTakerGets>(*ownerFunds_);
|
||||
bool const fixReduced = view_.rules().enabled(fixReducedOffersV1);
|
||||
|
||||
auto const effectiveAmounts = [&] {
|
||||
if (offer_.owner() != offer_.issueOut().account &&
|
||||
ownerFunds < ofrAmts.out)
|
||||
{
|
||||
// adjust the amounts by owner funds
|
||||
// adjust the amounts by owner funds.
|
||||
//
|
||||
// It turns out we can prevent order book blocking by rounding down
|
||||
// the ceil_out() result. This adjustment changes transaction
|
||||
// results, so it must be made under an amendment.
|
||||
if (fixReduced)
|
||||
return offer_.quality().ceil_out_strict(
|
||||
ofrAmts, ownerFunds, /* roundUp */ false);
|
||||
|
||||
return offer_.quality().ceil_out(ofrAmts, ownerFunds);
|
||||
}
|
||||
return ofrAmts;
|
||||
}();
|
||||
|
||||
// If either the effective in or out are zero then remove the offer.
|
||||
// This can happen with fixReducedOffersV1 since it rounds down.
|
||||
if (fixReduced &&
|
||||
(effectiveAmounts.in.signum() <= 0 ||
|
||||
effectiveAmounts.out.signum() <= 0))
|
||||
return true;
|
||||
|
||||
if (effectiveAmounts.in > TTakerPays::minPositiveAmount())
|
||||
return false;
|
||||
|
||||
|
||||
@@ -218,6 +218,37 @@ SetAccount::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Clawback
|
||||
//
|
||||
if (ctx.view.rules().enabled(featureClawback))
|
||||
{
|
||||
if (uSetFlag == asfAllowClawback)
|
||||
{
|
||||
if (uFlagsIn & lsfNoFreeze)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "Can't set Clawback if NoFreeze is set";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (!dirIsEmpty(ctx.view, keylet::ownerDir(id)))
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "Owner directory not empty.";
|
||||
return tecOWNERS;
|
||||
}
|
||||
}
|
||||
else if (uSetFlag == asfNoFreeze)
|
||||
{
|
||||
// Cannot set NoFreeze if clawback is enabled
|
||||
if (uFlagsIn & lsfAllowClawback)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "Can't set NoFreeze if clawback is enabled";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -562,6 +593,14 @@ SetAccount::doApply()
|
||||
uFlagsOut &= ~lsfDisallowIncomingTrustline;
|
||||
}
|
||||
|
||||
// Set flag for clawback
|
||||
if (ctx_.view().rules().enabled(featureClawback) &&
|
||||
uSetFlag == asfAllowClawback)
|
||||
{
|
||||
JLOG(j_.trace()) << "set allow clawback";
|
||||
uFlagsOut |= lsfAllowClawback;
|
||||
}
|
||||
|
||||
if (uFlagsIn != uFlagsOut)
|
||||
sle->setFieldU32(sfFlags, uFlagsOut);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <ripple/app/tx/impl/CancelOffer.h>
|
||||
#include <ripple/app/tx/impl/CashCheck.h>
|
||||
#include <ripple/app/tx/impl/Change.h>
|
||||
#include <ripple/app/tx/impl/Clawback.h>
|
||||
#include <ripple/app/tx/impl/CreateCheck.h>
|
||||
#include <ripple/app/tx/impl/CreateOffer.h>
|
||||
#include <ripple/app/tx/impl/CreateTicket.h>
|
||||
@@ -147,6 +148,8 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
return invoke_preflight_helper<NFTokenCancelOffer>(ctx);
|
||||
case ttNFTOKEN_ACCEPT_OFFER:
|
||||
return invoke_preflight_helper<NFTokenAcceptOffer>(ctx);
|
||||
case ttCLAWBACK:
|
||||
return invoke_preflight_helper<Clawback>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
|
||||
@@ -248,6 +251,8 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
return invoke_preclaim<NFTokenCancelOffer>(ctx);
|
||||
case ttNFTOKEN_ACCEPT_OFFER:
|
||||
return invoke_preclaim<NFTokenAcceptOffer>(ctx);
|
||||
case ttCLAWBACK:
|
||||
return invoke_preclaim<Clawback>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return temUNKNOWN;
|
||||
@@ -311,6 +316,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
return NFTokenCancelOffer::calculateBaseFee(view, tx);
|
||||
case ttNFTOKEN_ACCEPT_OFFER:
|
||||
return NFTokenAcceptOffer::calculateBaseFee(view, tx);
|
||||
case ttCLAWBACK:
|
||||
return Clawback::calculateBaseFee(view, tx);
|
||||
default:
|
||||
assert(false);
|
||||
return XRPAmount{0};
|
||||
@@ -463,6 +470,10 @@ invoke_apply(ApplyContext& ctx)
|
||||
NFTokenAcceptOffer p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttCLAWBACK: {
|
||||
Clawback p(ctx);
|
||||
return p();
|
||||
}
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, false};
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 58;
|
||||
static constexpr std::size_t numFeatures = 60;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -345,6 +345,8 @@ extern uint256 const featureXRPFees;
|
||||
extern uint256 const fixUniversalNumber;
|
||||
extern uint256 const fixNonFungibleTokensV1_2;
|
||||
extern uint256 const fixNFTokenRemint;
|
||||
extern uint256 const fixReducedOffersV1;
|
||||
extern uint256 const featureClawback;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -243,6 +243,9 @@ enum LedgerSpecificFlags {
|
||||
0x10000000, // True, reject new paychans
|
||||
lsfDisallowIncomingTrustline =
|
||||
0x20000000, // True, reject new trustlines (only if no issued assets)
|
||||
lsfAMM [[maybe_unused]] = 0x40000000, // True, AMM account
|
||||
lsfAllowClawback =
|
||||
0x80000000, // True, enable clawback
|
||||
|
||||
// ltOFFER
|
||||
lsfPassive = 0x00010000,
|
||||
|
||||
@@ -223,6 +223,29 @@ public:
|
||||
toAmount<In>(stRes.in), toAmount<Out>(stRes.out));
|
||||
}
|
||||
|
||||
Amounts
|
||||
ceil_out_strict(Amounts const& amount, STAmount const& limit, bool roundUp)
|
||||
const;
|
||||
|
||||
template <class In, class Out>
|
||||
TAmounts<In, Out>
|
||||
ceil_out_strict(
|
||||
TAmounts<In, Out> const& amount,
|
||||
Out const& limit,
|
||||
bool roundUp) const
|
||||
{
|
||||
if (amount.out <= limit)
|
||||
return amount;
|
||||
|
||||
// Use the existing STAmount implementation for now, but consider
|
||||
// replacing with code specific to IOUAMount and XRPAmount
|
||||
Amounts stAmt(toSTAmount(amount.in), toSTAmount(amount.out));
|
||||
STAmount stLim(toSTAmount(limit));
|
||||
auto const stRes = ceil_out_strict(stAmt, stLim, roundUp);
|
||||
return TAmounts<In, Out>(
|
||||
toAmount<In>(stRes.in), toAmount<Out>(stRes.out));
|
||||
}
|
||||
|
||||
/** Returns `true` if lhs is lower quality than `rhs`.
|
||||
Lower quality means the taker receives a worse deal.
|
||||
Higher quality is better for the taker.
|
||||
|
||||
@@ -273,13 +273,7 @@ struct TypedField : SField
|
||||
using type = T;
|
||||
|
||||
template <class... Args>
|
||||
explicit TypedField(Args&&... args) : SField(std::forward<Args>(args)...)
|
||||
{
|
||||
}
|
||||
|
||||
TypedField(TypedField&& u) : SField(std::move(u))
|
||||
{
|
||||
}
|
||||
explicit TypedField(private_access_tag_t pat, Args&&... args);
|
||||
};
|
||||
|
||||
/** Indicate std::optional field semantics. */
|
||||
|
||||
@@ -503,7 +503,7 @@ divide(STAmount const& v1, STAmount const& v2, Issue const& issue);
|
||||
STAmount
|
||||
multiply(STAmount const& v1, STAmount const& v2, Issue const& issue);
|
||||
|
||||
// multiply, or divide rounding result in specified direction
|
||||
// multiply rounding result in specified direction
|
||||
STAmount
|
||||
mulRound(
|
||||
STAmount const& v1,
|
||||
@@ -511,6 +511,15 @@ mulRound(
|
||||
Issue const& issue,
|
||||
bool roundUp);
|
||||
|
||||
// multiply following the rounding directions more precisely.
|
||||
STAmount
|
||||
mulRoundStrict(
|
||||
STAmount const& v1,
|
||||
STAmount const& v2,
|
||||
Issue const& issue,
|
||||
bool roundUp);
|
||||
|
||||
// divide rounding result in specified direction
|
||||
STAmount
|
||||
divRound(
|
||||
STAmount const& v1,
|
||||
@@ -518,6 +527,14 @@ divRound(
|
||||
Issue const& issue,
|
||||
bool roundUp);
|
||||
|
||||
// divide following the rounding directions more precisely.
|
||||
STAmount
|
||||
divRoundStrict(
|
||||
STAmount const& v1,
|
||||
STAmount const& v2,
|
||||
Issue const& issue,
|
||||
bool roundUp);
|
||||
|
||||
// Someone is offering X for Y, what is the rate?
|
||||
// Rate: smaller is better, the taker wants the most out: in/out
|
||||
// VFALCO TODO Return a Quality object
|
||||
|
||||
@@ -88,6 +88,7 @@ constexpr std::uint32_t asfDisallowIncomingNFTokenOffer = 12;
|
||||
constexpr std::uint32_t asfDisallowIncomingCheck = 13;
|
||||
constexpr std::uint32_t asfDisallowIncomingPayChan = 14;
|
||||
constexpr std::uint32_t asfDisallowIncomingTrustline = 15;
|
||||
constexpr std::uint32_t asfAllowClawback = 16;
|
||||
|
||||
// OfferCreate flags:
|
||||
constexpr std::uint32_t tfPassive = 0x00010000;
|
||||
@@ -159,6 +160,9 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal);
|
||||
// NFTokenAcceptOffer flags:
|
||||
constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal;
|
||||
|
||||
// Clawback flags:
|
||||
constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
|
||||
|
||||
// clang-format on
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -139,6 +139,9 @@ enum TxType : std::uint16_t
|
||||
/** This transaction accepts an existing offer to buy or sell an existing NFT. */
|
||||
ttNFTOKEN_ACCEPT_OFFER = 29,
|
||||
|
||||
/** This transaction claws back issued tokens. */
|
||||
ttCLAWBACK = 30,
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace BuildInfo {
|
||||
// and follow the format described at http://semver.org/
|
||||
//------------------------------------------------------------------------------
|
||||
// clang-format off
|
||||
char const* const versionString = "1.11.0"
|
||||
char const* const versionString = "1.12.0-b1"
|
||||
// clang-format on
|
||||
|
||||
#if defined(DEBUG) || defined(SANITIZER)
|
||||
|
||||
@@ -452,6 +452,8 @@ REGISTER_FEATURE(XRPFees, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX (fixUniversalNumber, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixNonFungibleTokensV1_2, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixNFTokenRemint, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
// because they could potentially get enabled.
|
||||
|
||||
@@ -81,12 +81,20 @@ Quality::ceil_in(Amounts const& amount, STAmount const& limit) const
|
||||
return amount;
|
||||
}
|
||||
|
||||
Amounts
|
||||
Quality::ceil_out(Amounts const& amount, STAmount const& limit) const
|
||||
template <STAmount (
|
||||
*MulRoundFunc)(STAmount const&, STAmount const&, Issue const&, bool)>
|
||||
static Amounts
|
||||
ceil_out_impl(
|
||||
Amounts const& amount,
|
||||
STAmount const& limit,
|
||||
bool roundUp,
|
||||
Quality const& quality)
|
||||
{
|
||||
if (amount.out > limit)
|
||||
{
|
||||
Amounts result(mulRound(limit, rate(), amount.in.issue(), true), limit);
|
||||
Amounts result(
|
||||
MulRoundFunc(limit, quality.rate(), amount.in.issue(), roundUp),
|
||||
limit);
|
||||
// Clamp in
|
||||
if (result.in > amount.in)
|
||||
result.in = amount.in;
|
||||
@@ -97,6 +105,21 @@ Quality::ceil_out(Amounts const& amount, STAmount const& limit) const
|
||||
return amount;
|
||||
}
|
||||
|
||||
Amounts
|
||||
Quality::ceil_out(Amounts const& amount, STAmount const& limit) const
|
||||
{
|
||||
return ceil_out_impl<mulRound>(amount, limit, /* roundUp */ true, *this);
|
||||
}
|
||||
|
||||
Amounts
|
||||
Quality::ceil_out_strict(
|
||||
Amounts const& amount,
|
||||
STAmount const& limit,
|
||||
bool roundUp) const
|
||||
{
|
||||
return ceil_out_impl<mulRoundStrict>(amount, limit, roundUp, *this);
|
||||
}
|
||||
|
||||
Quality
|
||||
composed_quality(Quality const& lhs, Quality const& rhs)
|
||||
{
|
||||
|
||||
@@ -38,6 +38,13 @@ struct SField::private_access_tag_t
|
||||
|
||||
static SField::private_access_tag_t access;
|
||||
|
||||
template <class T>
|
||||
template <class... Args>
|
||||
TypedField<T>::TypedField(private_access_tag_t pat, Args&&... args)
|
||||
: SField(pat, std::forward<Args>(args)...)
|
||||
{
|
||||
}
|
||||
|
||||
// Construct all compile-time SFields, and register them in the knownCodeToField
|
||||
// database:
|
||||
|
||||
|
||||
@@ -1266,8 +1266,28 @@ multiply(STAmount const& v1, STAmount const& v2, Issue const& issue)
|
||||
v1.negative() != v2.negative());
|
||||
}
|
||||
|
||||
// This is the legacy version of canonicalizeRound. It's been in use
|
||||
// for years, so it is deeply embedded in the behavior of cross-currency
|
||||
// transactions.
|
||||
//
|
||||
// However in 2022 it was noticed that the rounding characteristics were
|
||||
// surprising. When the code converts from IOU-like to XRP-like there may
|
||||
// be a fraction of the IOU-like representation that is too small to be
|
||||
// represented in drops. `canonicalizeRound()` currently does some unusual
|
||||
// rounding.
|
||||
//
|
||||
// 1. If the fractional part is greater than or equal to 0.1, then the
|
||||
// number of drops is rounded up.
|
||||
//
|
||||
// 2. However, if the fractional part is less than 0.1 (for example,
|
||||
// 0.099999), then the number of drops is rounded down.
|
||||
//
|
||||
// The XRP Ledger has this rounding behavior baked in. But there are
|
||||
// situations where this rounding behavior led to undesirable outcomes.
|
||||
// So an alternative rounding approach was introduced. You'll see that
|
||||
// alternative below.
|
||||
static void
|
||||
canonicalizeRound(bool native, std::uint64_t& value, int& offset)
|
||||
canonicalizeRound(bool native, std::uint64_t& value, int& offset, bool)
|
||||
{
|
||||
if (native)
|
||||
{
|
||||
@@ -1301,8 +1321,100 @@ canonicalizeRound(bool native, std::uint64_t& value, int& offset)
|
||||
}
|
||||
}
|
||||
|
||||
STAmount
|
||||
mulRound(
|
||||
// The original canonicalizeRound did not allow the rounding direction to
|
||||
// be specified. It also ignored some of the bits that could contribute to
|
||||
// rounding decisions. canonicalizeRoundStrict() tracks all of the bits in
|
||||
// the value being rounded.
|
||||
static void
|
||||
canonicalizeRoundStrict(
|
||||
bool native,
|
||||
std::uint64_t& value,
|
||||
int& offset,
|
||||
bool roundUp)
|
||||
{
|
||||
if (native)
|
||||
{
|
||||
if (offset < 0)
|
||||
{
|
||||
bool hadRemainder = false;
|
||||
|
||||
while (offset < -1)
|
||||
{
|
||||
// It would be better to use std::lldiv than to separately
|
||||
// compute the remainder. But std::lldiv does not support
|
||||
// unsigned arguments.
|
||||
std::uint64_t const newValue = value / 10;
|
||||
hadRemainder |= (value != (newValue * 10));
|
||||
value = newValue;
|
||||
++offset;
|
||||
}
|
||||
value +=
|
||||
(hadRemainder && roundUp) ? 10 : 9; // Add before last divide
|
||||
value /= 10;
|
||||
++offset;
|
||||
}
|
||||
}
|
||||
else if (value > STAmount::cMaxValue)
|
||||
{
|
||||
while (value > (10 * STAmount::cMaxValue))
|
||||
{
|
||||
value /= 10;
|
||||
++offset;
|
||||
}
|
||||
value += 9; // add before last divide
|
||||
value /= 10;
|
||||
++offset;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// saveNumberRoundMode doesn't do quite enough for us. What we want is a
|
||||
// Number::RoundModeGuard that sets the new mode and restores the old mode
|
||||
// when it leaves scope. Since Number doesn't have that facility, we'll
|
||||
// build it here.
|
||||
class NumberRoundModeGuard
|
||||
{
|
||||
saveNumberRoundMode saved_;
|
||||
|
||||
public:
|
||||
explicit NumberRoundModeGuard(Number::rounding_mode mode) noexcept
|
||||
: saved_{Number::setround(mode)}
|
||||
{
|
||||
}
|
||||
|
||||
NumberRoundModeGuard(NumberRoundModeGuard const&) = delete;
|
||||
|
||||
NumberRoundModeGuard&
|
||||
operator=(NumberRoundModeGuard const&) = delete;
|
||||
};
|
||||
|
||||
// We need a class that has an interface similar to NumberRoundModeGuard
|
||||
// but does nothing.
|
||||
class DontAffectNumberRoundMode
|
||||
{
|
||||
public:
|
||||
explicit DontAffectNumberRoundMode(Number::rounding_mode mode) noexcept
|
||||
{
|
||||
}
|
||||
|
||||
DontAffectNumberRoundMode(DontAffectNumberRoundMode const&) = delete;
|
||||
|
||||
DontAffectNumberRoundMode&
|
||||
operator=(DontAffectNumberRoundMode const&) = delete;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// Pass the canonicalizeRound function pointer as a template parameter.
|
||||
//
|
||||
// We might need to use NumberRoundModeGuard. Allow the caller
|
||||
// to pass either that or a replacement as a template parameter.
|
||||
template <
|
||||
void (*CanonicalizeFunc)(bool, std::uint64_t&, int&, bool),
|
||||
typename MightSaveRound>
|
||||
static STAmount
|
||||
mulRoundImpl(
|
||||
STAmount const& v1,
|
||||
STAmount const& v2,
|
||||
Issue const& issue,
|
||||
@@ -1365,8 +1477,15 @@ mulRound(
|
||||
|
||||
int offset = offset1 + offset2 + 14;
|
||||
if (resultNegative != roundUp)
|
||||
canonicalizeRound(xrp, amount, offset);
|
||||
STAmount result(issue, amount, offset, resultNegative);
|
||||
{
|
||||
CanonicalizeFunc(xrp, amount, offset, roundUp);
|
||||
}
|
||||
STAmount result = [&]() {
|
||||
// If appropriate, tell Number to round down. This gives the desired
|
||||
// result from STAmount::canonicalize.
|
||||
MightSaveRound const savedRound(Number::towards_zero);
|
||||
return STAmount(issue, amount, offset, resultNegative);
|
||||
}();
|
||||
|
||||
if (roundUp && !resultNegative && !result)
|
||||
{
|
||||
@@ -1388,7 +1507,32 @@ mulRound(
|
||||
}
|
||||
|
||||
STAmount
|
||||
divRound(
|
||||
mulRound(
|
||||
STAmount const& v1,
|
||||
STAmount const& v2,
|
||||
Issue const& issue,
|
||||
bool roundUp)
|
||||
{
|
||||
return mulRoundImpl<canonicalizeRound, DontAffectNumberRoundMode>(
|
||||
v1, v2, issue, roundUp);
|
||||
}
|
||||
|
||||
STAmount
|
||||
mulRoundStrict(
|
||||
STAmount const& v1,
|
||||
STAmount const& v2,
|
||||
Issue const& issue,
|
||||
bool roundUp)
|
||||
{
|
||||
return mulRoundImpl<canonicalizeRoundStrict, NumberRoundModeGuard>(
|
||||
v1, v2, issue, roundUp);
|
||||
}
|
||||
|
||||
// We might need to use NumberRoundModeGuard. Allow the caller
|
||||
// to pass either that or a replacement as a template parameter.
|
||||
template <typename MightSaveRound>
|
||||
static STAmount
|
||||
divRoundImpl(
|
||||
STAmount const& num,
|
||||
STAmount const& den,
|
||||
Issue const& issue,
|
||||
@@ -1437,9 +1581,18 @@ divRound(
|
||||
int offset = numOffset - denOffset - 17;
|
||||
|
||||
if (resultNegative != roundUp)
|
||||
canonicalizeRound(isXRP(issue), amount, offset);
|
||||
canonicalizeRound(isXRP(issue), amount, offset, roundUp);
|
||||
|
||||
STAmount result = [&]() {
|
||||
// If appropriate, tell Number the rounding mode we are using.
|
||||
// Note that "roundUp == true" actually means "round away from zero".
|
||||
// Otherwise round toward zero.
|
||||
using enum Number::rounding_mode;
|
||||
MightSaveRound const savedRound(
|
||||
roundUp ^ resultNegative ? upward : downward);
|
||||
return STAmount(issue, amount, offset, resultNegative);
|
||||
}();
|
||||
|
||||
STAmount result(issue, amount, offset, resultNegative);
|
||||
if (roundUp && !resultNegative && !result)
|
||||
{
|
||||
if (isXRP(issue))
|
||||
@@ -1459,4 +1612,24 @@ divRound(
|
||||
return result;
|
||||
}
|
||||
|
||||
STAmount
|
||||
divRound(
|
||||
STAmount const& num,
|
||||
STAmount const& den,
|
||||
Issue const& issue,
|
||||
bool roundUp)
|
||||
{
|
||||
return divRoundImpl<DontAffectNumberRoundMode>(num, den, issue, roundUp);
|
||||
}
|
||||
|
||||
STAmount
|
||||
divRoundStrict(
|
||||
STAmount const& num,
|
||||
STAmount const& den,
|
||||
Issue const& issue,
|
||||
bool roundUp)
|
||||
{
|
||||
return divRoundImpl<NumberRoundModeGuard>(num, den, issue, roundUp);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -328,6 +328,14 @@ TxFormats::TxFormats()
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Clawback,
|
||||
ttCLAWBACK,
|
||||
{
|
||||
{sfAmount, soeREQUIRED},
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -53,6 +53,7 @@ JSS(Check); // ledger type.
|
||||
JSS(CheckCancel); // transaction type.
|
||||
JSS(CheckCash); // transaction type.
|
||||
JSS(CheckCreate); // transaction type.
|
||||
JSS(Clawback); // transaction type.
|
||||
JSS(ClearFlag); // field.
|
||||
JSS(DeliverMin); // in: TransactionSign
|
||||
JSS(DepositPreauth); // transaction and ledger type.
|
||||
@@ -465,13 +466,14 @@ JSS(peers); // out: InboundLedger, handlers/Peers, Overlay
|
||||
JSS(peer_disconnects); // Severed peer connection counter.
|
||||
JSS(peer_disconnects_resources); // Severed peer connections because of
|
||||
// excess resource consumption.
|
||||
JSS(port); // in: Connect
|
||||
JSS(port); // in: Connect, out: NetworkOPs
|
||||
JSS(ports); // out: NetworkOPs
|
||||
JSS(previous); // out: Reservations
|
||||
JSS(previous_ledger); // out: LedgerPropose
|
||||
JSS(proof); // in: BookOffers
|
||||
JSS(propose_seq); // out: LedgerPropose
|
||||
JSS(proposers); // out: NetworkOPs, LedgerConsensus
|
||||
JSS(protocol); // out: PeerImp
|
||||
JSS(protocol); // out: NetworkOPs, PeerImp
|
||||
JSS(proxied); // out: RPC ping
|
||||
JSS(pubkey_node); // out: NetworkOPs
|
||||
JSS(pubkey_publisher); // out: ValidatorList
|
||||
|
||||
971
src/test/app/Clawback_test.cpp
Normal file
971
src/test/app/Clawback_test.cpp
Normal file
@@ -0,0 +1,971 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/basics/random.h>
|
||||
#include <ripple/json/to_string.h>
|
||||
#include <ripple/ledger/ApplyViewImpl.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <initializer_list>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/trust.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Clawback_test : public beast::unit_test::suite
|
||||
{
|
||||
template <class T>
|
||||
static std::string
|
||||
to_string(T const& t)
|
||||
{
|
||||
return boost::lexical_cast<std::string>(t);
|
||||
}
|
||||
|
||||
// Helper function that returns the owner count of an account root.
|
||||
static std::uint32_t
|
||||
ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct)
|
||||
{
|
||||
std::uint32_t ret{0};
|
||||
if (auto const sleAcct = env.le(acct))
|
||||
ret = sleAcct->at(sfOwnerCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Helper function that returns the number of tickets held by an account.
|
||||
static std::uint32_t
|
||||
ticketCount(test::jtx::Env const& env, test::jtx::Account const& acct)
|
||||
{
|
||||
std::uint32_t ret{0};
|
||||
if (auto const sleAcct = env.le(acct))
|
||||
ret = sleAcct->at(~sfTicketCount).value_or(0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Helper function that returns the freeze status of a trustline
|
||||
static bool
|
||||
getLineFreezeFlag(
|
||||
test::jtx::Env const& env,
|
||||
test::jtx::Account const& src,
|
||||
test::jtx::Account const& dst,
|
||||
Currency const& cur)
|
||||
{
|
||||
if (auto sle = env.le(keylet::line(src, dst, cur)))
|
||||
{
|
||||
auto const useHigh = src.id() > dst.id();
|
||||
return sle->isFlag(useHigh ? lsfHighFreeze : lsfLowFreeze);
|
||||
}
|
||||
Throw<std::runtime_error>("No line in getLineFreezeFlag");
|
||||
return false; // silence warning
|
||||
}
|
||||
|
||||
void
|
||||
testAllowClawbackFlag(FeatureBitset features)
|
||||
{
|
||||
testcase("Enable AllowClawback flag");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Test that one can successfully set asfAllowClawback flag.
|
||||
// If successful, asfNoFreeze can no longer be set.
|
||||
// Also, asfAllowClawback cannot be cleared.
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
// set asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// clear asfAllowClawback does nothing
|
||||
env(fclear(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// asfNoFreeze cannot be set when asfAllowClawback is set
|
||||
env.require(nflags(alice, asfNoFreeze));
|
||||
env(fset(alice, asfNoFreeze), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test that asfAllowClawback cannot be set when
|
||||
// asfNoFreeze has been set
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
env.require(nflags(alice, asfNoFreeze));
|
||||
|
||||
// set asfNoFreeze
|
||||
env(fset(alice, asfNoFreeze));
|
||||
env.close();
|
||||
|
||||
// NoFreeze is set
|
||||
env.require(flags(alice, asfNoFreeze));
|
||||
|
||||
// asfAllowClawback cannot be set if asfNoFreeze is set
|
||||
env(fset(alice, asfAllowClawback), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
env.require(nflags(alice, asfAllowClawback));
|
||||
}
|
||||
|
||||
// Test that asfAllowClawback is not allowed when owner dir is non-empty
|
||||
{
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
env.require(nflags(alice, asfAllowClawback));
|
||||
|
||||
// alice issues 10 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(10)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
|
||||
// alice fails to enable clawback because she has trustline with bob
|
||||
env(fset(alice, asfAllowClawback), ter(tecOWNERS));
|
||||
env.close();
|
||||
|
||||
// bob sets trustline to default limit and pays alice back to delete
|
||||
// the trustline
|
||||
env(trust(bob, USD(0), 0));
|
||||
env(pay(bob, alice, USD(10)));
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
|
||||
// alice now is able to set asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
}
|
||||
|
||||
// Test that one cannot enable asfAllowClawback when
|
||||
// featureClawback amendment is disabled
|
||||
{
|
||||
Env env(*this, features - featureClawback);
|
||||
|
||||
Account alice{"alice"};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
env.require(nflags(alice, asfAllowClawback));
|
||||
|
||||
// alice attempts to set asfAllowClawback flag while amendment is
|
||||
// disabled. no error is returned, but the flag remains to be unset.
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(nflags(alice, asfAllowClawback));
|
||||
|
||||
// now enable clawback amendment
|
||||
env.enableFeature(featureClawback);
|
||||
env.close();
|
||||
|
||||
// asfAllowClawback can be set
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testValidation(FeatureBitset features)
|
||||
{
|
||||
testcase("Validation");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Test that Clawback tx fails for the following:
|
||||
// 1. when amendment is disabled
|
||||
// 2. when asfAllowClawback flag has not been set
|
||||
{
|
||||
Env env(*this, features - featureClawback);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
env.require(nflags(alice, asfAllowClawback));
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice issues 10 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(10)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](10)));
|
||||
env.require(balance(alice, bob["USD"](-10)));
|
||||
|
||||
// clawback fails because amendment is disabled
|
||||
env(claw(alice, bob["USD"](5)), ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
// now enable clawback amendment
|
||||
env.enableFeature(featureClawback);
|
||||
env.close();
|
||||
|
||||
// clawback fails because asfAllowClawback has not been set
|
||||
env(claw(alice, bob["USD"](5)), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](10)));
|
||||
env.require(balance(alice, bob["USD"](-10)));
|
||||
}
|
||||
|
||||
// Test that Clawback tx fails for the following:
|
||||
// 1. invalid flag
|
||||
// 2. negative STAmount
|
||||
// 3. zero STAmount
|
||||
// 4. XRP amount
|
||||
// 5. `account` and `issuer` fields are same account
|
||||
// 6. trustline has a balance of 0
|
||||
// 7. trustline does not exist
|
||||
{
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice issues 10 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(10)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](10)));
|
||||
env.require(balance(alice, bob["USD"](-10)));
|
||||
|
||||
// fails due to invalid flag
|
||||
env(claw(alice, bob["USD"](5)),
|
||||
txflags(0x00008000),
|
||||
ter(temINVALID_FLAG));
|
||||
env.close();
|
||||
|
||||
// fails due to negative amount
|
||||
env(claw(alice, bob["USD"](-5)), ter(temBAD_AMOUNT));
|
||||
env.close();
|
||||
|
||||
// fails due to zero amount
|
||||
env(claw(alice, bob["USD"](0)), ter(temBAD_AMOUNT));
|
||||
env.close();
|
||||
|
||||
// fails because amount is in XRP
|
||||
env(claw(alice, XRP(10)), ter(temBAD_AMOUNT));
|
||||
env.close();
|
||||
|
||||
// fails when `issuer` field in `amount` is not token holder
|
||||
// NOTE: we are using the `issuer` field for the token holder
|
||||
env(claw(alice, alice["USD"](5)), ter(temBAD_AMOUNT));
|
||||
env.close();
|
||||
|
||||
// bob pays alice back, trustline has a balance of 0
|
||||
env(pay(bob, alice, USD(10)));
|
||||
env.close();
|
||||
|
||||
// bob still owns the trustline that has 0 balance
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
env.require(balance(bob, alice["USD"](0)));
|
||||
env.require(balance(alice, bob["USD"](0)));
|
||||
|
||||
// clawback fails because because balance is 0
|
||||
env(claw(alice, bob["USD"](5)), ter(tecINSUFFICIENT_FUNDS));
|
||||
env.close();
|
||||
|
||||
// set the limit to default, which should delete the trustline
|
||||
env(trust(bob, USD(0), 0));
|
||||
env.close();
|
||||
|
||||
// bob no longer owns the trustline
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
|
||||
// clawback fails because trustline does not exist
|
||||
env(claw(alice, bob["USD"](5)), ter(tecNO_LINE));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPermission(FeatureBitset features)
|
||||
{
|
||||
// Checks the tx submitter has the permission to clawback.
|
||||
// Exercises preclaim code
|
||||
testcase("Permission");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Clawing back from an non-existent account returns error
|
||||
{
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
// bob's account is not funded and does not exist
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// bob, the token holder, does not exist
|
||||
env(claw(alice, bob["USD"](5)), ter(terNO_ACCOUNT));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test that trustline cannot be clawed by someone who is
|
||||
// not the issuer of the currency
|
||||
{
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
Account cindy{"cindy"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob, cindy);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// cindy sets asfAllowClawback
|
||||
env(fset(cindy, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(cindy, asfAllowClawback));
|
||||
|
||||
// alice issues 1000 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(1000)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](1000)));
|
||||
env.require(balance(alice, bob["USD"](-1000)));
|
||||
|
||||
// cindy tries to claw from bob, and fails because trustline does
|
||||
// not exist
|
||||
env(claw(cindy, bob["USD"](200)), ter(tecNO_LINE));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// When a trustline is created between issuer and holder,
|
||||
// we must make sure the holder is unable to claw back from
|
||||
// the issuer by impersonating the issuer account.
|
||||
//
|
||||
// This must be tested bidirectionally for both accounts because the
|
||||
// issuer could be either the low or high account in the trustline
|
||||
// object
|
||||
{
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
auto const CAD = bob["CAD"];
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// bob sets asfAllowClawback
|
||||
env(fset(bob, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(bob, asfAllowClawback));
|
||||
|
||||
// alice issues 10 USD to bob.
|
||||
// bob then attempts to submit a clawback tx to claw USD from alice.
|
||||
// this must FAIL, because bob is not the issuer for this
|
||||
// trustline!!!
|
||||
{
|
||||
// bob creates a trustline with alice, and alice sends 10 USD to
|
||||
// bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(10)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](10)));
|
||||
env.require(balance(alice, bob["USD"](-10)));
|
||||
|
||||
// bob cannot claw back USD from alice because he's not the
|
||||
// issuer
|
||||
env(claw(bob, alice["USD"](5)), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// bob issues 10 CAD to alice.
|
||||
// alice then attempts to submit a clawback tx to claw CAD from bob.
|
||||
// this must FAIL, because alice is not the issuer for this
|
||||
// trustline!!!
|
||||
{
|
||||
// alice creates a trustline with bob, and bob sends 10 CAD to
|
||||
// alice
|
||||
env.trust(CAD(1000), alice);
|
||||
env(pay(bob, alice, CAD(10)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["CAD"](-10)));
|
||||
env.require(balance(alice, bob["CAD"](10)));
|
||||
|
||||
// alice cannot claw back CAD from bob because she's not the
|
||||
// issuer
|
||||
env(claw(alice, bob["CAD"](5)), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("Enable clawback");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Test that alice is able to successfully clawback tokens from bob
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// alice issues 1000 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(1000)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](1000)));
|
||||
env.require(balance(alice, bob["USD"](-1000)));
|
||||
|
||||
// alice claws back 200 USD from bob
|
||||
env(claw(alice, bob["USD"](200)));
|
||||
env.close();
|
||||
|
||||
// bob should have 800 USD left
|
||||
env.require(balance(bob, alice["USD"](800)));
|
||||
env.require(balance(alice, bob["USD"](-800)));
|
||||
|
||||
// alice claws back 800 USD from bob again
|
||||
env(claw(alice, bob["USD"](800)));
|
||||
env.close();
|
||||
|
||||
// trustline has a balance of 0
|
||||
env.require(balance(bob, alice["USD"](0)));
|
||||
env.require(balance(alice, bob["USD"](0)));
|
||||
}
|
||||
|
||||
void
|
||||
testMultiLine(FeatureBitset features)
|
||||
{
|
||||
// Test scenarios where multiple trustlines are involved
|
||||
testcase("Multi line");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Both alice and bob issues their own "USD" to cindy.
|
||||
// When alice and bob tries to claw back, they will only
|
||||
// claw back from their respective trustline.
|
||||
{
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
Account cindy{"cindy"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob, cindy);
|
||||
env.close();
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// bob sets asfAllowClawback
|
||||
env(fset(bob, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(bob, asfAllowClawback));
|
||||
|
||||
// alice sends 1000 USD to cindy
|
||||
env.trust(alice["USD"](1000), cindy);
|
||||
env(pay(alice, cindy, alice["USD"](1000)));
|
||||
env.close();
|
||||
|
||||
// bob sends 1000 USD to cindy
|
||||
env.trust(bob["USD"](1000), cindy);
|
||||
env(pay(bob, cindy, bob["USD"](1000)));
|
||||
env.close();
|
||||
|
||||
// alice claws back 200 USD from cindy
|
||||
env(claw(alice, cindy["USD"](200)));
|
||||
env.close();
|
||||
|
||||
// cindy has 800 USD left in alice's trustline after clawed by alice
|
||||
env.require(balance(cindy, alice["USD"](800)));
|
||||
env.require(balance(alice, cindy["USD"](-800)));
|
||||
|
||||
// cindy still has 1000 USD in bob's trustline
|
||||
env.require(balance(cindy, bob["USD"](1000)));
|
||||
env.require(balance(bob, cindy["USD"](-1000)));
|
||||
|
||||
// bob claws back 600 USD from cindy
|
||||
env(claw(bob, cindy["USD"](600)));
|
||||
env.close();
|
||||
|
||||
// cindy has 400 USD left in bob's trustline after clawed by bob
|
||||
env.require(balance(cindy, bob["USD"](400)));
|
||||
env.require(balance(bob, cindy["USD"](-400)));
|
||||
|
||||
// cindy still has 800 USD in alice's trustline
|
||||
env.require(balance(cindy, alice["USD"](800)));
|
||||
env.require(balance(alice, cindy["USD"](-800)));
|
||||
}
|
||||
|
||||
// alice issues USD to both bob and cindy.
|
||||
// when alice claws back from bob, only bob's USD balance is
|
||||
// affected, and cindy's balance remains unchanged, and vice versa.
|
||||
{
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
Account cindy{"cindy"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob, cindy);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// alice sends 600 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(600)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(alice, bob["USD"](-600)));
|
||||
env.require(balance(bob, alice["USD"](600)));
|
||||
|
||||
// alice sends 1000 USD to cindy
|
||||
env.trust(USD(1000), cindy);
|
||||
env(pay(alice, cindy, USD(1000)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(alice, cindy["USD"](-1000)));
|
||||
env.require(balance(cindy, alice["USD"](1000)));
|
||||
|
||||
// alice claws back 500 USD from bob
|
||||
env(claw(alice, bob["USD"](500)));
|
||||
env.close();
|
||||
|
||||
// bob's balance is reduced
|
||||
env.require(balance(alice, bob["USD"](-100)));
|
||||
env.require(balance(bob, alice["USD"](100)));
|
||||
|
||||
// cindy's balance is unchanged
|
||||
env.require(balance(alice, cindy["USD"](-1000)));
|
||||
env.require(balance(cindy, alice["USD"](1000)));
|
||||
|
||||
// alice claws back 300 USD from cindy
|
||||
env(claw(alice, cindy["USD"](300)));
|
||||
env.close();
|
||||
|
||||
// bob's balance is unchanged
|
||||
env.require(balance(alice, bob["USD"](-100)));
|
||||
env.require(balance(bob, alice["USD"](100)));
|
||||
|
||||
// cindy's balance is reduced
|
||||
env.require(balance(alice, cindy["USD"](-700)));
|
||||
env.require(balance(cindy, alice["USD"](700)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testBidirectionalLine(FeatureBitset features)
|
||||
{
|
||||
testcase("Bidirectional line");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Test when both alice and bob issues USD to each other.
|
||||
// This scenario creates only one trustline.
|
||||
// In this case, both alice and bob can be seen as the "issuer"
|
||||
// and they can send however many USDs to each other.
|
||||
// We test that only the person who has a negative balance from their
|
||||
// perspective is allowed to clawback
|
||||
Env env(*this, features);
|
||||
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// bob sets asfAllowClawback
|
||||
env(fset(bob, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(bob, asfAllowClawback));
|
||||
|
||||
// alice issues 1000 USD to bob
|
||||
env.trust(alice["USD"](1000), bob);
|
||||
env(pay(alice, bob, alice["USD"](1000)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
|
||||
// bob is the holder, and alice is the issuer
|
||||
env.require(balance(bob, alice["USD"](1000)));
|
||||
env.require(balance(alice, bob["USD"](-1000)));
|
||||
|
||||
// bob issues 1500 USD to alice
|
||||
env.trust(bob["USD"](1500), alice);
|
||||
env(pay(bob, alice, bob["USD"](1500)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
|
||||
// bob has negative 500 USD because bob issued 500 USD more than alice
|
||||
// bob can now been seen as the issuer, while alice is the holder
|
||||
env.require(balance(bob, alice["USD"](-500)));
|
||||
env.require(balance(alice, bob["USD"](500)));
|
||||
|
||||
// At this point, both alice and bob are the issuers of USD
|
||||
// and can send USD to each other through one trustline
|
||||
|
||||
// alice fails to clawback. Even though she is also an issuer,
|
||||
// the trustline balance is positive from her perspective
|
||||
env(claw(alice, bob["USD"](200)), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
// bob is able to successfully clawback from alice because
|
||||
// the trustline balance is negative from his perspective
|
||||
env(claw(bob, alice["USD"](200)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](-300)));
|
||||
env.require(balance(alice, bob["USD"](300)));
|
||||
|
||||
// alice pays bob 1000 USD
|
||||
env(pay(alice, bob, alice["USD"](1000)));
|
||||
env.close();
|
||||
|
||||
// bob's balance becomes positive from his perspective because
|
||||
// alice issued more USD than the balance
|
||||
env.require(balance(bob, alice["USD"](700)));
|
||||
env.require(balance(alice, bob["USD"](-700)));
|
||||
|
||||
// bob is now the holder and fails to clawback
|
||||
env(claw(bob, alice["USD"](200)), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
// alice successfully claws back
|
||||
env(claw(alice, bob["USD"](200)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](500)));
|
||||
env.require(balance(alice, bob["USD"](-500)));
|
||||
}
|
||||
|
||||
void
|
||||
testDeleteDefaultLine(FeatureBitset features)
|
||||
{
|
||||
testcase("Delete default trustline");
|
||||
using namespace test::jtx;
|
||||
|
||||
// If clawback results the trustline to be default,
|
||||
// trustline should be automatically deleted
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// alice issues 1000 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(1000)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
|
||||
env.require(balance(bob, alice["USD"](1000)));
|
||||
env.require(balance(alice, bob["USD"](-1000)));
|
||||
|
||||
// set limit to default,
|
||||
env(trust(bob, USD(0), 0));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
|
||||
// alice claws back full amount from bob, and should also delete
|
||||
// trustline
|
||||
env(claw(alice, bob["USD"](1000)));
|
||||
env.close();
|
||||
|
||||
// bob no longer owns the trustline because it was deleted
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testFrozenLine(FeatureBitset features)
|
||||
{
|
||||
testcase("Frozen trustline");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Claws back from frozen trustline
|
||||
// and the trustline should remain frozen
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// alice issues 1000 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(1000)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](1000)));
|
||||
env.require(balance(alice, bob["USD"](-1000)));
|
||||
|
||||
// freeze trustline
|
||||
env(trust(alice, bob["USD"](0), tfSetFreeze));
|
||||
env.close();
|
||||
|
||||
// alice claws back 200 USD from bob
|
||||
env(claw(alice, bob["USD"](200)));
|
||||
env.close();
|
||||
|
||||
// bob should have 800 USD left
|
||||
env.require(balance(bob, alice["USD"](800)));
|
||||
env.require(balance(alice, bob["USD"](-800)));
|
||||
|
||||
// trustline remains frozen
|
||||
BEAST_EXPECT(getLineFreezeFlag(env, alice, bob, USD.currency));
|
||||
}
|
||||
|
||||
void
|
||||
testAmountExceedsAvailable(FeatureBitset features)
|
||||
{
|
||||
testcase("Amount exceeds available");
|
||||
using namespace test::jtx;
|
||||
|
||||
// When alice tries to claw back an amount that is greater
|
||||
// than what bob holds, only the max available balance is clawed
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// alice issues 1000 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(1000)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](1000)));
|
||||
env.require(balance(alice, bob["USD"](-1000)));
|
||||
|
||||
// alice tries to claw back 2000 USD
|
||||
env(claw(alice, bob["USD"](2000)));
|
||||
env.close();
|
||||
|
||||
// check alice and bob's balance.
|
||||
// alice was only able to claw back 1000 USD at maximum
|
||||
env.require(balance(bob, alice["USD"](0)));
|
||||
env.require(balance(alice, bob["USD"](0)));
|
||||
|
||||
// bob still owns the trustline because trustline is not in default
|
||||
// state
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
|
||||
// set limit to default,
|
||||
env(trust(bob, USD(0), 0));
|
||||
env.close();
|
||||
|
||||
// verify that bob's trustline was deleted
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testTickets(FeatureBitset features)
|
||||
{
|
||||
testcase("Tickets");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Tests clawback with tickets
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const USD = alice["USD"];
|
||||
|
||||
// alice sets asfAllowClawback
|
||||
env(fset(alice, asfAllowClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowClawback));
|
||||
|
||||
// alice issues 100 USD to bob
|
||||
env.trust(USD(1000), bob);
|
||||
env(pay(alice, bob, USD(100)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, alice["USD"](100)));
|
||||
env.require(balance(alice, bob["USD"](-100)));
|
||||
|
||||
// alice creates 10 tickets
|
||||
std::uint32_t ticketCnt = 10;
|
||||
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
|
||||
env(ticket::create(alice, ticketCnt));
|
||||
env.close();
|
||||
std::uint32_t const aliceSeq{env.seq(alice)};
|
||||
BEAST_EXPECT(ticketCount(env, alice) == ticketCnt);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == ticketCnt);
|
||||
|
||||
while (ticketCnt > 0)
|
||||
{
|
||||
// alice claws back 5 USD using a ticket
|
||||
env(claw(alice, bob["USD"](5)), ticket::use(aliceTicketSeq++));
|
||||
env.close();
|
||||
|
||||
ticketCnt--;
|
||||
BEAST_EXPECT(ticketCount(env, alice) == ticketCnt);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == ticketCnt);
|
||||
}
|
||||
|
||||
// alice clawed back 50 USD total, trustline has 50 USD remaining
|
||||
env.require(balance(bob, alice["USD"](50)));
|
||||
env.require(balance(alice, bob["USD"](-50)));
|
||||
|
||||
// Verify that the account sequence numbers did not advance.
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testAllowClawbackFlag(features);
|
||||
testValidation(features);
|
||||
testPermission(features);
|
||||
testEnabled(features);
|
||||
testMultiLine(features);
|
||||
testBidirectionalLine(features);
|
||||
testDeleteDefaultLine(features);
|
||||
testFrozenLine(features);
|
||||
testAmountExceedsAvailable(features);
|
||||
testTickets(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{supported_amendments()};
|
||||
|
||||
testWithFeats(all);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Clawback, app, ripple);
|
||||
} // namespace ripple
|
||||
@@ -2126,18 +2126,17 @@ public:
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][sfBalance.fieldName][jss::value] ==
|
||||
"49.96666666666667");
|
||||
|
||||
jrr = ledgerEntryState(env, bob, gw, "USD");
|
||||
if (NumberSwitchOver)
|
||||
Json::Value const bobsUSD =
|
||||
jrr[jss::node][sfBalance.fieldName][jss::value];
|
||||
if (!NumberSwitchOver)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][sfBalance.fieldName][jss::value] ==
|
||||
"-0.9665000000333333");
|
||||
BEAST_EXPECT(bobsUSD == "-0.966500000033334");
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][sfBalance.fieldName][jss::value] ==
|
||||
"-0.966500000033334");
|
||||
BEAST_EXPECT(bobsUSD == "-0.9665000000333333");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
622
src/test/app/ReducedOffer_test.cpp
Normal file
622
src/test/app/ReducedOffer_test.cpp
Normal file
@@ -0,0 +1,622 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2022 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Quality.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class ReducedOffer_test : public beast::unit_test::suite
|
||||
{
|
||||
static auto
|
||||
ledgerEntryOffer(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& acct,
|
||||
std::uint32_t offer_seq)
|
||||
{
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::offer][jss::account] = acct.human();
|
||||
jvParams[jss::offer][jss::seq] = offer_seq;
|
||||
return env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
}
|
||||
|
||||
static bool
|
||||
offerInLedger(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& acct,
|
||||
std::uint32_t offerSeq)
|
||||
{
|
||||
Json::Value ledgerOffer = ledgerEntryOffer(env, acct, offerSeq);
|
||||
return !(
|
||||
ledgerOffer.isMember(jss::error) &&
|
||||
ledgerOffer[jss::error].asString() == "entryNotFound");
|
||||
}
|
||||
|
||||
// Common code to clean up unneeded offers.
|
||||
static void
|
||||
cleanupOldOffers(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& acct1,
|
||||
jtx::Account const& acct2,
|
||||
std::uint32_t acct1OfferSeq,
|
||||
std::uint32_t acct2OfferSeq)
|
||||
{
|
||||
env(offer_cancel(acct1, acct1OfferSeq));
|
||||
env(offer_cancel(acct2, acct2OfferSeq));
|
||||
env.close();
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testPartialCrossNewXrpIouQChange()
|
||||
{
|
||||
testcase("exercise partial cross new XRP/IOU offer Q change");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// Make one test run without fixReducedOffersV1 and one with.
|
||||
for (FeatureBitset features :
|
||||
{supported_amendments() - fixReducedOffersV1,
|
||||
supported_amendments() | fixReducedOffersV1})
|
||||
{
|
||||
Env env{*this, features};
|
||||
|
||||
// Make sure none of the offers we generate are under funded.
|
||||
env.fund(XRP(10'000'000), gw, alice, bob);
|
||||
env.close();
|
||||
|
||||
env(trust(alice, USD(10'000'000)));
|
||||
env(trust(bob, USD(10'000'000)));
|
||||
env.close();
|
||||
|
||||
env(pay(gw, bob, USD(10'000'000)));
|
||||
env.close();
|
||||
|
||||
// Lambda that:
|
||||
// 1. Exercises one offer pair,
|
||||
// 2. Collects the results, and
|
||||
// 3. Cleans up for the next offer pair.
|
||||
// Returns 1 if the crossed offer has a bad rate for the book.
|
||||
auto exerciseOfferPair =
|
||||
[this, &env, &alice, &bob](
|
||||
Amounts const& inLedger,
|
||||
Amounts const& newOffer) -> unsigned int {
|
||||
// Put inLedger offer in the ledger so newOffer can cross it.
|
||||
std::uint32_t const aliceOfferSeq = env.seq(alice);
|
||||
env(offer(alice, inLedger.in, inLedger.out));
|
||||
env.close();
|
||||
|
||||
// Now alice's offer will partially cross bob's offer.
|
||||
STAmount const initialRate = Quality(newOffer).rate();
|
||||
std::uint32_t const bobOfferSeq = env.seq(bob);
|
||||
STAmount const bobInitialBalance = env.balance(bob);
|
||||
STAmount const bobsFee = drops(10);
|
||||
env(offer(bob, newOffer.in, newOffer.out, tfSell),
|
||||
fee(bobsFee));
|
||||
env.close();
|
||||
STAmount const bobFinalBalance = env.balance(bob);
|
||||
|
||||
// alice's offer should be fully crossed and so gone from
|
||||
// the ledger.
|
||||
if (!BEAST_EXPECT(!offerInLedger(env, alice, aliceOfferSeq)))
|
||||
// If the in-ledger offer was not consumed then further
|
||||
// results are meaningless.
|
||||
return 1;
|
||||
|
||||
// bob's offer should be in the ledger, but reduced in size.
|
||||
unsigned int badRate = 1;
|
||||
{
|
||||
Json::Value bobOffer =
|
||||
ledgerEntryOffer(env, bob, bobOfferSeq);
|
||||
|
||||
STAmount const reducedTakerGets = amountFromJson(
|
||||
sfTakerGets, bobOffer[jss::node][sfTakerGets.jsonName]);
|
||||
STAmount const reducedTakerPays = amountFromJson(
|
||||
sfTakerPays, bobOffer[jss::node][sfTakerPays.jsonName]);
|
||||
STAmount const bobGot =
|
||||
env.balance(bob) + bobsFee - bobInitialBalance;
|
||||
BEAST_EXPECT(reducedTakerPays < newOffer.in);
|
||||
BEAST_EXPECT(reducedTakerGets < newOffer.out);
|
||||
STAmount const inLedgerRate =
|
||||
Quality(Amounts{reducedTakerPays, reducedTakerGets})
|
||||
.rate();
|
||||
|
||||
badRate = inLedgerRate > initialRate ? 1 : 0;
|
||||
|
||||
// If the inLedgerRate is less than initial rate, then
|
||||
// incrementing the mantissa of the reduced taker pays
|
||||
// should result in a rate higher than initial. Check
|
||||
// this to verify that the largest allowable TakerPays
|
||||
// was computed.
|
||||
if (badRate == 0)
|
||||
{
|
||||
STAmount const tweakedTakerPays =
|
||||
reducedTakerPays + drops(1);
|
||||
STAmount const tweakedRate =
|
||||
Quality(Amounts{tweakedTakerPays, reducedTakerGets})
|
||||
.rate();
|
||||
BEAST_EXPECT(tweakedRate > initialRate);
|
||||
}
|
||||
#if 0
|
||||
std::cout << "Placed rate: " << initialRate
|
||||
<< "; in-ledger rate: " << inLedgerRate
|
||||
<< "; TakerPays: " << reducedTakerPays
|
||||
<< "; TakerGets: " << reducedTakerGets
|
||||
<< "; bob already got: " << bobGot << std::endl;
|
||||
// #else
|
||||
std::string_view filler =
|
||||
inLedgerRate > initialRate ? "**" : " ";
|
||||
std::cout << "| `" << reducedTakerGets << "` | `"
|
||||
<< reducedTakerPays << "` | `" << initialRate
|
||||
<< "` | " << filler << "`" << inLedgerRate << "`"
|
||||
<< filler << " |`" << std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
// In preparation for the next iteration make sure the two
|
||||
// offers are gone from the ledger.
|
||||
cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq);
|
||||
return badRate;
|
||||
};
|
||||
|
||||
// bob's offer (the new offer) is the same every time:
|
||||
Amounts const bobsOffer{
|
||||
STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)};
|
||||
|
||||
// alice's offer has a slightly smaller TakerPays with each
|
||||
// iteration. This should mean that the size of the offer bob
|
||||
// places in the ledger should increase with each iteration.
|
||||
unsigned int blockedCount = 0;
|
||||
for (std::uint64_t mantissaReduce = 1'000'000'000ull;
|
||||
mantissaReduce <= 5'000'000'000ull;
|
||||
mantissaReduce += 20'000'000ull)
|
||||
{
|
||||
STAmount aliceUSD{
|
||||
bobsOffer.out.issue(),
|
||||
bobsOffer.out.mantissa() - mantissaReduce,
|
||||
bobsOffer.out.exponent()};
|
||||
STAmount aliceXRP{
|
||||
bobsOffer.in.issue(), bobsOffer.in.mantissa() - 1};
|
||||
Amounts alicesOffer{aliceUSD, aliceXRP};
|
||||
blockedCount += exerciseOfferPair(alicesOffer, bobsOffer);
|
||||
}
|
||||
|
||||
// If fixReducedOffersV1 is enabled, then none of the test cases
|
||||
// should produce a potentially blocking rate.
|
||||
//
|
||||
// Also verify that if fixReducedOffersV1 is not enabled then
|
||||
// some of the test cases produced a potentially blocking rate.
|
||||
if (features[fixReducedOffersV1])
|
||||
{
|
||||
BEAST_EXPECT(blockedCount == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(blockedCount >= 170);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPartialCrossOldXrpIouQChange()
|
||||
{
|
||||
testcase("exercise partial cross old XRP/IOU offer Q change");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// Make one test run without fixReducedOffersV1 and one with.
|
||||
for (FeatureBitset features :
|
||||
{supported_amendments() - fixReducedOffersV1,
|
||||
supported_amendments() | fixReducedOffersV1})
|
||||
{
|
||||
// Make sure none of the offers we generate are under funded.
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10'000'000), gw, alice, bob);
|
||||
env.close();
|
||||
|
||||
env(trust(alice, USD(10'000'000)));
|
||||
env(trust(bob, USD(10'000'000)));
|
||||
env.close();
|
||||
|
||||
env(pay(gw, alice, USD(10'000'000)));
|
||||
env.close();
|
||||
|
||||
// Lambda that:
|
||||
// 1. Exercises one offer pair,
|
||||
// 2. Collects the results, and
|
||||
// 3. Cleans up for the next offer pair.
|
||||
auto exerciseOfferPair =
|
||||
[this, &env, &alice, &bob](
|
||||
Amounts const& inLedger,
|
||||
Amounts const& newOffer) -> unsigned int {
|
||||
// Get the inLedger offer into the ledger so newOffer can cross
|
||||
// it.
|
||||
STAmount const initialRate = Quality(inLedger).rate();
|
||||
std::uint32_t const aliceOfferSeq = env.seq(alice);
|
||||
env(offer(alice, inLedger.in, inLedger.out));
|
||||
env.close();
|
||||
|
||||
// Now bob's offer will partially cross alice's offer.
|
||||
std::uint32_t const bobOfferSeq = env.seq(bob);
|
||||
STAmount const aliceInitialBalance = env.balance(alice);
|
||||
env(offer(bob, newOffer.in, newOffer.out));
|
||||
env.close();
|
||||
STAmount const aliceFinalBalance = env.balance(alice);
|
||||
|
||||
// bob's offer should not have made it into the ledger.
|
||||
if (!BEAST_EXPECT(!offerInLedger(env, bob, bobOfferSeq)))
|
||||
{
|
||||
// If the in-ledger offer was not consumed then further
|
||||
// results are meaningless.
|
||||
cleanupOldOffers(
|
||||
env, alice, bob, aliceOfferSeq, bobOfferSeq);
|
||||
return 1;
|
||||
}
|
||||
// alice's offer should still be in the ledger, but reduced in
|
||||
// size.
|
||||
unsigned int badRate = 1;
|
||||
{
|
||||
Json::Value aliceOffer =
|
||||
ledgerEntryOffer(env, alice, aliceOfferSeq);
|
||||
|
||||
STAmount const reducedTakerGets = amountFromJson(
|
||||
sfTakerGets,
|
||||
aliceOffer[jss::node][sfTakerGets.jsonName]);
|
||||
STAmount const reducedTakerPays = amountFromJson(
|
||||
sfTakerPays,
|
||||
aliceOffer[jss::node][sfTakerPays.jsonName]);
|
||||
STAmount const aliceGot =
|
||||
env.balance(alice) - aliceInitialBalance;
|
||||
BEAST_EXPECT(reducedTakerPays < inLedger.in);
|
||||
BEAST_EXPECT(reducedTakerGets < inLedger.out);
|
||||
STAmount const inLedgerRate =
|
||||
Quality(Amounts{reducedTakerPays, reducedTakerGets})
|
||||
.rate();
|
||||
badRate = inLedgerRate > initialRate ? 1 : 0;
|
||||
|
||||
// If the inLedgerRate is less than initial rate, then
|
||||
// incrementing the mantissa of the reduced taker pays
|
||||
// should result in a rate higher than initial. Check
|
||||
// this to verify that the largest allowable TakerPays
|
||||
// was computed.
|
||||
if (badRate == 0)
|
||||
{
|
||||
STAmount const tweakedTakerPays =
|
||||
reducedTakerPays + drops(1);
|
||||
STAmount const tweakedRate =
|
||||
Quality(Amounts{tweakedTakerPays, reducedTakerGets})
|
||||
.rate();
|
||||
BEAST_EXPECT(tweakedRate > initialRate);
|
||||
}
|
||||
#if 0
|
||||
std::cout << "Placed rate: " << initialRate
|
||||
<< "; in-ledger rate: " << inLedgerRate
|
||||
<< "; TakerPays: " << reducedTakerPays
|
||||
<< "; TakerGets: " << reducedTakerGets
|
||||
<< "; alice already got: " << aliceGot
|
||||
<< std::endl;
|
||||
// #else
|
||||
std::string_view filler = badRate ? "**" : " ";
|
||||
std::cout << "| `" << reducedTakerGets << "` | `"
|
||||
<< reducedTakerPays << "` | `" << initialRate
|
||||
<< "` | " << filler << "`" << inLedgerRate << "`"
|
||||
<< filler << " | `" << aliceGot << "` |"
|
||||
<< std::endl;
|
||||
#endif
|
||||
}
|
||||
|
||||
// In preparation for the next iteration make sure the two
|
||||
// offers are gone from the ledger.
|
||||
cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq);
|
||||
return badRate;
|
||||
};
|
||||
|
||||
// alice's offer (the old offer) is the same every time:
|
||||
Amounts const aliceOffer{
|
||||
STAmount(XRP(1)), STAmount(USD.issue(), 1, 0)};
|
||||
|
||||
// bob's offer has a slightly smaller TakerPays with each iteration.
|
||||
// This should mean that the size of the offer alice leaves in the
|
||||
// ledger should increase with each iteration.
|
||||
unsigned int blockedCount = 0;
|
||||
for (std::uint64_t mantissaReduce = 1'000'000'000ull;
|
||||
mantissaReduce <= 4'000'000'000ull;
|
||||
mantissaReduce += 20'000'000ull)
|
||||
{
|
||||
STAmount bobUSD{
|
||||
aliceOffer.out.issue(),
|
||||
aliceOffer.out.mantissa() - mantissaReduce,
|
||||
aliceOffer.out.exponent()};
|
||||
STAmount bobXRP{
|
||||
aliceOffer.in.issue(), aliceOffer.in.mantissa() - 1};
|
||||
Amounts bobsOffer{bobUSD, bobXRP};
|
||||
|
||||
blockedCount += exerciseOfferPair(aliceOffer, bobsOffer);
|
||||
}
|
||||
|
||||
// If fixReducedOffersV1 is enabled, then none of the test cases
|
||||
// should produce a potentially blocking rate.
|
||||
//
|
||||
// Also verify that if fixReducedOffersV1 is not enabled then
|
||||
// some of the test cases produced a potentially blocking rate.
|
||||
if (features[fixReducedOffersV1])
|
||||
{
|
||||
BEAST_EXPECT(blockedCount == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(blockedCount > 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUnderFundedXrpIouQChange()
|
||||
{
|
||||
testcase("exercise underfunded XRP/IOU offer Q change");
|
||||
|
||||
// Bob places an offer that is not fully funded.
|
||||
//
|
||||
// This unit test compares the behavior of this situation before and
|
||||
// after applying the fixReducedOffersV1 amendment.
|
||||
|
||||
using namespace jtx;
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const gw = Account{"gw"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// Make one test run without fixReducedOffersV1 and one with.
|
||||
for (FeatureBitset features :
|
||||
{supported_amendments() - fixReducedOffersV1,
|
||||
supported_amendments() | fixReducedOffersV1})
|
||||
{
|
||||
Env env{*this, features};
|
||||
|
||||
env.fund(XRP(10000), alice, bob, gw);
|
||||
env.close();
|
||||
env.trust(USD(1000), alice, bob);
|
||||
|
||||
int blockedOrderBookCount = 0;
|
||||
for (STAmount initialBobUSD = USD(0.45); initialBobUSD <= USD(1);
|
||||
initialBobUSD += USD(0.025))
|
||||
{
|
||||
// underfund bob's offer
|
||||
env(pay(gw, bob, initialBobUSD));
|
||||
env.close();
|
||||
|
||||
std::uint32_t const bobOfferSeq = env.seq(bob);
|
||||
env(offer(bob, drops(2), USD(1)));
|
||||
env.close();
|
||||
|
||||
// alice places an offer that would cross bob's if bob's were
|
||||
// well funded.
|
||||
std::uint32_t const aliceOfferSeq = env.seq(alice);
|
||||
env(offer(alice, USD(1), drops(2)));
|
||||
env.close();
|
||||
|
||||
// We want to detect order book blocking. If:
|
||||
// 1. bob's offer is still in the ledger and
|
||||
// 2. alice received no USD
|
||||
// then we use that as evidence that bob's offer blocked the
|
||||
// order book.
|
||||
{
|
||||
bool const bobsOfferGone =
|
||||
!offerInLedger(env, bob, bobOfferSeq);
|
||||
STAmount const aliceBalanceUSD = env.balance(alice, USD);
|
||||
|
||||
// Sanity check the ledger if alice got USD.
|
||||
if (aliceBalanceUSD.signum() > 0)
|
||||
{
|
||||
BEAST_EXPECT(aliceBalanceUSD == initialBobUSD);
|
||||
BEAST_EXPECT(env.balance(bob, USD) == USD(0));
|
||||
BEAST_EXPECT(bobsOfferGone);
|
||||
}
|
||||
|
||||
// Track occurrences of order book blocking.
|
||||
if (!bobsOfferGone && aliceBalanceUSD.signum() == 0)
|
||||
{
|
||||
++blockedOrderBookCount;
|
||||
}
|
||||
|
||||
// In preparation for the next iteration clean up any
|
||||
// leftover offers.
|
||||
cleanupOldOffers(
|
||||
env, alice, bob, aliceOfferSeq, bobOfferSeq);
|
||||
|
||||
// Zero out alice's and bob's USD balances.
|
||||
if (STAmount const aliceBalance = env.balance(alice, USD);
|
||||
aliceBalance.signum() > 0)
|
||||
env(pay(alice, gw, aliceBalance));
|
||||
|
||||
if (STAmount const bobBalance = env.balance(bob, USD);
|
||||
bobBalance.signum() > 0)
|
||||
env(pay(bob, gw, bobBalance));
|
||||
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
// If fixReducedOffersV1 is enabled, then none of the test cases
|
||||
// should produce a potentially blocking rate.
|
||||
//
|
||||
// Also verify that if fixReducedOffersV1 is not enabled then
|
||||
// some of the test cases produced a potentially blocking rate.
|
||||
if (features[fixReducedOffersV1])
|
||||
{
|
||||
BEAST_EXPECT(blockedOrderBookCount == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(blockedOrderBookCount > 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUnderFundedIouIouQChange()
|
||||
{
|
||||
testcase("exercise underfunded IOU/IOU offer Q change");
|
||||
|
||||
// Bob places an IOU/IOU offer that is not fully funded.
|
||||
//
|
||||
// This unit test compares the behavior of this situation before and
|
||||
// after applying the fixReducedOffersV1 amendment.
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const gw = Account{"gw"};
|
||||
|
||||
auto const USD = gw["USD"];
|
||||
auto const EUR = gw["EUR"];
|
||||
|
||||
STAmount const tinyUSD(USD.issue(), /*mantissa*/ 1, /*exponent*/ -81);
|
||||
|
||||
// Make one test run without fixReducedOffersV1 and one with.
|
||||
for (FeatureBitset features :
|
||||
{supported_amendments() - fixReducedOffersV1,
|
||||
supported_amendments() | fixReducedOffersV1})
|
||||
{
|
||||
Env env{*this, features};
|
||||
|
||||
env.fund(XRP(10000), alice, bob, gw);
|
||||
env.close();
|
||||
env.trust(USD(1000), alice, bob);
|
||||
env.trust(EUR(1000), alice, bob);
|
||||
|
||||
STAmount const eurOffer(
|
||||
EUR.issue(), /*mantissa*/ 2957, /*exponent*/ -76);
|
||||
STAmount const usdOffer(
|
||||
USD.issue(), /*mantissa*/ 7109, /*exponent*/ -76);
|
||||
|
||||
STAmount const endLoop(
|
||||
USD.issue(), /*mantissa*/ 50, /*exponent*/ -81);
|
||||
|
||||
int blockedOrderBookCount = 0;
|
||||
for (STAmount initialBobUSD = tinyUSD; initialBobUSD <= endLoop;
|
||||
initialBobUSD += tinyUSD)
|
||||
{
|
||||
// underfund bob's offer
|
||||
env(pay(gw, bob, initialBobUSD));
|
||||
env(pay(gw, alice, EUR(100)));
|
||||
env.close();
|
||||
|
||||
// This offer is underfunded
|
||||
std::uint32_t bobOfferSeq = env.seq(bob);
|
||||
env(offer(bob, eurOffer, usdOffer));
|
||||
env.close();
|
||||
env.require(offers(bob, 1));
|
||||
|
||||
// alice places an offer that crosses bob's.
|
||||
std::uint32_t aliceOfferSeq = env.seq(alice);
|
||||
env(offer(alice, usdOffer, eurOffer));
|
||||
env.close();
|
||||
|
||||
// Examine the aftermath of alice's offer.
|
||||
{
|
||||
bool const bobsOfferGone =
|
||||
!offerInLedger(env, bob, bobOfferSeq);
|
||||
STAmount aliceBalanceUSD = env.balance(alice, USD);
|
||||
#if 0
|
||||
std::cout
|
||||
<< "bobs initial: " << initialBobUSD
|
||||
<< "; alice final: " << aliceBalanceUSD
|
||||
<< "; bobs offer: " << bobsOfferJson.toStyledString()
|
||||
<< std::endl;
|
||||
#endif
|
||||
// Sanity check the ledger if alice got USD.
|
||||
if (aliceBalanceUSD.signum() > 0)
|
||||
{
|
||||
BEAST_EXPECT(aliceBalanceUSD == initialBobUSD);
|
||||
BEAST_EXPECT(env.balance(bob, USD) == USD(0));
|
||||
BEAST_EXPECT(bobsOfferGone);
|
||||
}
|
||||
|
||||
// Track occurrences of order book blocking.
|
||||
if (!bobsOfferGone && aliceBalanceUSD.signum() == 0)
|
||||
{
|
||||
++blockedOrderBookCount;
|
||||
}
|
||||
}
|
||||
|
||||
// In preparation for the next iteration clean up any
|
||||
// leftover offers.
|
||||
cleanupOldOffers(env, alice, bob, aliceOfferSeq, bobOfferSeq);
|
||||
|
||||
// Zero out alice's and bob's IOU balances.
|
||||
auto zeroBalance = [&env, &gw](
|
||||
Account const& acct, IOU const& iou) {
|
||||
if (STAmount const balance = env.balance(acct, iou);
|
||||
balance.signum() > 0)
|
||||
env(pay(acct, gw, balance));
|
||||
};
|
||||
|
||||
zeroBalance(alice, EUR);
|
||||
zeroBalance(alice, USD);
|
||||
zeroBalance(bob, EUR);
|
||||
zeroBalance(bob, USD);
|
||||
env.close();
|
||||
}
|
||||
|
||||
// If fixReducedOffersV1 is enabled, then none of the test cases
|
||||
// should produce a potentially blocking rate.
|
||||
//
|
||||
// Also verify that if fixReducedOffersV1 is not enabled then
|
||||
// some of the test cases produced a potentially blocking rate.
|
||||
if (features[fixReducedOffersV1])
|
||||
{
|
||||
BEAST_EXPECT(blockedOrderBookCount == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(blockedOrderBookCount > 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testPartialCrossNewXrpIouQChange();
|
||||
testPartialCrossOldXrpIouQChange();
|
||||
testUnderFundedXrpIouQChange();
|
||||
testUnderFundedIouIouQChange();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE_PRIO(ReducedOffer, tx, ripple, 2);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -80,6 +80,9 @@ private:
|
||||
case asfDepositAuth:
|
||||
mask_ |= lsfDepositAuth;
|
||||
break;
|
||||
case asfAllowClawback:
|
||||
mask_ |= lsfAllowClawback;
|
||||
break;
|
||||
default:
|
||||
Throw<std::runtime_error>("unknown flag");
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ setupConfigForUnitTests(Config& cfg)
|
||||
cfg.FEES.account_reserve = XRP(200).value().xrp().drops();
|
||||
cfg.FEES.owner_reserve = XRP(50).value().xrp().drops();
|
||||
|
||||
// The Beta API (currently v2) is always available to tests
|
||||
cfg.BETA_RPC_API = true;
|
||||
|
||||
cfg.overwrite(ConfigSection::nodeDatabase(), "type", "memory");
|
||||
cfg.overwrite(ConfigSection::nodeDatabase(), "path", "main");
|
||||
cfg.deprecatedClearSection(ConfigSection::importNodeDatabase());
|
||||
|
||||
@@ -59,6 +59,17 @@ trust(
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
claw(Account const& account, STAmount const& amount)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
jv[jss::TransactionType] = jss::Clawback;
|
||||
|
||||
return jv;
|
||||
}
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
@@ -40,6 +40,9 @@ trust(
|
||||
Account const& peer,
|
||||
std::uint32_t flags);
|
||||
|
||||
Json::Value
|
||||
claw(Account const& account, STAmount const& amount);
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
@@ -206,10 +206,7 @@ public:
|
||||
testSignerListsApiVersion2()
|
||||
{
|
||||
using namespace jtx;
|
||||
Env env{*this, envconfig([](std::unique_ptr<Config> c) {
|
||||
c->loadFromString("\n[beta_rpc_api]\n1\n");
|
||||
return c;
|
||||
})};
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
|
||||
|
||||
@@ -93,6 +93,12 @@ public:
|
||||
// and are tested elsewhere
|
||||
continue;
|
||||
}
|
||||
if (flag == asfAllowClawback)
|
||||
{
|
||||
// The asfAllowClawback flag can't be cleared. It is tested
|
||||
// elsewhere.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::find(goodFlags.begin(), goodFlags.end(), flag) !=
|
||||
goodFlags.end())
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/misc/NetworkOPs.h>
|
||||
#include <ripple/beast/unit_test.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
@@ -55,6 +56,16 @@ public:
|
||||
|
||||
[validators]
|
||||
%2%
|
||||
|
||||
[port_grpc]
|
||||
ip = 0.0.0.0
|
||||
port = 50051
|
||||
|
||||
[port_admin]
|
||||
ip = 0.0.0.0
|
||||
port = 50052
|
||||
protocol = wss2
|
||||
admin = 127.0.0.1
|
||||
)rippleConfig");
|
||||
|
||||
p->loadFromString(boost::str(
|
||||
@@ -77,8 +88,30 @@ public:
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == "success");
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::info));
|
||||
}
|
||||
|
||||
{
|
||||
Env env(*this, makeValidatorConfig());
|
||||
Env env(*this);
|
||||
|
||||
// Call NetworkOPs directly and set the admin flag to false.
|
||||
// Expect that the admin ports are not included in the result.
|
||||
auto const result =
|
||||
env.app().getOPs().getServerInfo(true, false, 0);
|
||||
auto const& ports = result[jss::ports];
|
||||
BEAST_EXPECT(ports.isArray() && ports.size() == 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto config = makeValidatorConfig();
|
||||
auto const rpc_port =
|
||||
(*config)["port_rpc"].get<unsigned int>("port");
|
||||
auto const grpc_port =
|
||||
(*config)["port_grpc"].get<unsigned int>("port");
|
||||
auto const ws_port = (*config)["port_ws"].get<unsigned int>("port");
|
||||
BEAST_EXPECT(grpc_port);
|
||||
BEAST_EXPECT(rpc_port);
|
||||
BEAST_EXPECT(ws_port);
|
||||
|
||||
Env env(*this, std::move(config));
|
||||
auto const result = env.rpc("server_info");
|
||||
BEAST_EXPECT(!result[jss::result].isMember(jss::error));
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == "success");
|
||||
@@ -86,6 +119,32 @@ public:
|
||||
BEAST_EXPECT(
|
||||
result[jss::result][jss::info][jss::pubkey_validator] ==
|
||||
validator_data::public_key);
|
||||
|
||||
auto const& ports = result[jss::result][jss::info][jss::ports];
|
||||
BEAST_EXPECT(ports.isArray() && ports.size() == 3);
|
||||
for (auto const& port : ports)
|
||||
{
|
||||
auto const& proto = port[jss::protocol];
|
||||
BEAST_EXPECT(proto.isArray());
|
||||
auto const p = port[jss::port].asUInt();
|
||||
BEAST_EXPECT(p == rpc_port || p == ws_port || p == grpc_port);
|
||||
if (p == grpc_port)
|
||||
{
|
||||
BEAST_EXPECT(proto.size() == 1);
|
||||
BEAST_EXPECT(proto[0u].asString() == "grpc");
|
||||
}
|
||||
if (p == rpc_port)
|
||||
{
|
||||
BEAST_EXPECT(proto.size() == 2);
|
||||
BEAST_EXPECT(proto[0u].asString() == "http");
|
||||
BEAST_EXPECT(proto[1u].asString() == "ws2");
|
||||
}
|
||||
if (p == ws_port)
|
||||
{
|
||||
BEAST_EXPECT(proto.size() == 1);
|
||||
BEAST_EXPECT(proto[0u].asString() == "ws");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,11 +76,16 @@ class Version_test : public beast::unit_test::suite
|
||||
std::to_string(RPC::apiMinimumSupportedVersion - 1) + "}");
|
||||
BEAST_EXPECT(badVersion(re));
|
||||
|
||||
BEAST_EXPECT(env.app().config().BETA_RPC_API);
|
||||
re = env.rpc(
|
||||
"json",
|
||||
"version",
|
||||
"{\"api_version\": " +
|
||||
std::to_string(RPC::apiMaximumSupportedVersion + 1) + "}");
|
||||
std::to_string(
|
||||
std::max(
|
||||
RPC::apiMaximumSupportedVersion, RPC::apiBetaVersion) +
|
||||
1) +
|
||||
"}");
|
||||
BEAST_EXPECT(badVersion(re));
|
||||
|
||||
re = env.rpc("json", "version", "{\"api_version\": \"a\"}");
|
||||
@@ -190,20 +195,25 @@ class Version_test : public beast::unit_test::suite
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
|
||||
BEAST_EXPECT(env.app().config().BETA_RPC_API);
|
||||
auto const without_api_verion = std::string("{ ") +
|
||||
"\"jsonrpc\": \"2.0\", "
|
||||
"\"ripplerpc\": \"2.0\", "
|
||||
"\"id\": 5, "
|
||||
"\"method\": \"version\", "
|
||||
"\"params\": {}}";
|
||||
auto const with_wrong_api_verion = std::string("{ ") +
|
||||
auto const with_wrong_api_verion =
|
||||
std::string("{ ") +
|
||||
"\"jsonrpc\": \"2.0\", "
|
||||
"\"ripplerpc\": \"2.0\", "
|
||||
"\"id\": 6, "
|
||||
"\"method\": \"version\", "
|
||||
"\"params\": { "
|
||||
"\"api_version\": " +
|
||||
std::to_string(RPC::apiMaximumSupportedVersion + 1) + "}}";
|
||||
std::to_string(
|
||||
std::max(RPC::apiMaximumSupportedVersion, RPC::apiBetaVersion) +
|
||||
1) +
|
||||
"}}";
|
||||
auto re = env.rpc(
|
||||
"json2",
|
||||
'[' + without_api_verion + ", " + with_wrong_api_verion + ']');
|
||||
|
||||
Reference in New Issue
Block a user