-
Notifications
You must be signed in to change notification settings - Fork 1.6k
VaultClawback: Burn shares of an empty vault #6120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
gregtatcam
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
1ba2f8d to
2c63982
Compare
|
Sorry for the force-push, the devcontainer commit wasn't signed |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #6120 +/- ##
=======================================
Coverage 79.1% 79.1%
=======================================
Files 839 839
Lines 71386 71419 +33
Branches 8343 8334 -9
=======================================
+ Hits 56447 56485 +38
+ Misses 14939 14934 -5
🚀 New features to boost your workflow:
|
| if (sharesTotal > 0 && assetsTotal == 0 && assetsAvailable == 0 && | ||
| account == owner) | ||
| { | ||
| if (auto const amount = ctx.tx[~sfAmount]; amount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't need to have this additional amount check.
if (auto const amount = ctx.tx[~sfAmount])
| else if (isXRP(amount->asset())) | ||
| { | ||
| JLOG(ctx.j.debug()) << "VaultClawback: cannot clawback XRP."; | ||
| return temMALFORMED; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense to leave the XRP check here in preflight.
To summarize 3.3.1.1 from the proposed spec changes:
- If the transaction is submitted by the vault owner, then amount must be specified as the share type (MPT).
- If the transaction is submitted by the asset issuer, the amount must be specified as the asset type (IOU, MPT).
Thus, specifying sfAmount as XRP is never valid. May as well check it here, and save the effort later.
| Asset const vaultAsset = vault->at(sfAsset); | ||
| if (auto const amount = ctx.tx[~sfAmount]; | ||
| amount && vaultAsset != amount->asset()) | ||
| return tecWRONG_ASSET; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need two cases for this check:
- When the vault is worthless, and the tx is submitted by the vault owner. I'd suggest checking that the asset is the shares MPT in the block below where you check for that.
- When the tx is submitted by the asset issuer. You should move this check to after the worthless vault block.
| if (sharesTotal > 0 && assetsTotal == 0 && assetsAvailable == 0 && | ||
| account == owner) | ||
| { | ||
| if (auto const amount = ctx.tx[~sfAmount]; amount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What should happen if the holder has no shares, and sfAmount is 0?
| // Clamp to maximum. | ||
| if (assetsRecovered > *assetsAvailable) | ||
| { | ||
| assetsRecovered = *assetsAvailable; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to do two passes when assetsRecovered > *assetsAvailable?
Intuitively, it seems like you could clamp assetsRecovered as the first step, then do one set of calls to assetsToSharesWithdraw and sharesToAssetsWithdraw, probably truncating either way, and get the same result with less work.
| std::shared_ptr<SLE> const& vault, | ||
| std::shared_ptr<SLE const> const& sleShareIssuance, | ||
| AccountID const& holder, | ||
| STAmount const& clawbackAmount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm mistaken, clawbackAmount must be the vault asset type in this function, but there's nothing in here that checks. There is an assert followed by a check in assetsToSharesWithdraw, but that will crash nodes running in debug, and we want to avoid that.
| { | ||
| auto tx = vault.clawback( | ||
| {.issuer = owner, | ||
| .id = keylet.key, | ||
| .holder = issuer, | ||
| .amount = asset(50)}); | ||
| env(tx, ter(temMALFORMED)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not usually a fan of removing test cases unless they're redundant or mess up later tests. I think it's usually better to update the test with the new expected result. Do you think that is appropriate here?
| auto const [availablePreDefault, totalPreDefault] = | ||
| vaultAssetBalance(vaultKeylet); | ||
| BEAST_EXPECT(availablePreDefault == totalPreDefault); | ||
| BEAST_EXPECT(availablePreDefault == asset(100).value()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should do an owner clawback here, before the loan to show that it doesn't work when there are assets available.
| // 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should also do an owner clawback here, before the default to show that it doesn't work when assets total is not zero, even if available is.
| // 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be worth another test where amount is explicitly zero.
.amount = shareasset(0),
| env(vault.clawback({ | ||
| .issuer = owner, | ||
| .id = vaultKeylet.key, | ||
| .holder = depositor, | ||
| .amount = asset(vaultShareBalance(vaultKeylet)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is subtle. The spec is very clear that the amount must be the shares asset type. But this is the vault asset type. This test should be failing.
However, as I noted in VaultClawback, if you pass the shares type, you'll get tecWRONG_ASSET. So you've got two mistakes that work together to make it look as if they're doing the right thing.
(When you fix the issues, you should keep these transactions to verify they fail.)
In the context of the Single Asset Vault there exists a situation where
Vault.AssetsTotalandVault.AssetsAvailableare zero, but the Vault still has shares. Such a situation may arise due to a Loan that was issued for the total of Vault's assets defaulting. The problem with such situation is that the Vault becomes permanently stuck, deposits will not be able to settle the balance, and the Vault object is non-deletable. This PR fixes this problem, namely:The PR updates the logic of
VaultClawbacktransaction allowing the Owner of the Vault to submitVaultClawbacktransaction to burn shares of a depositor only when the there are no assets in the vault.Edit:
The link to specification PR: XRPLF/XRPL-Standards#422
High Level Overview of Change
Context of Change
Type of Change
.gitignore, formatting, dropping support for older tooling)API Impact
libxrplchange (any change that may affectlibxrplor dependents oflibxrpl)