Enforce reserve when creating trust line or MPToken in VaultWithdraw#5857
Enforce reserve when creating trust line or MPToken in VaultWithdraw#5857
VaultWithdraw#5857Conversation
6fde241 to
fd178c5
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #5857 +/- ##
=========================================
- Coverage 79.5% 79.5% -0.0%
=========================================
Files 816 816
Lines 72185 72176 -9
Branches 8297 8273 -24
=========================================
- Hits 57372 57362 -10
- Misses 14813 14814 +1
🚀 New features to boost your workflow:
|
src/test/app/Vault_test.cpp
Outdated
| @@ -1888,6 +1888,37 @@ class Vault_test : public beast::unit_test::suite | |||
| env(tx, ter(tecNO_AUTH)); | |||
There was a problem hiding this comment.
It's hard to tell with your test case/suite structure but do you have tests where the issuer is the depositor? Also you test frozen but I don't see deep freeze or auth or no ripple?
There was a problem hiding this comment.
In this branch, there is withdrawal to 3rd party which happens to be issuer
rippled/src/test/app/Vault_test.cpp
Line 376 in fd178c5
However this branch does not allow the issuer to be also depositor. This is fixed in #5518 and the test to match is
rippled/src/test/app/Vault_test.cpp
Line 399 in 42be446
| auto const sleAcct = view().read(keylet::account(dstAcct)); | ||
| if (!sleAcct) | ||
| return tecINTERNAL; // LCOV_EXCL_LINE | ||
|
|
||
| if (auto const sleLine = | ||
| view().read(keylet::line(dstAcct, vaultAsset.get<Issue>())); | ||
| sleLine == nullptr) | ||
| { | ||
| // This should be enforced by `requireAuth` called in `preclaim` | ||
| if (dstAcct != account_) | ||
| return tecINTERNAL; // LCOV_EXCL_LINE | ||
|
|
||
| // Can the account cover the trust line's reserve ? | ||
| std::uint32_t const ownerCount = sleAcct->at(sfOwnerCount); | ||
| if (mPriorBalance < view().fees().accountReserve(ownerCount + 1)) | ||
| return tecNO_LINE_INSUF_RESERVE; | ||
| } |
There was a problem hiding this comment.
I'm going to suggest a little reorganization here.
| auto const sleAcct = view().read(keylet::account(dstAcct)); | |
| if (!sleAcct) | |
| return tecINTERNAL; // LCOV_EXCL_LINE | |
| if (auto const sleLine = | |
| view().read(keylet::line(dstAcct, vaultAsset.get<Issue>())); | |
| sleLine == nullptr) | |
| { | |
| // This should be enforced by `requireAuth` called in `preclaim` | |
| if (dstAcct != account_) | |
| return tecINTERNAL; // LCOV_EXCL_LINE | |
| // Can the account cover the trust line's reserve ? | |
| std::uint32_t const ownerCount = sleAcct->at(sfOwnerCount); | |
| if (mPriorBalance < view().fees().accountReserve(ownerCount + 1)) | |
| return tecNO_LINE_INSUF_RESERVE; | |
| } | |
| if (auto const sleLine = | |
| view().read(keylet::line(dstAcct, vaultAsset.get<Issue>())); | |
| sleLine == nullptr) | |
| { | |
| // This should be enforced by `requireAuth` called in `preclaim` | |
| if (dstAcct != account_) | |
| return tecINTERNAL; // LCOV_EXCL_LINE | |
| auto const sleAcct = view().read(keylet::account(account_)); | |
| if (!sleAcct) | |
| return tecINTERNAL; // LCOV_EXCL_LINE | |
| // Can account_ cover the trust line's reserve ? | |
| std::uint32_t const ownerCount = sleAcct->at(sfOwnerCount); | |
| if (mPriorBalance < view().fees().accountReserve(ownerCount + 1)) | |
| return tecNO_LINE_INSUF_RESERVE; | |
| } |
Basically, don't load the AccountRoot SLE until after you've verified that the line doesn't exist, and then when you do, specify it as account_, so it's clearer which account you're dealing with.
Even though computing the Keylet for the line is slightly more expensive than computing the Keylet for the account, if the line already exists, or if dstAcct != account_, you'll skip computing the account key entirely, and loading the object entirely.
There was a problem hiding this comment.
Thanks, this is good.
|
While writing this I've realised that This is similar to what the |
|
|
||
| if (sleAcct->isFlag(lsfRequireDestTag) && | ||
| !ctx.tx.isFieldPresent(sfDestinationTag)) | ||
| return tecDST_TAG_NEEDED; // Cannot send without a tag |
There was a problem hiding this comment.
this change is for consistency with other transaction types
There was a problem hiding this comment.
this change is for consistency with other transaction types
This code is basically duplicated from the account != dstAccount block, other than the result for a missing account.
Would it make more sense to take this code out of the if statement, and check which account before deciding which TER to return?
Something like
auto const sleDst = ctx.view.read(keylet::account(dstAcct));
if (sleDst == nullptr)
return account == dstAcct : tecINTERNAL : tecNO_DST;
if (sleDst->isFlag(lsfRequireDestTag) &&
!ctx.tx.isFieldPresent(sfDestinationTag))
return tecDST_TAG_NEEDED; // Cannot send without a tag
// If sending to Account (i.e. not a transfer), we will also create
// (only if authorized) a trust line or MPToken as needed, in doApply().
AuthType authType = AuthType::WeakAuth;
if (account != dstAcct)
{
if (sleDst->isFlag(lsfDepositAuth))
{
if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account)))
return tecNO_PERMISSION;
}
// The destination account must have consented to receive the asset by
// creating a RippleState or MPToken
authType = AuthType::StrongAuth;
}
VaultWithdraw
596441a to
9a64b43
Compare
| return account_; | ||
| }(); | ||
|
|
||
| if (!vaultAsset.native() && dstAcct != vaultAsset.getIssuer()) |
There was a problem hiding this comment.
Shouldn't also check if dstAcct == account_? A trusdtline or MPToken only should be created if that condition is true.
There was a problem hiding this comment.
Good idea, I will do it.
|
|
||
| // If the above succeeded then the required trust line | ||
| // will be created indirectly by `accountSend` below. | ||
| return tesSUCCESS; |
There was a problem hiding this comment.
Don't need this. Can just let it fall through.
|
|
||
| // Above will perform reserve check, create MPToken, | ||
| // attach it to acccount_, adjust owner count etc. | ||
| return tesSUCCESS; |
There was a problem hiding this comment.
Don't need this. Can just let it fall through.
| sleLine == nullptr) | ||
| { | ||
| // Enforced by `requireAuth` called in `preclaim` | ||
| if (dstAcct != account_) |
There was a problem hiding this comment.
dstAcct may be different from account_. I think should only execute std::visit if dstAcct == account_.
There was a problem hiding this comment.
Agree, however this condition is not faulty - it just relies on check performed in preclaim. But I like your idea better.
src/test/app/Vault_test.cpp
Outdated
| Account const& owner, | ||
| Account const& depositor, | ||
| Account const& charlie, | ||
| Account const& dave, |
There was a problem hiding this comment.
I think can simplify testSequence and testCases by defining the accounts once at the start of teseSequences:
void
testSequences()
{
using namespace test::jtx;
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const depositor{"depositor"};
Account const charlie{"charlie"}; // authorized 3rd party
Account const dave{"dave"};
auto const testSequence = [this](
std::string const& prefix,
Env& env,
Vault& vault,
PrettyAsset const& asset)
There was a problem hiding this comment.
This could be true for some other code when the passed accounts are always the same. For instance, issuer is always `Account{"issuer"}'.
c04fd01 to
7a55863
Compare
|
@gregtatcam your feedback is addressed in 7a55863 - thank you ! |
| if constexpr (std::is_same_v<TIss, Issue>) | ||
| { | ||
| std::shared_ptr<SLE> dummy = {}; | ||
| if (auto const sleLine = |
There was a problem hiding this comment.
This is more efficient:
if (!view().exists(keylet::line(dstAcct, issue)))
There was a problem hiding this comment.
also more explicit - thanks !
| } | ||
| else | ||
| { | ||
| if (auto const sleMPT = view().read( |
There was a problem hiding this comment.
This is more efficient:
if (!view().exists(keylet::mptoken(issue.getMptID(), account_)))
src/test/app/Vault_test.cpp
Outdated
| @@ -67,6 +67,7 @@ class Vault_test : public beast::unit_test::suite | |||
| Account const& owner, | |||
| Account const& depositor, | |||
| Account const& charlie, | |||
| Account const& dave, | |||
There was a problem hiding this comment.
If this list of Account params ever gets too unwieldy, you can simply "recreate" them inside the function. The values are deterministic and cached, so you don't have to worry about wasting resources either.
There was a problem hiding this comment.
If this list of
Accountparams ever gets too unwieldy, you can simply "recreate" them inside the function. The values are deterministic and cached, so you don't have to worry about wasting resources either.
I see you have already rewritten them to have the accounts be in an outer scope, which is even simpler.
|
|
||
| if (sleAcct->isFlag(lsfRequireDestTag) && | ||
| !ctx.tx.isFieldPresent(sfDestinationTag)) | ||
| return tecDST_TAG_NEEDED; // Cannot send without a tag |
There was a problem hiding this comment.
this change is for consistency with other transaction types
This code is basically duplicated from the account != dstAccount block, other than the result for a missing account.
Would it make more sense to take this code out of the if statement, and check which account before deciding which TER to return?
Something like
auto const sleDst = ctx.view.read(keylet::account(dstAcct));
if (sleDst == nullptr)
return account == dstAcct : tecINTERNAL : tecNO_DST;
if (sleDst->isFlag(lsfRequireDestTag) &&
!ctx.tx.isFieldPresent(sfDestinationTag))
return tecDST_TAG_NEEDED; // Cannot send without a tag
// If sending to Account (i.e. not a transfer), we will also create
// (only if authorized) a trust line or MPToken as needed, in doApply().
AuthType authType = AuthType::WeakAuth;
if (account != dstAcct)
{
if (sleDst->isFlag(lsfDepositAuth))
{
if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account)))
return tecNO_PERMISSION;
}
// The destination account must have consented to receive the asset by
// creating a RippleState or MPToken
authType = AuthType::StrongAuth;
}
| { | ||
| if (auto const ter = addEmptyHolding( | ||
| view(), account_, mPriorBalance, vaultAsset, j_); | ||
| ter && ter != tecDUPLICATE) |
There was a problem hiding this comment.
This could/should return tesSUCCESS if the line already exists imo.
Also you call isGlobalFrozen here but also call it in preclaim too right? canAddHolding does the same checks as this but I don't see canAddHolding called? Does that mean that this function can return internal?
There was a problem hiding this comment.
This could/should return tesSUCCESS if the line already exists imo.
There are callers where it would be a weird problem if the holding already exists. For example, the transactions that create new accounts.
There was a problem hiding this comment.
Normally I would suggest an additional flag as a function input parameter to choose the semantics of duplicates handling. In this case, if we want to go this way, #5270 would be a better fit to make a change (I would not want this small PR to be an obstacle for the larger PR to trip on).
…draw Similarly to other transaction typed that can create a trust line or MPToken for the transaction submitter (e.g. CashCheck #5285, EscrowFinish #5185 ), VaultWithdraw should enforce reserve before creating a new object. Additionally, the lsfRequireDestTag account flag should be enforced for the transaction submitter.
fdfc354 to
0763af9
Compare
…draw (#5857) Similarly to other transaction typed that can create a trust line or MPToken for the transaction submitter (e.g. CashCheck #5285, EscrowFinish #5185 ), VaultWithdraw should enforce reserve before creating a new object. Additionally, the lsfRequireDestTag account flag should be enforced for the transaction submitter. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com>
…draw (#5857) Similarly to other transaction typed that can create a trust line or MPToken for the transaction submitter (e.g. CashCheck #5285, EscrowFinish #5185 ), VaultWithdraw should enforce reserve before creating a new object. Additionally, the lsfRequireDestTag account flag should be enforced for the transaction submitter. Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com>
High Level Overview of Change
Similarly to other transaction typed which can create a trust line or MPToken for the transaction submitter (e.g.
CashCheck#5285 ,EscrowFinish#5185 ),VaultWithdrawshould enforce reserve before creating a new object.Additionally, enforce
lsfRequireDestTagaccount flag for the transaction submitter.Context of Change
This fixes #5837 and improves consistency with other transaction types.
Type of Change