mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-04 10:45:50 +00:00
XLS-39 Clawback: (#4553)
Introduces: * AccountRoot flag: lsfAllowClawback * New Clawback transaction * More info on clawback spec: https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-39d-clawback
This commit is contained in:
@@ -440,6 +440,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/CashCheck.cpp
|
||||
src/ripple/app/tx/impl/Change.cpp
|
||||
src/ripple/app/tx/impl/ClaimReward.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
|
||||
@@ -721,6 +722,7 @@ if (tests)
|
||||
src/test/app/BaseFee_test.cpp
|
||||
src/test/app/Check_test.cpp
|
||||
src/test/app/ClaimReward_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
|
||||
|
||||
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
|
||||
@@ -24,6 +24,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>
|
||||
@@ -847,4 +848,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
|
||||
|
||||
@@ -390,6 +390,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<
|
||||
@@ -403,7 +431,8 @@ using InvariantChecks = std::tuple<
|
||||
NoZeroEscrow,
|
||||
ValidNewAccountRoot,
|
||||
ValidNFTokenPage,
|
||||
NFTokenCountTracking>;
|
||||
NFTokenCountTracking,
|
||||
ValidClawback>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -587,6 +618,14 @@ SetAccount::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <ripple/app/tx/impl/CashCheck.h>
|
||||
#include <ripple/app/tx/impl/Change.h>
|
||||
#include <ripple/app/tx/impl/ClaimReward.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>
|
||||
@@ -178,6 +179,8 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return invoke_preflight_helper<URIToken>(ctx);
|
||||
case ttCLAWBACK:
|
||||
return invoke_preflight_helper<Clawback>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
|
||||
@@ -301,6 +304,8 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return invoke_preclaim<URIToken>(ctx);
|
||||
case ttCLAWBACK:
|
||||
return invoke_preclaim<Clawback>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return temUNKNOWN;
|
||||
@@ -386,6 +391,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
case ttURITOKEN_CREATE_SELL_OFFER:
|
||||
case ttURITOKEN_CANCEL_SELL_OFFER:
|
||||
return URIToken::calculateBaseFee(view, tx);
|
||||
case ttCLAWBACK:
|
||||
return Clawback::calculateBaseFee(view, tx);
|
||||
default:
|
||||
return XRPAmount{0};
|
||||
}
|
||||
@@ -575,6 +582,10 @@ invoke_apply(ApplyContext& ctx)
|
||||
URIToken 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 = 82;
|
||||
static constexpr std::size_t numFeatures = 83;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -370,6 +370,7 @@ extern uint256 const fix20250131;
|
||||
extern uint256 const featureHookCanEmit;
|
||||
extern uint256 const fixRewardClaimFlags;
|
||||
extern uint256 const fixReducedOffersV1;
|
||||
extern uint256 const featureClawback;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -287,6 +287,9 @@ enum LedgerSpecificFlags {
|
||||
0x40000000, // True, has minted tokens in the past
|
||||
lsfDisallowIncomingRemit = // True, no remits allowed to this account
|
||||
0x80000000,
|
||||
lsfAMM [[maybe_unused]] = 0x0004000, // True, AMM account
|
||||
lsfAllowClawback =
|
||||
0x00008000, // True, enable clawback
|
||||
|
||||
// ltOFFER
|
||||
lsfPassive = 0x00010000,
|
||||
|
||||
@@ -92,6 +92,7 @@ enum AccountFlags : uint32_t {
|
||||
asfDisallowIncomingPayChan = 14,
|
||||
asfDisallowIncomingTrustline = 15,
|
||||
asfDisallowIncomingRemit = 16,
|
||||
asfAllowClawback = 17,
|
||||
};
|
||||
|
||||
// OfferCreate flags:
|
||||
@@ -194,6 +195,9 @@ constexpr std::uint32_t const tfClaimRewardMask = ~(tfUniversal | tfOptOut);
|
||||
// Remarks flags:
|
||||
constexpr std::uint32_t const tfImmutable = 1;
|
||||
|
||||
// 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 transaction mints/burns/buys/sells a URI TOKEN */
|
||||
ttURITOKEN_MINT = 45,
|
||||
ttURITOKEN_BURN = 46,
|
||||
@@ -165,10 +168,10 @@ enum TxType : std::uint16_t
|
||||
/** This transaction resets accumulator/counters and claims a reward for holding an average balance
|
||||
* from a specified hook */
|
||||
ttCLAIM_REWARD = 98,
|
||||
|
||||
|
||||
/** This transaction invokes a hook, providing arbitrary data. Essentially as a 0 drop payment. **/
|
||||
ttINVOKE = 99,
|
||||
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -476,6 +476,7 @@ REGISTER_FIX (fix20250131, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixRewardClaimFlags, Supported::yes, VoteBehavior::DefaultYes);
|
||||
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.
|
||||
|
||||
@@ -464,6 +464,14 @@ TxFormats::TxFormats()
|
||||
{sfRemarks, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Clawback,
|
||||
ttCLAWBACK,
|
||||
{
|
||||
{sfAmount, soeREQUIRED},
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -55,6 +55,7 @@ JSS(Check); // ledger type.
|
||||
JSS(CheckCancel); // transaction type.
|
||||
JSS(CheckCash); // transaction type.
|
||||
JSS(CheckCreate); // transaction type.
|
||||
JSS(Clawback); // transaction type.
|
||||
JSS(ClaimReward); // transaction type.
|
||||
JSS(ClearFlag); // field.
|
||||
JSS(CreateCode); // field.
|
||||
|
||||
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
|
||||
@@ -80,6 +80,9 @@ private:
|
||||
case asfDepositAuth:
|
||||
mask_ |= lsfDepositAuth;
|
||||
break;
|
||||
case asfAllowClawback:
|
||||
mask_ |= lsfAllowClawback;
|
||||
break;
|
||||
default:
|
||||
Throw<std::runtime_error>("unknown flag");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -94,6 +94,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())
|
||||
|
||||
Reference in New Issue
Block a user