mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 23:20:33 +00:00
Merge branch 'xrplf/sponsor' of https://github.com/XRPLF/rippled into mvadari/sponsor/apply-view-context
This commit is contained in:
@@ -109,7 +109,7 @@ baseAccountReserve(ReadView const& view, std::int32_t ownerCount, std::int32_t a
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkInsufficientReserve(
|
||||
ReadView const& view,
|
||||
ApplyView const& view,
|
||||
STTx const& tx,
|
||||
SLE::const_ref accSle,
|
||||
STAmount const& accBalance,
|
||||
|
||||
19
include/xrpl/ledger/helpers/OracleHelpers.h
Normal file
19
include/xrpl/ledger/helpers/OracleHelpers.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
constexpr uint32_t kMinOracleReserveCount = 1;
|
||||
constexpr uint32_t kMaxOracleReserveCount = 2;
|
||||
constexpr std::size_t kOracleReserveCountThreshold = 5;
|
||||
|
||||
inline uint32_t
|
||||
calculateOracleReserve(std::size_t priceDataSeriesCount)
|
||||
{
|
||||
return priceDataSeriesCount > kOracleReserveCountThreshold ? kMaxOracleReserveCount
|
||||
: kMinOracleReserveCount;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/OracleHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
@@ -123,4 +124,121 @@ removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor)
|
||||
sle->makeFieldAbsent(field);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline std::optional<AccountID>
|
||||
getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account)
|
||||
{
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltCHECK:
|
||||
case ltESCROW:
|
||||
case ltPAYCHAN:
|
||||
case ltMPTOKEN:
|
||||
case ltDELEGATE:
|
||||
case ltDEPOSIT_PREAUTH:
|
||||
return sle->getAccountID(sfAccount);
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
return sle->getAccountID(sfIssuer);
|
||||
case ltSIGNER_LIST: {
|
||||
auto const signerList = view.read(keylet::signers(account));
|
||||
if (!signerList)
|
||||
return std::nullopt;
|
||||
if (signerList->key() == sle->key())
|
||||
return account;
|
||||
return std::nullopt;
|
||||
}
|
||||
case ltCREDENTIAL: {
|
||||
if (sle->isFlag(lsfAccepted))
|
||||
return sle->getAccountID(sfSubject);
|
||||
return sle->getAccountID(sfIssuer);
|
||||
}
|
||||
case ltRIPPLE_STATE: {
|
||||
if (sle->isFlag(lsfHighReserve))
|
||||
{
|
||||
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
|
||||
if (highAccount == account)
|
||||
return highAccount;
|
||||
}
|
||||
if (sle->isFlag(lsfLowReserve))
|
||||
{
|
||||
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
|
||||
if (lowAccount == account)
|
||||
return lowAccount;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
default:
|
||||
UNREACHABLE("Object is not supported by sponsorship.");
|
||||
return std::nullopt;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool
|
||||
isLedgerEntrySupportedBySponsorship(T const& sle)
|
||||
{
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltCHECK:
|
||||
case ltESCROW:
|
||||
case ltPAYCHAN:
|
||||
case ltMPTOKEN:
|
||||
case ltDELEGATE:
|
||||
case ltDEPOSIT_PREAUTH:
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
case ltSIGNER_LIST:
|
||||
case ltCREDENTIAL:
|
||||
case ltRIPPLE_STATE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline std::uint32_t
|
||||
getLedgerEntryOwnerCount(T const& sle)
|
||||
{
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltORACLE: {
|
||||
return calculateOracleReserve(sle->getFieldArray(sfPriceDataSeries).size());
|
||||
}
|
||||
// Vaults require 2 owner counts (the vault and a pseudo-account)
|
||||
case ltVAULT:
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
inline SF_ACCOUNT const&
|
||||
getLedgerEntrySponsorField(T const& sle, AccountID const& owner)
|
||||
{
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltRIPPLE_STATE: {
|
||||
if (sle->isFlag(lsfHighReserve))
|
||||
{
|
||||
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
|
||||
if (highAccount == owner)
|
||||
return sfHighSponsor;
|
||||
}
|
||||
if (sle->isFlag(lsfLowReserve))
|
||||
{
|
||||
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
|
||||
if (lowAccount == owner)
|
||||
return sfLowSponsor;
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("Should not happen. Owner should be checked before calling this function.");
|
||||
return sfSponsor;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
default:
|
||||
return sfSponsor;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -22,12 +22,6 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
calculateOracleReserve(std::size_t count)
|
||||
{
|
||||
return count > 5 ? 2 : 1;
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ baseAccountReserve(ReadView const& view, std::int32_t ownerCount, std::int32_t a
|
||||
|
||||
TER
|
||||
checkInsufficientReserve(
|
||||
ReadView const& view,
|
||||
ApplyView const& view,
|
||||
STTx const& tx,
|
||||
SLE::const_ref accSle,
|
||||
STAmount const& accBalance,
|
||||
|
||||
@@ -640,7 +640,28 @@ Transactor::payFee()
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const feeAmountAfter = sle->getFieldAmount(feePayer.balanceField) - feePaid;
|
||||
if (feePaid == beast::kZero)
|
||||
return tesSUCCESS;
|
||||
|
||||
XRPAmount balance = beast::kZero;
|
||||
if (sle->isFieldPresent(feePayer.balanceField))
|
||||
{
|
||||
balance = sle->getFieldAmount(feePayer.balanceField).xrp();
|
||||
}
|
||||
else if (feePayer.balanceField != sfFeeAmount)
|
||||
{
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (feePaid > balance)
|
||||
{
|
||||
if ((balance > beast::kZero) && !view().open())
|
||||
return tecINSUFF_FEE;
|
||||
|
||||
return terINSUF_FEE_B;
|
||||
}
|
||||
|
||||
auto const feeAmountAfter = balance - feePaid;
|
||||
|
||||
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
|
||||
{
|
||||
@@ -1302,7 +1323,15 @@ Transactor::reset(XRPAmount fee)
|
||||
if (!payerSle)
|
||||
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
|
||||
|
||||
auto const balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
|
||||
XRPAmount balance = beast::kZero;
|
||||
if (payerSle->isFieldPresent(feePayer.balanceField))
|
||||
{
|
||||
balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
|
||||
}
|
||||
else if (feePayer.balanceField != sfFeeAmount)
|
||||
{
|
||||
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee))
|
||||
{
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/OracleHelpers.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/transactors/oracle/OracleSet.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
@@ -62,7 +62,7 @@ SponsorshipOwnerCountsMatch::visitEntry(
|
||||
if (!sle->isFieldPresent(sfSponsor))
|
||||
return 0;
|
||||
auto const priceDataSeries = sle->getFieldArray(sfPriceDataSeries);
|
||||
return OracleSet::calculateOracleReserve(priceDataSeries.size());
|
||||
return calculateOracleReserve(priceDataSeries.size());
|
||||
}
|
||||
case ltVAULT: {
|
||||
if (!sle->isFieldPresent(sfSponsor))
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -53,6 +52,9 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
|
||||
if (ctx.tx.isFlag(tfSponsorshipCreate))
|
||||
{
|
||||
// Sponsor must be included
|
||||
// SponsorFlags.spfSponsorReserve must be included
|
||||
// Sponsee must be excluded
|
||||
if (!isReserveSponsored(ctx.tx))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
@@ -68,6 +70,9 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
if (ctx.tx.isFlag(tfSponsorshipReassign))
|
||||
{
|
||||
// Sponsor must be included
|
||||
// SponsorFlags.spfSponsorReserve must be included
|
||||
// Sponsee must be excluded
|
||||
if (!isReserveSponsored(ctx.tx))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
@@ -83,6 +88,8 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
if (ctx.tx.isFlag(tfSponsorshipEnd))
|
||||
{
|
||||
// Sponsor must be excluded
|
||||
// SponsorFlags.spfSponsorReserve must be excluded
|
||||
if (isReserveSponsored(ctx.tx))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
@@ -116,96 +123,18 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline std::optional<AccountID>
|
||||
getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account)
|
||||
{
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltCHECK:
|
||||
case ltESCROW:
|
||||
case ltPAYCHAN:
|
||||
case ltMPTOKEN:
|
||||
case ltDELEGATE:
|
||||
case ltDEPOSIT_PREAUTH:
|
||||
return sle->getAccountID(sfAccount);
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
return sle->getAccountID(sfIssuer);
|
||||
case ltSIGNER_LIST: {
|
||||
auto const signerList = view.read(keylet::signers(account));
|
||||
if (!signerList)
|
||||
return std::nullopt;
|
||||
if (signerList->key() == sle->key())
|
||||
return account;
|
||||
return std::nullopt;
|
||||
}
|
||||
case ltCREDENTIAL: {
|
||||
if (sle->isFlag(lsfAccepted))
|
||||
return sle->getAccountID(sfSubject);
|
||||
return sle->getAccountID(sfIssuer);
|
||||
}
|
||||
case ltRIPPLE_STATE: {
|
||||
if (sle->isFlag(lsfHighReserve))
|
||||
{
|
||||
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
|
||||
if (highAccount == account)
|
||||
return highAccount;
|
||||
}
|
||||
if (sle->isFlag(lsfLowReserve))
|
||||
{
|
||||
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
|
||||
if (lowAccount == account)
|
||||
return lowAccount;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
default:
|
||||
UNREACHABLE("Object is not supported by sponsorship.");
|
||||
return std::nullopt;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline SF_ACCOUNT const&
|
||||
getLedgerEntrySponsorField(T const& sle, AccountID const& owner)
|
||||
{
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltRIPPLE_STATE: {
|
||||
if (sle->isFlag(lsfHighReserve))
|
||||
{
|
||||
auto const highAccount = sle->getFieldAmount(sfHighLimit).getIssuer();
|
||||
if (highAccount == owner)
|
||||
return sfHighSponsor;
|
||||
}
|
||||
if (sle->isFlag(lsfLowReserve))
|
||||
{
|
||||
auto const lowAccount = sle->getFieldAmount(sfLowLimit).getIssuer();
|
||||
if (lowAccount == owner)
|
||||
return sfLowSponsor;
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("Should not happen. Owner should be checked before calling this function.");
|
||||
return sfSponsor;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
default:
|
||||
return sfSponsor;
|
||||
}
|
||||
};
|
||||
|
||||
TER
|
||||
SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const index = ctx.tx[~sfObjectID];
|
||||
auto const newSponsorSle = getTxReserveSponsor(ctx.view, ctx.tx);
|
||||
if (!newSponsorSle)
|
||||
return newSponsorSle.error(); // LCOV_EXCL_LINE
|
||||
auto const newSponsorSleExpected = getTxReserveSponsor(ctx.view, ctx.tx);
|
||||
if (!newSponsorSleExpected)
|
||||
return newSponsorSleExpected.error(); // LCOV_EXCL_LINE
|
||||
auto const newSponsorSle = *newSponsorSleExpected;
|
||||
|
||||
bool const isObjectSponsor = index != std::nullopt;
|
||||
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)
|
||||
@@ -217,49 +146,27 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
if (!sle)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
// v1 scope: an object is only sponsorable via SponsorshipTransfer if
|
||||
// its creating transaction type is itself permitted to set
|
||||
// spfSponsorReserve (the allow-list in preflight1Sponsor). Otherwise
|
||||
// an Oracle / Ticket / DID / etc. could be retroactively sponsored
|
||||
// even though its creating tx cannot be, leaving downstream
|
||||
// transactors with no path to maintain the sponsorship invariants.
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltDELEGATE:
|
||||
case ltDEPOSIT_PREAUTH:
|
||||
case ltMPTOKEN:
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
case ltCREDENTIAL:
|
||||
case ltRIPPLE_STATE:
|
||||
case ltSIGNER_LIST:
|
||||
case ltCHECK:
|
||||
case ltESCROW:
|
||||
case ltPAYCHAN:
|
||||
break;
|
||||
default:
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
std::uint32_t const ownerCountDelta = 1;
|
||||
|
||||
auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeID);
|
||||
if (!owner || owner != sponseeID)
|
||||
if (!isLedgerEntrySupportedBySponsorship(sle))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const& sponsorField = getLedgerEntrySponsorField(sle, *owner);
|
||||
auto const owner = getLedgerEntryOwner(ctx.view, sle, 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)
|
||||
if (!newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check object is not sponsored yet
|
||||
// check that the object is not sponsored yet
|
||||
if (sle->isFieldPresent(sponsorField))
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipReassign))
|
||||
{
|
||||
if (!*newSponsorSle)
|
||||
if (!newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check object is already ctx.sponsored
|
||||
@@ -268,7 +175,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipEnd))
|
||||
{
|
||||
if (*newSponsorSle)
|
||||
if (newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check object is sponsored
|
||||
@@ -280,26 +187,12 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
if (account != sponsor && account != sponseeID)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// check new sponsor have sufficient balance
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
ctx.view,
|
||||
ctx.tx,
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
*newSponsorSle,
|
||||
ownerCountDelta,
|
||||
0,
|
||||
ctx.j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ctx.tx.isFlag(tfSponsorshipCreate))
|
||||
{
|
||||
if (!*newSponsorSle)
|
||||
if (!newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check account is not sponsored yet
|
||||
@@ -308,7 +201,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipReassign))
|
||||
{
|
||||
if (!*newSponsorSle)
|
||||
if (!newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check account is already sponsored
|
||||
@@ -317,7 +210,7 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
else if (ctx.tx.isFlag(tfSponsorshipEnd))
|
||||
{
|
||||
if (*newSponsorSle)
|
||||
if (newSponsorSle)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// check account is sponsored
|
||||
@@ -329,23 +222,6 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
if (account != sponsor && account != sponseeID)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// check account have sufficient balance
|
||||
// In the case of removing an account sponsor, accSle should have no sfSponsor set
|
||||
// (AccountReserve = 0). However, by setting accountCountDelta = 1 here, we are able to
|
||||
// calculate the actual required Account Reserve.
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
ctx.view,
|
||||
ctx.tx,
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
*newSponsorSle,
|
||||
0,
|
||||
1,
|
||||
ctx.j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
@@ -408,6 +284,12 @@ SponsorshipTransfer::doApply()
|
||||
return tesSUCCESS;
|
||||
};
|
||||
|
||||
auto const balanceBeforeFee = [&](SLE::const_ref sle) -> STAmount {
|
||||
if (sle->getAccountID(sfAccount) == accountID_)
|
||||
return STAmount{preFeeBalance_};
|
||||
return sle->getFieldAmount(sfBalance);
|
||||
};
|
||||
|
||||
if (isObjectSponsor)
|
||||
{
|
||||
auto const hasSignature = tx.isFieldPresent(sfSponsorSignature);
|
||||
@@ -433,6 +315,23 @@ SponsorshipTransfer::doApply()
|
||||
{
|
||||
auto const newSponsorID = tx.getAccountID(sfSponsor);
|
||||
XRPL_ASSERT(!!newSponsorID, "New sponsor is required when creating sponsorship");
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// check new sponsor have sufficient balance
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
newSponsorSle,
|
||||
ownerCountDelta,
|
||||
0,
|
||||
ctx_.journal);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// update owner's sponsored count
|
||||
if (auto const ter =
|
||||
@@ -442,9 +341,6 @@ SponsorshipTransfer::doApply()
|
||||
view().update(ownerSle);
|
||||
|
||||
// increment new sponsor's sponsoring count
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
@@ -468,14 +364,31 @@ SponsorshipTransfer::doApply()
|
||||
{
|
||||
auto const newSponsorID = tx.getAccountID(sfSponsor);
|
||||
XRPL_ASSERT(!!newSponsorID, "New sponsor is required when reassigning sponsorship");
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const oldSponsorID = objSle->getAccountID(sponsorField);
|
||||
XRPL_ASSERT(!!oldSponsorID, "Old sponsor is required when reassigning sponsorship");
|
||||
|
||||
// decrement old sponsor's sponsoring count
|
||||
auto const oldSponsorSle = view().peek(keylet::account(oldSponsorID));
|
||||
if (!oldSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// check new sponsor have sufficient balance
|
||||
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
newSponsorSle,
|
||||
ownerCountDelta,
|
||||
0,
|
||||
ctx_.journal);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// decrement old sponsor's sponsoring count
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(oldSponsorSle, sfSponsoringOwnerCount, -ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
@@ -483,9 +396,6 @@ SponsorshipTransfer::doApply()
|
||||
view().update(oldSponsorSle);
|
||||
|
||||
// increment new sponsor's sponsoring count
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(newSponsorSle, sfSponsoringOwnerCount, ownerCountDelta);
|
||||
!isTesSuccess(ter))
|
||||
@@ -514,6 +424,20 @@ SponsorshipTransfer::doApply()
|
||||
if (!oldSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// The owner takes the reserve burden back when the object is
|
||||
// no longer sponsored.
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
ownerSle,
|
||||
balanceBeforeFee(ownerSle),
|
||||
SLE::pointer(),
|
||||
ownerCountDelta,
|
||||
0,
|
||||
ctx_.journal);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// decrement sponsored count
|
||||
if (auto const ter =
|
||||
setSponsorFieldU32(sponseeSle, sfSponsoredOwnerCount, -ownerCountDelta);
|
||||
@@ -543,6 +467,19 @@ SponsorshipTransfer::doApply()
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
newSponsorSle,
|
||||
0,
|
||||
1,
|
||||
ctx_.journal);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
@@ -560,6 +497,19 @@ SponsorshipTransfer::doApply()
|
||||
auto const newSponsorSle = view().peek(keylet::account(newSponsorID));
|
||||
if (!newSponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
sponseeSle,
|
||||
sponseeSle->getFieldAmount(sfBalance),
|
||||
newSponsorSle,
|
||||
0,
|
||||
1,
|
||||
ctx_.journal);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = setSponsorFieldU32(newSponsorSle, sfSponsoringAccountCount, 1);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
@@ -583,6 +533,21 @@ SponsorshipTransfer::doApply()
|
||||
{
|
||||
// dissolve account sponsor
|
||||
auto const oldSponsorID = sponseeSle->getAccountID(sfSponsor);
|
||||
|
||||
// The sponsee must be able to hold its own account reserve after
|
||||
// the sponsorship is removed.
|
||||
if (auto const ter = checkInsufficientReserve(
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
sponseeSle,
|
||||
balanceBeforeFee(sponseeSle),
|
||||
SLE::pointer(),
|
||||
0,
|
||||
1,
|
||||
ctx_.journal);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
sponseeSle->makeFieldAbsent(sfSponsor);
|
||||
view().update(sponseeSle);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
@@ -440,16 +441,37 @@ EscrowCreate::doApply()
|
||||
auto const sponsorSle = getTxReserveSponsor(applyViewContext);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
// First check: whoever is on the hook for the new owner increment
|
||||
// can cover it. When sponsored this hits the sponsor branch and
|
||||
// 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_.view(), ctx_.tx, sle, balance, *sponsorSle, 1, 0, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
// Check reserve and funds availability
|
||||
if (isXRP(amount))
|
||||
{
|
||||
// Second check (XRP escrow only): after locking the escrowed
|
||||
// amount, the source must still meet its own reserve floor.
|
||||
// Always passes `{}` so the source branch runs (the sponsor's
|
||||
// reserve was already validated above; here we're verifying the
|
||||
// source can fund the lock without dipping below its own
|
||||
// reserve). ownerCountAdj differs by case:
|
||||
// - sponsored: adj=0 — sponsor covers the new owner increment,
|
||||
// so the source only owes its base reserve.
|
||||
// - unsponsored: adj=1 — source owes base + the new increment.
|
||||
std::int32_t const ownerCountAdj = *sponsorSle ? 0 : 1;
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), {}, 1, 0, j_);
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
sle,
|
||||
balance - STAmount(amount).xrp(),
|
||||
{},
|
||||
ownerCountAdj,
|
||||
0,
|
||||
j_);
|
||||
!isTesSuccess(ret))
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -134,22 +135,38 @@ PaymentChannelCreate::doApply()
|
||||
return tecEXPIRED;
|
||||
}
|
||||
|
||||
auto const sponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext());
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (ctx_.view().rules().enabled(featureSponsor))
|
||||
{
|
||||
auto const sponsorSle = getTxReserveSponsor(ctx_.view(), ctx_.tx);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error();
|
||||
// First check: whoever is on the hook for the new owner increment
|
||||
// can cover it. When sponsored this hits the sponsor branch and
|
||||
// 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_.view(), ctx_.tx, sle, STAmount{preFeeBalance_}, *sponsorSle, 1, 0, j_);
|
||||
ctx_.view(), ctx_.tx, sle, preFeeBalance_, *sponsorSle, 1, 0, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
// Second check: after locking sfAmount in the channel, the source
|
||||
// must still meet its own reserve floor. Always passes `{}` so the
|
||||
// source branch runs (the sponsor's reserve was already validated
|
||||
// above; here we're verifying the source can fund the lock without
|
||||
// dipping below its own reserve). ownerCountAdj differs by case:
|
||||
// - sponsored: adj=0 — sponsor covers the new owner increment,
|
||||
// so the source only owes its base reserve.
|
||||
// - unsponsored: adj=1 — source owes base + the new increment.
|
||||
std::int32_t const ownerCountAdj = *sponsorSle ? 0 : 1;
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
ctx_.view(),
|
||||
ctx_.tx,
|
||||
sle,
|
||||
STAmount{preFeeBalance_ - ctx_.tx[sfAmount].xrp()},
|
||||
preFeeBalance_ - ctx_.tx[sfAmount].xrp(),
|
||||
{},
|
||||
1,
|
||||
ownerCountAdj,
|
||||
0,
|
||||
j_);
|
||||
!isTesSuccess(ret))
|
||||
@@ -203,10 +220,6 @@ PaymentChannelCreate::doApply()
|
||||
|
||||
// Deduct owner's balance, increment owner count
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
|
||||
auto applyViewContext = ctx_.getApplyViewContext();
|
||||
auto const sponsorSle = getTxReserveSponsor(applyViewContext);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
adjustOwnerCount(ctx_.view(), sle, *sponsorSle, 1, ctx_.journal);
|
||||
addSponsorToLedgerEntry(slep, *sponsorSle);
|
||||
ctx_.view().update(sle);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <test/jtx/escrow.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/ledgerStateFix.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/multisign.h>
|
||||
#include <test/jtx/noop.h>
|
||||
@@ -29,6 +30,7 @@
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
@@ -39,9 +41,11 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.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/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -50,12 +54,16 @@
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/tx/apply.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
@@ -1834,6 +1842,66 @@ public:
|
||||
BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == drops(990)); // 1000 - MaxFee(10)
|
||||
}
|
||||
|
||||
// LedgerStateFix charges an owner-reserve fee and can claim that fee
|
||||
// while returning tecFAILED_PROCESSING. That path must be safe when the
|
||||
// fee is pre-funded by a sponsorship object.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(1000), alice, sponsor);
|
||||
env.close();
|
||||
|
||||
auto const fixFee = drops(env.current()->fees().increment);
|
||||
env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
env(ledgerStateFix::nftPageLinks(alice, alice),
|
||||
Fee(fixFee),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Ter(tecFAILED_PROCESSING));
|
||||
|
||||
if (auto const sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle))
|
||||
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
|
||||
}
|
||||
|
||||
// If preclaim saw spendable sponsored FeeAmount but the apply view no
|
||||
// longer has it, the fee path should fail cleanly instead of throwing.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(1000), alice, sponsor);
|
||||
env.close();
|
||||
|
||||
auto const fixFee = drops(env.current()->fees().increment);
|
||||
env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
OpenView overlay(&*env.closed());
|
||||
auto jt = env.jt(
|
||||
ledgerStateFix::nftPageLinks(alice, alice),
|
||||
Fee(fixFee),
|
||||
sponsor::As(sponsor, spfSponsorFee));
|
||||
|
||||
auto const pf = preflight(env.app(), overlay.rules(), *jt.stx, TapNone, env.journal);
|
||||
BEAST_EXPECT(isTesSuccess(pf.ter));
|
||||
auto const pc = preclaim(pf, env.app(), overlay);
|
||||
BEAST_EXPECT(isTesSuccess(pc.ter));
|
||||
|
||||
auto const original = overlay.read(keylet::sponsorship(sponsor, alice));
|
||||
if (BEAST_EXPECT(original))
|
||||
{
|
||||
auto sle = std::make_shared<SLE>(*original);
|
||||
sle->makeFieldAbsent(sfFeeAmount);
|
||||
overlay.rawReplace(sle);
|
||||
}
|
||||
|
||||
auto const result = doApply(pc, env.app(), overlay);
|
||||
BEAST_EXPECT(result.ter == terINSUF_FEE_B);
|
||||
BEAST_EXPECT(!result.applied);
|
||||
}
|
||||
|
||||
// test lsfSponsorshipRequireSignForFee
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
@@ -2168,10 +2236,11 @@ public:
|
||||
std::optional<std::function<void()>> expected = std::nullopt)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
// auto const sponsorOwnerCountBefore = ownerCount(env, sponsor);
|
||||
auto const sponseeOwnerCountBefore = ownerCount(env, sponsee);
|
||||
auto const sponseeSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsee);
|
||||
auto const sponseeSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsee);
|
||||
auto const sponsorOwnerCountBefore = ownerCount(env, sponsor);
|
||||
auto const sponsorSponsoredOwnerCountBefore = sponsoredOwnerCount(env, sponsor);
|
||||
auto const sponsorSponsoringOwnerCountBefore = sponsoringOwnerCount(env, sponsor);
|
||||
|
||||
std::optional<Sig> sponsorSig =
|
||||
@@ -2211,7 +2280,7 @@ public:
|
||||
env.close();
|
||||
}
|
||||
|
||||
if (sponsorReserveCount - 1 > 0)
|
||||
if (sponsorReserveCount > 1)
|
||||
{
|
||||
env(sponsor::set(sponsor, 0, sponsorReserveCount - 1, XRP(1)),
|
||||
sponsor::SponseeAcc(sponsee));
|
||||
@@ -2224,8 +2293,53 @@ public:
|
||||
}
|
||||
env.close();
|
||||
}
|
||||
|
||||
// A failed sponsored create must not consume prefunded reserve or mutate owner counts.
|
||||
auto const sponseeOwnerCountBeforeAttempt = ownerCount(env, sponsee);
|
||||
auto const sponseeSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsee);
|
||||
auto const sponseeSponsoringOwnerCountBeforeAttempt =
|
||||
sponsoringOwnerCount(env, sponsee);
|
||||
auto const sponsorOwnerCountBeforeAttempt = ownerCount(env, sponsor);
|
||||
auto const sponsorSponsoredOwnerCountBeforeAttempt = sponsoredOwnerCount(env, sponsor);
|
||||
auto const sponsorSponsoringOwnerCountBeforeAttempt =
|
||||
sponsoringOwnerCount(env, sponsor);
|
||||
auto const sponsorshipSleBeforeAttempt = env.le(keylet::sponsorship(sponsor, sponsee));
|
||||
bool const reserveCountPresentBeforeAttempt = sponsorshipSleBeforeAttempt &&
|
||||
sponsorshipSleBeforeAttempt->isFieldPresent(sfRemainingOwnerCount);
|
||||
std::uint32_t const reserveCountBeforeAttempt = reserveCountPresentBeforeAttempt
|
||||
? sponsorshipSleBeforeAttempt->getFieldU32(sfRemainingOwnerCount)
|
||||
: 0;
|
||||
|
||||
callback(env, submit(insufficientReserveResult));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, sponsee) == sponseeOwnerCountBeforeAttempt);
|
||||
BEAST_EXPECT(
|
||||
sponsoredOwnerCount(env, sponsee) == sponseeSponsoredOwnerCountBeforeAttempt);
|
||||
BEAST_EXPECT(
|
||||
sponsoringOwnerCount(env, sponsee) == sponseeSponsoringOwnerCountBeforeAttempt);
|
||||
BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBeforeAttempt);
|
||||
BEAST_EXPECT(
|
||||
sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBeforeAttempt);
|
||||
BEAST_EXPECT(
|
||||
sponsoringOwnerCount(env, sponsor) == sponsorSponsoringOwnerCountBeforeAttempt);
|
||||
|
||||
auto const sponsorshipSleAfterAttempt = env.le(keylet::sponsorship(sponsor, sponsee));
|
||||
BEAST_EXPECT(
|
||||
static_cast<bool>(sponsorshipSleAfterAttempt) ==
|
||||
static_cast<bool>(sponsorshipSleBeforeAttempt));
|
||||
if (sponsorshipSleAfterAttempt)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
sponsorshipSleAfterAttempt->isFieldPresent(sfRemainingOwnerCount) ==
|
||||
reserveCountPresentBeforeAttempt);
|
||||
if (reserveCountPresentBeforeAttempt)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
sponsorshipSleAfterAttempt->getFieldU32(sfRemainingOwnerCount) ==
|
||||
reserveCountBeforeAttempt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success
|
||||
@@ -2248,6 +2362,13 @@ public:
|
||||
|
||||
if (!cosigning)
|
||||
{
|
||||
// Prefunded success consumes the reserved owner slot before cleanup.
|
||||
auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, sponsee));
|
||||
BEAST_EXPECT(sponsorshipSle);
|
||||
BEAST_EXPECT(
|
||||
!sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) ||
|
||||
sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0);
|
||||
|
||||
// cleanup sponsorship
|
||||
env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee));
|
||||
env.close();
|
||||
@@ -2266,6 +2387,8 @@ public:
|
||||
sponsorReserveCount);
|
||||
BEAST_EXPECT(
|
||||
sponsoringOwnerCount(env, sponsee) - sponseeSponsoringOwnerCountBefore == 0);
|
||||
BEAST_EXPECT(ownerCount(env, sponsor) == sponsorOwnerCountBefore);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor) == sponsorSponsoredOwnerCountBefore);
|
||||
BEAST_EXPECT(
|
||||
sponsoringOwnerCount(env, sponsor) - sponsorSponsoringOwnerCountBefore ==
|
||||
sponsorReserveCount);
|
||||
@@ -2508,6 +2631,7 @@ public:
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
Account const sponsor2("sponsor2");
|
||||
auto const credType = std::string("credType");
|
||||
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
@@ -2535,6 +2659,114 @@ public:
|
||||
env.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
// No sponsor signature here: this exercises the prefunded reassign path.
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve));
|
||||
env.close();
|
||||
|
||||
auto const sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice));
|
||||
BEAST_EXPECT(sponsor2Sle);
|
||||
if (sponsor2Sle)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
!sponsor2Sle->isFieldPresent(sfRemainingOwnerCount) ||
|
||||
sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
||||
|
||||
// DepositPreauthDelete
|
||||
env(deposit::unauth(alice, sponsor));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, sponsor);
|
||||
env.close();
|
||||
auto const authCreds = std::vector<deposit::AuthorizeCredentials>{
|
||||
{.issuer = sponsor, .credType = credType}};
|
||||
auto const preauthKeylet = keylet::depositPreauth(
|
||||
alice.id(),
|
||||
std::set<std::pair<AccountID, Slice>>{
|
||||
{sponsor.id(), Slice(credType.data(), credType.size())}});
|
||||
|
||||
// Cover DepositPreauth's sfAuthorizeCredentials sponsor-reserve branch.
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
alice,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env&, auto const& submit) {
|
||||
submit(deposit::authCredentials(alice, authCreds));
|
||||
});
|
||||
|
||||
// Cover sfUnauthorizeCredentials cleanup for a sponsored preauth object.
|
||||
BEAST_EXPECT(env.le(preauthKeylet));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
|
||||
env(deposit::unauthCredentials(alice, authCreds));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(preauthKeylet));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDID(bool cosigning)
|
||||
{
|
||||
testcase("DID");
|
||||
using namespace test::jtx;
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
Account const sponsor2("sponsor2");
|
||||
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, sponsor, sponsor2);
|
||||
env.close();
|
||||
|
||||
// DIDSet
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
alice,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) { submit(did::set(alice), did::Uri("uri")); });
|
||||
|
||||
// transfer sponsor
|
||||
auto const keylet = keylet::did(alice);
|
||||
if (cosigning)
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor2));
|
||||
env.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
@@ -2552,11 +2784,15 @@ public:
|
||||
auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor2, alice));
|
||||
BEAST_EXPECT(sponsorshipSle);
|
||||
if (sponsorshipSle)
|
||||
BEAST_EXPECT(sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0);
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
!sponsorshipSle->isFieldPresent(sfRemainingOwnerCount) ||
|
||||
sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
// DepositPreauthDelete
|
||||
env(deposit::unauth(alice, sponsor));
|
||||
// DIDDelete
|
||||
env(did::del(alice));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
@@ -2749,6 +2985,50 @@ public:
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
}
|
||||
|
||||
// A sponsored EscrowCreate must still verify that the source
|
||||
// can fund the escrow amount and stay above its own base
|
||||
// reserve. The sponsor covers the new object's owner
|
||||
// increment, but cannot cover the source's base reserve.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// alice's balance is just above the base reserve. After
|
||||
// locking escrowAmount she would dip below it.
|
||||
adjustAccountXRPBalance(env, alice, accountReserve(env, 1) + XRP(1));
|
||||
|
||||
auto const escrowAmount = XRP(2);
|
||||
auto const seq = env.seq(alice);
|
||||
|
||||
if (cosigning)
|
||||
{
|
||||
env(escrow::create(alice, bob, escrowAmount),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tecUNFUNDED));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::set(sponsor, 0, 1, XRP(1)), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
env(escrow::create(alice, bob, escrowAmount),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Ter(tecUNFUNDED));
|
||||
}
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice, seq)));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -3015,6 +3295,48 @@ public:
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
||||
}
|
||||
|
||||
// A sponsored PaymentChannelCreate must still verify that the
|
||||
// source can fund the channel amount and stay above its own
|
||||
// base reserve. The sponsor covers the new object's owner
|
||||
// increment, but cannot cover the source's base reserve.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// alice's balance is just above the base reserve. After
|
||||
// locking channelAmount she would dip below it.
|
||||
adjustAccountXRPBalance(env, alice, accountReserve(env, 1) + XRP(1));
|
||||
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 10s;
|
||||
auto const channelAmount = XRP(2);
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
|
||||
if (cosigning)
|
||||
{
|
||||
env(paychan::create(alice, bob, channelAmount, settleDelay, pk),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tecUNFUNDED));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::set(sponsor, 0, 1, XRP(1)), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
env(paychan::create(alice, bob, channelAmount, settleDelay, pk),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Ter(tecUNFUNDED));
|
||||
}
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -3427,6 +3749,12 @@ public:
|
||||
auto const requiredFee = drops(env.current()->fees().increment);
|
||||
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecNO_SPONSOR_PERMISSION));
|
||||
|
||||
// The failed delete must leave the account sponsored by the original sponsor.
|
||||
auto const aliceSle = env.le(keylet::account(alice));
|
||||
BEAST_EXPECT(aliceSle);
|
||||
if (aliceSle)
|
||||
BEAST_EXPECT(aliceSle->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
auto const sponsorSle = env.le(keylet::account(sponsor));
|
||||
BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1);
|
||||
|
||||
@@ -3470,13 +3798,19 @@ public:
|
||||
// Verify sfSponsoringOwnerCount is set on sponsor
|
||||
auto const sponsorSle = env.le(keylet::account(sponsor));
|
||||
BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringOwnerCount));
|
||||
BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringOwnerCount) >= 1);
|
||||
auto const sponsoringOwnerCount = sponsorSle->getFieldU32(sfSponsoringOwnerCount);
|
||||
BEAST_EXPECT(sponsoringOwnerCount >= 1);
|
||||
|
||||
incLgrSeqForAccDel(env, sponsor);
|
||||
|
||||
// AccountDelete should fail
|
||||
auto const requiredFee = drops(env.current()->fees().increment);
|
||||
env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
|
||||
// The failed delete must not decrement the outstanding sponsored-object count.
|
||||
auto const sponsorSleAfter = env.le(keylet::account(sponsor));
|
||||
BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringOwnerCount));
|
||||
BEAST_EXPECT(
|
||||
sponsorSleAfter->getFieldU32(sfSponsoringOwnerCount) == sponsoringOwnerCount);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -3493,13 +3827,19 @@ public:
|
||||
// Verify sfSponsoringAccountCount is set on sponsor
|
||||
auto const sponsorSle = env.le(keylet::account(sponsor));
|
||||
BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringAccountCount));
|
||||
BEAST_EXPECT(sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1);
|
||||
auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount);
|
||||
BEAST_EXPECT(sponsoringAccountCount == 1);
|
||||
|
||||
incLgrSeqForAccDel(env, sponsor);
|
||||
|
||||
// AccountDelete should fail
|
||||
auto const requiredFee = drops(env.current()->fees().increment);
|
||||
env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
|
||||
// The failed delete must not decrement the outstanding sponsored-account count.
|
||||
auto const sponsorSleAfter = env.le(keylet::account(sponsor));
|
||||
BEAST_EXPECT(sponsorSleAfter->isFieldPresent(sfSponsoringAccountCount));
|
||||
BEAST_EXPECT(
|
||||
sponsorSleAfter->getFieldU32(sfSponsoringAccountCount) == sponsoringAccountCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include <test/jtx/require.h>
|
||||
#include <test/jtx/sendmax.h>
|
||||
#include <test/jtx/seq.h>
|
||||
#include <test/jtx/sig.h>
|
||||
#include <test/jtx/sponsor.h>
|
||||
#include <test/jtx/tags.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/ticket.h>
|
||||
@@ -2335,6 +2337,43 @@ public:
|
||||
BEAST_EXPECT(env.balance(alice) == drops(5));
|
||||
}
|
||||
|
||||
void
|
||||
testSponsorTxCannotQueue()
|
||||
{
|
||||
using namespace jtx;
|
||||
testcase("disallow sponsored transaction from being queued");
|
||||
|
||||
Env env(*this, makeConfig({{Keys::kMinimumTxnInLedgerStandalone, "3"}}));
|
||||
|
||||
auto sponsor = Account("sponsor");
|
||||
auto sponsee = Account("sponsee");
|
||||
auto filler = Account("filler");
|
||||
|
||||
env.fund(XRP(50000), noripple(sponsor, sponsee));
|
||||
env.close();
|
||||
env.fund(XRP(50000), noripple(filler));
|
||||
env.close();
|
||||
|
||||
fillQueue(env, filler);
|
||||
checkMetrics(*this, env, 0, 6, 4, 3);
|
||||
|
||||
// Sponsored transactions are not allowed to be queued.
|
||||
env(noop(sponsee),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(telCAN_NOT_QUEUE));
|
||||
checkMetrics(*this, env, 0, 6, 4, 3);
|
||||
|
||||
// Sponsored transactions may still apply directly if they pay the
|
||||
// open ledger fee. They just cannot be held in the queue.
|
||||
env(noop(sponsee),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Fee(openLedgerCost(env)),
|
||||
Ter(tesSUCCESS));
|
||||
checkMetrics(*this, env, 0, 6, 5, 3);
|
||||
}
|
||||
|
||||
void
|
||||
testConsequences()
|
||||
{
|
||||
@@ -4662,6 +4701,7 @@ public:
|
||||
testBlockersSeq();
|
||||
testBlockersTicket();
|
||||
testInFlightBalance();
|
||||
testSponsorTxCannotQueue();
|
||||
testConsequences();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <xrpl/ledger/ApplyViewImpl.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -398,6 +399,10 @@ TxQ::canBeHeld(
|
||||
((flags & TapFailHard) != 0u))
|
||||
return telCAN_NOT_QUEUE;
|
||||
|
||||
// Disallow sponsored transactions from being queued.
|
||||
if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx))
|
||||
return telCAN_NOT_QUEUE;
|
||||
|
||||
{
|
||||
// To be queued and relayed, the transaction needs to
|
||||
// promise to stick around for long enough that it has
|
||||
|
||||
Reference in New Issue
Block a user