mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-06 10:16:45 +00:00
Compare commits
4 Commits
bthomee/no
...
gregtatcam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
981caa344f | ||
|
|
2e662bc55d | ||
|
|
90565e6450 | ||
|
|
7481430413 |
@@ -310,17 +310,20 @@ public:
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariant: Token holder's trustline balance cannot be negative after
|
||||
* Clawback.
|
||||
* @brief Invariant: Token holder's trustline/MPT balance cannot be invalid
|
||||
* 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.
|
||||
* non-negative. When featureMPTokensV2 is enabled, also verify the holder's
|
||||
* raw trustline/MPToken balance decreased by the clawed amount.
|
||||
*/
|
||||
class ValidClawback
|
||||
{
|
||||
std::uint32_t trustlinesChanged_ = 0;
|
||||
std::uint32_t mptokensChanged_ = 0;
|
||||
std::shared_ptr<SLE const> tokenBefore_;
|
||||
std::shared_ptr<SLE const> tokenAfter_;
|
||||
|
||||
public:
|
||||
void
|
||||
@@ -413,7 +416,7 @@ using InvariantChecks = std::tuple<
|
||||
ValidLoanBroker,
|
||||
ValidLoan,
|
||||
ValidVault,
|
||||
ValidMPTPayment,
|
||||
ValidMPTBalanceChanges,
|
||||
ValidAmounts,
|
||||
ValidMPTTransfer>;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public:
|
||||
* - OutstandingAmount after = OutstandingAmount before +
|
||||
* sum (MPT after - MPT before) - this is total MPT credit/debit
|
||||
*/
|
||||
class ValidMPTPayment
|
||||
class ValidMPTBalanceChanges
|
||||
{
|
||||
enum class Order { Before = 0, After = 1 };
|
||||
struct MPTData
|
||||
|
||||
@@ -755,14 +755,49 @@ ValidNewAccountRoot::finalize(
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
static std::optional<STAmount>
|
||||
clawbackTrustLineBalanceInHolderTerms(
|
||||
std::shared_ptr<SLE const> const& sle,
|
||||
AccountID const& holder,
|
||||
AccountID const& issuer,
|
||||
Currency const& currency)
|
||||
{
|
||||
if (!sle)
|
||||
return STAmount{Issue{currency, issuer}};
|
||||
|
||||
if (sle->getType() != ltRIPPLE_STATE ||
|
||||
sle->key() != keylet::line(holder, issuer, currency).key)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
STAmount balance = sle->getFieldAmount(sfBalance);
|
||||
if (holder > issuer)
|
||||
balance.negate();
|
||||
balance.get<Issue>().account = issuer;
|
||||
return balance;
|
||||
}
|
||||
|
||||
void
|
||||
ValidClawback::visitEntry(bool, SLE::const_ref before, SLE::const_ref)
|
||||
ValidClawback::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
if (before && before->getType() == ltRIPPLE_STATE)
|
||||
{
|
||||
trustlinesChanged_++;
|
||||
tokenBefore_ = before;
|
||||
}
|
||||
|
||||
if (!isDelete && after && after->getType() == ltRIPPLE_STATE)
|
||||
tokenAfter_ = after;
|
||||
|
||||
if (before && before->getType() == ltMPTOKEN)
|
||||
{
|
||||
mptokensChanged_++;
|
||||
tokenBefore_ = before;
|
||||
}
|
||||
|
||||
if (!isDelete && after && after->getType() == ltMPTOKEN)
|
||||
tokenAfter_ = after;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -791,31 +826,110 @@ ValidClawback::finalize(
|
||||
}
|
||||
|
||||
bool const mptV2Enabled = view.rules().enabled(featureMPTokensV2);
|
||||
if (trustlinesChanged_ != 0 && mptokensChanged_ != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: trustline and MPToken both changed.";
|
||||
if (mptV2Enabled)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tokenBefore_ && tokenAfter_ && tokenBefore_->getType() != tokenAfter_->getType())
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: token entry type changed.";
|
||||
if (mptV2Enabled)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trustlinesChanged_ == 1 || (mptV2Enabled && mptokensChanged_ == 1))
|
||||
{
|
||||
AccountID const issuer = tx.getAccountID(sfAccount);
|
||||
STAmount const& amount = tx.getFieldAmount(sfAmount);
|
||||
AccountID const& holder = amount.getIssuer();
|
||||
STAmount const holderBalance = amount.asset().visit(
|
||||
|
||||
return amount.asset().visit(
|
||||
[&](Issue const& issue) {
|
||||
return accountHolds(
|
||||
AccountID const issuer = tx.getAccountID(sfAccount);
|
||||
AccountID const& holder = amount.getIssuer();
|
||||
STAmount const holderBalance = accountHolds(
|
||||
view, holder, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
|
||||
|
||||
if (holderBalance.signum() < 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const beforeBalance = clawbackTrustLineBalanceInHolderTerms(
|
||||
tokenBefore_, holder, issuer, issue.currency);
|
||||
auto const afterBalance = clawbackTrustLineBalanceInHolderTerms(
|
||||
tokenAfter_, holder, issuer, issue.currency);
|
||||
if (!beforeBalance || !afterBalance)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: trustline clawback changed the wrong line";
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
STAmount clawAmount = amount;
|
||||
clawAmount.get<Issue>().account = issuer;
|
||||
if (clawAmount <= beast::kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: trustline clawback amount is invalid";
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
if (*afterBalance > *beforeBalance ||
|
||||
(*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: trustline clawback balance change is invalid";
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[&](MPTIssue const& issue) {
|
||||
return accountHolds(
|
||||
view,
|
||||
holder,
|
||||
issue,
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j);
|
||||
});
|
||||
auto const holder = tx[~sfHolder];
|
||||
if (!holder)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT clawback missing holder";
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
if (holderBalance.signum() < 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative";
|
||||
return false;
|
||||
}
|
||||
if (!tokenBefore_ || !tokenAfter_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT clawback token is missing";
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
if (tokenBefore_->getAccountID(sfAccount) != *holder ||
|
||||
tokenAfter_->getAccountID(sfAccount) != *holder ||
|
||||
(*tokenBefore_)[sfMPTokenIssuanceID] != issue.getMptID() ||
|
||||
(*tokenAfter_)[sfMPTokenIssuanceID] != issue.getMptID())
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT clawback changed the wrong token";
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
auto const before = tokenBefore_->getFieldU64(sfMPTAmount);
|
||||
auto const after = tokenAfter_->getFieldU64(sfMPTAmount);
|
||||
auto const clawValue = amount.mpt().value();
|
||||
if (clawValue <= 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPT clawback amount is invalid";
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
auto const clawAmount = static_cast<std::uint64_t>(clawValue);
|
||||
|
||||
// MPT balances are unsigned, so validate the raw holder
|
||||
// debit instead of routing through accountHolds().
|
||||
if (after > before || (before - after) != std::min(before, clawAmount))
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: MPT clawback balance change is invalid";
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -365,7 +365,7 @@ ValidMPTIssuance::finalize(
|
||||
}
|
||||
|
||||
void
|
||||
ValidMPTPayment::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
|
||||
ValidMPTBalanceChanges::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
if (overflow_)
|
||||
return;
|
||||
@@ -427,7 +427,7 @@ ValidMPTPayment::visitEntry(bool, SLE::const_ref before, SLE::const_ref after)
|
||||
}
|
||||
|
||||
bool
|
||||
ValidMPTPayment::finalize(
|
||||
ValidMPTBalanceChanges::finalize(
|
||||
STTx const& tx,
|
||||
TER const result,
|
||||
XRPAmount const,
|
||||
|
||||
@@ -4265,6 +4265,208 @@ class Invariants_test : public beast::unit_test::Suite
|
||||
return true;
|
||||
});
|
||||
|
||||
// Invalid IOU clawback delta must fail once MPTokensV2 enforces before/after validation.
|
||||
{
|
||||
Env env(*this, defaultAmendments());
|
||||
Account const issuer{"issuer"};
|
||||
Account const holder{"holder"};
|
||||
Account const other{"other"};
|
||||
env.fund(XRP(1'000), issuer, holder, other);
|
||||
auto const usd = issuer["USD"];
|
||||
env.trust(usd(100), holder);
|
||||
env(pay(issuer, holder, usd(100)));
|
||||
env.close();
|
||||
|
||||
doInvariantCheck(
|
||||
std::move(env),
|
||||
holder,
|
||||
other,
|
||||
{{"Invariant failed: trustline clawback balance change is invalid"}},
|
||||
[issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::line(holder.id(), issuer.id(), usd.currency));
|
||||
if (!sle)
|
||||
return false;
|
||||
|
||||
STAmount balance{Issue{usd.currency, issuer.id()}, 80};
|
||||
if (holder.id() > issuer.id())
|
||||
balance.negate();
|
||||
sle->setFieldAmount(sfBalance, balance);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{
|
||||
ttCLAWBACK,
|
||||
[&](STObject& tx) {
|
||||
tx[sfAccount] = issuer.id();
|
||||
tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
|
||||
}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
// Full IOU clawback may delete the trustline; missing after-SLE represents zero balance.
|
||||
{
|
||||
Env env(*this, defaultAmendments());
|
||||
Account const issuer{"issuer"};
|
||||
Account const holder{"holder"};
|
||||
Account const other{"other"};
|
||||
env.fund(XRP(1'000), issuer, holder, other);
|
||||
auto const usd = issuer["USD"];
|
||||
env.trust(usd(100), holder);
|
||||
env(pay(issuer, holder, usd(100)));
|
||||
env.close();
|
||||
|
||||
doInvariantCheck(
|
||||
std::move(env),
|
||||
holder,
|
||||
other,
|
||||
{},
|
||||
[issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
|
||||
auto const sle =
|
||||
ac.view().peek(keylet::line(holder.id(), issuer.id(), usd.currency));
|
||||
if (!sle)
|
||||
return false;
|
||||
|
||||
ac.view().erase(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{
|
||||
ttCLAWBACK,
|
||||
[&](STObject& tx) {
|
||||
tx[sfAccount] = issuer.id();
|
||||
tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 100};
|
||||
}},
|
||||
{tesSUCCESS, tesSUCCESS});
|
||||
}
|
||||
|
||||
// Pre-MPTokensV2 invalid IOU clawback delta logs but remains non-enforcing.
|
||||
{
|
||||
Env env(*this, defaultAmendments() - featureMPTokensV2);
|
||||
Account const issuer{"issuer"};
|
||||
Account const holder{"holder"};
|
||||
Account const other{"other"};
|
||||
env.fund(XRP(1'000), issuer, holder, other);
|
||||
auto const usd = issuer["USD"];
|
||||
env.trust(usd(100), holder);
|
||||
env(pay(issuer, holder, usd(100)));
|
||||
env.close();
|
||||
|
||||
doInvariantCheck(
|
||||
std::move(env),
|
||||
holder,
|
||||
other,
|
||||
{{"Invariant failed: trustline clawback balance change is invalid"}},
|
||||
[issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::line(holder.id(), issuer.id(), usd.currency));
|
||||
if (!sle)
|
||||
return false;
|
||||
|
||||
STAmount balance{Issue{usd.currency, issuer.id()}, 80};
|
||||
if (holder.id() > issuer.id())
|
||||
balance.negate();
|
||||
sle->setFieldAmount(sfBalance, balance);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{
|
||||
ttCLAWBACK,
|
||||
[&](STObject& tx) {
|
||||
tx[sfAccount] = issuer.id();
|
||||
tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
|
||||
}},
|
||||
{tesSUCCESS, tesSUCCESS});
|
||||
}
|
||||
|
||||
// Invalid MPT clawback delta must fail when raw MPToken debit mismatches sfAmount.
|
||||
{
|
||||
Env env(*this, defaultAmendments());
|
||||
Account const issuer{"issuer"};
|
||||
Account const holder{"holder"};
|
||||
Account const other{"other"};
|
||||
env.fund(XRP(1'000), issuer, holder, other);
|
||||
MPTTester const mpt(
|
||||
{.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
|
||||
auto const id = mpt.issuanceID();
|
||||
|
||||
doInvariantCheck(
|
||||
std::move(env),
|
||||
holder,
|
||||
other,
|
||||
{{"Invariant failed: MPT clawback balance change is invalid"}},
|
||||
[id](Account const& holder, Account const&, ApplyContext& ac) {
|
||||
auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
|
||||
auto const sleIssuance = ac.view().peek(keylet::mptIssuance(id));
|
||||
if (!sleToken || !sleIssuance)
|
||||
return false;
|
||||
|
||||
sleToken->setFieldU64(sfMPTAmount, 80);
|
||||
sleIssuance->setFieldU64(sfOutstandingAmount, 80);
|
||||
ac.view().update(sleToken);
|
||||
ac.view().update(sleIssuance);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{
|
||||
ttCLAWBACK,
|
||||
[&](STObject& tx) {
|
||||
tx[sfAccount] = issuer.id();
|
||||
tx[sfHolder] = holder.id();
|
||||
tx[sfAmount] = STAmount{MPTIssue{id}, 10};
|
||||
}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
// A clawback that mutates both IOU and MPT entries must fail under MPTokensV2.
|
||||
{
|
||||
Env env(*this, defaultAmendments());
|
||||
Account const issuer{"issuer"};
|
||||
Account const holder{"holder"};
|
||||
Account const other{"other"};
|
||||
env.fund(XRP(1'000), issuer, holder, other);
|
||||
auto const usd = issuer["USD"];
|
||||
env.trust(usd(100), holder);
|
||||
env(pay(issuer, holder, usd(100)));
|
||||
MPTTester const mpt(
|
||||
{.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
|
||||
auto const id = mpt.issuanceID();
|
||||
|
||||
doInvariantCheck(
|
||||
std::move(env),
|
||||
holder,
|
||||
other,
|
||||
{{"Invariant failed: trustline and MPToken both changed"}},
|
||||
[issuer, usd, id](Account const& holder, Account const&, ApplyContext& ac) {
|
||||
auto const sleLine =
|
||||
ac.view().peek(keylet::line(holder.id(), issuer.id(), usd.currency));
|
||||
auto const sleToken = ac.view().peek(keylet::mptoken(id, holder.id()));
|
||||
auto const sleIssuance = ac.view().peek(keylet::mptIssuance(id));
|
||||
if (!sleLine || !sleToken || !sleIssuance)
|
||||
return false;
|
||||
|
||||
STAmount balance{Issue{usd.currency, issuer.id()}, 90};
|
||||
if (holder.id() > issuer.id())
|
||||
balance.negate();
|
||||
sleLine->setFieldAmount(sfBalance, balance);
|
||||
sleToken->setFieldU64(sfMPTAmount, 90);
|
||||
sleIssuance->setFieldU64(sfOutstandingAmount, 90);
|
||||
ac.view().update(sleLine);
|
||||
ac.view().update(sleToken);
|
||||
ac.view().update(sleIssuance);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{
|
||||
ttCLAWBACK,
|
||||
[&](STObject& tx) {
|
||||
tx[sfAccount] = issuer.id();
|
||||
tx[sfHolder] = holder.id();
|
||||
tx[sfAmount] = STAmount{MPTIssue{id}, 10};
|
||||
}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
// More MPTokens created than expected
|
||||
std::array<std::pair<xrpl::TxType, std::uint8_t>, 4> const tests = {
|
||||
std::make_pair(ttAMM_WITHDRAW, 2),
|
||||
|
||||
Reference in New Issue
Block a user