Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 5 additions & 2 deletions src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ authorizeMPToken(
{
auto const mptokenKey = keylet::mptoken(mptIssuanceID, account);
auto const sleMpt = view.peek(mptokenKey);
if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0)
if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0 ||
(view.rules().enabled(fixSecurity3_1_3) &&
(*sleMpt)[~sfLockedAmount].value_or(0) != 0))
return tecINTERNAL; // LCOV_EXCL_LINE

if (!view.dirRemove(
Expand Down Expand Up @@ -252,7 +254,8 @@ removeEmptyHolding(
// balance, it can not just be deleted, because that will throw the issuance
// accounting out of balance, so fail. Since this should be impossible
// anyway, I'm not going to put any effort into it.
if (mptoken->at(sfMPTAmount) != 0)
if (mptoken->at(sfMPTAmount) != 0 ||
(view.rules().enabled(fixSecurity3_1_3) && (*mptoken)[~sfLockedAmount].value_or(0) != 0))
return tecHAS_OBLIGATIONS;

return authorizeMPToken(
Expand Down
80 changes: 80 additions & 0 deletions src/test/app/Vault_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <test/jtx/AMMTest.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/escrow.h>
#include <test/jtx/mpt.h>

#include <xrpl/basics/base_uint.h>
Expand Down Expand Up @@ -5231,6 +5232,84 @@ class Vault_test : public beast::unit_test::suite
}
}

void
testRemoveEmptyHoldingLockedAmount()
{
testcase("removeEmptyHolding deletes MPToken with sfLockedAmount");
using namespace test::jtx;
using namespace std::literals;

Env env{*this, testable_amendments() | featureSingleAssetVault | fixSecurity3_1_3};
auto const baseFee = env.current()->fees().base;

Account const issuer{"issuer"};
Account const owner{"owner"};
Account const depositor{"depositor"};
Account const bob{"bob"};

env.fund(XRP(100000), issuer, owner, depositor, bob);
env.close();

Vault vault{env};

// Create an MPT asset for the vault
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
PrettyAsset asset = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = depositor});
env(pay(issuer, depositor, asset(1000)));
env.close();

// Create vault
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();

auto const vaultSle = env.le(keylet);
BEAST_EXPECT(vaultSle != nullptr);
auto const shareMptID = vaultSle->at(sfShareMPTID);
MPTIssue const shareIssue{shareMptID};

// Depositor deposits 1000 asset units into vault, receiving shares
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)}));
env.close();

// Check depositor has shares
{
auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
BEAST_EXPECT(sleMpt != nullptr);
BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 1000);
}

// Escrow 500 of those shares
env(escrow::create(depositor, bob, STAmount{shareIssue, 500}),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
fee(baseFee * 150),
ter(tesSUCCESS));
env.close();

// Verify: sfMPTAmount=500, sfLockedAmount=500
{
auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
BEAST_EXPECT(sleMpt != nullptr);
BEAST_EXPECT(sleMpt->at(sfLockedAmount) == 500);
BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 500);
}

// Withdraw remaining spendable shares — triggers removeEmptyHolding
env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(500)}),
ter(tesSUCCESS));
env.close();

// With the fix applied, MPToken must still exist with sfLockedAmount > 0
auto const sleMptAfter = env.le(keylet::mptoken(shareMptID, depositor));
BEAST_EXPECT(sleMptAfter != nullptr);
if (sleMptAfter)
BEAST_EXPECT(sleMptAfter->at(sfLockedAmount) == 500);
}

public:
void
run() override
Expand All @@ -5251,6 +5330,7 @@ class Vault_test : public beast::unit_test::suite
testVaultClawbackBurnShares();
testVaultClawbackAssets();
testAssetsMaximum();
testRemoveEmptyHoldingLockedAmount();
}
};

Expand Down
Loading