mirror of
https://github.com/Xahau/xahaud.git
synced 2026-01-10 01:35:16 +00:00
Compare commits
7 Commits
fix-online
...
clawback
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88a054a547 | ||
|
|
2f2ee17ae5 | ||
|
|
d2f76185e0 | ||
|
|
16c7cd5e69 | ||
|
|
1002659103 | ||
|
|
fe6a08bb24 | ||
|
|
0a30ef8b19 |
@@ -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
|
||||
|
||||
@@ -496,6 +496,12 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
break;
|
||||
}
|
||||
|
||||
case ttCLAWBACK: {
|
||||
auto const amount = tx.getFieldAmount(sfAmount);
|
||||
ADD_TSH(amount.getIssuer(), tshWEAK);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
140
src/ripple/app/tx/impl/Clawback.cpp
Normal file
140
src/ripple/app/tx/impl/Clawback.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 AllowTrustLineClawback is not set or NoFreeze is set, return no
|
||||
// permission
|
||||
if (!(issuerFlagsIn & lsfAllowTrustLineClawback) ||
|
||||
(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
|
||||
@@ -1145,4 +1145,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
|
||||
|
||||
@@ -498,6 +498,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<
|
||||
@@ -513,7 +541,8 @@ using InvariantChecks = std::tuple<
|
||||
NoZeroEscrow,
|
||||
ValidNewAccountRoot,
|
||||
ValidNFTokenPage,
|
||||
NFTokenCountTracking>;
|
||||
NFTokenCountTracking,
|
||||
ValidClawback>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
@@ -274,6 +274,7 @@ PayChanCreate::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
TER const result = trustTransferAllowed(
|
||||
ctx.view, {account, dst}, amount.issue(), ctx.j, lhLOCKING);
|
||||
|
||||
JLOG(ctx.j.trace())
|
||||
<< "PayChanCreate::preclaim trustTransferAllowed result="
|
||||
<< result;
|
||||
|
||||
@@ -218,6 +218,37 @@ SetAccount::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Clawback
|
||||
//
|
||||
if (ctx.view.rules().enabled(featureClawback))
|
||||
{
|
||||
if (uSetFlag == asfAllowTrustLineClawback)
|
||||
{
|
||||
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 & lsfAllowTrustLineClawback)
|
||||
{
|
||||
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 == asfAllowTrustLineClawback)
|
||||
{
|
||||
JLOG(j_.trace()) << "set allow clawback";
|
||||
uFlagsOut |= lsfAllowTrustLineClawback;
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -108,6 +109,8 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
return invoke_preflight_helper<DeleteAccount>(ctx);
|
||||
case ttACCOUNT_SET:
|
||||
return invoke_preflight_helper<SetAccount>(ctx);
|
||||
case ttCLAWBACK:
|
||||
return invoke_preflight_helper<Clawback>(ctx);
|
||||
case ttCHECK_CANCEL:
|
||||
return invoke_preflight_helper<CancelCheck>(ctx);
|
||||
case ttCHECK_CASH:
|
||||
@@ -231,6 +234,8 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
return invoke_preclaim<DeleteAccount>(ctx);
|
||||
case ttACCOUNT_SET:
|
||||
return invoke_preclaim<SetAccount>(ctx);
|
||||
case ttCLAWBACK:
|
||||
return invoke_preclaim<Clawback>(ctx);
|
||||
case ttCHECK_CANCEL:
|
||||
return invoke_preclaim<CancelCheck>(ctx);
|
||||
case ttCHECK_CASH:
|
||||
@@ -316,6 +321,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
return DeleteAccount::calculateBaseFee(view, tx);
|
||||
case ttACCOUNT_SET:
|
||||
return SetAccount::calculateBaseFee(view, tx);
|
||||
case ttCLAWBACK:
|
||||
return Clawback::calculateBaseFee(view, tx);
|
||||
case ttCHECK_CANCEL:
|
||||
return CancelCheck::calculateBaseFee(view, tx);
|
||||
case ttCHECK_CASH:
|
||||
@@ -443,6 +450,10 @@ invoke_apply(ApplyContext& ctx)
|
||||
SetAccount p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttCLAWBACK: {
|
||||
Clawback p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttCHECK_CANCEL: {
|
||||
CancelCheck p(ctx);
|
||||
return p();
|
||||
|
||||
@@ -670,6 +670,12 @@ trustTransferAllowed(
|
||||
|
||||
uint32_t issuerFlags = sleIssuerAcc->getFieldU32(sfFlags);
|
||||
|
||||
// reject the creation of a locked balance (lhLOCKING) if the
|
||||
// issuer has enabled clawback
|
||||
if (lockHandling == lhLOCKING && view.rules().enabled(featureClawback) &&
|
||||
issuerFlags & lsfAllowTrustLineClawback)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
bool requireAuth = issuerFlags & lsfRequireAuth;
|
||||
|
||||
for (AccountID const& p : parties)
|
||||
|
||||
@@ -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 = 83;
|
||||
static constexpr std::size_t numFeatures = 84;
|
||||
|
||||
/** 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 fixProvisionalDoubleThreading;
|
||||
extern uint256 const featureClawback;
|
||||
extern uint256 const featureDeepFreeze;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -287,6 +287,8 @@ enum LedgerSpecificFlags {
|
||||
0x40000000, // True, has minted tokens in the past
|
||||
lsfDisallowIncomingRemit = // True, no remits allowed to this account
|
||||
0x80000000,
|
||||
lsfAllowTrustLineClawback =
|
||||
0x00001000, // True, enable clawback
|
||||
|
||||
// ltOFFER
|
||||
lsfPassive = 0x00010000,
|
||||
|
||||
@@ -92,6 +92,7 @@ enum AccountFlags : uint32_t {
|
||||
asfDisallowIncomingPayChan = 14,
|
||||
asfDisallowIncomingTrustline = 15,
|
||||
asfDisallowIncomingRemit = 16,
|
||||
asfAllowTrustLineClawback = 17,
|
||||
};
|
||||
|
||||
// OfferCreate flags:
|
||||
@@ -196,6 +197,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,
|
||||
|
||||
@@ -475,6 +475,7 @@ REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX (fix20250131, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixRewardClaimFlags, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
|
||||
@@ -464,6 +464,14 @@ TxFormats::TxFormats()
|
||||
{sfRemarks, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Clawback,
|
||||
ttCLAWBACK,
|
||||
{
|
||||
{sfAmount, soeREQUIRED},
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -56,6 +56,7 @@ JSS(CheckCancel); // transaction type.
|
||||
JSS(CheckCash); // transaction type.
|
||||
JSS(CheckCreate); // transaction type.
|
||||
JSS(ClaimReward); // transaction type.
|
||||
JSS(Clawback); // transaction type.
|
||||
JSS(ClearFlag); // field.
|
||||
JSS(CreateCode); // field.
|
||||
JSS(DeliverMin); // in: TransactionSign
|
||||
|
||||
@@ -99,6 +99,10 @@ doAccountInfo(RPC::JsonContext& context)
|
||||
{"disallowIncomingTrustline", lsfDisallowIncomingTrustline},
|
||||
{"disallowIncomingRemit", lsfDisallowIncomingRemit}}};
|
||||
|
||||
static constexpr std::pair<std::string_view, LedgerSpecificFlags>
|
||||
allowTrustLineClawbackFlag{
|
||||
"allowTrustLineClawback", lsfAllowTrustLineClawback};
|
||||
|
||||
auto const sleAccepted = ledger->read(keylet::account(accountID));
|
||||
if (sleAccepted)
|
||||
{
|
||||
@@ -126,6 +130,11 @@ doAccountInfo(RPC::JsonContext& context)
|
||||
for (auto const& lsf : disallowIncomingFlags)
|
||||
acctFlags[lsf.first.data()] = sleAccepted->isFlag(lsf.second);
|
||||
}
|
||||
|
||||
if (ledger->rules().enabled(featureClawback))
|
||||
acctFlags[allowTrustLineClawbackFlag.first.data()] =
|
||||
sleAccepted->isFlag(allowTrustLineClawbackFlag.second);
|
||||
|
||||
result[jss::account_flags] = std::move(acctFlags);
|
||||
|
||||
// Return SignerList(s) if that is requested.
|
||||
|
||||
985
src/test/app/Clawback_test.cpp
Normal file
985
src/test/app/Clawback_test.cpp
Normal file
@@ -0,0 +1,985 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
testAllowTrustLineClawbackFlag(FeatureBitset features)
|
||||
{
|
||||
testcase("Enable AllowTrustLineClawback flag");
|
||||
using namespace test::jtx;
|
||||
|
||||
// Test that one can successfully set asfAllowTrustLineClawback flag.
|
||||
// If successful, asfNoFreeze can no longer be set.
|
||||
// Also, asfAllowTrustLineClawback cannot be cleared.
|
||||
{
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
// set asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// clear asfAllowTrustLineClawback does nothing
|
||||
env(fclear(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// asfNoFreeze cannot be set when asfAllowTrustLineClawback is set
|
||||
env.require(nflags(alice, asfNoFreeze));
|
||||
env(fset(alice, asfNoFreeze), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test that asfAllowTrustLineClawback 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));
|
||||
|
||||
// asfAllowTrustLineClawback cannot be set if asfNoFreeze is set
|
||||
env(fset(alice, asfAllowTrustLineClawback), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
env.require(nflags(alice, asfAllowTrustLineClawback));
|
||||
}
|
||||
|
||||
// Test that asfAllowTrustLineClawback 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, asfAllowTrustLineClawback));
|
||||
|
||||
// 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, asfAllowTrustLineClawback), 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
}
|
||||
|
||||
// Test that one cannot enable asfAllowTrustLineClawback when
|
||||
// featureClawback amendment is disabled
|
||||
{
|
||||
Env env(*this, features - featureClawback);
|
||||
|
||||
Account alice{"alice"};
|
||||
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
env.require(nflags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// alice attempts to set asfAllowTrustLineClawback flag while
|
||||
// amendment is disabled. no error is returned, but the flag remains
|
||||
// to be unset.
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(nflags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// now enable clawback amendment
|
||||
env.enableFeature(featureClawback);
|
||||
env.close();
|
||||
|
||||
// asfAllowTrustLineClawback can be set
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
}
|
||||
}
|
||||
|
||||
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 asfAllowTrustLineClawback 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, asfAllowTrustLineClawback));
|
||||
|
||||
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 asfAllowTrustLineClawback 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// cindy sets asfAllowTrustLineClawback
|
||||
env(fset(cindy, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(cindy, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// bob sets asfAllowTrustLineClawback
|
||||
env(fset(bob, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(bob, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// bob sets asfAllowTrustLineClawback
|
||||
env(fset(bob, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(bob, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// bob sets asfAllowTrustLineClawback
|
||||
env(fset(bob, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(bob, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
static STAmount
|
||||
lockedAmount(
|
||||
test::jtx::Env const& env,
|
||||
test::jtx::Account const& account,
|
||||
test::jtx::Account const& gw,
|
||||
test::jtx::IOU const& iou)
|
||||
{
|
||||
auto const sle = env.le(keylet::line(account, gw, iou.currency));
|
||||
if (sle->isFieldPresent(sfLockedBalance))
|
||||
return (*sle)[sfLockedBalance];
|
||||
return STAmount(iou, 0);
|
||||
}
|
||||
|
||||
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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// 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 asfAllowTrustLineClawback
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.require(flags(alice, asfAllowTrustLineClawback));
|
||||
|
||||
// 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)
|
||||
{
|
||||
testAllowTrustLineClawbackFlag(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
|
||||
@@ -4320,6 +4320,54 @@ struct Escrow_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIOUClawback(FeatureBitset features)
|
||||
{
|
||||
testcase("IOU Clawback");
|
||||
using namespace test::jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
Account gw{"gw"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob, gw);
|
||||
env.close();
|
||||
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// gw sets asfAllowTrustLineClawback
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
bool const clawbackEnabled = features[featureClawback];
|
||||
if (clawbackEnabled)
|
||||
{
|
||||
env.require(flags(gw, asfAllowTrustLineClawback));
|
||||
}
|
||||
else
|
||||
{
|
||||
env.require(nflags(gw, asfAllowTrustLineClawback));
|
||||
}
|
||||
|
||||
// gw issues 1000 USD to alice
|
||||
env.trust(USD(1000), alice);
|
||||
env(pay(gw, alice, USD(1000)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(alice, USD) == USD(1000));
|
||||
BEAST_EXPECT(env.balance(bob, USD) == USD(0));
|
||||
|
||||
// alice escrows token fails; cannot escrow clawable tokens
|
||||
auto const createResult =
|
||||
clawbackEnabled ? ter(tecNO_PERMISSION) : ter(tesSUCCESS);
|
||||
env(escrow::create(alice, bob, USD(10)),
|
||||
escrow::finish_time(env.now() + 1s),
|
||||
createResult);
|
||||
env.close();
|
||||
}
|
||||
|
||||
static uint256
|
||||
getEscrowIndex(AccountID const& account, std::uint32_t uSequence)
|
||||
{
|
||||
@@ -4589,6 +4637,7 @@ struct Escrow_test : public beast::unit_test::suite
|
||||
testIOUTLFreeze(features);
|
||||
testIOUTLINSF(features);
|
||||
testIOUPrecisionLoss(features);
|
||||
testIOUClawback(features);
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -4599,6 +4648,7 @@ public:
|
||||
FeatureBitset const all{supported_amendments()};
|
||||
testWithFeats(all - featurePaychanAndEscrowForTokens);
|
||||
testWithFeats(all);
|
||||
testIOUWithFeats(all - featureClawback);
|
||||
testIOUWithFeats(all);
|
||||
testEscrowID(all);
|
||||
}
|
||||
|
||||
@@ -5616,6 +5616,53 @@ struct PayChan_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIOUClawback(FeatureBitset features)
|
||||
{
|
||||
testcase("IOU Clawback");
|
||||
using namespace test::jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Env env(*this, features);
|
||||
Account alice{"alice"};
|
||||
Account bob{"bob"};
|
||||
Account gw{"gw"};
|
||||
|
||||
env.fund(XRP(1000), alice, bob, gw);
|
||||
env.close();
|
||||
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
// gw sets asfAllowTrustLineClawback
|
||||
env(fset(gw, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
bool const clawbackEnabled = features[featureClawback];
|
||||
if (clawbackEnabled)
|
||||
{
|
||||
env.require(flags(gw, asfAllowTrustLineClawback));
|
||||
}
|
||||
else
|
||||
{
|
||||
env.require(nflags(gw, asfAllowTrustLineClawback));
|
||||
}
|
||||
|
||||
// gw issues 1000 USD to alice
|
||||
env.trust(USD(1000), alice);
|
||||
env(pay(gw, alice, USD(1000)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(alice, USD) == USD(1000));
|
||||
BEAST_EXPECT(env.balance(bob, USD) == USD(0));
|
||||
|
||||
// alice paychan token fails; cannot escrow clawable tokens
|
||||
auto const createResult =
|
||||
clawbackEnabled ? ter(tecNO_PERMISSION) : ter(tesSUCCESS);
|
||||
env(paychan::create(alice, bob, USD(10), 100s, alice.pk()),
|
||||
ter(createResult));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
@@ -5672,6 +5719,7 @@ struct PayChan_test : public beast::unit_test::suite
|
||||
testIOUTLINSF(features);
|
||||
testIOUMismatchFunding(features);
|
||||
testIOUPrecisionLoss(features);
|
||||
testIOUClawback(features);
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -5685,6 +5733,7 @@ public:
|
||||
all - disallowIncoming - featurePaychanAndEscrowForTokens);
|
||||
testWithFeats(all);
|
||||
testIOUWithFeats(all - disallowIncoming);
|
||||
testIOUWithFeats(all - featureClawback);
|
||||
testIOUWithFeats(all);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1237,6 +1237,90 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testClawbackTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("clawback tsh");
|
||||
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
// otxn: IOU issuer
|
||||
// tsh issuer
|
||||
// w/s: strong
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const issuer = Account("gw");
|
||||
auto const holder = Account("bob");
|
||||
env.fund(XRP(1000), issuer, holder);
|
||||
env.close();
|
||||
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
env.trust(issuer["USD"](1000), holder);
|
||||
env(pay(issuer, holder, issuer["USD"](1000)));
|
||||
env.close();
|
||||
|
||||
// set tsh collect
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, issuer);
|
||||
|
||||
// set tsh hook
|
||||
setTSHHook(env, issuer, testStrong);
|
||||
|
||||
// clawback
|
||||
env(claw(issuer, holder["USD"](1000)), fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
// verify tsh hook triggered
|
||||
testTSHStrongWeak(env, tshSTRONG, __LINE__);
|
||||
}
|
||||
|
||||
// otxn: IOU issuer
|
||||
// tsh holder
|
||||
// w/s: weak
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const issuer = Account("gw");
|
||||
auto const holder = Account("bob");
|
||||
env.fund(XRP(1000), issuer, holder);
|
||||
env.close();
|
||||
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
env.trust(issuer["USD"](1000), holder);
|
||||
env(pay(issuer, holder, issuer["USD"](1000)));
|
||||
env.close();
|
||||
|
||||
// set tsh collect
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, holder);
|
||||
|
||||
// set tsh hook
|
||||
setTSHHook(env, holder, testStrong);
|
||||
|
||||
// clawback
|
||||
env(claw(issuer, holder["USD"](1000)), fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
// verify tsh hook triggered
|
||||
auto const expected = testStrong ? tshNONE : tshWEAK;
|
||||
testTSHStrongWeak(env, expected, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
// DepositPreauth
|
||||
// | otxn | tsh | preauth |
|
||||
// | A | A | S |
|
||||
@@ -5499,6 +5583,7 @@ private:
|
||||
testCheckCashTSH(features);
|
||||
testCheckCreateTSH(features);
|
||||
testClaimRewardTSH(features);
|
||||
testClawbackTSH(features);
|
||||
testDepositPreauthTSH(features);
|
||||
testEscrowCancelTSH(features);
|
||||
testEscrowIDCancelTSH(features);
|
||||
|
||||
@@ -80,6 +80,9 @@ private:
|
||||
case asfDepositAuth:
|
||||
mask_ |= lsfDepositAuth;
|
||||
break;
|
||||
case asfAllowTrustLineClawback:
|
||||
mask_ |= lsfAllowTrustLineClawback;
|
||||
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
|
||||
|
||||
@@ -511,13 +511,16 @@ public:
|
||||
|
||||
Env env(*this, features);
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
|
||||
auto getAccountFlag = [&env, &alice](std::string_view fName) {
|
||||
auto getAccountFlag = [&env](
|
||||
std::string_view fName,
|
||||
Account const& account) {
|
||||
auto const info = env.rpc(
|
||||
"json",
|
||||
"account_info",
|
||||
R"({"account" : ")" + alice.human() + R"("})");
|
||||
R"({"account" : ")" + account.human() + R"("})");
|
||||
|
||||
std::optional<bool> res;
|
||||
if (info[jss::result][jss::status] == "success" &&
|
||||
@@ -546,7 +549,7 @@ public:
|
||||
// as expected
|
||||
env(fclear(alice, asf.second));
|
||||
env.close();
|
||||
auto const f1 = getAccountFlag(asf.first);
|
||||
auto const f1 = getAccountFlag(asf.first, alice);
|
||||
BEAST_EXPECT(f1.has_value());
|
||||
BEAST_EXPECT(!f1.value());
|
||||
|
||||
@@ -554,7 +557,7 @@ public:
|
||||
// as expected
|
||||
env(fset(alice, asf.second));
|
||||
env.close();
|
||||
auto const f2 = getAccountFlag(asf.first);
|
||||
auto const f2 = getAccountFlag(asf.first, alice);
|
||||
BEAST_EXPECT(f2.has_value());
|
||||
BEAST_EXPECT(f2.value());
|
||||
}
|
||||
@@ -578,7 +581,7 @@ public:
|
||||
// as expected
|
||||
env(fclear(alice, asf.second));
|
||||
env.close();
|
||||
auto const f1 = getAccountFlag(asf.first);
|
||||
auto const f1 = getAccountFlag(asf.first, alice);
|
||||
BEAST_EXPECT(f1.has_value());
|
||||
BEAST_EXPECT(!f1.value());
|
||||
|
||||
@@ -586,7 +589,7 @@ public:
|
||||
// as expected
|
||||
env(fset(alice, asf.second));
|
||||
env.close();
|
||||
auto const f2 = getAccountFlag(asf.first);
|
||||
auto const f2 = getAccountFlag(asf.first, alice);
|
||||
BEAST_EXPECT(f2.has_value());
|
||||
BEAST_EXPECT(f2.value());
|
||||
}
|
||||
@@ -595,9 +598,35 @@ public:
|
||||
{
|
||||
for (auto& asf : disallowIncomingFlags)
|
||||
{
|
||||
BEAST_EXPECT(!getAccountFlag(asf.first));
|
||||
BEAST_EXPECT(!getAccountFlag(asf.first, alice));
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr std::pair<std::string_view, std::uint32_t>
|
||||
allowTrustLineClawbackFlag{
|
||||
"allowTrustLineClawback", asfAllowTrustLineClawback};
|
||||
|
||||
if (features[featureClawback])
|
||||
{
|
||||
// must use bob's account because alice has noFreeze set
|
||||
auto const f1 =
|
||||
getAccountFlag(allowTrustLineClawbackFlag.first, bob);
|
||||
BEAST_EXPECT(f1.has_value());
|
||||
BEAST_EXPECT(!f1.value());
|
||||
|
||||
// Set allowTrustLineClawback
|
||||
env(fset(bob, allowTrustLineClawbackFlag.second));
|
||||
env.close();
|
||||
auto const f2 =
|
||||
getAccountFlag(allowTrustLineClawbackFlag.first, bob);
|
||||
BEAST_EXPECT(f2.has_value());
|
||||
BEAST_EXPECT(f2.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
!getAccountFlag(allowTrustLineClawbackFlag.first, bob));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -612,6 +641,8 @@ public:
|
||||
ripple::test::jtx::supported_amendments()};
|
||||
testAccountFlags(allFeatures);
|
||||
testAccountFlags(allFeatures - featureDisallowIncoming);
|
||||
testAccountFlags(
|
||||
allFeatures - featureDisallowIncoming - featureClawback);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -95,6 +95,13 @@ public:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flag == asfAllowTrustLineClawback)
|
||||
{
|
||||
// The asfAllowTrustLineClawback 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