Skip to content

Commit

Permalink
MPT clawback (XRPLF#24)
Browse files Browse the repository at this point in the history
* wip

* test pass

* remove newline

* tests

* use getcurrency

* clang

* comment

* move clawback test to mpt

* comments

* clang

* remove ref for temp value
  • Loading branch information
shawnxie999 authored Feb 29, 2024
1 parent cb829d0 commit f5cde4b
Show file tree
Hide file tree
Showing 13 changed files with 498 additions and 64 deletions.
172 changes: 121 additions & 51 deletions src/ripple/app/tx/impl/Clawback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//==============================================================================

#include <ripple/app/tx/impl/Clawback.h>
#include <ripple/basics/MPTAmount.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
Expand All @@ -33,23 +34,44 @@ Clawback::preflight(PreflightContext const& ctx)
if (!ctx.rules.enabled(featureClawback))
return temDISABLED;

auto const mptHolder = ctx.tx[~sfMPTokenHolder];
STAmount const clawAmount = ctx.tx[sfAmount];
if ((mptHolder || clawAmount.isMPT()) &&
!ctx.rules.enabled(featureMPTokensV1))
return temDISABLED;

if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;

if (!mptHolder && clawAmount.isMPT())
return temMALFORMED;

if (mptHolder && !clawAmount.isMPT())
return temMALFORMED;

if (ctx.tx.getFlags() & tfClawbackMask)
return temINVALID_FLAG;

AccountID const issuer = ctx.tx[sfAccount];
STAmount const clawAmount = ctx.tx[sfAmount];

if (clawAmount.isMPT())
return temMPT_NOT_SUPPORTED;

// The issuer field is used for the token holder instead
AccountID const& holder = clawAmount.getIssuer();
// The issuer field is used for the token holder if asset is IOU
AccountID const& holder =
clawAmount.isMPT() ? *mptHolder : clawAmount.getIssuer();

if (issuer == holder || isXRP(clawAmount) || clawAmount <= beast::zero)
return temBAD_AMOUNT;
if (clawAmount.isMPT())
{
if (issuer == holder)
return temMALFORMED;

if (clawAmount.mpt() > MPTAmount{maxMPTokenAmount} ||
clawAmount <= beast::zero)
return temBAD_AMOUNT;
}
else
{
if (issuer == holder || isXRP(clawAmount) || clawAmount <= beast::zero)
return temBAD_AMOUNT;
}

return preflight2(ctx);
}
Expand All @@ -59,7 +81,8 @@ Clawback::preclaim(PreclaimContext const& ctx)
{
AccountID const issuer = ctx.tx[sfAccount];
STAmount const clawAmount = ctx.tx[sfAmount];
AccountID const& holder = clawAmount.getIssuer();
AccountID const& holder =
clawAmount.isMPT() ? ctx.tx[sfMPTokenHolder] : clawAmount.getIssuer();

auto const sleIssuer = ctx.view.read(keylet::account(issuer));
auto const sleHolder = ctx.view.read(keylet::account(holder));
Expand All @@ -69,46 +92,75 @@ Clawback::preclaim(PreclaimContext const& ctx)
if (sleHolder->isFieldPresent(sfAMMID))
return tecAMM_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;
if (clawAmount.isMPT())
{
auto const issuanceKey =
keylet::mptIssuance(clawAmount.mptIssue().mpt());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;

if (!((*sleIssuance)[sfFlags] & lsfMPTCanClawback))
return tecNO_PERMISSION;

if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION;

if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, holder)))
return tecOBJECT_NOT_FOUND;

if (accountHolds(
ctx.view,
holder,
clawAmount.mptIssue(),
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
ctx.j) <= beast::zero)
return tecINSUFFICIENT_FUNDS;
}
else
{
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;
}
Expand All @@ -118,9 +170,27 @@ Clawback::doApply()
{
AccountID const& issuer = account_;
STAmount clawAmount = ctx_.tx[sfAmount];
AccountID const holder = clawAmount.getIssuer(); // cannot be reference
AccountID const holder = clawAmount.isMPT()
? ctx_.tx[sfMPTokenHolder]
: clawAmount.getIssuer(); // cannot be reference because clawAmount is
// modified beblow

if (clawAmount.isMPT())
{
// Get the spendable balance. Must use `accountHolds`.
STAmount const spendableAmount = accountHolds(
view(),
holder,
clawAmount.mptIssue(),
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_);

return rippleMPTCredit(
view(), holder, issuer, std::min(spendableAmount, clawAmount), j_);
}

// Replace the `issuer` field with issuer's account
// Replace the `issuer` field with issuer's account if asset is IOU
clawAmount.setIssuer(issuer);
if (holder == issuer)
return tecINTERNAL;
Expand Down
36 changes: 28 additions & 8 deletions src/ripple/app/tx/impl/InvariantCheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,9 @@ ValidClawback::visitEntry(
{
if (before && before->getType() == ltRIPPLE_STATE)
trustlinesChanged++;

if (before && before->getType() == ltMPTOKEN)
mptokensChanged++;
}

bool
Expand All @@ -775,18 +778,28 @@ ValidClawback::finalize(
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)
if (mptokensChanged > 1)
{
JLOG(j.fatal())
<< "Invariant failed: trustline balance is negative";
<< "Invariant failed: more than one mptokens changed.";
return false;
}

if (trustlinesChanged == 1)
{
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
{
Expand All @@ -796,6 +809,13 @@ ValidClawback::finalize(
"despite failure of the transaction.";
return false;
}

if (mptokensChanged != 0)
{
JLOG(j.fatal()) << "Invariant failed: some mptokens were changed "
"despite failure of the transaction.";
return false;
}
}

return true;
Expand Down
1 change: 1 addition & 0 deletions src/ripple/app/tx/impl/InvariantCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ class NFTokenCountTracking
class ValidClawback
{
std::uint32_t trustlinesChanged = 0;
std::uint32_t mptokensChanged = 0;

public:
void
Expand Down
12 changes: 12 additions & 0 deletions src/ripple/ledger/View.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ hasExpired(ReadView const& view, std::optional<std::uint32_t> const& exp);
/** Controls the treatment of frozen account balances */
enum FreezeHandling { fhIGNORE_FREEZE, fhZERO_IF_FROZEN };

/** Controls the treatment of unauthorized MPT balances */
enum AuthHandling { ahIGNORE_AUTH, ahZERO_IF_UNAUTHORIZED };

[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, AccountID const& issuer);

Expand Down Expand Up @@ -142,6 +145,15 @@ accountHolds(
FreezeHandling zeroIfFrozen,
beast::Journal j);

[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
AccountID const& account,
MPTIssue const& issue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j);

// Returns the amount an account can spend of the currency type saDefault, or
// returns saDefault if this account is the issuer of the currency in
// question. Should be used in favor of accountHolds when questioning how much
Expand Down
41 changes: 41 additions & 0 deletions src/ripple/ledger/impl/View.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,47 @@ accountHolds(
view, account, issue.currency, issue.account, zeroIfFrozen, j);
}

STAmount
accountHolds(
ReadView const& view,
AccountID const& account,
MPTIssue const& issue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j)
{
STAmount amount;

auto const sleMpt = view.read(keylet::mptoken(issue.mpt(), account));
if (!sleMpt)
amount.clear(issue);
else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(view, account, issue))
amount.clear(issue);
else
{
auto const amt = sleMpt->getFieldU64(sfMPTAmount);
auto const locked = sleMpt->getFieldU64(sfLockedAmount);
if (amt > locked)
amount = STAmount{issue, amt - locked};

// only if auth check is needed, as it needs to do an additional read
// operation
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED)
{
auto const sleIssuance =
view.read(keylet::mptIssuance(issue.getMptID()));

// if auth is enabled on the issuance and mpt is not authorized,
// clear amount
if (sleIssuance && sleIssuance->isFlag(lsfMPTRequireAuth) &&
!sleMpt->isFlag(lsfMPTAuthorized))
amount.clear(issue);
}
}

return amount;
}

STAmount
accountFunds(
ReadView const& view,
Expand Down
8 changes: 5 additions & 3 deletions src/ripple/protocol/impl/STTx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -535,19 +535,21 @@ isMemoOkay(STObject const& st, std::string& reason)
}

// Ensure all account fields are 160-bits and that MPT amount is only passed
// to Payment tx (until MPT is supported in more tx)
// to Payment or Clawback tx (until MPT is supported in more tx)
static bool
isAccountAndMPTFieldOkay(STObject const& st)
{
auto const txType = st[~sfTransactionType];
bool const isPaymentTx = txType && safe_cast<TxType>(*txType) == ttPAYMENT;
static std::unordered_set<TxType> const mptAmountTx{ttPAYMENT, ttCLAWBACK};
bool const isMPTAmountAllowed = txType &&
(mptAmountTx.find(safe_cast<TxType>(*txType)) != mptAmountTx.end());
for (int i = 0; i < st.getCount(); ++i)
{
auto t = dynamic_cast<STAccount const*>(st.peekAtPIndex(i));
if (t && t->isDefault())
return false;
auto amt = dynamic_cast<STAmount const*>(st.peekAtPIndex(i));
if (amt && amt->isMPT() && !isPaymentTx)
if (amt && amt->isMPT() && !isMPTAmountAllowed)
return false;
}

Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/impl/TxFormats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ TxFormats::TxFormats()
ttCLAWBACK,
{
{sfAmount, soeREQUIRED},
{sfMPTokenHolder, soeOPTIONAL},
},
commonFields);

Expand Down
Loading

0 comments on commit f5cde4b

Please sign in to comment.