mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 23:20:33 +00:00
VaultClawback: Burn shares of an empty vault (#6120)
- Adds a mechanism for the vault owner to burn user shares when the vault is stuck. If the Vault has 0 AssetsAvailable and Total, the owner may submit a VaultClawback to reclaim the worthless fees, and thus allow the Vault to be deleted. The Amount must be left off (unless the owner is the asset issuer), specified as 0 Shares, or specified as the number of Shares held.
This commit is contained in:
@@ -40,7 +40,6 @@
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -959,25 +958,6 @@ class Vault_test : public beast::unit_test::suite
|
||||
}
|
||||
});
|
||||
|
||||
testCase([&](Env& env,
|
||||
Account const& issuer,
|
||||
Account const& owner,
|
||||
Asset const& asset,
|
||||
Vault& vault) {
|
||||
testcase("clawback from self");
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
{
|
||||
auto tx = vault.clawback(
|
||||
{.issuer = issuer,
|
||||
.id = keylet.key,
|
||||
.holder = issuer,
|
||||
.amount = asset(10)});
|
||||
env(tx, ter{temMALFORMED});
|
||||
}
|
||||
});
|
||||
|
||||
testCase([&](Env& env,
|
||||
Account const&,
|
||||
Account const& owner,
|
||||
@@ -1216,11 +1196,13 @@ class Vault_test : public beast::unit_test::suite
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
|
||||
// Preclaim only checks for native assets.
|
||||
if (asset.native())
|
||||
{
|
||||
auto tx = vault.clawback(
|
||||
{.issuer = owner,
|
||||
{.issuer = issuer,
|
||||
.id = keylet.key,
|
||||
.holder = issuer,
|
||||
.holder = owner,
|
||||
.amount = asset(50)});
|
||||
env(tx, ter(temMALFORMED));
|
||||
}
|
||||
@@ -1943,8 +1925,20 @@ class Vault_test : public beast::unit_test::suite
|
||||
env.close();
|
||||
|
||||
{
|
||||
auto tx = vault.clawback(
|
||||
{.issuer = owner, .id = keylet.key, .holder = depositor});
|
||||
auto tx = vault.clawback({
|
||||
.issuer = depositor,
|
||||
.id = keylet.key,
|
||||
.holder = depositor,
|
||||
});
|
||||
env(tx, ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
{
|
||||
auto tx = vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = keylet.key,
|
||||
.holder = depositor,
|
||||
});
|
||||
env(tx, ter(tecNO_PERMISSION));
|
||||
}
|
||||
});
|
||||
@@ -2396,6 +2390,15 @@ class Vault_test : public beast::unit_test::suite
|
||||
env(tx, ter(tecNO_AUTH));
|
||||
}
|
||||
|
||||
{
|
||||
// Cannot clawback if issuer is the holder
|
||||
tx = vault.clawback(
|
||||
{.issuer = issuer,
|
||||
.id = keylet.key,
|
||||
.holder = issuer,
|
||||
.amount = asset(800)});
|
||||
env(tx, ter(tecNO_PERMISSION));
|
||||
}
|
||||
// Clawback works
|
||||
tx = vault.clawback(
|
||||
{.issuer = issuer,
|
||||
@@ -5262,6 +5265,542 @@ class Vault_test : public beast::unit_test::suite
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testVaultClawbackBurnShares()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace loanBroker;
|
||||
using namespace loan;
|
||||
Env env(*this, beast::severities::kWarning);
|
||||
|
||||
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,
|
||||
Account const& owner,
|
||||
Account const& depositor) -> std::pair<Vault, Keylet> {
|
||||
Vault vault{env};
|
||||
|
||||
auto const& [tx, vaultKeylet] =
|
||||
vault.create({.owner = owner, .asset = asset});
|
||||
env(tx, ter(tesSUCCESS), THISLINE);
|
||||
env.close();
|
||||
|
||||
auto const& vaultSle = env.le(vaultKeylet);
|
||||
BEAST_EXPECT(vaultSle != nullptr);
|
||||
|
||||
Asset share = vaultSle->at(sfShareMPTID);
|
||||
|
||||
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());
|
||||
|
||||
// attempt to clawback shares while there are assets fails
|
||||
env(vault.clawback(
|
||||
{.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = share(0).value()}),
|
||||
ter(tecNO_PERMISSION),
|
||||
THISLINE);
|
||||
env.close();
|
||||
|
||||
auto const& sharesAvailable = vaultShareBalance(vaultKeylet);
|
||||
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();
|
||||
|
||||
// attempt to clawback shares while there assetsAvailable == 0 and
|
||||
// assetsTotal > 0 fails
|
||||
env(vault.clawback(
|
||||
{.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = share(0).value()}),
|
||||
ter(tecNO_PERMISSION),
|
||||
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());
|
||||
BEAST_EXPECT(vaultShareBalance(vaultKeylet) == sharesAvailable);
|
||||
|
||||
return std::make_pair(vault, vaultKeylet);
|
||||
};
|
||||
|
||||
auto const testCase = [&](PrettyAsset const& asset,
|
||||
std::string const& prefix,
|
||||
Account const& owner,
|
||||
Account const& depositor) {
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (share) - " + prefix +
|
||||
" owner asset clawback fails");
|
||||
auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = asset(100).value(),
|
||||
}),
|
||||
// when asset is XRP or owner is not issuer clawback fail
|
||||
// when owner is issuer precision loss occurs as vault is
|
||||
// empty
|
||||
asset.native() ? ter(temMALFORMED)
|
||||
: asset.raw().getIssuer() != owner.id()
|
||||
? ter(tecNO_PERMISSION)
|
||||
: ter(tecPRECISION_LOSS),
|
||||
THISLINE);
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (share) - " + prefix +
|
||||
" owner incomplete share clawback fails");
|
||||
auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
|
||||
auto const& vaultSle = env.le(vaultKeylet);
|
||||
BEAST_EXPECT(vaultSle != nullptr);
|
||||
if (!vaultSle)
|
||||
return;
|
||||
Asset share = vaultSle->at(sfShareMPTID);
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = share(1).value(),
|
||||
}),
|
||||
ter(tecLIMIT_EXCEEDED),
|
||||
THISLINE);
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (share) - " + prefix +
|
||||
" owner implicit complete share clawback");
|
||||
auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
}),
|
||||
// when owner is issuer implicit clawback fails
|
||||
asset.native() || asset.raw().getIssuer() != owner.id()
|
||||
? ter(tesSUCCESS)
|
||||
: ter(tecWRONG_ASSET),
|
||||
THISLINE);
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (share) - " + prefix +
|
||||
" owner explicit complete share clawback succeeds");
|
||||
auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
|
||||
auto const& vaultSle = env.le(vaultKeylet);
|
||||
BEAST_EXPECT(vaultSle != nullptr);
|
||||
if (!vaultSle)
|
||||
return;
|
||||
Asset share = vaultSle->at(sfShareMPTID);
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = share(vaultShareBalance(vaultKeylet)).value(),
|
||||
}),
|
||||
ter(tesSUCCESS),
|
||||
THISLINE);
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (share) - " + prefix +
|
||||
" owner can clawback own shares");
|
||||
auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
|
||||
auto const& vaultSle = env.le(vaultKeylet);
|
||||
BEAST_EXPECT(vaultSle != nullptr);
|
||||
if (!vaultSle)
|
||||
return;
|
||||
Asset share = vaultSle->at(sfShareMPTID);
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = owner,
|
||||
.amount = share(vaultShareBalance(vaultKeylet)).value(),
|
||||
}),
|
||||
ter(tesSUCCESS),
|
||||
THISLINE);
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (share) - " + prefix +
|
||||
" empty vault share clawback fails");
|
||||
auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
|
||||
auto const& vaultSle = env.le(vaultKeylet);
|
||||
if (BEAST_EXPECT(vaultSle != nullptr))
|
||||
return;
|
||||
Asset share = vaultSle->at(sfShareMPTID);
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = owner,
|
||||
.amount = share(vaultShareBalance(vaultKeylet)).value(),
|
||||
}),
|
||||
ter(tesSUCCESS),
|
||||
THISLINE);
|
||||
|
||||
// Now the vault is empty, clawback again fails
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = owner,
|
||||
}),
|
||||
ter(tecNO_PERMISSION),
|
||||
THISLINE);
|
||||
env.close();
|
||||
}
|
||||
};
|
||||
|
||||
Account owner{"alice"};
|
||||
Account depositor{"bob"};
|
||||
Account issuer{"issuer"};
|
||||
|
||||
env.fund(XRP(10000), issuer, owner, depositor);
|
||||
env.close();
|
||||
|
||||
// Test XRP
|
||||
PrettyAsset xrp = xrpIssue();
|
||||
testCase(xrp, "XRP", owner, depositor);
|
||||
testCase(xrp, "XRP (depositor is owner)", owner, owner);
|
||||
|
||||
// Test IOU
|
||||
PrettyAsset IOU = issuer["IOU"];
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
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, "IOU", owner, depositor);
|
||||
testCase(IOU, "IOU (owner is issuer)", issuer, depositor);
|
||||
|
||||
// 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, owner, MPT(1000)));
|
||||
env(pay(issuer, depositor, MPT(1000)));
|
||||
env.close();
|
||||
testCase(MPT, "MPT", owner, depositor);
|
||||
testCase(MPT, "MPT (owner is issuer)", issuer, depositor);
|
||||
}
|
||||
|
||||
void
|
||||
testVaultClawbackAssets()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace loanBroker;
|
||||
using namespace loan;
|
||||
Env env(*this);
|
||||
|
||||
auto const setupVault =
|
||||
[&](PrettyAsset const& asset,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
Account const& issuer) -> std::pair<Vault, Keylet> {
|
||||
Vault vault{env};
|
||||
|
||||
auto const& [tx, vaultKeylet] =
|
||||
vault.create({.owner = owner, .asset = asset});
|
||||
env(tx, ter(tesSUCCESS), THISLINE);
|
||||
env.close();
|
||||
|
||||
auto const& vaultSle = env.le(vaultKeylet);
|
||||
BEAST_EXPECT(vaultSle != nullptr);
|
||||
env(vault.deposit(
|
||||
{.depositor = depositor,
|
||||
.id = vaultKeylet.key,
|
||||
.amount = asset(100)}),
|
||||
ter(tesSUCCESS),
|
||||
THISLINE);
|
||||
env.close();
|
||||
|
||||
return std::make_pair(vault, vaultKeylet);
|
||||
};
|
||||
|
||||
auto const testCase = [&](PrettyAsset const& asset,
|
||||
std::string const& prefix,
|
||||
Account const& owner,
|
||||
Account const& depositor,
|
||||
Account const& issuer) {
|
||||
if (asset.native())
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" issuer XRP clawback fails");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, owner, depositor, issuer);
|
||||
// If the asset is XRP, clawback with amount fails as malfored
|
||||
// when asset is specified.
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = issuer,
|
||||
.amount = asset(1).value(),
|
||||
}),
|
||||
ter(temMALFORMED),
|
||||
THISLINE);
|
||||
// When asset is implicit, clawback fails as no permission.
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = issuer,
|
||||
}),
|
||||
ter(tecNO_PERMISSION),
|
||||
THISLINE);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" clawback for different asset fails");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, owner, depositor, issuer);
|
||||
|
||||
Account issuer2{"issuer2"};
|
||||
PrettyAsset asset2 = issuer2["FOO"];
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = asset2(1).value(),
|
||||
}),
|
||||
ter(tecWRONG_ASSET),
|
||||
THISLINE);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" ambiguous owner/issuer asset clawback fails");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, issuer, depositor, issuer);
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = issuer,
|
||||
}),
|
||||
ter(tecWRONG_ASSET),
|
||||
THISLINE);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" non-issuer asset clawback fails");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, owner, depositor, issuer);
|
||||
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
}),
|
||||
ter(tecNO_PERMISSION),
|
||||
THISLINE);
|
||||
|
||||
env(vault.clawback({
|
||||
.issuer = owner,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = asset(1).value(),
|
||||
}),
|
||||
ter(tecNO_PERMISSION),
|
||||
THISLINE);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" issuer clawback from self fails");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, owner, issuer, issuer);
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = issuer,
|
||||
}),
|
||||
ter(tecNO_PERMISSION),
|
||||
THISLINE);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" issuer share clawback fails");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, owner, depositor, issuer);
|
||||
auto const& vaultSle = env.le(vaultKeylet);
|
||||
BEAST_EXPECT(vaultSle != nullptr);
|
||||
if (!vaultSle)
|
||||
return;
|
||||
Asset share = vaultSle->at(sfShareMPTID);
|
||||
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = share(1).value(),
|
||||
}),
|
||||
ter(tecNO_PERMISSION),
|
||||
THISLINE);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" partial issuer asset clawback succeeds");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, owner, depositor, issuer);
|
||||
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = asset(1).value(),
|
||||
}),
|
||||
ter(tesSUCCESS),
|
||||
THISLINE);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" full issuer asset clawback succeeds");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, owner, depositor, issuer);
|
||||
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
.amount = asset(100).value(),
|
||||
}),
|
||||
ter(tesSUCCESS),
|
||||
THISLINE);
|
||||
}
|
||||
|
||||
{
|
||||
testcase(
|
||||
"VaultClawback (asset) - " + prefix +
|
||||
" implicit full issuer asset clawback succeeds");
|
||||
auto [vault, vaultKeylet] =
|
||||
setupVault(asset, owner, depositor, issuer);
|
||||
|
||||
env(vault.clawback({
|
||||
.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = depositor,
|
||||
}),
|
||||
ter(tesSUCCESS),
|
||||
THISLINE);
|
||||
}
|
||||
};
|
||||
|
||||
Account owner{"alice"};
|
||||
Account depositor{"bob"};
|
||||
Account issuer{"issuer"};
|
||||
|
||||
env.fund(XRP(10000), issuer, owner, depositor);
|
||||
env.close();
|
||||
|
||||
// Test XRP
|
||||
PrettyAsset xrp = xrpIssue();
|
||||
testCase(xrp, "XRP", owner, depositor, issuer);
|
||||
|
||||
// Test IOU
|
||||
PrettyAsset IOU = issuer["IOU"];
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
env.trust(IOU(1000), owner);
|
||||
env.trust(IOU(1000), depositor);
|
||||
env(pay(issuer, owner, IOU(1000)));
|
||||
env(pay(issuer, depositor, IOU(1000)));
|
||||
env.close();
|
||||
testCase(IOU, "IOU", owner, depositor, issuer);
|
||||
|
||||
// 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, "MPT", owner, depositor, issuer);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -5280,6 +5819,8 @@ public:
|
||||
testScaleIOU();
|
||||
testRPC();
|
||||
testDelegate();
|
||||
testVaultClawbackBurnShares();
|
||||
testVaultClawbackAssets();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ hasPrivilege(STTx const& tx, Privilege priv)
|
||||
switch (tx.getTxnType())
|
||||
{
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
|
||||
// Deprecated types
|
||||
default:
|
||||
return false;
|
||||
@@ -2647,6 +2648,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);
|
||||
@@ -3092,6 +3094,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
|
||||
@@ -3474,29 +3480,56 @@ 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, or by the vault owner of an "
|
||||
"empty vault";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
}
|
||||
|
||||
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||
if (vaultDeltaAssets)
|
||||
{
|
||||
if (*vaultDeltaAssets >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must decrease vault "
|
||||
"balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!vaultDeltaAssets)
|
||||
if (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)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets available "
|
||||
"must add up";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else if (!vaultHoldsNoAssets(beforeVault))
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must change vault balance";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
if (*vaultDeltaAssets >= zero)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback must decrease vault "
|
||||
"balance";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
|
||||
if (!accountDeltaShares)
|
||||
{
|
||||
@@ -3529,24 +3562,6 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (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)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: clawback and assets available must "
|
||||
"add up";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -880,6 +880,7 @@ class ValidVault
|
||||
uint256 key = beast::zero;
|
||||
Asset asset = {};
|
||||
AccountID pseudoId = {};
|
||||
AccountID owner = {};
|
||||
uint192 shareMPTID = beast::zero;
|
||||
Number assetsTotal = 0;
|
||||
Number assetsAvailable = 0;
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/tx/detail/VaultClawback.h>
|
||||
|
||||
//
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -41,15 +41,6 @@ VaultClawback::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
AccountID const issuer = ctx.tx[sfAccount];
|
||||
AccountID const holder = ctx.tx[sfHolder];
|
||||
|
||||
if (issuer == holder)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: issuer cannot be holder.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
auto const amount = ctx.tx[~sfAmount];
|
||||
if (amount)
|
||||
{
|
||||
@@ -61,17 +52,27 @@ VaultClawback::preflight(PreflightContext const& ctx)
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
else if (amount->asset().getIssuer() != issuer)
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "VaultClawback: only asset issuer can clawback.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
clawbackAmount(
|
||||
std::shared_ptr<SLE const> const& vault,
|
||||
std::optional<STAmount> const& maybeAmount,
|
||||
AccountID const& account)
|
||||
{
|
||||
if (maybeAmount)
|
||||
return *maybeAmount;
|
||||
|
||||
Asset const share = MPTIssue{vault->at(sfShareMPTID)};
|
||||
if (account == vault->at(sfOwner))
|
||||
return STAmount{share};
|
||||
|
||||
return STAmount{vault->at(sfAsset)};
|
||||
}
|
||||
|
||||
TER
|
||||
VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
@@ -79,61 +80,264 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
|
||||
if (!vault)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
auto account = ctx.tx[sfAccount];
|
||||
auto const issuer = ctx.view.read(keylet::account(account));
|
||||
if (!issuer)
|
||||
Asset const vaultAsset = vault->at(sfAsset);
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const holder = ctx.tx[sfHolder];
|
||||
auto const maybeAmount = ctx.tx[~sfAmount];
|
||||
auto const mptIssuanceID = vault->at(sfShareMPTID);
|
||||
auto const sleShareIssuance =
|
||||
ctx.view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleShareIssuance)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(ctx.j.error()) << "VaultClawback: missing issuer account.";
|
||||
JLOG(ctx.j.error())
|
||||
<< "VaultClawback: missing issuance of vault shares.";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
Asset const vaultAsset = vault->at(sfAsset);
|
||||
if (auto const amount = ctx.tx[~sfAmount];
|
||||
amount && vaultAsset != amount->asset())
|
||||
Asset const share = MPTIssue{mptIssuanceID};
|
||||
|
||||
// Ambiguous case: If Issuer is Owner they must specify the asset
|
||||
if (!maybeAmount && !vaultAsset.native() &&
|
||||
vaultAsset.getIssuer() == vault->at(sfOwner))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "VaultClawback: must specify amount when issuer is owner.";
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
if (vaultAsset.native())
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
|
||||
return tecNO_PERMISSION; // Cannot clawback XRP.
|
||||
}
|
||||
else if (vaultAsset.getIssuer() != account)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: only asset issuer can clawback.";
|
||||
return tecNO_PERMISSION; // Only issuers can clawback.
|
||||
}
|
||||
|
||||
if (vaultAsset.holds<MPTIssue>())
|
||||
{
|
||||
auto const mpt = vaultAsset.get<MPTIssue>();
|
||||
auto const mptIssue =
|
||||
ctx.view.read(keylet::mptIssuance(mpt.getMptID()));
|
||||
if (mptIssue == nullptr)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
auto const amount = clawbackAmount(vault, maybeAmount, account);
|
||||
|
||||
std::uint32_t const issueFlags = mptIssue->getFieldU32(sfFlags);
|
||||
if (!(issueFlags & lsfMPTCanClawback))
|
||||
// There is a special case that allows the VaultOwner to use clawback to
|
||||
// burn shares when Vault assets total and available are zero, but
|
||||
// shares remain. However, that case is handled in doApply() directly,
|
||||
// so here we just enforce checks.
|
||||
if (amount.asset() == share)
|
||||
{
|
||||
// Only the Vault Owner may clawback shares
|
||||
if (account != vault->at(sfOwner))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "VaultClawback: cannot clawback MPT vault asset.";
|
||||
<< "VaultClawback: only vault owner can clawback shares.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
else if (vaultAsset.holds<Issue>())
|
||||
{
|
||||
std::uint32_t const issuerFlags = issuer->getFieldU32(sfFlags);
|
||||
if (!(issuerFlags & lsfAllowTrustLineClawback) ||
|
||||
(issuerFlags & lsfNoFreeze))
|
||||
|
||||
auto const assetsTotal = vault->at(sfAssetsTotal);
|
||||
auto const assetsAvailable = vault->at(sfAssetsAvailable);
|
||||
auto const sharesTotal = sleShareIssuance->at(sfOutstandingAmount);
|
||||
|
||||
// Owner can clawback funds when the vault has shares but no assets
|
||||
if (sharesTotal == 0 || (assetsTotal != 0 || assetsAvailable != 0))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "VaultClawback: cannot clawback IOU vault asset.";
|
||||
<< "VaultClawback: vault owner can clawback shares only"
|
||||
" when vault has no assets.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// If amount is non-zero, the VaultOwner must burn all shares
|
||||
if (amount != beast::zero)
|
||||
{
|
||||
Number const& sharesHeld = accountHolds(
|
||||
ctx.view,
|
||||
holder,
|
||||
share,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
AuthHandling::ahIGNORE_AUTH,
|
||||
ctx.j);
|
||||
|
||||
// The VaultOwner must burn all shares
|
||||
if (amount != sharesHeld)
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "VaultClawback: vault owner must clawback all "
|
||||
"shares.";
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
// The asset that is being clawed back is the vault asset
|
||||
if (amount.asset() == vaultAsset)
|
||||
{
|
||||
// XRP cannot be clawed back
|
||||
if (vaultAsset.native())
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// Only the Asset Issuer may clawback the asset
|
||||
if (account != vaultAsset.getIssuer())
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "VaultClawback: only asset issuer can clawback asset.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// The issuer cannot clawback from itself
|
||||
if (account == holder)
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "VaultClawback: issuer cannot be the holder.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return std::visit(
|
||||
[&]<ValidIssueType TIss>(TIss const& issue) -> TER {
|
||||
if constexpr (std::is_same_v<TIss, MPTIssue>)
|
||||
{
|
||||
auto const mptIssue =
|
||||
ctx.view.read(keylet::mptIssuance(issue.getMptID()));
|
||||
if (mptIssue == nullptr)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
std::uint32_t const issueFlags =
|
||||
mptIssue->getFieldU32(sfFlags);
|
||||
if (!(issueFlags & lsfMPTCanClawback))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback "
|
||||
"MPT vault asset.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<TIss, Issue>)
|
||||
{
|
||||
auto const issuerSle =
|
||||
ctx.view.read(keylet::account(account));
|
||||
if (!issuerSle)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(ctx.j.error())
|
||||
<< "VaultClawback: missing submitter account.";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
std::uint32_t const issuerFlags =
|
||||
issuerSle->getFieldU32(sfFlags);
|
||||
if (!(issuerFlags & lsfAllowTrustLineClawback) ||
|
||||
(issuerFlags & lsfNoFreeze))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback "
|
||||
"IOU vault asset.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
return tesSUCCESS;
|
||||
},
|
||||
vaultAsset.value());
|
||||
}
|
||||
|
||||
// Invalid asset
|
||||
return tecWRONG_ASSET;
|
||||
}
|
||||
|
||||
Expected<std::pair<STAmount, STAmount>, TER>
|
||||
VaultClawback::assetsToClawback(
|
||||
std::shared_ptr<SLE> const& vault,
|
||||
std::shared_ptr<SLE const> const& sleShareIssuance,
|
||||
AccountID const& holder,
|
||||
STAmount const& clawbackAmount)
|
||||
{
|
||||
if (clawbackAmount.asset() != vault->at(sfAsset))
|
||||
{
|
||||
// preclaim should have blocked this , now it's an internal error
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "VaultClawback: asset mismatch in clawback.";
|
||||
return Unexpected(tecINTERNAL);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const assetsAvailable = vault->at(sfAssetsAvailable);
|
||||
auto const mptIssuanceID = *vault->at(sfShareMPTID);
|
||||
MPTIssue const share{mptIssuanceID};
|
||||
|
||||
if (clawbackAmount == beast::zero)
|
||||
{
|
||||
auto const sharesDestroyed = accountHolds(
|
||||
view(),
|
||||
holder,
|
||||
share,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
AuthHandling::ahIGNORE_AUTH,
|
||||
j_);
|
||||
auto const maybeAssets =
|
||||
sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
|
||||
if (!maybeAssets)
|
||||
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
|
||||
return std::make_pair(*maybeAssets, sharesDestroyed);
|
||||
}
|
||||
|
||||
STAmount sharesDestroyed;
|
||||
STAmount assetsRecovered = clawbackAmount;
|
||||
try
|
||||
{
|
||||
{
|
||||
auto const maybeShares = assetsToSharesWithdraw(
|
||||
vault, sleShareIssuance, assetsRecovered);
|
||||
if (!maybeShares)
|
||||
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
sharesDestroyed = *maybeShares;
|
||||
}
|
||||
|
||||
auto const maybeAssets =
|
||||
sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
|
||||
if (!maybeAssets)
|
||||
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
assetsRecovered = *maybeAssets;
|
||||
|
||||
// Clamp to maximum.
|
||||
if (assetsRecovered > *assetsAvailable)
|
||||
{
|
||||
assetsRecovered = *assetsAvailable;
|
||||
// Note, it is important to truncate the number of shares,
|
||||
// otherwise the corresponding assets might breach the
|
||||
// AssetsAvailable
|
||||
{
|
||||
auto const maybeShares = assetsToSharesWithdraw(
|
||||
vault,
|
||||
sleShareIssuance,
|
||||
assetsRecovered,
|
||||
TruncateShares::yes);
|
||||
if (!maybeShares)
|
||||
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
sharesDestroyed = *maybeShares;
|
||||
}
|
||||
|
||||
auto const maybeAssets = sharesToAssetsWithdraw(
|
||||
vault, sleShareIssuance, sharesDestroyed);
|
||||
if (!maybeAssets)
|
||||
return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE
|
||||
assetsRecovered = *maybeAssets;
|
||||
if (assetsRecovered > *assetsAvailable)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error())
|
||||
<< "VaultClawback: invalid rounding of shares.";
|
||||
return Unexpected(tecINTERNAL);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough
|
||||
// Scale so we avoid spamming the log and only use debug here.
|
||||
JLOG(j_.debug()) //
|
||||
<< "VaultClawback: overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale).value() //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleShareIssuance->at(sfOutstandingAmount)
|
||||
<< ", amount=" << clawbackAmount.value();
|
||||
return Unexpected(tecPATH_DRY);
|
||||
}
|
||||
|
||||
return std::make_pair(assetsRecovered, sharesDestroyed);
|
||||
}
|
||||
|
||||
TER
|
||||
@@ -144,7 +348,7 @@ VaultClawback::doApply()
|
||||
if (!vault)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const mptIssuanceID = *((*vault)[sfShareMPTID]);
|
||||
auto const mptIssuanceID = *vault->at(sfShareMPTID);
|
||||
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
{
|
||||
@@ -153,105 +357,47 @@ VaultClawback::doApply()
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
MPTIssue const share{mptIssuanceID};
|
||||
|
||||
Asset const vaultAsset = vault->at(sfAsset);
|
||||
STAmount const amount = [&]() -> STAmount {
|
||||
auto const maybeAmount = tx[~sfAmount];
|
||||
if (maybeAmount)
|
||||
return *maybeAmount;
|
||||
return {sfAmount, vaultAsset, 0};
|
||||
}();
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vaultAsset,
|
||||
"ripple::VaultClawback::doApply : matching asset");
|
||||
STAmount const amount = clawbackAmount(vault, tx[~sfAmount], account_);
|
||||
|
||||
auto assetsAvailable = vault->at(sfAssetsAvailable);
|
||||
auto assetsTotal = vault->at(sfAssetsTotal);
|
||||
|
||||
[[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
|
||||
XRPL_ASSERT(
|
||||
lossUnrealized <= (assetsTotal - assetsAvailable),
|
||||
"ripple::VaultClawback::doApply : loss and assets do balance");
|
||||
|
||||
AccountID holder = tx[sfHolder];
|
||||
MPTIssue const share{mptIssuanceID};
|
||||
STAmount sharesDestroyed = {share};
|
||||
STAmount assetsRecovered;
|
||||
try
|
||||
STAmount assetsRecovered = {vault->at(sfAsset)};
|
||||
|
||||
// The Owner is burning shares
|
||||
if (account_ == vault->at(sfOwner) && amount.asset() == share)
|
||||
{
|
||||
if (amount == beast::zero)
|
||||
{
|
||||
sharesDestroyed = accountHolds(
|
||||
view(),
|
||||
holder,
|
||||
share,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
AuthHandling::ahIGNORE_AUTH,
|
||||
j_);
|
||||
|
||||
auto const maybeAssets =
|
||||
sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
|
||||
if (!maybeAssets)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
assetsRecovered = *maybeAssets;
|
||||
}
|
||||
else
|
||||
{
|
||||
assetsRecovered = amount;
|
||||
{
|
||||
auto const maybeShares =
|
||||
assetsToSharesWithdraw(vault, sleIssuance, assetsRecovered);
|
||||
if (!maybeShares)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
sharesDestroyed = *maybeShares;
|
||||
}
|
||||
|
||||
auto const maybeAssets =
|
||||
sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
|
||||
if (!maybeAssets)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
assetsRecovered = *maybeAssets;
|
||||
}
|
||||
|
||||
// Clamp to maximum.
|
||||
if (assetsRecovered > *assetsAvailable)
|
||||
{
|
||||
assetsRecovered = *assetsAvailable;
|
||||
// Note, it is important to truncate the number of shares, otherwise
|
||||
// the corresponding assets might breach the AssetsAvailable
|
||||
{
|
||||
auto const maybeShares = assetsToSharesWithdraw(
|
||||
vault, sleIssuance, assetsRecovered, TruncateShares::yes);
|
||||
if (!maybeShares)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
sharesDestroyed = *maybeShares;
|
||||
}
|
||||
|
||||
auto const maybeAssets =
|
||||
sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed);
|
||||
if (!maybeAssets)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
assetsRecovered = *maybeAssets;
|
||||
if (assetsRecovered > *assetsAvailable)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error())
|
||||
<< "VaultClawback: invalid rounding of shares.";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
sharesDestroyed = accountHolds(
|
||||
view(),
|
||||
holder,
|
||||
share,
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
AuthHandling::ahIGNORE_AUTH,
|
||||
j_);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
else // The Issuer is clawbacking vault assets
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j_.debug()) //
|
||||
<< "VaultClawback: overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale).value() //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
|
||||
<< ", amount=" << amount.value();
|
||||
return tecPATH_DRY;
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vaultAsset,
|
||||
"xrpl::VaultClawback::doApply : matching asset");
|
||||
|
||||
auto const clawbackParts =
|
||||
assetsToClawback(vault, sleIssuance, holder, amount);
|
||||
if (!clawbackParts)
|
||||
return clawbackParts.error();
|
||||
|
||||
assetsRecovered = clawbackParts->first;
|
||||
sharesDestroyed = clawbackParts->second;
|
||||
}
|
||||
|
||||
if (sharesDestroyed == beast::zero)
|
||||
@@ -301,30 +447,34 @@ VaultClawback::doApply()
|
||||
// else quietly ignore, holder balance is not zero
|
||||
}
|
||||
|
||||
// Transfer assets from vault to issuer.
|
||||
if (auto const ter = accountSend(
|
||||
view(),
|
||||
vaultAccount,
|
||||
account_,
|
||||
assetsRecovered,
|
||||
j_,
|
||||
WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Sanity check
|
||||
if (accountHolds(
|
||||
view(),
|
||||
vaultAccount,
|
||||
assetsRecovered.asset(),
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
AuthHandling::ahIGNORE_AUTH,
|
||||
j_) < beast::zero)
|
||||
if (assetsRecovered > beast::zero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "VaultClawback: negative balance of vault assets.";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
// Transfer assets from vault to issuer.
|
||||
if (auto const ter = accountSend(
|
||||
view(),
|
||||
vaultAccount,
|
||||
account_,
|
||||
assetsRecovered,
|
||||
j_,
|
||||
WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Sanity check
|
||||
if (accountHolds(
|
||||
view(),
|
||||
vaultAccount,
|
||||
assetsRecovered.asset(),
|
||||
FreezeHandling::fhIGNORE_FREEZE,
|
||||
AuthHandling::ahIGNORE_AUTH,
|
||||
j_) < beast::zero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error())
|
||||
<< "VaultClawback: negative balance of vault assets.";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -41,6 +41,14 @@ public:
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
private:
|
||||
Expected<std::pair<STAmount, STAmount>, TER>
|
||||
assetsToClawback(
|
||||
std::shared_ptr<SLE> const& vault,
|
||||
std::shared_ptr<SLE const> const& sleShareIssuance,
|
||||
AccountID const& holder,
|
||||
STAmount const& clawbackAmount);
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
Reference in New Issue
Block a user