Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XLS-39 Clawback: #4553

Merged
merged 19 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
173 changes: 173 additions & 0 deletions src/ripple/app/tx/impl/Clawback.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//------------------------------------------------------------------------------
/*
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/Directory.h>
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Protocol.h>
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/st.h>
#include <boost/endian/conversion.hpp>
#include <array>
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

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.getAccountID(sfAccount);
STAmount const clawAmount(ctx.tx.getFieldAmount(sfAmount));
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// The issuer field is used for the token holder instead
AccountID const holder = clawAmount.getIssuer();
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

if (issuer == holder || isXRP(clawAmount) || clawAmount <= beast::zero)
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
return temBAD_AMOUNT;

return preflight2(ctx);
}

TER
Clawback::preclaim(PreclaimContext const& ctx)
{
AccountID const issuer = ctx.tx.getAccountID(sfAccount);
STAmount const clawAmount(ctx.tx.getFieldAmount(sfAmount));
AccountID const holder = clawAmount.getIssuer();
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

auto const sleIssuer = ctx.view.read(keylet::account(issuer));
auto const sleHolder = ctx.view.read(keylet::account(holder));
if (!sleIssuer || !sleHolder)
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
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;
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

auto const sleRippleState =
ctx.view.read(keylet::line(holder, issuer, clawAmount.getCurrency()));
if (!sleRippleState)
return tecNO_LINE;
Comment on lines +72 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loading a SLE is an expensive operation, and you could avoid it, since accountHolds will also do it. Of course, it won't tell you if the trust line doesn't exist, but you could (a) either improve the interface of the (legacy) accountHolds to return an std::optional<STAmount>; or (b) by returning a more generic error code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SLE of the trustline is fetched to check the account must be the issuer in the trustline. Without loading it and purely by using accountHolds, I would not be able to know that

    // 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;

Copy link
Collaborator Author

@shawnxie999 shawnxie999 Jun 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would like to explicitly check the high/low account and the balance polarity to verify that the issuer is the one who submitted this transaction. Even though checking the sign of what accountHolds returns could work too, but that is not the purpose of this function, and behaviors could possibly change too. I think making an extra call is worth it in this case, because I would like to be fully confident that the real issuer is the one submitting this transaction, not the token holder.


STAmount const balance = sleRippleState->getFieldAmount(sfBalance);
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're doing precisely what you advise should not be done in your comment a few lines down (starting on line 89).

I think the better path forward is to add a flag to accountHolds similar to fhIGNORE_FREEZE, called fhINCLUDE_ESCROW which instructs that function to return the full balance, including any escrowed amounts (since, IMO, the issuer should be able to clawback escrowed amounts), and leaving it up to the IOU escrow code to handle the situation in accountHolds when/if it gets merged.

Also, I think that tecINSUFFICIENT_FUNDS is a better error code here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if-condition is purely used to check that the account of this transaction is the issuer, by comparing high/low account with the polarity of the balance. It's not used to check the balance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if we want to be able to claw back escrowed amounts, I think it would be better to propose a separate amendment after both PRs are merged. I'm don't think it is a good idea to have two PRs making changes for it in parallel

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think escrow or pay channels should be included in the clawback if AMM is excluded.

The justification is "it only allows an issuer to claw back the funds that are spendable".

Escrowed or Paychan iou tokens are locked and are not spendable. Therefore escrow and paychannel should be excluded.


// 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 tecNO_LINE;
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

return tesSUCCESS;
}

TER
Clawback::clawback(
AccountID const& issuer,
AccountID const& holder,
STAmount const& amount)
{
// This should never happen
if (amount <= beast::zero || issuer != amount.getIssuer())
return tecINTERNAL;

STAmount const holderBalance = accountHolds(
view(),
holder,
amount.getCurrency(),
amount.getIssuer(),
fhIGNORE_FREEZE,
j_);
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// The amount to be clawed back can never exceed the amount that
// the token holder current owns
if (amount > holderBalance)
return tecINTERNAL;

auto const result = accountSend(view(), holder, issuer, amount, j_);

if (!isTesSuccess(result))
return result;

if (accountHolds(
view(),
holder,
amount.getCurrency(),
amount.getIssuer(),
fhIGNORE_FREEZE,
j_)
.signum() < 0)
return tecINTERNAL;
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

return tesSUCCESS;
}

TER
Clawback::doApply()
{
AccountID const issuer = account_;
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
STAmount clawAmount(ctx_.tx.getFieldAmount(sfAmount));
AccountID const holder = clawAmount.getIssuer();
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// Replace the `issuer` field with holder's account
clawAmount.setIssuer(issuer);
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// Get the spendable balance. Must use `accountHolds`.
STAmount const spendableAmount = accountHolds(
view(),
holder,
clawAmount.getCurrency(),
clawAmount.getIssuer(),
fhIGNORE_FREEZE,
j_);

return clawback(issuer, holder, std::min(spendableAmount, clawAmount));
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
}

} // namespace ripple
55 changes: 55 additions & 0 deletions src/ripple/app/tx/impl/Clawback.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
/*
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
{
private:
TER
clawback(
AccountID const& issuer,
AccountID const& holder,
STAmount const& amount);

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
35 changes: 35 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,25 @@ SetAccount::preclaim(PreclaimContext const& ctx)
}
}

//
// Clawback
//
if (ctx.view.rules().enabled(featureClawback) &&
(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;
}
}

return tesSUCCESS;
}

Expand Down Expand Up @@ -361,6 +380,14 @@ SetAccount::doApply()
return tecNEED_MASTER_KEY;
}

// Cannot set NoFreeze if clawback is enabled
if (ctx_.view().rules().enabled(featureClawback) &&
(uFlagsIn & lsfAllowClawback))
{
JLOG(j_.trace()) << "Can't set NoFreeze if clawback is enabled";
return tecNO_PERMISSION;
}

shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
JLOG(j_.trace()) << "Set NoFreeze flag";
uFlagsOut |= lsfNoFreeze;
}
Expand Down Expand Up @@ -562,6 +589,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;
}
Comment on lines +597 to +602
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code to clear the lsfAllowClawback flag (i.e. to disclaim your ability to clawback) is missing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this flag is never meant to be cleared. Once set, it cannot be cleared

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why? This is the inverse of "no freeze" and it should definitely be a flag you can clear.

Copy link
Collaborator Author

@shawnxie999 shawnxie999 Jun 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nbougalis The reason for making clawback opt-out by default was purely because, having clawback opt-in was a poor idea and would raise a lot of concerns, just like how having freeze opt-in by default was a bad choice. The verdict is that we would like "opt-out" these kind of regulatory features in the future, and clawback would be the first one to follow that.

What is really the incentive for making allow clawback a mutable field? If they really want to mutate it, then I don't see why can't they create a new account. It would also create unnecessary complexity and confusion as well, since AllowClawback and NoFreeze are interdependent on each other.

But I think it would be a good practice to stick with immutable fields for regulatory features like freeze or clawback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you misunderstand: I'm not suggesting that clawback should be opt-out, that would be a bad idea. I was just saying that I would allow it to be unset, but I don't feel strongly enough about it.

Copy link
Collaborator Author

@shawnxie999 shawnxie999 Jun 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure if there is any need for this. But if we find out that unsetting the flag is really wanted somehow, I think it would be fine to add it through an amendment later. This would be a rather small change. But I think it would be a good idea to keep it simple for now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going on a tangent here: Since Clawback and NoFreeze are complimentary to one another, is it a good idea to frame this condition as an invariant? I would eliminate such a check in other parts of the code right?

Copy link
Collaborator Author

@shawnxie999 shawnxie999 Jun 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ckeshava I don't think this change would eliminate such checks in other parts of the code. Invariant check is executed after a transaction.

And it will still be mandatory to check this complementary property during the preclaim (before an transaction) of an AccountSet.


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

Expand Down
11 changes: 11 additions & 0 deletions src/ripple/app/tx/impl/applySteps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <ripple/app/tx/impl/CancelOffer.h>
#include <ripple/app/tx/impl/CashCheck.h>
#include <ripple/app/tx/impl/Change.h>
#include <ripple/app/tx/impl/Clawback.h>
#include <ripple/app/tx/impl/CreateCheck.h>
#include <ripple/app/tx/impl/CreateOffer.h>
#include <ripple/app/tx/impl/CreateTicket.h>
Expand Down Expand Up @@ -147,6 +148,8 @@ invoke_preflight(PreflightContext const& ctx)
return invoke_preflight_helper<NFTokenCancelOffer>(ctx);
case ttNFTOKEN_ACCEPT_OFFER:
return invoke_preflight_helper<NFTokenAcceptOffer>(ctx);
case ttCLAWBACK:
return invoke_preflight_helper<Clawback>(ctx);
default:
assert(false);
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
Expand Down Expand Up @@ -248,6 +251,8 @@ invoke_preclaim(PreclaimContext const& ctx)
return invoke_preclaim<NFTokenCancelOffer>(ctx);
case ttNFTOKEN_ACCEPT_OFFER:
return invoke_preclaim<NFTokenAcceptOffer>(ctx);
case ttCLAWBACK:
return invoke_preclaim<Clawback>(ctx);
default:
assert(false);
return temUNKNOWN;
Expand Down Expand Up @@ -311,6 +316,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
return NFTokenCancelOffer::calculateBaseFee(view, tx);
case ttNFTOKEN_ACCEPT_OFFER:
return NFTokenAcceptOffer::calculateBaseFee(view, tx);
case ttCLAWBACK:
return Clawback::calculateBaseFee(view, tx);
default:
assert(false);
return XRPAmount{0};
Expand Down Expand Up @@ -463,6 +470,10 @@ invoke_apply(ApplyContext& ctx)
NFTokenAcceptOffer p(ctx);
return p();
}
case ttCLAWBACK: {
Clawback p(ctx);
return p();
}
default:
assert(false);
return {temUNKNOWN, false};
Expand Down
3 changes: 2 additions & 1 deletion src/ripple/protocol/Feature.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 58;
static constexpr std::size_t numFeatures = 59;

/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
Expand Down Expand Up @@ -345,6 +345,7 @@ extern uint256 const featureXRPFees;
extern uint256 const fixUniversalNumber;
extern uint256 const fixNonFungibleTokensV1_2;
extern uint256 const fixNFTokenRemint;
extern uint256 const featureClawback;

} // namespace ripple

Expand Down
5 changes: 5 additions & 0 deletions src/ripple/protocol/LedgerFormats.h
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ enum LedgerSpecificFlags {
0x10000000, // True, reject new paychans
lsfDisallowIncomingTrustline =
0x20000000, // True, reject new trustlines (only if no issued assets)
/* // reserved for AMM amendment
lsfAMM = 0x40000000, // True, AMM account
*/
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
lsfAllowClawback =
0x80000000, // True, enable clawback
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// ltOFFER
lsfPassive = 0x00010000,
Expand Down
4 changes: 4 additions & 0 deletions src/ripple/protocol/TxFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ constexpr std::uint32_t asfDisallowIncomingNFTokenOffer = 12;
constexpr std::uint32_t asfDisallowIncomingCheck = 13;
constexpr std::uint32_t asfDisallowIncomingPayChan = 14;
constexpr std::uint32_t asfDisallowIncomingTrustline = 15;
constexpr std::uint32_t asfAllowClawback = 16;

// OfferCreate flags:
constexpr std::uint32_t tfPassive = 0x00010000;
Expand Down Expand Up @@ -159,6 +160,9 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal);
// NFTokenAcceptOffer flags:
constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal;

// Clawback flags:
constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// clang-format on

} // namespace ripple
Expand Down
Loading