Skip to content

Commit

Permalink
XLS-39 Clawback: (#4553)
Browse files Browse the repository at this point in the history
Introduces:
* AccountRoot flag: lsfAllowClawback
* New Clawback transaction
* More info on clawback spec: https:/XRPLF/XRPL-Standards/tree/master/XLS-39d-clawback
  • Loading branch information
shawnxie999 authored Jun 26, 2023
1 parent 9eb30d4 commit b7e902d
Show file tree
Hide file tree
Showing 19 changed files with 1,343 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Builds/CMake/RippledCore.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ target_sources (rippled PRIVATE
src/ripple/app/tx/impl/CancelOffer.cpp
src/ripple/app/tx/impl/CashCheck.cpp
src/ripple/app/tx/impl/Change.cpp
src/ripple/app/tx/impl/Clawback.cpp
src/ripple/app/tx/impl/CreateCheck.cpp
src/ripple/app/tx/impl/CreateOffer.cpp
src/ripple/app/tx/impl/CreateTicket.cpp
Expand Down Expand Up @@ -692,6 +693,7 @@ if (tests)
src/test/app/AccountTxPaging_test.cpp
src/test/app/AmendmentTable_test.cpp
src/test/app/Check_test.cpp
src/test/app/Clawback_test.cpp
src/test/app/CrossingLimits_test.cpp
src/test/app/DeliverMin_test.cpp
src/test/app/DepositAuth_test.cpp
Expand Down
138 changes: 138 additions & 0 deletions src/ripple/app/tx/impl/Clawback.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https:/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 changes: 48 additions & 0 deletions src/ripple/app/tx/impl/Clawback.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https:/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
59 changes: 59 additions & 0 deletions src/ripple/app/tx/impl/InvariantCheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <ripple/basics/FeeUnits.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/ReadView.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/STArray.h>
#include <ripple/protocol/SystemParameters.h>
Expand Down Expand Up @@ -717,4 +718,62 @@ NFTokenCountTracking::finalize(
return true;
}

//------------------------------------------------------------------------------

void
ValidClawback::visitEntry(
bool,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const&)
{
if (before && before->getType() == ltRIPPLE_STATE)
trustlinesChanged++;
}

bool
ValidClawback::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
if (tx.getTxnType() != ttCLAWBACK)
return true;

if (result == tesSUCCESS)
{
if (trustlinesChanged > 1)
{
JLOG(j.fatal())
<< "Invariant failed: more than one trustline changed.";
return false;
}

AccountID const issuer = tx.getAccountID(sfAccount);
STAmount const amount = tx.getFieldAmount(sfAmount);
AccountID const& holder = amount.getIssuer();
STAmount const holderBalance = accountHolds(
view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);

if (holderBalance.signum() < 0)
{
JLOG(j.fatal())
<< "Invariant failed: trustline balance is negative";
return false;
}
}
else
{
if (trustlinesChanged != 0)
{
JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
"despite failure of the transaction.";
return false;
}
}

return true;
}

} // namespace ripple
31 changes: 30 additions & 1 deletion src/ripple/app/tx/impl/InvariantCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,34 @@ class NFTokenCountTracking
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<
Expand All @@ -402,7 +430,8 @@ using InvariantChecks = std::tuple<
NoZeroEscrow,
ValidNewAccountRoot,
ValidNFTokenPage,
NFTokenCountTracking>;
NFTokenCountTracking,
ValidClawback>;

/**
* @brief get a tuple of all invariant checks
Expand Down
39 changes: 39 additions & 0 deletions src/ripple/app/tx/impl/SetAccount.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -562,6 +593,14 @@ SetAccount::doApply()
uFlagsOut &= ~lsfDisallowIncomingTrustline;
}

// Set flag for clawback
if (ctx_.view().rules().enabled(featureClawback) &&
uSetFlag == asfAllowClawback)
{
JLOG(j_.trace()) << "set allow clawback";
uFlagsOut |= lsfAllowClawback;
}

if (uFlagsIn != uFlagsOut)
sle->setFieldU32(sfFlags, uFlagsOut);

Expand Down
Loading

0 comments on commit b7e902d

Please sign in to comment.