mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-24 07:30:30 +00:00
Merge branch 'xrplf/sponsor' of https://github.com/XRPLF/rippled into mvadari/sponsor/reserve-context
This commit is contained in:
@@ -83,7 +83,7 @@ accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adju
|
||||
return accountReserve(view, view.read(keylet::account(id)), j, adj);
|
||||
}
|
||||
|
||||
/** Check if an account has insufficient reserve.
|
||||
/** Check if an account has sufficient reserve.
|
||||
*
|
||||
* The transaction-level reserve sponsor (if any) is taken from
|
||||
* `ctx.txReserveContext` and, per XLS-68, is only applied when `accSle` is the
|
||||
@@ -98,7 +98,7 @@ accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adju
|
||||
* @return Transaction result code
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkInsufficientReserve(
|
||||
checkReserve(
|
||||
ApplyViewContext ctx,
|
||||
SLE::const_ref accSle,
|
||||
STAmount const& accBalance,
|
||||
|
||||
@@ -70,7 +70,7 @@ escrowUnlockApplyHelper<Issue>(
|
||||
{
|
||||
// Can the account cover the trust line's reserve?
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal);
|
||||
checkReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
JLOG(journal.trace()) << "Trust line does not exist. "
|
||||
@@ -194,7 +194,7 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
|
||||
{
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal);
|
||||
checkReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -31,88 +31,35 @@ isReserveSponsored(STTx const& tx)
|
||||
return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u;
|
||||
}
|
||||
|
||||
inline std::optional<AccountID>
|
||||
getTxReserveSponsorAccountID(STTx const& tx)
|
||||
{
|
||||
if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx))
|
||||
{
|
||||
return tx.getAccountID(sfSponsor);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::optional<AccountID>
|
||||
getTxReserveSponsorAccountID(STTx const& tx);
|
||||
|
||||
inline std::expected<SLE::const_pointer, TER>
|
||||
getTxReserveSponsor(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const sponsorID = getTxReserveSponsorAccountID(tx);
|
||||
if (sponsorID)
|
||||
{
|
||||
auto sle = view.read(keylet::account(*sponsorID));
|
||||
std::expected<SLE::const_pointer, TER>
|
||||
getTxReserveSponsor(ReadView const& view, STTx const& tx);
|
||||
|
||||
// already checked in Transactor::checkSponsor
|
||||
if (!sle)
|
||||
return std::unexpected(tecINTERNAL);
|
||||
return sle;
|
||||
}
|
||||
return SLE::pointer();
|
||||
}
|
||||
std::optional<AccountID>
|
||||
getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor);
|
||||
|
||||
inline std::optional<AccountID>
|
||||
getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor)
|
||||
{
|
||||
if (sle->isFieldPresent(field))
|
||||
return sle->getAccountID(field);
|
||||
return {};
|
||||
}
|
||||
|
||||
inline SLE::pointer
|
||||
SLE::pointer
|
||||
getLedgerEntryReserveSponsor(
|
||||
ApplyView& view,
|
||||
SLE::const_ref sle,
|
||||
SF_ACCOUNT const& field = sfSponsor)
|
||||
{
|
||||
auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field);
|
||||
if (sponsorID)
|
||||
return view.peek(keylet::account(*sponsorID));
|
||||
return {};
|
||||
}
|
||||
SF_ACCOUNT const& field = sfSponsor);
|
||||
|
||||
inline SLE::const_pointer
|
||||
SLE::const_pointer
|
||||
getLedgerEntryReserveSponsor(
|
||||
ReadView const& view,
|
||||
SLE::const_ref sle,
|
||||
SF_ACCOUNT const& field = sfSponsor)
|
||||
{
|
||||
auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field);
|
||||
if (sponsorID)
|
||||
return view.read(keylet::account(*sponsorID));
|
||||
return {};
|
||||
}
|
||||
SF_ACCOUNT const& field = sfSponsor);
|
||||
|
||||
inline void
|
||||
void
|
||||
addSponsorToLedgerEntry(
|
||||
SLE::ref sle,
|
||||
SLE::const_ref sponsorSle,
|
||||
SF_ACCOUNT const& field = sfSponsor)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
|
||||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
|
||||
"addSponsorToLedgerEntry : Invalid field to the LedgerEntry");
|
||||
if (sponsorSle)
|
||||
sle->setAccountID(field, sponsorSle->getAccountID(sfAccount));
|
||||
}
|
||||
SF_ACCOUNT const& field = sfSponsor);
|
||||
|
||||
inline void
|
||||
removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
|
||||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
|
||||
"removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry");
|
||||
if (sle->isFieldPresent(field))
|
||||
sle->makeFieldAbsent(field);
|
||||
}
|
||||
void
|
||||
removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor);
|
||||
|
||||
template <typename T>
|
||||
inline std::optional<AccountID>
|
||||
|
||||
@@ -141,6 +141,9 @@ public:
|
||||
[[nodiscard]] std::vector<uint256> const&
|
||||
getBatchTransactionIDs() const;
|
||||
|
||||
[[nodiscard]] AccountID
|
||||
getFeePayerID() const;
|
||||
|
||||
private:
|
||||
/** Check the signature.
|
||||
@param rules The current ledger rules.
|
||||
|
||||
@@ -137,7 +137,8 @@ enum class FeePayerType {
|
||||
|
||||
struct FeePayer
|
||||
{
|
||||
Keylet entry;
|
||||
AccountID id;
|
||||
Keylet keylet;
|
||||
SF_AMOUNT const& balanceField;
|
||||
FeePayerType type{FeePayerType::Account};
|
||||
};
|
||||
@@ -317,6 +318,7 @@ public:
|
||||
|
||||
static NotTEC
|
||||
checkSponsor(ReadView const& view, STTx const& tx);
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
// Interface used by AccountDelete
|
||||
|
||||
@@ -347,7 +347,7 @@ accountReserve(ReadView const& view, SLE::const_ref sle, beast::Journal j, Adjus
|
||||
}
|
||||
|
||||
TER
|
||||
checkInsufficientReserve(
|
||||
checkReserve(
|
||||
ApplyViewContext ctx,
|
||||
// NOTE: we still need the accSle to be passed in as the ctx.txReserveContext.accountSle may be
|
||||
// outdated
|
||||
|
||||
@@ -206,8 +206,8 @@ authorizeMPToken(
|
||||
// budget), so this check always runs for sponsored transactions.
|
||||
if (isSponsored || ownerCount(sleAcct, journal) >= 2)
|
||||
{
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
ctx, sleAcct, priorBalance, {.ownerCountDelta = 1}, journal);
|
||||
if (auto const ret =
|
||||
checkReserve(ctx, sleAcct, priorBalance, {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -662,8 +662,7 @@ addEmptyHolding(
|
||||
return tecDUPLICATE;
|
||||
|
||||
// Can the account cover the trust line reserve ?
|
||||
if (auto const ret =
|
||||
checkInsufficientReserve(ctx, sleDst, priorBalance, {.ownerCountDelta = 1}, journal);
|
||||
if (auto const ret = checkReserve(ctx, sleDst, priorBalance, {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
return tecNO_LINE_INSUF_RESERVE;
|
||||
|
||||
|
||||
93
src/libxrpl/ledger/helpers/SponsorHelpers.cpp
Normal file
93
src/libxrpl/ledger/helpers/SponsorHelpers.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
std::optional<AccountID>
|
||||
getTxReserveSponsorAccountID(STTx const& tx)
|
||||
{
|
||||
if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx))
|
||||
{
|
||||
return tx.getAccountID(sfSponsor);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<SLE::const_pointer, TER>
|
||||
getTxReserveSponsor(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const sponsorID = getTxReserveSponsorAccountID(tx);
|
||||
if (sponsorID)
|
||||
{
|
||||
auto sle = view.read(keylet::account(*sponsorID));
|
||||
|
||||
// already checked in Transactor::checkSponsor
|
||||
if (!sle)
|
||||
return std::unexpected(tecINTERNAL);
|
||||
return sle;
|
||||
}
|
||||
return SLE::pointer();
|
||||
}
|
||||
|
||||
std::optional<AccountID>
|
||||
getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field)
|
||||
{
|
||||
if (sle->isFieldPresent(field))
|
||||
return sle->getAccountID(field);
|
||||
return {};
|
||||
}
|
||||
|
||||
SLE::pointer
|
||||
getLedgerEntryReserveSponsor(ApplyView& view, SLE::const_ref sle, SF_ACCOUNT const& field)
|
||||
{
|
||||
auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field);
|
||||
if (sponsorID)
|
||||
return view.peek(keylet::account(*sponsorID));
|
||||
return {};
|
||||
}
|
||||
|
||||
SLE::const_pointer
|
||||
getLedgerEntryReserveSponsor(ReadView const& view, SLE::const_ref sle, SF_ACCOUNT const& field)
|
||||
{
|
||||
auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field);
|
||||
if (sponsorID)
|
||||
return view.read(keylet::account(*sponsorID));
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
addSponsorToLedgerEntry(SLE::ref sle, SLE::const_ref sponsorSle, SF_ACCOUNT const& field)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
|
||||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
|
||||
"addSponsorToLedgerEntry : Invalid field to the LedgerEntry");
|
||||
if (sponsorSle)
|
||||
sle->setAccountID(field, sponsorSle->getAccountID(sfAccount));
|
||||
}
|
||||
|
||||
void
|
||||
removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
|
||||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
|
||||
"removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry");
|
||||
if (sle->isFieldPresent(field))
|
||||
sle->makeFieldAbsent(field);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
@@ -606,6 +607,15 @@ STTx::getBatchTransactionIDs() const
|
||||
return *batchTxnIds_;
|
||||
}
|
||||
|
||||
AccountID
|
||||
STTx::getFeePayerID() const
|
||||
{
|
||||
if (isFieldPresent(sfSponsor) && ((getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u))
|
||||
return at(sfSponsor);
|
||||
|
||||
return getInitiator();
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
static bool
|
||||
|
||||
@@ -578,7 +578,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const feePayer = getFeePayer(ctx.view, ctx.tx);
|
||||
auto const payerSle = ctx.view.read(feePayer.entry);
|
||||
auto const payerSle = ctx.view.read(feePayer.keylet);
|
||||
|
||||
if (!payerSle)
|
||||
{
|
||||
@@ -653,9 +653,9 @@ Transactor::payFee()
|
||||
auto const feePaid = ctx_.tx[sfFee].xrp();
|
||||
|
||||
auto const feePayer = getFeePayer(view(), ctx_.tx);
|
||||
auto const sle = view().peek(feePayer.entry);
|
||||
auto const sle = view().peek(feePayer.keylet);
|
||||
|
||||
JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.entry.key);
|
||||
JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.id);
|
||||
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -1306,7 +1306,7 @@ Transactor::reset(XRPAmount fee)
|
||||
return {tefINTERNAL, beast::kZero};
|
||||
|
||||
auto const feePayer = getFeePayer(view(), ctx_.tx);
|
||||
auto const payerSle = view().peek(feePayer.entry);
|
||||
auto const payerSle = view().peek(feePayer.keylet);
|
||||
|
||||
if (!payerSle)
|
||||
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
|
||||
@@ -1375,31 +1375,39 @@ Transactor::getFeePayer(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const sponsorID = tx.getAccountID(sfSponsor);
|
||||
auto const sponseeID = tx.getInitiator();
|
||||
auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature);
|
||||
auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID);
|
||||
|
||||
// if pre-funded sponsorship exists, prefer it
|
||||
if (hasSponsorSignature && !view.exists(sponsorshipKeylet))
|
||||
if (view.exists(sponsorshipKeylet))
|
||||
{
|
||||
// co-signed
|
||||
// pre funded
|
||||
return FeePayer{
|
||||
.entry = keylet::account(sponsorID),
|
||||
.balanceField = sfBalance,
|
||||
.type = FeePayerType::SponsorCoSigned};
|
||||
.id = sponsorID,
|
||||
.keylet = sponsorshipKeylet,
|
||||
.balanceField = sfFeeAmount,
|
||||
.type = FeePayerType::SponsorPreFunded};
|
||||
}
|
||||
|
||||
// pre funded
|
||||
// Checked in Transactor::checkSponsor
|
||||
XRPL_ASSERT(
|
||||
tx.isFieldPresent(sfSponsorSignature),
|
||||
"xrpl::getFeePayer has sponsor signature without a sponsorship object");
|
||||
|
||||
// co-signed
|
||||
return FeePayer{
|
||||
.entry = sponsorshipKeylet,
|
||||
.balanceField = sfFeeAmount,
|
||||
.type = FeePayerType::SponsorPreFunded};
|
||||
.id = sponsorID,
|
||||
.keylet = keylet::account(sponsorID),
|
||||
.balanceField = sfBalance,
|
||||
.type = FeePayerType::SponsorCoSigned};
|
||||
}
|
||||
|
||||
auto const payerAccountKeylet = keylet::account(tx.getInitiator());
|
||||
AccountID const payerID = tx.getInitiator();
|
||||
auto const payerAccountKeylet = keylet::account(payerID);
|
||||
auto const payerType =
|
||||
tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account;
|
||||
|
||||
return FeePayer{.entry = payerAccountKeylet, .balanceField = sfBalance, .type = payerType};
|
||||
return FeePayer{
|
||||
.id = payerID, .keylet = payerAccountKeylet, .balanceField = sfBalance, .type = payerType};
|
||||
}
|
||||
|
||||
// The sole purpose of this function is to provide a convenient, named
|
||||
|
||||
@@ -224,7 +224,7 @@ SponsorshipSet::doApply()
|
||||
if (hasPositiveFeeAmount)
|
||||
sponsorBalanceAfterFee -= *feeAmount;
|
||||
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
applyViewContext,
|
||||
sponsorAccSle,
|
||||
sponsorBalanceAfterFee.xrp(),
|
||||
@@ -294,7 +294,7 @@ SponsorshipSet::doApply()
|
||||
STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance];
|
||||
sponsorBalanceAfterFee -= feeAmountDelta;
|
||||
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponsorAccSle,
|
||||
sponsorBalanceAfterFee.xrp(),
|
||||
|
||||
@@ -112,12 +112,18 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// sfSponsorFlags should not be present if it is ending sponsorship
|
||||
// sfSponsorFlags should not be present if it is ending sponsorship.
|
||||
if (ctx.tx.isFieldPresent(sfSponsorFlags))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "preflight: sfSponsorFlags should not be present when ending sponsorship";
|
||||
// Unreachable: reaching here means sfSponsor is absent, which is already checked above,
|
||||
// and preflight1Sponsor already rejects sfSponsorFlags present without sfSponsor with
|
||||
// temINVALID_FLAG. Keep this as a defensive check.
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE(
|
||||
"xrpl::SponsorshipTransfer::preflight : sfSponsorFlags present without sfSponsor "
|
||||
"when ending sponsorship");
|
||||
return temINVALID_FLAG;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfSponsee) &&
|
||||
@@ -149,102 +155,79 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
TER
|
||||
SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const index = ctx.tx[~sfObjectID];
|
||||
auto const objectID = ctx.tx[~sfObjectID];
|
||||
auto const newSponsorSleExpected = getTxReserveSponsor(ctx.view, ctx.tx);
|
||||
if (!newSponsorSleExpected)
|
||||
return newSponsorSleExpected.error(); // LCOV_EXCL_LINE
|
||||
auto const newSponsorSle = *newSponsorSleExpected;
|
||||
|
||||
bool const isObjectSponsor = !!index;
|
||||
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const sponseeID = ctx.tx[~sfSponsee].value_or(account);
|
||||
auto const sponseeSle = ctx.view.read(keylet::account(sponseeID));
|
||||
if (!sponseeSle)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (isObjectSponsor)
|
||||
{
|
||||
auto const sle = ctx.view.read(keylet::unchecked(*index));
|
||||
if (!sle)
|
||||
// If it is ending sponsorship, sfSponsee is user input, return terNO_ACCOUNT if it does not
|
||||
// exist.
|
||||
if (ctx.tx.isFieldPresent(sfSponsee))
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// If it is creating or reassigning sponsorship, sfSponsee is the account itself, which is
|
||||
// always present by the time preclaim runs. Return tecINTERNAL if it does not exist.
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// Default setup with an account sponsorship transfer. If it is an object transfer, they will be
|
||||
// overridden to the object SLE and its type-specific sponsor field:
|
||||
// sfHighSponsor/sfLowSponsor for a RippleState, sfSponsor for other object types.
|
||||
SLE::const_pointer targetSle = sponseeSle;
|
||||
auto const* sponsorField = &sfSponsor;
|
||||
|
||||
if (objectID.has_value())
|
||||
{
|
||||
auto const objectSle = ctx.view.read(keylet::unchecked(*objectID));
|
||||
if (!objectSle)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
if (!isLedgerEntrySupportedBySponsorship(sle))
|
||||
if (!isLedgerEntrySupportedBySponsorship(objectSle))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeID);
|
||||
auto const owner = getLedgerEntryOwner(ctx.view, objectSle, sponseeID);
|
||||
if (!owner.has_value() || owner.value() != sponseeID)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const& sponsorField = getLedgerEntrySponsorField(sle, owner.value());
|
||||
|
||||
if (ctx.tx.isFlag(tfSponsorshipCreate))
|
||||
{
|
||||
if (!newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check that the object is not sponsored yet
|
||||
if (sle->isFieldPresent(sponsorField))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipReassign))
|
||||
{
|
||||
if (!newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check object is already ctx.sponsored
|
||||
if (!sle->isFieldPresent(sponsorField))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipEnd))
|
||||
{
|
||||
if (newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check object is sponsored
|
||||
if (!sle->isFieldPresent(sponsorField))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// only the sponsor or sponsee can end sponsorship
|
||||
auto const sponsor = sle->getAccountID(sponsorField);
|
||||
if (account != sponsor && account != sponseeID)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
// Object transfer: the target is the object, and its sponsor field
|
||||
// depends on the object type, a RippleState stores the sponsor in
|
||||
// sfHighSponsor/sfLowSponsor, while other object type uses sfSponsor.
|
||||
targetSle = objectSle;
|
||||
sponsorField = &getLedgerEntrySponsorField(objectSle, owner.value());
|
||||
}
|
||||
else
|
||||
|
||||
bool const isSponsored = targetSle->isFieldPresent(*sponsorField);
|
||||
|
||||
if (ctx.tx.isFlag(tfSponsorshipCreate))
|
||||
{
|
||||
if (ctx.tx.isFlag(tfSponsorshipCreate))
|
||||
{
|
||||
if (!newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
// Creating a new sponsorship: needs a new reserve sponsor, and the
|
||||
// target must not already be sponsored
|
||||
if (!newSponsorSle || isSponsored)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipReassign))
|
||||
{
|
||||
// Reassigning sponsorship: needs a new reserve sponsor, and the target must already
|
||||
// be sponsored
|
||||
if (!newSponsorSle || !isSponsored)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipEnd))
|
||||
{
|
||||
// Ending sponsorship: no new reserve sponsor, the target must be sponsored.
|
||||
if (newSponsorSle || !isSponsored)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check account is not sponsored yet
|
||||
if (sponseeSle->isFieldPresent(sfSponsor))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipReassign))
|
||||
{
|
||||
if (!newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check account is already sponsored
|
||||
if (!sponseeSle->isFieldPresent(sfSponsor))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipEnd))
|
||||
{
|
||||
if (newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check account is sponsored
|
||||
if (!sponseeSle->isFieldPresent(sfSponsor))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// only the sponsor or sponsee can end sponsorship
|
||||
auto const sponsor = sponseeSle->getAccountID(sfSponsor);
|
||||
if (account != sponsor && account != sponseeID)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
// Only the sponsor or sponsee can end sponsorship.
|
||||
auto const sponsor = targetSle->getAccountID(*sponsorField);
|
||||
if (account != sponsor && account != sponseeID)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -344,7 +327,7 @@ SponsorshipTransfer::doApply()
|
||||
|
||||
// check new sponsor have sufficient balance
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
if (auto const ter = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
@@ -396,7 +379,7 @@ SponsorshipTransfer::doApply()
|
||||
|
||||
// check new sponsor have sufficient balance
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
if (auto const ter = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
@@ -443,7 +426,7 @@ SponsorshipTransfer::doApply()
|
||||
|
||||
// The owner takes the reserve burden back when the object is
|
||||
// no longer sponsored.
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
if (auto const ter = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
ownerSle,
|
||||
balanceBeforeFee(ownerSle),
|
||||
@@ -482,7 +465,7 @@ SponsorshipTransfer::doApply()
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
if (auto const ter = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
@@ -509,7 +492,7 @@ SponsorshipTransfer::doApply()
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
if (auto const ter = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
@@ -544,7 +527,7 @@ SponsorshipTransfer::doApply()
|
||||
|
||||
// The sponsee must be able to hold its own account reserve after
|
||||
// the sponsorship is removed.
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
if (auto const ter = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sponseeSle,
|
||||
balanceBeforeFee(sponseeSle),
|
||||
|
||||
@@ -321,7 +321,7 @@ SignerListSet::replaceSignerList()
|
||||
// with TicketCreate.
|
||||
auto const applyViewContext = ctx_.getApplyViewContext();
|
||||
auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle;
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sle,
|
||||
preFeeBalance_,
|
||||
|
||||
@@ -393,11 +393,11 @@ CheckCash::doApply()
|
||||
|
||||
// Check reserve. Return destination account SLE if enough reserve,
|
||||
// otherwise return nullptr.
|
||||
auto checkReserve = [&]() -> SLE::pointer {
|
||||
auto checkDstReserve = [&]() -> SLE::pointer {
|
||||
auto sleDst = psb.peek(keylet::account(accountID_));
|
||||
|
||||
// Can the account cover the trust line's or MPT reserve?
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
applyViewContext, sleDst, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
@@ -433,7 +433,7 @@ CheckCash::doApply()
|
||||
// a. this (destination) account and
|
||||
// b. issuing account (not sending account).
|
||||
|
||||
auto const sleDst = checkReserve();
|
||||
auto const sleDst = checkDstReserve();
|
||||
if (sleDst == nullptr)
|
||||
return tecNO_LINE_INSUF_RESERVE;
|
||||
|
||||
@@ -500,7 +500,7 @@ CheckCash::doApply()
|
||||
auto const mptokenKey = keylet::mptoken(mptID, accountID_);
|
||||
if (!psb.exists(mptokenKey))
|
||||
{
|
||||
auto sleDst = checkReserve();
|
||||
auto sleDst = checkDstReserve();
|
||||
if (sleDst == nullptr)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ CheckCreate::doApply()
|
||||
// reserve to pay fees.
|
||||
auto const applyViewContext = ctx_.getApplyViewContext();
|
||||
auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle;
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sle, preFeeBalance_, {.ownerCountDelta = 1}, ctx_.journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -100,7 +100,7 @@ DelegateSet::doApply()
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const applyViewContext = ctx_.getApplyViewContext();
|
||||
auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle;
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sleOwner,
|
||||
preFeeBalance_,
|
||||
|
||||
@@ -443,8 +443,8 @@ EscrowCreate::doApply()
|
||||
// validates the sponsor's reserve + remaining credit. When
|
||||
// unsponsored this hits the source branch and validates the
|
||||
// source's pre-lock balance against base + (currentOC+1)*increment.
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
|
||||
if (auto const ret =
|
||||
checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -455,7 +455,7 @@ EscrowCreate::doApply()
|
||||
// is always the source's own balance against the source's own
|
||||
// reserve — the sponsor's reserve was already validated above, and
|
||||
// a sponsor never covers the locked funds. We compare directly
|
||||
// (rather than via checkInsufficientReserve) because that helper
|
||||
// (rather than via checkReserve) because that helper
|
||||
// diverts to the sponsor's balance when a sponsor is present and
|
||||
// would ignore the source's post-lock balance entirely.
|
||||
// ownerCountDelta differs by case:
|
||||
|
||||
@@ -163,7 +163,7 @@ DepositPreauth::doApply()
|
||||
// A preauth counts against the reserve of the issuing account, but we
|
||||
// check the starting balance because we want to allow dipping into the
|
||||
// reserve to pay fees.
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
@@ -209,7 +209,7 @@ DepositPreauth::doApply()
|
||||
// A preauth counts against the reserve of the issuing account, but we
|
||||
// check the starting balance because we want to allow dipping into the
|
||||
// reserve to pay fees.
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -691,9 +691,8 @@ Payment::doApply()
|
||||
// reserve.
|
||||
auto const reserve = accountReserve(view(), sleSrc, j_);
|
||||
|
||||
// In a delegated payment, the fee payer is the delegated account,
|
||||
// not the source account (accountID_).
|
||||
bool const accountIsPayer = (ctx_.tx.getInitiator() == accountID_);
|
||||
// In a delegated / fee sponsored payment, the fee payer is not the source account (accountID_).
|
||||
bool const accountIsPayer = ctx_.tx.getFeePayerID() == accountID_;
|
||||
|
||||
// preFeeBalance_ is the balance on the source account (accountID_) BEFORE the fees
|
||||
// were charged. If source account is the fee payer, it must also cover the fee.
|
||||
|
||||
@@ -144,8 +144,8 @@ PaymentChannelCreate::doApply()
|
||||
// validates the sponsor's reserve + remaining credit. When
|
||||
// unsponsored this hits the source branch and validates the
|
||||
// source's pre-lock balance against base + (currentOC+1)*increment.
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
applyViewContext, sle, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
if (auto const ret =
|
||||
checkReserve(applyViewContext, sle, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
@@ -154,7 +154,7 @@ PaymentChannelCreate::doApply()
|
||||
// source's own balance against the source's own reserve — the
|
||||
// sponsor's reserve was already validated above, and a sponsor
|
||||
// never covers the locked funds. We compare directly (rather than
|
||||
// via checkInsufficientReserve) because that helper diverts to the
|
||||
// via checkReserve) because that helper diverts to the
|
||||
// sponsor's balance when a sponsor is present and would ignore the
|
||||
// source's post-lock balance entirely. ownerCountDelta differs by
|
||||
// case:
|
||||
|
||||
@@ -90,12 +90,12 @@ PaymentChannelFund::doApply()
|
||||
// Check reserve and funds availability
|
||||
auto const balance = (*sle)[sfBalance];
|
||||
auto const applyViewContext = ctx_.getApplyViewContext();
|
||||
if (auto const ret = checkInsufficientReserve(applyViewContext, sle, balance, {}, j_);
|
||||
if (auto const ret = checkReserve(applyViewContext, sle, balance, {}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
applyViewContext, sle, balance - ctx_.tx[sfAmount], {}, j_);
|
||||
if (auto const ret =
|
||||
checkReserve(applyViewContext, sle, balance - ctx_.tx[sfAmount], {}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
|
||||
@@ -128,8 +128,8 @@ MPTokenIssuanceCreate::create(
|
||||
|
||||
if (args.priorBalance)
|
||||
{
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
ctx, acct, *(args.priorBalance), {.ownerCountDelta = 1}, journal);
|
||||
if (auto const ret =
|
||||
checkReserve(ctx, acct, *(args.priorBalance), {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
return std::unexpected(ret); // tecINSUFFICIENT_RESERVE
|
||||
}
|
||||
|
||||
@@ -330,12 +330,18 @@ TrustSet::doApply()
|
||||
auto const applyViewContext = ctx_.getApplyViewContext();
|
||||
auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle;
|
||||
|
||||
std::uint32_t const uOwnerCount = ownerCount(sponsorSle ? sponsorSle : sle, j_);
|
||||
auto const sponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext());
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
auto getSponsor = [&sponsorSle, this](AccountID const& account) {
|
||||
return (sponsorSle && account == accountID_) ? sponsorSle : SLE::pointer();
|
||||
};
|
||||
|
||||
// The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor.
|
||||
// With any sponsor on the tx, the sponsor must cover the reserve (via balance or
|
||||
// prefunded budget), so the reserve check always runs.
|
||||
bool const freeTrustLine = uOwnerCount < 2 && !sponsorSle;
|
||||
bool const freeTrustLine = !sponsorSle && (ownerCount(sle, j_) < 2);
|
||||
|
||||
std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0);
|
||||
std::uint32_t uQualityOut(bQualityOut ? ctx_.tx.getFieldU32(sfQualityOut) : 0);
|
||||
@@ -533,10 +539,12 @@ TrustSet::doApply()
|
||||
|
||||
if (bLowReserveSet && !bLowReserved)
|
||||
{
|
||||
SLE::pointer const lowSponsor = getSponsor(uLowAccountID);
|
||||
|
||||
// should be checked PreFunded Sponsor before increaseOwnerCount()
|
||||
// For PreFunded sponsors, we need to check if there are sufficient reserves before
|
||||
// calling increaseOwnerCount().
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sleLowAccount,
|
||||
preFeeBalance_,
|
||||
@@ -548,12 +556,12 @@ TrustSet::doApply()
|
||||
// Set reserve for low account.
|
||||
increaseOwnerCount(
|
||||
view(),
|
||||
ReserveContext::makeFromAccount(view(), sleLowAccount, sponsorSle),
|
||||
ReserveContext::makeFromAccount(view(), sleLowAccount, lowSponsor),
|
||||
1,
|
||||
viewJ);
|
||||
uFlagsOut |= lsfLowReserve;
|
||||
|
||||
addSponsorToLedgerEntry(sleRippleState, sponsorSle, sfLowSponsor);
|
||||
addSponsorToLedgerEntry(sleRippleState, lowSponsor, sfLowSponsor);
|
||||
|
||||
if (!bHigh)
|
||||
bReserveIncrease = true;
|
||||
@@ -574,27 +582,29 @@ TrustSet::doApply()
|
||||
|
||||
if (bHighReserveSet && !bHighReserved)
|
||||
{
|
||||
SLE::pointer const highSponsor = getSponsor(uHighAccountID);
|
||||
|
||||
// should be checked PreFunded Sponsor before increaseOwnerCount()
|
||||
// For PreFunded sponsors, we need to check if there are sufficient reserves before
|
||||
// calling increaseOwnerCount().
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sleHighAccount,
|
||||
preFeeBalance_,
|
||||
{.ownerCountDelta = 1},
|
||||
j_);
|
||||
sponsorSle && !isTesSuccess(ret))
|
||||
highSponsor && !isTesSuccess(ret))
|
||||
return tecINSUF_RESERVE_LINE;
|
||||
|
||||
// Set reserve for high account.
|
||||
increaseOwnerCount(
|
||||
view(),
|
||||
ReserveContext::makeFromAccount(view(), sleHighAccount, sponsorSle),
|
||||
ReserveContext::makeFromAccount(view(), sleHighAccount, highSponsor),
|
||||
1,
|
||||
viewJ);
|
||||
uFlagsOut |= lsfHighReserve;
|
||||
|
||||
addSponsorToLedgerEntry(sleRippleState, sponsorSle, sfHighSponsor);
|
||||
addSponsorToLedgerEntry(sleRippleState, highSponsor, sfHighSponsor);
|
||||
|
||||
if (bHigh)
|
||||
bReserveIncrease = true;
|
||||
@@ -624,8 +634,7 @@ TrustSet::doApply()
|
||||
}
|
||||
// Reserve is not scaled by load.
|
||||
else if (
|
||||
auto const ret =
|
||||
checkInsufficientReserve(ctx_.getApplyViewContext(), sle, preFeeBalance_, {}, j_);
|
||||
auto const ret = checkReserve(ctx_.getApplyViewContext(), sle, preFeeBalance_, {}, j_);
|
||||
!freeTrustLine && bReserveIncrease && !isTesSuccess(ret))
|
||||
{
|
||||
JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to "
|
||||
@@ -655,7 +664,7 @@ TrustSet::doApply()
|
||||
return tecNO_LINE_REDUNDANT;
|
||||
}
|
||||
else if (
|
||||
auto const ret = checkInsufficientReserve(
|
||||
auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(),
|
||||
sle,
|
||||
preFeeBalance_,
|
||||
|
||||
@@ -921,6 +921,14 @@ public:
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecNO_PERMISSION));
|
||||
}
|
||||
{
|
||||
// The provided sfSponsee account does not exist
|
||||
// when ending sponsorship.
|
||||
Account const ghost("ghost"); // never funded, absent from ledger
|
||||
env(sponsor::transfer(sponsor, tfSponsorshipEnd),
|
||||
sponsor::SponseeAcc(ghost),
|
||||
Ter(terNO_ACCOUNT));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1112,6 +1120,13 @@ public:
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
// Reassign an object that is not sponsored yet
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
||||
sponsor::As(sponsor1, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor1),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
// Valid Owner
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
||||
sponsor::As(sponsor1, spfSponsorReserve),
|
||||
@@ -1129,6 +1144,13 @@ public:
|
||||
BEAST_EXPECT(sle1->isFieldPresent(sfSponsor));
|
||||
BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id());
|
||||
|
||||
// Create on an object that is already sponsored
|
||||
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
||||
sponsor::As(sponsor2, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor2),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
// transfer sponsor
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
||||
sponsor::As(sponsor2, spfSponsorReserve),
|
||||
@@ -4398,6 +4420,151 @@ public:
|
||||
testTrustSet(cosigning);
|
||||
}
|
||||
|
||||
void
|
||||
testZeroBalanceSponsoredPaymentFeePayerCheck()
|
||||
{
|
||||
// Zero-balance sponsored Payment: getFeePayer() consistency check
|
||||
testcase("Sponsored Payment: minimal-balance account with sponsor-pays-fee");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
Account const dest("dest");
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
auto const baseReserve = env.current()->fees().reserve;
|
||||
|
||||
// Fund sponsor and dest generously, alice with base reserve + 1 XRP for payment
|
||||
env.fund(XRP(10000), sponsor, dest);
|
||||
env.fund(baseReserve + XRP(1), alice);
|
||||
env.close();
|
||||
|
||||
// Precondition: alice has base reserve + 1 XRP (enough for payment but not fee)
|
||||
BEAST_EXPECT(env.balance(alice) == baseReserve + XRP(1));
|
||||
|
||||
// Alice tries to send a Payment to dest where sponsor pays the fee via spfSponsorFee.
|
||||
// Passed even alice balance doesn't have enough to pay fee.
|
||||
auto const preDest = env.balance(dest);
|
||||
auto const preSponsor = env.balance(sponsor);
|
||||
|
||||
// Alice sends 1 XRP to dest, sponsor pays the fee
|
||||
env(pay(alice, dest, XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Fee(baseFee));
|
||||
env.close();
|
||||
|
||||
// Payment succeeded
|
||||
// Alice's balance decreased by 1 XRP (the payment amount, NOT the fee)
|
||||
BEAST_EXPECT(env.balance(alice) == baseReserve);
|
||||
|
||||
// Dest received 1 XRP
|
||||
BEAST_EXPECT(env.balance(dest) == preDest + XRP(1));
|
||||
|
||||
// Sponsor paid the fee (NOT alice)
|
||||
BEAST_EXPECT(env.balance(sponsor) == preSponsor - baseFee);
|
||||
}
|
||||
|
||||
void
|
||||
testTrustSetCounterpartySponsorMisroute()
|
||||
{
|
||||
// TrustSet's modify path applies the tx-level reserve sponsor to whichever
|
||||
// side has its reserve gate trip on this update, regardless of whether that
|
||||
// side belongs to the tx submitter. trustCreate only sets the submitter's
|
||||
// reserve flag and snapshots the counterparty's asfDefaultRipple state into
|
||||
// the line's NoRipple bit; if the counterparty later toggles asfDefaultRipple
|
||||
// (the canonical issuer flow), the line and account flags disagree and on
|
||||
// the submitter's next TrustSet the counterparty-side gate fires. Sponsor still will be
|
||||
// checked if it can be applied to that end of the trustLine.
|
||||
|
||||
testcase("TrustSet modify with sponsor does not misroute onto counterparty side");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice_t2178"};
|
||||
Account const bob{"bob_t2178"};
|
||||
Account const carol{"carol_t2178"};
|
||||
|
||||
// Fund without auto-setting asfDefaultRipple
|
||||
env.fund(XRP(100'000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// Determine account ordering
|
||||
bool const aliceIsHigh = alice.id() > bob.id();
|
||||
|
||||
// To trigger the bug, we need the COUNTERPARTY's reserve gate to trip
|
||||
// We use issuer/holder terminology where:
|
||||
// - holder creates the trust line (their reserve is set first)
|
||||
// - issuer enables DefaultRipple after (creates flag mismatch)
|
||||
// - holder's second TrustSet triggers issuer's reserve gate
|
||||
|
||||
auto const issuer = aliceIsHigh ? bob : alice;
|
||||
auto const holder = aliceIsHigh ? alice : bob;
|
||||
auto const usd = issuer["USD"];
|
||||
|
||||
// Issuer must NOT have DefaultRipple set initially
|
||||
// Clear it explicitly (env.fund may have set it)
|
||||
env(fclear(issuer, asfDefaultRipple));
|
||||
env.close();
|
||||
|
||||
// Holder creates the trust line first (holder's reserve flag is set)
|
||||
// At this point, issuer does NOT have DefaultRipple set, so
|
||||
// the NoRipple bit on issuer's side is set according to issuer's current flag
|
||||
env(trust(holder, usd(1'000)));
|
||||
env.close();
|
||||
|
||||
// Issuer now enables asfDefaultRipple (canonical issuer flow)
|
||||
// This creates a mismatch: issuer's account flag says DefaultRipple=true
|
||||
// but the trust line's NoRipple bit on issuer's side is still set
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
env.close();
|
||||
|
||||
SF_ACCOUNT const& issuerSponsorField = aliceIsHigh ? sfLowSponsor : sfHighSponsor;
|
||||
SF_ACCOUNT const& holderSponsorField = aliceIsHigh ? sfHighSponsor : sfLowSponsor;
|
||||
|
||||
auto const lineKey = keylet::trustLine(alice, bob, usd.currency);
|
||||
auto const sleLineBefore = env.le(lineKey);
|
||||
if (!BEAST_EXPECT(sleLineBefore))
|
||||
return;
|
||||
BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfLowSponsor));
|
||||
BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfHighSponsor));
|
||||
|
||||
auto const carolBefore = sponsoringOwnerCount(env, carol);
|
||||
BEAST_EXPECT(carolBefore == 0);
|
||||
auto const issuerSponsoredBefore = sponsoredOwnerCount(env, issuer);
|
||||
BEAST_EXPECT(issuerSponsoredBefore == 0);
|
||||
|
||||
// Holder modifies the trust line with Carol as sponsor
|
||||
// This should trigger the issuer's reserve gate because of the DefaultRipple mismatch
|
||||
// Carol (sponsor) should NOT be applied to issuer's side (issuer != tx submitter)
|
||||
env(trust(holder, usd(2'000)),
|
||||
sponsor::As(carol, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, carol),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const sleLineAfter = env.le(lineKey);
|
||||
if (!BEAST_EXPECT(sleLineAfter))
|
||||
return;
|
||||
|
||||
// Carol only agreed to back the holder, not the issuer
|
||||
BEAST_EXPECT(!sleLineAfter->isFieldPresent(issuerSponsorField));
|
||||
|
||||
// Holder's side also has no sponsor because holder's reserve flag was
|
||||
// already set on the FIRST TrustSet (no sponsor in scope then)
|
||||
BEAST_EXPECT(!sleLineAfter->isFieldPresent(holderSponsorField));
|
||||
|
||||
// Carol's sponsoring count should remain unchanged (no misroute)
|
||||
auto const carolAfter = sponsoringOwnerCount(env, carol);
|
||||
BEAST_EXPECT(carolAfter == carolBefore);
|
||||
|
||||
// Issuer's sponsored count should remain unchanged (no misroute)
|
||||
auto const issuerSponsoredAfter = sponsoredOwnerCount(env, issuer);
|
||||
BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore);
|
||||
}
|
||||
|
||||
protected:
|
||||
void
|
||||
testSponsor()
|
||||
@@ -4432,6 +4599,9 @@ protected:
|
||||
testSponsoredTrustLineNoFreeReserve();
|
||||
testCoSignReserveBoundedBySponsorshipBudget();
|
||||
testReserveSponsorGate();
|
||||
|
||||
testZeroBalanceSponsoredPaymentFeePayerCheck();
|
||||
testTrustSetCounterpartySponsorMisroute();
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user