Compare commits

..

4 Commits

Author SHA1 Message Date
Bart
04249ae86e Apply clang-tidy diff 2026-04-28 13:39:39 -04:00
Bart
7962ed3c22 Merge branch 'develop' into bthomee/logic 2026-04-28 13:39:18 -04:00
Bart
58beb2d9ba Remove unused function 2026-04-28 11:16:59 -04:00
Bart
0d6f9b8428 refactor: Revert certain Throws by LogicErrors 2026-04-27 15:49:02 -04:00
4 changed files with 5 additions and 370 deletions

View File

@@ -229,19 +229,6 @@ public:
std::shared_ptr<Serializer const> const& txn,
std::shared_ptr<Serializer const> const& metaData) override;
// Insert the transaction, and return the hash of the SHAMap leaf node
// holding the transaction. The hash can be used to fetch the transaction
// directly, instead of traversing the SHAMap
// @param key transaction ID
// @param txn transaction
// @param metaData transaction metadata
// @return hash of SHAMap leaf node that holds the transaction
uint256
rawTxInsertWithHash(
uint256 const& key,
std::shared_ptr<Serializer const> const& txn,
std::shared_ptr<Serializer const> const& metaData);
//--------------------------------------------------------------------------
void

View File

@@ -14,7 +14,6 @@
#include <xrpl/nodestore/NodeObject.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/Keylet.h>
@@ -31,7 +30,6 @@
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/shamap/Family.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapItem.h>
@@ -43,7 +41,6 @@
#include <exception>
#include <memory>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
@@ -493,14 +490,14 @@ void
Ledger::rawErase(std::shared_ptr<SLE> const& sle)
{
if (!stateMap_.delItem(sle->key()))
Throw<std::logic_error>("Ledger::rawErase: key not found");
LogicError("Ledger::rawErase: key not found");
}
void
Ledger::rawErase(uint256 const& key)
{
if (!stateMap_.delItem(key))
Throw<std::logic_error>("Ledger::rawErase: key not found");
LogicError("Ledger::rawErase: key not found");
}
void
@@ -510,7 +507,7 @@ Ledger::rawInsert(std::shared_ptr<SLE> const& sle)
sle->add(ss);
if (!stateMap_.addGiveItem(
SHAMapNodeType::tnACCOUNT_STATE, make_shamapitem(sle->key(), ss.slice())))
Throw<std::logic_error>("Ledger::rawInsert: key already exists");
LogicError("Ledger::rawInsert: key already exists");
}
void
@@ -520,7 +517,7 @@ Ledger::rawReplace(std::shared_ptr<SLE> const& sle)
sle->add(ss);
if (!stateMap_.updateGiveItem(
SHAMapNodeType::tnACCOUNT_STATE, make_shamapitem(sle->key(), ss.slice())))
Throw<std::logic_error>("Ledger::rawReplace: key not found");
LogicError("Ledger::rawReplace: key not found");
}
void
@@ -536,27 +533,7 @@ Ledger::rawTxInsert(
s.addVL(txn->peekData());
s.addVL(metaData->peekData());
if (!txMap_.addGiveItem(SHAMapNodeType::tnTRANSACTION_MD, make_shamapitem(key, s.slice())))
Throw<std::logic_error>("duplicate_tx: " + to_string(key));
}
uint256
Ledger::rawTxInsertWithHash(
uint256 const& key,
std::shared_ptr<Serializer const> const& txn,
std::shared_ptr<Serializer const> const& metaData)
{
XRPL_ASSERT(metaData, "xrpl::Ledger::rawTxInsertWithHash : non-null metadata input");
// low-level - just add to table
Serializer s(txn->getDataLength() + metaData->getDataLength() + 16);
s.addVL(txn->peekData());
s.addVL(metaData->peekData());
auto item = make_shamapitem(key, s.slice());
auto hash = sha512Half(HashPrefix::txNode, item->slice(), item->key());
if (!txMap_.addGiveItem(SHAMapNodeType::tnTRANSACTION_MD, std::move(item)))
Throw<std::logic_error>("duplicate_tx: " + to_string(key));
return hash;
LogicError("duplicate_tx: " + to_string(key));
}
bool

View File

@@ -8,7 +8,6 @@
#include <test/jtx/escrow.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
@@ -20,7 +19,6 @@
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
@@ -37,7 +35,6 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rules.h>
@@ -7063,200 +7060,10 @@ private:
{all});
}
// Create a single-asset vault, deposit assets so the depositor receives
// shares (an MPT issued by the vault pseudo-account), then pair those
// shares with XRP in an AMM. Finally do a single-asset deposit of more
// shares into the AMM.
void
testVaultSharesAMM()
{
testcase("Vault Shares paired with XRP in AMM");
using namespace jtx;
// Vaults rely on featureSingleAssetVault (which the AMM_test class
// strips by default). MPT-AMM pairs require featureMPTokensV2.
FeatureBitset const features{
jtx::testable_amendments() | featureSingleAssetVault | featureMPTokensV2};
Env env{*this, features};
Account const owner{"vaultOwner"};
env.fund(XRP(1'000'000), owner);
env.close();
// Use XRP as the vault asset for simplicity.
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault const vault{env};
auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
env(vaultTx);
env.close();
if (!BEAST_EXPECT(env.le(vaultKeylet)))
return;
// Deposit 10,000 XRP into the vault. Owner receives shares (MPT)
// issued by the vault's pseudo-account.
env(vault.deposit(
{.depositor = owner, .id = vaultKeylet.key, .amount = asset(10'000).value()}));
env.close();
auto const vaultSle = env.le(vaultKeylet);
if (!BEAST_EXPECT(vaultSle))
return;
MPTID const shareMptID = vaultSle->at(sfShareMPTID);
MPTIssue const shareIssue{shareMptID};
// The share MPT is issued by the vault's pseudo-account. Memoize so
// env.balance() can format share amounts.
env.memoize(Account{"vaultPseudo", vaultSle->at(sfAccount)});
// XRP vaults use scale=6, so a 10,000 XRP deposit yields
// 10,000 * 1e6 = 10^10 share units (raw MPT amount).
STAmount const sharesHeld = env.balance(owner, shareIssue);
BEAST_EXPECT(sharesHeld.mantissa() == 10'000'000'000ull);
BEAST_EXPECT(sharesHeld.asset() == shareIssue);
// Seed the AMM with half the shares + 5,000 XRP.
STAmount const halfShares(shareIssue, std::uint64_t{5'000'000'000});
AMM ammOwner(env, owner, halfShares, XRP(5'000));
BEAST_EXPECT(ammOwner.ammExists());
// Single-asset deposit: add 2,500,000,000 more shares (a quarter of
// the original holding) to the share side of the pool.
STAmount const extraShares(shareIssue, std::uint64_t{2'500'000'000});
ammOwner.deposit(owner, extraShares);
// The share-side pool should now equal halfShares + extraShares,
// while the XRP-side balance is unchanged at 5,000 XRP.
auto const [shareBalance, xrpBalance, lpt] = ammOwner.balances(shareIssue, xrpIssue());
BEAST_EXPECT(shareBalance == halfShares + extraShares);
BEAST_EXPECT(xrpBalance == XRP(5'000));
// Owner now holds the original 10B shares minus what was put into the
// AMM (5B seed + 2.5B single-asset deposit) = 2.5B.
STAmount const expectedOwnerShares(shareIssue, std::uint64_t{2'500'000'000});
BEAST_EXPECT(env.balance(owner, shareIssue) == expectedOwnerShares);
}
// Create a Vault whose underlying asset is a lockable / clawback-able
// MPT. Pair the vault shares with XRP in an AMM. Transfer half of the
// owner's LP tokens to a second account, then issuer-lock the
// underlying MPT, then try to transfer LP tokens / cash out again.
//
// Locking the underlying MPT cascades up via
// `isVaultPseudoAccountFrozen`: the vault-share MPT is treated as
// frozen because its underlying is locked. So:
// - LP-token Payment after lock fails (`tecPATH_DRY`).
// - AMM withdrawal of LP tokens fails (`tecFROZEN`).
// The LP tokens are effectively stuck for as long as the underlying
// MPT remains locked.
void
testLockedVaultMPTCashOut()
{
testcase("Cash out LP Tokens after vault MPT locked");
using namespace jtx;
FeatureBitset const features{
jtx::testable_amendments() | featureSingleAssetVault | featureMPTokensV2};
Env env{*this, features};
Account const issuer{"issuer"};
Account const owner{"vaultOwner"};
Account const trader{"trader"};
env.fund(XRP(1'000'000), issuer, owner, trader);
env.close();
// Underlying MPT supports lock + clawback. MPTDEXFlags adds
// CanTransfer + CanTrade so the vault and AMM can route it.
MPTTester mpt(
{.env = env,
.issuer = issuer,
.holders = {owner},
.pay = 100'000,
.flags = tfMPTCanLock | tfMPTCanClawback | MPTDEXFlags});
PrettyAsset const asset = MPT(mpt);
// Create the vault.
Vault const vault{env};
auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
env(vaultTx);
env.close();
if (!BEAST_EXPECT(env.le(vaultKeylet)))
return;
// Deposit 50,000 of the underlying MPT.
env(vault.deposit(
{.depositor = owner, .id = vaultKeylet.key, .amount = asset(50'000).value()}));
env.close();
auto const vaultSle = env.le(vaultKeylet);
if (!BEAST_EXPECT(vaultSle))
return;
MPTID const shareMptID = vaultSle->at(sfShareMPTID);
MPTIssue const shareIssue{shareMptID};
env.memoize(Account{"vaultPseudo", vaultSle->at(sfAccount)});
// MPT vaults use scale=0, so 50,000 deposit -> 50,000 share units.
STAmount const sharesHeld = env.balance(owner, shareIssue);
BEAST_EXPECT(sharesHeld.mantissa() == 50'000);
// Create the AMM: 25,000 vault shares + 1,000 XRP.
STAmount const seedShares(shareIssue, std::uint64_t{25'000});
AMM ammOwner(env, owner, seedShares, XRP(1'000));
BEAST_EXPECT(ammOwner.ammExists());
// The AMM pseudo-account issues the LP tokens; memoize so
// env.balance() can format LP-token amounts.
env.memoize(Account{"ammPseudo", ammOwner.ammAccount()});
// Owner's LP token balance after AMM creation.
auto const lptIssue = ammOwner.lptIssue();
STAmount const lptOwner0 = env.balance(owner, lptIssue);
STAmount const lptZero(lptIssue, std::uint32_t{0});
BEAST_EXPECT(lptOwner0 != lptZero);
// Trader needs a trust line to receive LP tokens.
STAmount const lptTrustLimit(lptIssue, std::uint64_t{1'000'000'000});
env(trust(trader, lptTrustLimit));
env.close();
// Step 1: transfer half the LP tokens from owner -> trader.
STAmount const halfLpt(lptIssue, lptOwner0.mantissa() / 2, lptOwner0.exponent());
env(pay(owner, trader, halfLpt));
env.close();
BEAST_EXPECT(env.balance(trader, lptIssue) == halfLpt);
// Step 2: issuer locks the underlying MPT.
mpt.set({.flags = tfMPTLock});
env.close();
// Step 3: transfer LP tokens again. The lock on the underlying MPT
// cascades through the vault-share issuance via
// isVaultPseudoAccountFrozen, so the AMM-routed Payment fails.
STAmount const quarterLpt(lptIssue, lptOwner0.mantissa() / 4, lptOwner0.exponent());
env(pay(owner, trader, quarterLpt), ter(tecPATH_DRY));
env.close();
// Trader's balance is still just the half from before the lock.
BEAST_EXPECT(env.balance(trader, lptIssue) == halfLpt);
// Step 4: try to cash out the LP tokens. The AMM withdrawal must
// touch the vault-share side, which is now treated as frozen
// because its underlying is locked, so the withdrawal fails.
ammOwner.withdrawAll(trader, std::nullopt, ter(tecFROZEN));
env.close();
// Trader still holds the LP tokens; nothing was redeemed.
BEAST_EXPECT(env.balance(trader, lptIssue) == halfLpt);
BEAST_EXPECT(env.balance(trader, shareIssue) == STAmount(shareIssue, std::uint64_t{0}));
}
void
run() override
{
FeatureBitset const all{testable_amendments()};
testVaultSharesAMM();
testLockedVaultMPTCashOut();
testInvalidInstance();
testInstanceCreate();
testInvalidDeposit(all);

View File

@@ -6139,141 +6139,6 @@ class Vault_test : public beast::unit_test::suite
runTest(amendments);
}
// Issuer mutates the underlying MPT's lsfMPTCanTransfer / lsfMPTCanTrade
// flags after holders have already deposited into a vault. Demonstrates:
//
// - VaultDeposit and VaultWithdraw both go through `canTransfer`,
// so clearing lsfMPTCanTransfer freezes every holder's funds in
// the vault until the issuer re-enables the flag (`tecNO_AUTH`).
//
// - The issuer is exempt: `canTransfer` short-circuits when either
// side of the transfer is the issuer, so the issuer can still
// deposit and withdraw.
//
// - lsfMPTCanTrade is *not* checked by VaultDeposit/VaultWithdraw at
// all — clearing it has no effect on vault I/O. (It only gates
// DEX/AMM operations via `canTrade`.)
void
testMutateCanTransferAfterDeposit()
{
using namespace test::jtx;
testcase("MPT vault: clearing CanTransfer/CanTrade after deposit");
Env env{*this, testable_amendments() | featureSingleAssetVault};
Account const issuer{"issuer"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(1'000), issuer, alice, bob);
env.close();
// MPT is transferable, tradable, lockable, and clawback-capable. Both
// CanTransfer and CanTrade are mutable so the issuer can flip them
// later via MPTokenIssuanceSet.
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create(
{.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock | tfMPTCanClawback,
.mutableFlags = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanTrade});
PrettyAsset const asset = mptt.issuanceID();
mptt.authorize({.account = alice});
mptt.authorize({.account = bob});
env(pay(issuer, alice, asset(100'000)));
env(pay(issuer, bob, asset(100'000)));
env.close();
Vault const vault{env};
auto [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
env(createTx);
env.close();
BEAST_EXPECT(env.le(vaultKeylet));
// Both holders deposit. Issuer also deposits (issuer can be a
// depositor too) so we can later confirm the issuer-exempt path.
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50'000)}));
env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = asset(30'000)}));
env(vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = asset(20'000)}));
env.close();
// -- 1. Issuer clears lsfMPTCanTransfer ---------------------------
mptt.set({.mutableFlags = tmfMPTClearCanTransfer});
env.close();
{
auto const sle = env.le(keylet::mptIssuance(asset.raw().get<MPTIssue>().getMptID()));
BEAST_EXPECT(sle && !sle->isFlag(lsfMPTCanTransfer));
BEAST_EXPECT(sle && sle->isFlag(lsfMPTCanTrade));
}
// 2. Holder deposits and withdrawals are blocked: vault pseudo-
// account is neither sender nor receiver = issuer, so
// canTransfer returns tecNO_AUTH.
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(1'000)}),
ter(tecNO_AUTH));
env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = asset(1'000)}),
ter(tecNO_AUTH));
env(vault.withdraw({.depositor = bob, .id = vaultKeylet.key, .amount = asset(1'000)}),
ter(tecNO_AUTH));
env.close();
// 3. Issuer-as-depositor is exempt — `canTransfer` short-circuits
// on the issuer side. Both deposit and withdraw succeed.
env(vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = asset(5'000)}));
env(vault.withdraw({.depositor = issuer, .id = vaultKeylet.key, .amount = asset(5'000)}));
env.close();
// 3b. A holder can also escape by withdrawing *to the issuer* via
// sfDestination. `canTransfer`'s issuer short-circuit fires on
// `to == issuer`, so the withdrawal succeeds even though
// CanTransfer is cleared. The holder's shares are burned and
// the underlying MPT lands at the issuer (presumably part of
// an off-ledger redemption arrangement).
auto const aliceMptBefore = env.balance(alice, asset);
auto withdrawToIssuer =
vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = asset(2'000)});
withdrawToIssuer[sfDestination] = issuer.human();
env(withdrawToIssuer);
env.close();
// Alice's MPT balance is unchanged — the asset went to the issuer,
// not back to her — but her share holding was burned.
BEAST_EXPECT(env.balance(alice, asset) == aliceMptBefore);
// -- 4. Also clear lsfMPTCanTrade. Vault paths don't consult
// CanTrade, so this changes nothing for vault I/O. ----------
mptt.set({.mutableFlags = tmfMPTClearCanTrade});
env.close();
{
auto const sle = env.le(keylet::mptIssuance(asset.raw().get<MPTIssue>().getMptID()));
BEAST_EXPECT(sle && !sle->isFlag(lsfMPTCanTrade));
}
// Holder ops still fail the same way (CanTransfer-driven), and the
// issuer is still exempt.
env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = asset(1'000)}),
ter(tecNO_AUTH));
env(vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = asset(1'000)}));
env.close();
// -- 5. Re-enable CanTransfer; leave CanTrade cleared. ------------
mptt.set({.mutableFlags = tmfMPTSetCanTransfer});
env.close();
// Holders can now withdraw all their stake — confirms CanTrade is
// not consulted by the vault transactors. Alice already redeemed
// 2,000 to the issuer, so only 48,000 remains for her.
env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = asset(48'000)}));
env(vault.withdraw({.depositor = bob, .id = vaultKeylet.key, .amount = asset(30'000)}));
env.close();
{
auto const sle = env.le(keylet::mptIssuance(asset.raw().get<MPTIssue>().getMptID()));
BEAST_EXPECT(sle && sle->isFlag(lsfMPTCanTransfer));
BEAST_EXPECT(sle && !sle->isFlag(lsfMPTCanTrade));
}
}
public:
void
run() override
@@ -6297,7 +6162,6 @@ public:
testAssetsMaximum();
testBug6_LimitBypassWithShares();
testRemoveEmptyHoldingLockedAmount();
testMutateCanTransferAfterDeposit();
}
};