mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-21 22:30:57 +00:00
fix: Check credential for LoanBrokerCoverWithdraw and VaultWithdraw (#7107)
Co-authored-by: Peter Chen <ychen@ripple.com> Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -198,7 +199,10 @@ dirLink(
|
||||
* if withdrawing to self.
|
||||
* - If withdrawing to self, succeed.
|
||||
* - If not, checks if the receiver requires deposit authorization, and if
|
||||
* the sender has it.
|
||||
* the sender has it (account-based or credential-based).
|
||||
* - Expects any credentials passed in to already exist in the ledger, and
|
||||
* returns an internal error otherwise. Validate them beforehand with
|
||||
* credentials::valid().
|
||||
* - Checks that the receiver will not exceed the limit (IOU trustline limit
|
||||
* or MPT MaximumAmount).
|
||||
*/
|
||||
@@ -209,7 +213,8 @@ canWithdraw(
|
||||
AccountID const& to,
|
||||
SLE::const_ref toSle,
|
||||
STAmount const& amount,
|
||||
bool hasDestinationTag);
|
||||
bool hasDestinationTag,
|
||||
std::optional<std::vector<uint256>> const& credentialIDs = std::nullopt);
|
||||
|
||||
/**
|
||||
* Checks that can withdraw funds from an object to itself or a destination.
|
||||
@@ -222,7 +227,10 @@ canWithdraw(
|
||||
* if withdrawing to self.
|
||||
* - If withdrawing to self, succeed.
|
||||
* - If not, checks if the receiver requires deposit authorization, and if
|
||||
* the sender has it.
|
||||
* the sender has it (account-based or credential-based).
|
||||
* - Expects any credentials passed in to already exist in the ledger, and
|
||||
* returns an internal error otherwise. Validate them beforehand with
|
||||
* credentials::valid().
|
||||
* - Checks that the receiver will not exceed the limit (IOU trustline limit
|
||||
* or MPT MaximumAmount).
|
||||
*/
|
||||
@@ -232,20 +240,25 @@ canWithdraw(
|
||||
AccountID const& from,
|
||||
AccountID const& to,
|
||||
STAmount const& amount,
|
||||
bool hasDestinationTag);
|
||||
bool hasDestinationTag,
|
||||
std::optional<std::vector<uint256>> const& credentialIDs = std::nullopt);
|
||||
|
||||
/**
|
||||
* Checks that can withdraw funds from an object to itself or a destination.
|
||||
*
|
||||
* The receiver may be either the submitting account (sfAccount) or a different
|
||||
* destination account (sfDestination).
|
||||
* destination account (sfDestination). Credentials, if any, are taken from the
|
||||
* transaction's sfCredentialIDs field.
|
||||
*
|
||||
* - Checks that the receiver account exists.
|
||||
* - If the receiver requires a destination tag, check that one exists, even
|
||||
* if withdrawing to self.
|
||||
* - If withdrawing to self, succeed.
|
||||
* - If not, checks if the receiver requires deposit authorization, and if
|
||||
* the sender has it.
|
||||
* the sender has it (account-based or credential-based).
|
||||
* - Expects any credentials in sfCredentialIDs to already exist in the
|
||||
* ledger, and returns an internal error otherwise. Validate them
|
||||
* beforehand with credentials::valid().
|
||||
* - Checks that the receiver will not exceed the limit (IOU trustline limit
|
||||
* or MPT MaximumAmount).
|
||||
*/
|
||||
|
||||
@@ -921,6 +921,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw,
|
||||
{sfAmount, SoeRequired, SoeMptSupported},
|
||||
{sfDestination, SoeOptional},
|
||||
{sfDestinationTag, SoeOptional},
|
||||
{sfCredentialIDs, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction claws back tokens from a vault. */
|
||||
@@ -1004,6 +1005,7 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw,
|
||||
{sfAmount, SoeRequired, SoeMptSupported},
|
||||
{sfDestination, SoeOptional},
|
||||
{sfDestinationTag, SoeOptional},
|
||||
{sfCredentialIDs, SoeOptional},
|
||||
}))
|
||||
|
||||
/** This transaction claws back First Loss Capital from a Loan Broker to
|
||||
|
||||
@@ -121,6 +121,32 @@ public:
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfDestinationTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfCredentialIDs (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VECTOR256::type::value_type>
|
||||
getCredentialIDs() const
|
||||
{
|
||||
if (hasCredentialIDs())
|
||||
{
|
||||
return this->tx_->at(sfCredentialIDs);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfCredentialIDs is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasCredentialIDs() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfCredentialIDs);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -214,6 +240,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfCredentialIDs (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
LoanBrokerCoverWithdrawBuilder&
|
||||
setCredentialIDs(std::decay_t<typename SF_VECTOR256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfCredentialIDs] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the LoanBrokerCoverWithdraw wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
|
||||
@@ -121,6 +121,32 @@ public:
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfDestinationTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfCredentialIDs (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_VECTOR256::type::value_type>
|
||||
getCredentialIDs() const
|
||||
{
|
||||
if (hasCredentialIDs())
|
||||
{
|
||||
return this->tx_->at(sfCredentialIDs);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfCredentialIDs is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasCredentialIDs() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfCredentialIDs);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -214,6 +240,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfCredentialIDs (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultWithdrawBuilder&
|
||||
setCredentialIDs(std::decay_t<typename SF_VECTOR256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfCredentialIDs] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the VaultWithdraw wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
|
||||
@@ -20,6 +20,9 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -467,7 +468,8 @@ canWithdraw(
|
||||
AccountID const& to,
|
||||
SLE::const_ref toSle,
|
||||
STAmount const& amount,
|
||||
bool hasDestinationTag)
|
||||
bool hasDestinationTag,
|
||||
std::optional<std::vector<uint256>> const& credentialIDs)
|
||||
{
|
||||
if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag))
|
||||
return ret;
|
||||
@@ -478,7 +480,28 @@ canWithdraw(
|
||||
if (toSle->isFlag(lsfDepositAuth))
|
||||
{
|
||||
if (!view.exists(keylet::depositPreauth(to, from)))
|
||||
return tecNO_PERMISSION;
|
||||
{
|
||||
if (credentialIDs.has_value())
|
||||
{
|
||||
STVector256 const credIDs{*credentialIDs};
|
||||
|
||||
// Callers must have validated these in preclaim, so a missing
|
||||
// credential here is an invariant violation.
|
||||
for (auto const& h : credIDs)
|
||||
{
|
||||
if (!view.exists(keylet::credential(h)))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (auto const ret = credentials::authorizedDepositPreauth(view, credIDs, to);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
else
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return withdrawToDestExceedsLimit(view, from, to, amount);
|
||||
@@ -490,11 +513,12 @@ canWithdraw(
|
||||
AccountID const& from,
|
||||
AccountID const& to,
|
||||
STAmount const& amount,
|
||||
bool hasDestinationTag)
|
||||
bool hasDestinationTag,
|
||||
std::optional<std::vector<uint256>> const& credentialIDs)
|
||||
{
|
||||
auto const toSle = view.read(keylet::account(to));
|
||||
|
||||
return canWithdraw(view, from, to, toSle, amount, hasDestinationTag);
|
||||
return canWithdraw(view, from, to, toSle, amount, hasDestinationTag, credentialIDs);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
@@ -503,7 +527,8 @@ canWithdraw(ReadView const& view, STTx const& tx)
|
||||
auto const from = tx[sfAccount];
|
||||
auto const to = tx[~sfDestination].value_or(from);
|
||||
|
||||
return canWithdraw(view, from, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag));
|
||||
return canWithdraw(
|
||||
view, from, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag), tx[~sfCredentialIDs]);
|
||||
}
|
||||
|
||||
TER
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -25,7 +26,11 @@ namespace xrpl {
|
||||
bool
|
||||
LoanBrokerCoverWithdraw::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
return checkLendingProtocolDependencies(ctx.rules, ctx.tx);
|
||||
if (!checkLendingProtocolDependencies(ctx.rules, ctx.tx))
|
||||
return false;
|
||||
|
||||
return !ctx.tx.isFieldPresent(sfCredentialIDs) ||
|
||||
(ctx.rules.enabled(featureCredentials) && ctx.rules.enabled(fixCleanup3_4_0));
|
||||
}
|
||||
|
||||
NotTEC
|
||||
@@ -49,6 +54,9 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -109,6 +117,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
if (auto const ret = canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct, waive))
|
||||
return ret;
|
||||
|
||||
// Validate credentials (if any) before canWithdraw, since canWithdraw may
|
||||
// call credentials::authorizedDepositPreauth which assumes credentials
|
||||
// already exist.
|
||||
if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
// Withdrawal to a 3rd party destination account is essentially a transfer.
|
||||
// Enforce all the usual asset transfer checks.
|
||||
AuthType authType = AuthType::WeakAuth;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -27,6 +28,13 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
bool
|
||||
VaultWithdraw::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
return !ctx.tx.isFieldPresent(sfCredentialIDs) ||
|
||||
(ctx.rules.enabled(featureCredentials) && ctx.rules.enabled(fixCleanup3_4_0));
|
||||
}
|
||||
|
||||
static WaiveUnrealizedLoss
|
||||
shouldWaiveWithdrawal(ReadView const& view, AccountID const& account, SLE::const_ref issuance)
|
||||
{
|
||||
@@ -59,6 +67,9 @@ VaultWithdraw::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -113,6 +124,12 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// Validate credentials (if any) before canWithdraw, since canWithdraw may
|
||||
// call credentials::authorizedDepositPreauth which assumes credentials
|
||||
// already exist.
|
||||
if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
if (fix313Enabled && amount.asset() == vaultShare)
|
||||
{
|
||||
// Post-fixCleanup3_1_3: if the user specified shares, convert
|
||||
@@ -144,7 +161,8 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
|
||||
account,
|
||||
dstAcct,
|
||||
*maybeAssets,
|
||||
ctx.tx.isFieldPresent(sfDestinationTag)))
|
||||
ctx.tx.isFieldPresent(sfDestinationTag),
|
||||
ctx.tx[~sfCredentialIDs]))
|
||||
return ret;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/deposit.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
@@ -2532,6 +2534,132 @@ class LoanBroker_test : public beast::unit_test::Suite
|
||||
testRIPD4274MPT();
|
||||
}
|
||||
|
||||
void
|
||||
testCoverWithdrawCredentialDepositPreauth(FeatureBitset features)
|
||||
{
|
||||
testcase(
|
||||
std::string{"CoverWithdraw with credential-based deposit preauth "} +
|
||||
(features[fixCleanup3_4_0] ? "post-fix" : "pre-fix"));
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
Account const broker{"broker"};
|
||||
Account const dest{"dest"};
|
||||
Account const credIssuer{"credIssuer"};
|
||||
char const credType[] = "abcde";
|
||||
|
||||
env.fund(XRP(10'000), broker, dest, credIssuer);
|
||||
env(fset(dest, asfDepositAuth));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const asset{xrpIssue(), 1'000'000};
|
||||
|
||||
Vault const vault(env);
|
||||
auto const [vaultTx, vaultKeylet] = vault.create({.owner = broker, .asset = asset});
|
||||
env(vaultTx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = broker, .id = vaultKeylet.key, .amount = asset(1'000)}));
|
||||
env.close();
|
||||
|
||||
auto const brokerKeylet =
|
||||
keylet::loanBroker(broker.id(), SeqProxy::rawSequence(env.seq(broker)));
|
||||
env(loan_broker::set(broker, vaultKeylet.key));
|
||||
env.close();
|
||||
|
||||
env(loan_broker::coverDeposit(broker, brokerKeylet.key, asset(500)));
|
||||
env.close();
|
||||
|
||||
auto coverWithdrawToDest = [&]() {
|
||||
return loan_broker::coverWithdraw(broker, brokerKeylet.key, asset(10));
|
||||
};
|
||||
|
||||
// Without any preauth, coverWithdraw to dest fails
|
||||
env(coverWithdrawToDest(), loan_broker::kDestination(dest), Ter{tecNO_PERMISSION});
|
||||
env.close();
|
||||
|
||||
// Issue and accept a credential for the broker (with expiration)
|
||||
auto jv = credentials::create(broker, credIssuer, credType);
|
||||
std::uint32_t const expiration =
|
||||
env.current()->header().parentCloseTime.time_since_epoch().count() + 100;
|
||||
jv[sfExpiration.jsonName] = expiration;
|
||||
env(jv);
|
||||
env(credentials::accept(broker, credIssuer, credType));
|
||||
env.close();
|
||||
|
||||
auto const credKeylet = credentials::keylet(broker, credIssuer, credType);
|
||||
auto const credIdx =
|
||||
credentials::ledgerEntry(env, broker, credIssuer, credType)[jss::result][jss::index]
|
||||
.asString();
|
||||
|
||||
// dest authorizes deposits from holders of credentials issued by credIssuer
|
||||
env(deposit::authCredentials(dest, {{.issuer = credIssuer, .credType = credType}}));
|
||||
env.close();
|
||||
|
||||
// Without supplying credentials, still fails
|
||||
env(coverWithdrawToDest(), loan_broker::kDestination(dest), Ter{tecNO_PERMISSION});
|
||||
env.close();
|
||||
|
||||
if (!fixEnabled)
|
||||
{
|
||||
// Pre-fix: sfCredentialIDs in LoanBrokerCoverWithdraw is disabled
|
||||
env(coverWithdrawToDest(),
|
||||
loan_broker::kDestination(dest),
|
||||
credentials::Ids({credIdx}),
|
||||
Ter{temDISABLED});
|
||||
env.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// With credentials, succeeds
|
||||
env(coverWithdrawToDest(), loan_broker::kDestination(dest), credentials::Ids({credIdx}));
|
||||
env.close();
|
||||
|
||||
// Bad credential id is rejected
|
||||
std::string const invalidIdx =
|
||||
"0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E01E034";
|
||||
env(coverWithdrawToDest(),
|
||||
loan_broker::kDestination(dest),
|
||||
credentials::Ids({invalidIdx}),
|
||||
Ter{tecBAD_CREDENTIALS});
|
||||
env.close();
|
||||
|
||||
// Malformed credential array (duplicates) is rejected by checkFields
|
||||
env(coverWithdrawToDest(),
|
||||
loan_broker::kDestination(dest),
|
||||
credentials::Ids({credIdx, credIdx}),
|
||||
Ter{temMALFORMED});
|
||||
env.close();
|
||||
|
||||
// Valid credential not authorized by dest hits authorizedDepositPreauth error path
|
||||
char const credType2[] = "fghij";
|
||||
env(credentials::create(broker, credIssuer, credType2));
|
||||
env(credentials::accept(broker, credIssuer, credType2));
|
||||
env.close();
|
||||
auto const credIdx2 =
|
||||
credentials::ledgerEntry(env, broker, credIssuer, credType2)[jss::result][jss::index]
|
||||
.asString();
|
||||
env(coverWithdrawToDest(),
|
||||
loan_broker::kDestination(dest),
|
||||
credentials::Ids({credIdx2}),
|
||||
Ter{tecNO_PERMISSION});
|
||||
env.close();
|
||||
|
||||
// Advance time past expiration: credentials yield tecEXPIRED and are deleted
|
||||
env.close(150s);
|
||||
BEAST_EXPECT(env.le(credKeylet));
|
||||
env(coverWithdrawToDest(),
|
||||
loan_broker::kDestination(dest),
|
||||
credentials::Ids({credIdx}),
|
||||
Ter{tecEXPIRED});
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(credKeylet));
|
||||
}
|
||||
|
||||
// Exercises canApplyToBrokerCover (fixCleanup3_2_0): a deposit, withdraw,
|
||||
// or clawback whose amount rounds to zero at sfCoverAvailable's precision
|
||||
// scale must be rejected with tecPRECISION_LOSS once the amendment is on,
|
||||
@@ -2770,6 +2898,9 @@ public:
|
||||
|
||||
testRIPD4274();
|
||||
|
||||
testCoverWithdrawCredentialDepositPreauth(all_ - fixCleanup3_4_0);
|
||||
testCoverWithdrawCredentialDepositPreauth(all_);
|
||||
|
||||
testLoanBrokerDeleteLockedMPT(all_);
|
||||
testLoanBrokerDeleteLockedMPT(all_ - fixCleanup3_2_0);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/deposit.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/offer.h>
|
||||
#include <test/jtx/pay.h>
|
||||
@@ -18,6 +19,7 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -570,6 +572,112 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithdrawCredentialDepositPreauth(FeatureBitset features)
|
||||
{
|
||||
testcase(
|
||||
"withdraw with credential-based deposit preauth " +
|
||||
std::string{features[fixCleanup3_4_0] ? "post-fix" : "pre-fix"});
|
||||
using namespace test::jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
bool const fixEnabled = features[fixCleanup3_4_0];
|
||||
|
||||
Env env{*this, features};
|
||||
|
||||
Account const owner{"owner"};
|
||||
Account const depositor{"depositor"};
|
||||
Account const dest{"dest"};
|
||||
Account const credIssuer{"credIssuer"};
|
||||
char const credType[] = "abcde";
|
||||
|
||||
env.fund(XRP(1000), owner, depositor, dest, credIssuer);
|
||||
env(fset(dest, asfDepositAuth));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const asset{xrpIssue(), 1'000'000};
|
||||
Vault vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}));
|
||||
env.close();
|
||||
|
||||
auto withdrawToDest = [&]() {
|
||||
auto wtx =
|
||||
vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
|
||||
wtx[sfDestination] = dest.human();
|
||||
return wtx;
|
||||
};
|
||||
|
||||
// Without any preauth, withdraw to dest fails
|
||||
env(withdrawToDest(), Ter{tecNO_PERMISSION});
|
||||
env.close();
|
||||
|
||||
// Issue and accept a credential for the depositor (with expiration)
|
||||
auto jv = credentials::create(depositor, credIssuer, credType);
|
||||
std::uint32_t const expiration =
|
||||
env.current()->header().parentCloseTime.time_since_epoch().count() + 100;
|
||||
jv[sfExpiration.jsonName] = expiration;
|
||||
env(jv);
|
||||
env(credentials::accept(depositor, credIssuer, credType));
|
||||
env.close();
|
||||
|
||||
auto const credKeylet = credentials::keylet(depositor, credIssuer, credType);
|
||||
auto const credIdx =
|
||||
credentials::ledgerEntry(env, depositor, credIssuer, credType)[jss::result][jss::index]
|
||||
.asString();
|
||||
|
||||
// dest authorizes deposits from holders of credentials issued by credIssuer
|
||||
env(deposit::authCredentials(dest, {{.issuer = credIssuer, .credType = credType}}));
|
||||
env.close();
|
||||
|
||||
// Withdraw without supplying credentials still fails
|
||||
env(withdrawToDest(), Ter{tecNO_PERMISSION});
|
||||
env.close();
|
||||
|
||||
if (!fixEnabled)
|
||||
{
|
||||
// Pre-fix: sfCredentialIDs in VaultWithdraw is rejected as disabled
|
||||
env(withdrawToDest(), credentials::Ids({credIdx}), Ter{temDISABLED});
|
||||
env.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Withdraw with credentials succeeds
|
||||
env(withdrawToDest(), credentials::Ids({credIdx}));
|
||||
env.close();
|
||||
|
||||
// Bad credential id is rejected
|
||||
std::string const invalidIdx =
|
||||
"0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E01E034";
|
||||
env(withdrawToDest(), credentials::Ids({invalidIdx}), Ter{tecBAD_CREDENTIALS});
|
||||
env.close();
|
||||
|
||||
// Malformed credential array (duplicates) is rejected by checkFields
|
||||
env(withdrawToDest(), credentials::Ids({credIdx, credIdx}), Ter{temMALFORMED});
|
||||
env.close();
|
||||
|
||||
// Valid credential not authorized by dest hits authorizedDepositPreauth error path
|
||||
char const credType2[] = "fghij";
|
||||
env(credentials::create(depositor, credIssuer, credType2));
|
||||
env(credentials::accept(depositor, credIssuer, credType2));
|
||||
env.close();
|
||||
auto const credIdx2 =
|
||||
credentials::ledgerEntry(env, depositor, credIssuer, credType2)[jss::result][jss::index]
|
||||
.asString();
|
||||
env(withdrawToDest(), credentials::Ids({credIdx2}), Ter{tecNO_PERMISSION});
|
||||
env.close();
|
||||
|
||||
// Advance time past expiration: credentials yield tecEXPIRED and are deleted
|
||||
env.close(150s);
|
||||
BEAST_EXPECT(env.le(credKeylet));
|
||||
env(withdrawToDest(), credentials::Ids({credIdx}), Ter{tecEXPIRED});
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(credKeylet));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -578,6 +686,8 @@ public:
|
||||
testDomainLossAfterAcquisition();
|
||||
testDomainCheckBuyerSideOffer();
|
||||
testWithDomainChecXRP();
|
||||
testWithdrawCredentialDepositPreauth(all_ - fixCleanup3_4_0);
|
||||
testWithdrawCredentialDepositPreauth(all_);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip)
|
||||
auto const amountValue = canonical_AMOUNT();
|
||||
auto const destinationValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const credentialIDsValue = canonical_VECTOR256();
|
||||
|
||||
LoanBrokerCoverWithdrawBuilder builder{
|
||||
accountValue,
|
||||
@@ -45,6 +46,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip)
|
||||
// Set optional fields
|
||||
builder.setDestination(destinationValue);
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
builder.setCredentialIDs(credentialIDsValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
@@ -90,6 +92,14 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(tx.hasDestinationTag());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = credentialIDsValue;
|
||||
auto const actualOpt = tx.getCredentialIDs();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfCredentialIDs");
|
||||
EXPECT_TRUE(tx.hasCredentialIDs());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
@@ -110,6 +120,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip)
|
||||
auto const amountValue = canonical_AMOUNT();
|
||||
auto const destinationValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const credentialIDsValue = canonical_VECTOR256();
|
||||
|
||||
// Build an initial transaction
|
||||
LoanBrokerCoverWithdrawBuilder initialBuilder{
|
||||
@@ -122,6 +133,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip)
|
||||
|
||||
initialBuilder.setDestination(destinationValue);
|
||||
initialBuilder.setDestinationTag(destinationTagValue);
|
||||
initialBuilder.setCredentialIDs(credentialIDsValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
@@ -166,6 +178,13 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip)
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = credentialIDsValue;
|
||||
auto const actualOpt = rebuiltTx.getCredentialIDs();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfCredentialIDs");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
@@ -229,6 +248,8 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(tx.getDestination().has_value());
|
||||
EXPECT_FALSE(tx.hasDestinationTag());
|
||||
EXPECT_FALSE(tx.getDestinationTag().has_value());
|
||||
EXPECT_FALSE(tx.hasCredentialIDs());
|
||||
EXPECT_FALSE(tx.getCredentialIDs().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip)
|
||||
auto const amountValue = canonical_AMOUNT();
|
||||
auto const destinationValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const credentialIDsValue = canonical_VECTOR256();
|
||||
|
||||
VaultWithdrawBuilder builder{
|
||||
accountValue,
|
||||
@@ -45,6 +46,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip)
|
||||
// Set optional fields
|
||||
builder.setDestination(destinationValue);
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
builder.setCredentialIDs(credentialIDsValue);
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
@@ -90,6 +92,14 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(tx.hasDestinationTag());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = credentialIDsValue;
|
||||
auto const actualOpt = tx.getCredentialIDs();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfCredentialIDs");
|
||||
EXPECT_TRUE(tx.hasCredentialIDs());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
@@ -110,6 +120,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip)
|
||||
auto const amountValue = canonical_AMOUNT();
|
||||
auto const destinationValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const credentialIDsValue = canonical_VECTOR256();
|
||||
|
||||
// Build an initial transaction
|
||||
VaultWithdrawBuilder initialBuilder{
|
||||
@@ -122,6 +133,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip)
|
||||
|
||||
initialBuilder.setDestination(destinationValue);
|
||||
initialBuilder.setDestinationTag(destinationTagValue);
|
||||
initialBuilder.setCredentialIDs(credentialIDsValue);
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
@@ -166,6 +178,13 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip)
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = credentialIDsValue;
|
||||
auto const actualOpt = rebuiltTx.getCredentialIDs();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfCredentialIDs");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
@@ -229,6 +248,8 @@ TEST(TransactionsVaultWithdrawTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(tx.getDestination().has_value());
|
||||
EXPECT_FALSE(tx.hasDestinationTag());
|
||||
EXPECT_FALSE(tx.getDestinationTag().has_value());
|
||||
EXPECT_FALSE(tx.hasCredentialIDs());
|
||||
EXPECT_FALSE(tx.getCredentialIDs().has_value());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user