diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 2565372ebc..119c3f8b7b 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -909,7 +909,7 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, featureSingleAssetVault, - mayDeleteMPT | mustModifyVault, + mayDeleteMPT | mayAuthorizeMPT | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index ace7a34f81..e55d08795d 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1242,6 +1242,12 @@ addEmptyHolding( // If the line already exists, don't create it again. if (view.read(index)) return tecDUPLICATE; + + // Can the account cover the trust line reserve ? + std::uint32_t const ownerCount = sleDst->at(sfOwnerCount); + if (priorBalance < view.fees().accountReserve(ownerCount + 1)) + return tecNO_LINE_INSUF_RESERVE; + return trustCreate( view, high, diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 4a731ddd57..8b2254a840 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -59,14 +59,15 @@ class Vault_test : public beast::unit_test::suite testSequences() { using namespace test::jtx; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + Account charlie{"charlie"}; // authorized 3rd party + Account dave{"dave"}; - auto const testSequence = [this]( + auto const testSequence = [&, this]( std::string const& prefix, Env& env, - Account const& issuer, - Account const& owner, - Account const& depositor, - Account const& charlie, Vault& vault, PrettyAsset const& asset) { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); @@ -104,11 +105,9 @@ class Vault_test : public beast::unit_test::suite // Several 3rd party accounts which cannot receive funds Account alice{"alice"}; - Account dave{"dave"}; Account erin{"erin"}; // not authorized by issuer - env.fund(XRP(1000), alice, dave, erin); + env.fund(XRP(1000), alice, erin); env(fset(alice, asfDepositAuth)); - env(fset(dave, asfRequireDest)); env.close(); { @@ -328,19 +327,6 @@ class Vault_test : public beast::unit_test::suite env.close(); } - { - testcase( - prefix + - " fail to withdraw with tag but without destination"); - auto tx = vault.withdraw( - {.depositor = depositor, - .id = keylet.key, - .amount = asset(1000)}); - tx[sfDestinationTag] = "0"; - env(tx, ter(temMALFORMED)); - env.close(); - } - if (!asset.raw().native()) { testcase( @@ -368,12 +354,49 @@ class Vault_test : public beast::unit_test::suite env.close(); } + { + testcase(prefix + " withdraw to 3rd party lsfRequireDestTag"); + auto tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + tx[sfDestination] = dave.human(); + tx[sfDestinationTag] = "0"; + env(tx); + env.close(); + } + + { + testcase(prefix + " deposit again"); + auto tx = vault.deposit( + {.depositor = dave, .id = keylet.key, .amount = asset(50)}); + env(tx); + env.close(); + } + + { + testcase(prefix + " fail to withdraw lsfRequireDestTag"); + auto tx = vault.withdraw( + {.depositor = dave, .id = keylet.key, .amount = asset(50)}); + env(tx, ter{tecDST_TAG_NEEDED}); + env.close(); + } + + { + testcase(prefix + " withdraw with tag"); + auto tx = vault.withdraw( + {.depositor = dave, .id = keylet.key, .amount = asset(50)}); + tx[sfDestinationTag] = "0"; + env(tx); + env.close(); + } + { testcase(prefix + " withdraw to authorized 3rd party"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, - .amount = asset(100)}); + .amount = asset(50)}); tx[sfDestination] = charlie.human(); env(tx); env.close(); @@ -523,80 +546,56 @@ class Vault_test : public beast::unit_test::suite } }; - auto testCases = [this, &testSequence]( + auto testCases = [&, this]( std::string prefix, - std::function setup) { + std::function setup) { Env env{*this, testable_amendments() | featureSingleAssetVault}; - Account issuer{"issuer"}; - Account owner{"owner"}; - Account depositor{"depositor"}; - Account charlie{"charlie"}; // authorized 3rd party + Vault vault{env}; - env.fund(XRP(1000), issuer, owner, depositor, charlie); + env.fund(XRP(1000), issuer, owner, depositor, charlie, dave); env.close(); env(fset(issuer, asfAllowTrustLineClawback)); env(fset(issuer, asfRequireAuth)); + env(fset(dave, asfRequireDest)); env.close(); env.require(flags(issuer, asfAllowTrustLineClawback)); env.require(flags(issuer, asfRequireAuth)); - PrettyAsset asset = setup(env, issuer, owner, depositor, charlie); - testSequence( - prefix, env, issuer, owner, depositor, charlie, vault, asset); + PrettyAsset asset = setup(env); + testSequence(prefix, env, vault, asset); }; - testCases( - "XRP", - [](Env& env, - Account const& issuer, - Account const& owner, - Account const& depositor, - Account const& charlie) -> PrettyAsset { - return {xrpIssue(), 1'000'000}; - }); + testCases("XRP", [&](Env& env) -> PrettyAsset { + return {xrpIssue(), 1'000'000}; + }); - testCases( - "IOU", - [](Env& env, - Account const& issuer, - Account const& owner, - Account const& depositor, - Account const& charlie) -> Asset { - PrettyAsset asset = issuer["IOU"]; - env(trust(owner, asset(1000))); - env(trust(depositor, asset(1000))); - env(trust(charlie, asset(1000))); - env(trust(issuer, asset(0), owner, tfSetfAuth)); - env(trust(issuer, asset(0), depositor, tfSetfAuth)); - env(trust(issuer, asset(0), charlie, tfSetfAuth)); - env(pay(issuer, depositor, asset(1000))); - env.close(); - return asset; - }); + testCases("IOU", [&](Env& env) -> Asset { + PrettyAsset asset = issuer["IOU"]; + env(trust(owner, asset(1000))); + env(trust(depositor, asset(1000))); + env(trust(charlie, asset(1000))); + env(trust(dave, asset(1000))); + env(trust(issuer, asset(0), owner, tfSetfAuth)); + env(trust(issuer, asset(0), depositor, tfSetfAuth)); + env(trust(issuer, asset(0), charlie, tfSetfAuth)); + env(trust(issuer, asset(0), dave, tfSetfAuth)); + env(pay(issuer, depositor, asset(1000))); + env.close(); + return asset; + }); - testCases( - "MPT", - [](Env& env, - Account const& issuer, - Account const& owner, - Account const& depositor, - Account const& charlie) -> Asset { - MPTTester mptt{env, issuer, mptInitNoFund}; - mptt.create( - {.flags = - tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - PrettyAsset asset = mptt.issuanceID(); - mptt.authorize({.account = depositor}); - mptt.authorize({.account = charlie}); - env(pay(issuer, depositor, asset(1000))); - env.close(); - return asset; - }); + testCases("MPT", [&](Env& env) -> Asset { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset asset = mptt.issuanceID(); + mptt.authorize({.account = depositor}); + mptt.authorize({.account = charlie}); + mptt.authorize({.account = dave}); + env(pay(issuer, depositor, asset(1000))); + env.close(); + return asset; + }); } void @@ -1672,6 +1671,7 @@ class Vault_test : public beast::unit_test::suite { bool enableClawback = true; bool requireAuth = true; + int initialXRP = 1000; }; auto testCase = [this]( @@ -1688,7 +1688,7 @@ class Vault_test : public beast::unit_test::suite Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; - env.fund(XRP(1000), issuer, owner, depositor); + env.fund(XRP(args.initialXRP), issuer, owner, depositor); env.close(); Vault vault{env}; @@ -1868,9 +1868,7 @@ class Vault_test : public beast::unit_test::suite PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { - testcase( - "MPT 3rd party without MPToken cannot be withdrawal " - "destination"); + testcase("MPT depositor without MPToken, auth required"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); @@ -1880,10 +1878,32 @@ class Vault_test : public beast::unit_test::suite tx = vault.deposit( {.depositor = depositor, .id = keylet.key, - .amount = asset(100)}); + .amount = asset(1000)}); env(tx); env.close(); + { + // Remove depositor MPToken and it will not be re-created + mptt.authorize( + {.account = depositor, .flags = tfMPTUnauthorize}); + env.close(); + + auto const mptoken = + keylet::mptoken(mptt.issuanceID(), depositor); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 == nullptr); + + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter{tecNO_AUTH}); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 == nullptr); + } + { // Set destination to 3rd party without MPToken Account charlie{"charlie"}; @@ -1898,7 +1918,7 @@ class Vault_test : public beast::unit_test::suite env(tx, ter(tecNO_AUTH)); } }, - {.requireAuth = false}); + {.requireAuth = true}); testCase( [this]( @@ -1909,7 +1929,7 @@ class Vault_test : public beast::unit_test::suite PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { - testcase("MPT depositor without MPToken cannot withdraw"); + testcase("MPT depositor without MPToken, no auth required"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); @@ -1917,7 +1937,6 @@ class Vault_test : public beast::unit_test::suite env.close(); auto v = env.le(keylet); BEAST_EXPECT(v); - MPTID share = (*v)[sfShareMPTID]; tx = vault.deposit( {.depositor = depositor, @@ -1927,41 +1946,120 @@ class Vault_test : public beast::unit_test::suite env.close(); { - // Remove depositor's MPToken and withdraw will fail + // Remove depositor's MPToken and it will be re-created mptt.authorize( {.account = depositor, .flags = tfMPTUnauthorize}); env.close(); + auto const mptoken = - env.le(keylet::mptoken(mptt.issuanceID(), depositor)); - BEAST_EXPECT(mptoken == nullptr); + keylet::mptoken(mptt.issuanceID(), depositor); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 == nullptr); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); - env(tx, ter(tecNO_AUTH)); + env(tx); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 != nullptr); + BEAST_EXPECT(sleMPT2->at(sfMPTAmount) == 100); } { - // Restore depositor's MPToken and withdraw will succeed - mptt.authorize({.account = depositor}); + // Remove 3rd party MPToken and it will not be re-created + mptt.authorize( + {.account = owner, .flags = tfMPTUnauthorize}); env.close(); + auto const mptoken = + keylet::mptoken(mptt.issuanceID(), owner); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 == nullptr); + tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, - .amount = asset(1000)}); - env(tx); + .amount = asset(100)}); + tx[sfDestination] = owner.human(); + env(tx, ter(tecNO_AUTH)); env.close(); - // Withdraw removed shares MPToken - auto const mptSle = - env.le(keylet::mptoken(share, depositor.id())); - BEAST_EXPECT(mptSle == nullptr); + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 == nullptr); } }, {.requireAuth = false}); + auto const [acctReserve, incReserve] = [this]() -> std::pair { + Env env{*this, testable_amendments()}; + return { + env.current()->fees().accountReserve(0).drops() / + DROPS_PER_XRP.drops(), + env.current()->fees().increment.drops() / + DROPS_PER_XRP.drops()}; + }(); + + testCase( + [&, this]( + Env& env, + Account const& issuer, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT failed reserve to re-create MPToken"); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + auto v = env.le(keylet); + BEAST_EXPECT(v); + + env(pay(depositor, owner, asset(1000))); + env.close(); + + tx = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(1000)}); // all assets held by owner + env(tx); + env.close(); + + { + // Remove owners's MPToken and it will not be re-created + mptt.authorize( + {.account = owner, .flags = tfMPTUnauthorize}); + env.close(); + + auto const mptoken = + keylet::mptoken(mptt.issuanceID(), owner); + auto const sleMPT = env.le(mptoken); + BEAST_EXPECT(sleMPT == nullptr); + + // No reserve to create MPToken for asset in VaultWithdraw + tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter{tecINSUFFICIENT_RESERVE}); + env.close(); + + env(pay(depositor, owner, XRP(incReserve))); + env.close(); + + // Withdraw can now create asset MPToken, tx will succeed + env(tx); + env.close(); + } + }, + {.requireAuth = false, + .initialXRP = acctReserve + incReserve * 4 - 1}); + testCase([this]( Env& env, Account const& issuer, @@ -2320,23 +2418,30 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; + struct CaseArgs + { + int initialXRP = 1000; + double transferRate = 1.0; + }; + auto testCase = - [&, - this](std::function vaultAccount, - Vault& vault, - PrettyAsset const& asset, - std::function issuanceId)> test) { + [&, this]( + std::function vaultAccount, + Vault& vault, + PrettyAsset const& asset, + std::function issuanceId)> test, + CaseArgs args = {}) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Account const issuer{"issuer"}; Account const charlie{"charlie"}; Vault vault{env}; - env.fund(XRP(1000), issuer, owner, charlie); + env.fund(XRP(args.initialXRP), issuer, owner, charlie); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); @@ -2344,7 +2449,7 @@ class Vault_test : public beast::unit_test::suite env.trust(asset(1000), owner); env.trust(asset(1000), charlie); env(pay(issuer, owner, asset(200))); - env(rate(issuer, 1.25)); + env(rate(issuer, args.transferRate)); env.close(); auto const vaultAccount = @@ -2505,73 +2610,81 @@ class Vault_test : public beast::unit_test::suite env.close(); }); - testCase([&, this]( - Env& env, - Account const& owner, - Account const& issuer, - Account const& charlie, - auto vaultAccount, - Vault& vault, - PrettyAsset const& asset, - auto issuanceId) { - testcase("IOU transfer fees not applied"); + testCase( + [&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto vaultAccount, + Vault& vault, + PrettyAsset const& asset, + auto issuanceId) { + testcase("IOU transfer fees not applied"); - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - env(vault.deposit( - {.depositor = owner, .id = keylet.key, .amount = asset(100)})); - env.close(); - - auto const issue = asset.raw().get(); - Asset const share = Asset(issuanceId(keylet)); - - // transfer fees ignored on deposit - BEAST_EXPECT(env.balance(owner, issue) == asset(100)); - BEAST_EXPECT( - env.balance(vaultAccount(keylet), issue) == asset(100)); - - { - auto tx = vault.clawback( - {.issuer = issuer, - .id = keylet.key, - .holder = owner, - .amount = asset(50)}); + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); - } - // transfer fees ignored on clawback - BEAST_EXPECT(env.balance(owner, issue) == asset(100)); - BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50)); - - env(vault.withdraw( - {.depositor = owner, - .id = keylet.key, - .amount = share(20'000'000)})); - - // transfer fees ignored on withdraw - BEAST_EXPECT(env.balance(owner, issue) == asset(120)); - BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30)); - - { - auto tx = vault.withdraw( + env(vault.deposit( {.depositor = owner, .id = keylet.key, - .amount = share(30'000'000)}); - tx[sfDestination] = charlie.human(); - env(tx); - } + .amount = asset(100)})); + env.close(); - // transfer fees ignored on withdraw to 3rd party - BEAST_EXPECT(env.balance(owner, issue) == asset(120)); - BEAST_EXPECT(env.balance(charlie, issue) == asset(30)); - BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0)); + auto const issue = asset.raw().get(); + Asset const share = Asset(issuanceId(keylet)); - env(vault.del({.owner = owner, .id = keylet.key})); - env.close(); - }); + // transfer fees ignored on deposit + BEAST_EXPECT(env.balance(owner, issue) == asset(100)); + BEAST_EXPECT( + env.balance(vaultAccount(keylet), issue) == asset(100)); + + { + auto tx = vault.clawback( + {.issuer = issuer, + .id = keylet.key, + .holder = owner, + .amount = asset(50)}); + env(tx); + env.close(); + } + + // transfer fees ignored on clawback + BEAST_EXPECT(env.balance(owner, issue) == asset(100)); + BEAST_EXPECT( + env.balance(vaultAccount(keylet), issue) == asset(50)); + + env(vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = share(20'000'000)})); + + // transfer fees ignored on withdraw + BEAST_EXPECT(env.balance(owner, issue) == asset(120)); + BEAST_EXPECT( + env.balance(vaultAccount(keylet), issue) == asset(30)); + + { + auto tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = share(30'000'000)}); + tx[sfDestination] = charlie.human(); + env(tx); + } + + // transfer fees ignored on withdraw to 3rd party + BEAST_EXPECT(env.balance(owner, issue) == asset(120)); + BEAST_EXPECT(env.balance(charlie, issue) == asset(30)); + BEAST_EXPECT( + env.balance(vaultAccount(keylet), issue) == asset(0)); + + env(vault.del({.owner = owner, .id = keylet.key})); + env.close(); + }, + CaseArgs{.transferRate = 1.25}); testCase([&, this]( Env& env, @@ -2713,6 +2826,103 @@ class Vault_test : public beast::unit_test::suite env(tx1); }); + auto const [acctReserve, incReserve] = [this]() -> std::pair { + Env env{*this, testable_amendments()}; + return { + env.current()->fees().accountReserve(0).drops() / + DROPS_PER_XRP.drops(), + env.current()->fees().increment.drops() / + DROPS_PER_XRP.drops()}; + }(); + + testCase( + [&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto, + Vault& vault, + PrettyAsset const& asset, + auto&&...) { + testcase("IOU no trust line to depositor no reserve"); + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + // reset limit, so deposit of all funds will delete the trust + // line + env.trust(asset(0), owner); + env.close(); + + env(vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(200)})); + env.close(); + + auto trustline = + env.le(keylet::line(owner, asset.raw().get())); + BEAST_EXPECT(trustline == nullptr); + + // Fail because not enough reserve to create trust line + tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(10)}); + env(tx, ter{tecNO_LINE_INSUF_RESERVE}); + env.close(); + + env(pay(charlie, owner, XRP(incReserve))); + env.close(); + + // Withdraw can now create trust line, will succeed + env(tx); + env.close(); + }, + CaseArgs{.initialXRP = acctReserve + incReserve * 4 - 1}); + + testCase( + [&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto, + Vault& vault, + PrettyAsset const& asset, + auto&&...) { + testcase("IOU no reserve for share MPToken"); + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(pay(owner, charlie, asset(100))); + env.close(); + + // Use up some reserve on tickets + env(ticket::create(charlie, 2)); + env.close(); + + // Fail because not enough reserve to create MPToken for shares + tx = vault.deposit( + {.depositor = charlie, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter{tecINSUFFICIENT_RESERVE}); + env.close(); + + env(pay(issuer, charlie, XRP(incReserve))); + env.close(); + + // Deposit can now create MPToken, will succeed + env(tx); + env.close(); + }, + CaseArgs{.initialXRP = acctReserve + incReserve * 4 - 1}); + testCase([&, this]( Env& env, Account const& owner, diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 80382934ad..05fd70de4e 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -202,8 +202,7 @@ VaultDeposit::doApply() else // !vault->isFlag(lsfVaultPrivate) || account_ == vault->at(sfOwner) { // No authorization needed, but must ensure there is MPToken - auto sleMpt = view().read(keylet::mptoken(mptIssuanceID, account_)); - if (!sleMpt) + if (!view().exists(keylet::mptoken(mptIssuanceID, account_))) { if (auto const err = authorizeMPToken( view(), diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 509b795058..bdf00f5c56 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -52,12 +52,6 @@ VaultWithdraw::preflight(PreflightContext const& ctx) return temMALFORMED; } } - else if (ctx.tx.isFieldPresent(sfDestinationTag)) - { - JLOG(ctx.j.debug()) << "VaultWithdraw: sfDestinationTag is set but " - "sfDestination is not"; - return temMALFORMED; - } return tesSUCCESS; } @@ -116,37 +110,28 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) } auto const account = ctx.tx[sfAccount]; - auto const dstAcct = [&]() -> AccountID { - if (ctx.tx.isFieldPresent(sfDestination)) - return ctx.tx.getAccountID(sfDestination); - return account; - }(); + auto const dstAcct = ctx.tx[~sfDestination].value_or(account); + 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 // Withdrawal to a 3rd party destination account is essentially a transfer, // via shares in the vault. Enforce all the usual asset transfer checks. - AuthType authType = AuthType::Legacy; - if (account != dstAcct) + if (account != dstAcct && sleDst->isFlag(lsfDepositAuth)) { - auto const sleDst = ctx.view.read(keylet::account(dstAcct)); - if (sleDst == nullptr) - return tecNO_DST; - - if (sleDst->isFlag(lsfRequireDestTag) && - !ctx.tx.isFieldPresent(sfDestinationTag)) - return tecDST_TAG_NEEDED; // Cannot send without a tag - - 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 (!ctx.view.exists(keylet::depositPreauth(dstAcct, account))) + return tecNO_PERMISSION; } - // Destination MPToken (for an MPT) or trust line (for an IOU) must exist - // if not sending to Account. + // 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(). + // Destination MPToken or trust line must exist if _not_ sending to Account. + AuthType const authType = + account == dstAcct ? AuthType::WeakAuth : AuthType::StrongAuth; if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType); !isTesSuccess(ter)) return ter; @@ -307,11 +292,16 @@ VaultWithdraw::doApply() // else quietly ignore, account balance is not zero } - auto const dstAcct = [&]() -> AccountID { - if (ctx_.tx.isFieldPresent(sfDestination)) - return ctx_.tx.getAccountID(sfDestination); - return account_; - }(); + auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_); + if (!vaultAsset.native() && // + dstAcct != vaultAsset.getIssuer() && // + dstAcct == account_) + { + if (auto const ter = addEmptyHolding( + view(), account_, mPriorBalance, vaultAsset, j_); + !isTesSuccess(ter) && ter != tecDUPLICATE) + return ter; + } // Transfer assets from vault to depositor or destination account. if (auto const ter = accountSend(