Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
865bbc9
adds a mechanism for the vault owner to burn user shares when the vau…
Tapanito Dec 8, 2025
d46ed4e
minor fixes
Tapanito Dec 8, 2025
c58724e
Merge branch 'develop' into tapanito/vault-owner-clawback
Tapanito Dec 8, 2025
74f75ce
fixes formatting
Tapanito Dec 8, 2025
04e1af6
Merge branch 'develop' into tapanito/vault-owner-clawback
Tapanito Dec 8, 2025
8a935e1
formatting issues
Tapanito Dec 8, 2025
d049434
addresses first comment
Tapanito Dec 10, 2025
61f078d
improves invariant check
Tapanito Dec 10, 2025
733bb00
attempt to fix optional dereference
Tapanito Dec 11, 2025
2c63982
fixes an elusive optional dereference
Tapanito Dec 11, 2025
ef44ca7
Merge branch 'develop' into tapanito/vault-owner-clawback
Tapanito Dec 11, 2025
2af722f
Merge branch 'develop' into tapanito/vault-owner-clawback
Tapanito Dec 15, 2025
0ad2c70
minor comment changes
Tapanito Dec 11, 2025
3ab6623
improves vaultClawback logic
Tapanito Dec 23, 2025
d3d47d3
further code improvements
Tapanito Dec 23, 2025
8cc6eac
Apply suggestions from code review
Tapanito Dec 23, 2025
ee24339
restores a previous unit-test
Tapanito Dec 23, 2025
b3ae984
addresses review comments
Tapanito Dec 24, 2025
8456cbc
syntax fixes
Tapanito Dec 24, 2025
89fe41d
additional unit test
Tapanito Dec 24, 2025
294f7bf
more syntax
Tapanito Dec 24, 2025
07c1bb7
addresses review comments pt1
Tapanito Jan 6, 2026
e241e50
addresses review comments pt2
Tapanito Jan 6, 2026
cc6b9fc
Merge branch 'develop' into tapanito/vault-owner-clawback
Tapanito Jan 6, 2026
8034a79
Merge branch 'develop' into tapanito/vault-owner-clawback
Tapanito Jan 7, 2026
203a23c
Merge branch 'develop' into tapanito/vault-owner-clawback
ximinez Jan 9, 2026
b6f5749
Merge branch 'develop' into tapanito/vault-owner-clawback
bthomee Jan 9, 2026
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
175 changes: 164 additions & 11 deletions src/test/app/Vault_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ class Vault_test : public beast::unit_test::suite

{
testcase(prefix + " clawback some");
auto code =
asset.raw().native() ? ter(temMALFORMED) : ter(tesSUCCESS);
auto code = asset.raw().native() ? ter(tecNO_PERMISSION)
: ter(tesSUCCESS);
auto tx = vault.clawback(
{.issuer = issuer,
.id = keylet.key,
Expand Down Expand Up @@ -1197,15 +1197,6 @@ class Vault_test : public beast::unit_test::suite

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

{
auto tx = vault.clawback(
{.issuer = owner,
.id = keylet.key,
.holder = issuer,
.amount = asset(50)});
env(tx, ter(temMALFORMED));
}

{
auto tx = vault.clawback(
{.issuer = issuer,
Expand Down Expand Up @@ -5243,6 +5234,167 @@ class Vault_test : public beast::unit_test::suite
});
}

void
testVaultClawbackBurnShares()
{
testcase("testVaultClawbackBurnShares - vault owner burn shares");
using namespace test::jtx;
using namespace loanBroker;
using namespace loan;
Env env(*this, beast::severities::kWarning);
Account owner{"alice"};
Account depositor{"bob"};
Account issuer{"issuer"};

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

auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) {
auto const sleVault = env.le(vaultKeylet);
BEAST_EXPECT(sleVault != nullptr);

return std::make_pair(
sleVault->at(sfAssetsAvailable), sleVault->at(sfAssetsTotal));
};

auto const vaultShareBalance = [&](Keylet const& vaultKeylet) {
auto const sleVault = env.le(vaultKeylet);
BEAST_EXPECT(sleVault != nullptr);

auto const sleIssuance =
env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
BEAST_EXPECT(sleIssuance != nullptr);

return sleIssuance->at(sfOutstandingAmount);
};

auto const setupVault =
[&](PrettyAsset const& asset) -> std::pair<Vault, Keylet> {
Vault vault{env};

auto const [tx, vaultKeylet] =
vault.create({.owner = owner, .asset = asset});
env(tx, ter(tesSUCCESS), THISLINE);
env.close();

env(vault.deposit(
{.depositor = depositor,
.id = vaultKeylet.key,
.amount = asset(100)}),
ter(tesSUCCESS),
THISLINE);
env.close();

auto const [availablePreDefault, totalPreDefault] =
vaultAssetBalance(vaultKeylet);
BEAST_EXPECT(availablePreDefault == totalPreDefault);
BEAST_EXPECT(availablePreDefault == asset(100).value());

auto const brokerKeylet =
keylet::loanbroker(owner.id(), env.seq(owner));

env(set(owner, vaultKeylet.key), THISLINE);
env.close();

auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);

// Create a simple Loan for the full amount of Vault assets
env(set(depositor, brokerKeylet.key, asset(100).value()),
loan::interestRate(TenthBips32(0)),
gracePeriod(10),
paymentInterval(120),
paymentTotal(10),
sig(sfCounterpartySignature, owner),
fee(env.current()->fees().base * 2),
ter(tesSUCCESS),
THISLINE);
env.close();

env.close(std::chrono::seconds{120 + 10});

env(manage(owner, loanKeylet.key, tfLoanDefault),
ter(tesSUCCESS),
THISLINE);

auto const [availablePostDefault, totalPostDefault] =
vaultAssetBalance(vaultKeylet);

BEAST_EXPECT(availablePostDefault == totalPostDefault);
BEAST_EXPECT(availablePostDefault == asset(0).value());

return std::make_pair(vault, vaultKeylet);
};

auto const testCase = [&](PrettyAsset const& asset) {
// The owner cannot perform a non-zero share burn
{
auto [vault, vaultKeylet] = setupVault(asset);
env(vault.clawback({
.issuer = owner,
.id = vaultKeylet.key,
.holder = depositor,
.amount = asset(1).value(),
}),
ter(tecLIMIT_EXCEEDED),
THISLINE);
env.close();
}

// // The owner can clawback all shares, burning them
{
auto [vault, vaultKeylet] = setupVault(asset);
env(vault.clawback({
.issuer = owner,
.id = vaultKeylet.key,
.holder = depositor,
}),
ter(tesSUCCESS),
THISLINE);
env.close();
BEAST_EXPECT(vaultShareBalance(vaultKeylet) == 0);
}

// The owner can clawback explicitly all shares, burning them
{
auto [vault, vaultKeylet] = setupVault(asset);
env(vault.clawback({
.issuer = owner,
.id = vaultKeylet.key,
.holder = depositor,
.amount = asset(vaultShareBalance(vaultKeylet)),
}),
ter(tesSUCCESS),
THISLINE);
env.close();
BEAST_EXPECT(vaultShareBalance(vaultKeylet) == 0);
}
};

// Test XRP
PrettyAsset xrp = xrpIssue();
testCase(xrp);

// Test IOU
PrettyAsset IOU = issuer["IOU"];
env.trust(IOU(1000), owner);
env.trust(IOU(1000), depositor);
env(pay(issuer, owner, IOU(100)));
env(pay(issuer, depositor, IOU(100)));
env.close();
testCase(IOU);

// Test MPT
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create(
{.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
PrettyAsset MPT = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = depositor});
env(pay(issuer, depositor, MPT(1000)));
env.close();
testCase(MPT);
}

public:
void
run() override
Expand All @@ -5261,6 +5413,7 @@ class Vault_test : public beast::unit_test::suite
testScaleIOU();
testRPC();
testDelegate();
testVaultClawbackBurnShares();
}
};

Expand Down
39 changes: 26 additions & 13 deletions src/xrpld/app/tx/detail/InvariantCheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@
#include <xrpl/protocol/Units.h>
#include <xrpl/protocol/nftPageMask.h>

#include <cstdint>
#include <optional>

namespace ripple {

/*
Expand Down Expand Up @@ -95,6 +92,7 @@ hasPrivilege(STTx const& tx, Privilege priv)
switch (tx.getTxnType())
{
#include <xrpl/protocol/detail/transactions.macro>

// Deprecated types
default:
return false;
Expand Down Expand Up @@ -2623,6 +2621,7 @@ ValidVault::Vault::make(SLE const& from)
self.key = from.key();
self.asset = from.at(sfAsset);
self.pseudoId = from.getAccountID(sfAccount);
self.owner = from.at(sfOwner);
self.shareMPTID = from.getFieldH192(sfShareMPTID);
self.assetsTotal = from.at(sfAssetsTotal);
self.assetsAvailable = from.at(sfAssetsAvailable);
Expand Down Expand Up @@ -3068,6 +3067,10 @@ ValidVault::finalize(
: std::nullopt;
};

auto const vaultHoldsNoAssets = [&](Vault const& vault) {
return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
};

// Technically this does not need to be a lambda, but it's more
// convenient thanks to early "return false"; the not-so-nice
// alternatives are several layers of nested if/else or more complex
Expand Down Expand Up @@ -3450,22 +3453,30 @@ ValidVault::finalize(
if (vaultAsset.native() ||
vaultAsset.getIssuer() != tx[sfAccount])
{
JLOG(j.fatal()) << //
"Invariant failed: clawback may only be performed by "
"the asset issuer";
return false; // That's all we can do
// The owner can use clawback to force-burn shares when the
// vault is empty but there are outstanding shares
if (!(beforeShares && beforeShares->sharesTotal > 0 &&
vaultHoldsNoAssets(beforeVault) &&
beforeVault.owner == tx[sfAccount]))
{
JLOG(j.fatal()) << //
"Invariant failed: clawback may only be performed "
"by the asset issuer";
return false; // That's all we can do
}
}

auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);

if (!vaultDeltaAssets)
if (!vaultDeltaAssets && !vaultHoldsNoAssets(beforeVault))
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault balance";
return false; // That's all we can do
}

if (*vaultDeltaAssets >= zero)
if (*vaultDeltaAssets >= zero &&
!vaultHoldsNoAssets(beforeVault))
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease vault "
Expand Down Expand Up @@ -3505,17 +3516,19 @@ ValidVault::finalize(
result = false;
}

if (beforeVault.assetsTotal + *vaultDeltaAssets !=
afterVault.assetsTotal)
if (!vaultHoldsNoAssets(beforeVault) &&
beforeVault.assetsTotal + *vaultDeltaAssets !=
afterVault.assetsTotal)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets outstanding "
"must add up";
result = false;
}

if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
afterVault.assetsAvailable)
if (!vaultHoldsNoAssets(beforeVault) &&
beforeVault.assetsAvailable + *vaultDeltaAssets !=
afterVault.assetsAvailable)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets available must "
Expand Down
1 change: 1 addition & 0 deletions src/xrpld/app/tx/detail/InvariantCheck.h
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,7 @@ class ValidVault
uint256 key = beast::zero;
Asset asset = {};
AccountID pseudoId = {};
AccountID owner = {};
uint192 shareMPTID = beast::zero;
Number assetsTotal = 0;
Number assetsAvailable = 0;
Expand Down
Loading
Loading