mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-02 03:52:13 +00:00
Merge branch 'xrplf/sponsor' into mvadari/sponsor/apply-view-context
This commit is contained in:
@@ -319,6 +319,7 @@ words:
|
||||
- unserviced
|
||||
- unshareable
|
||||
- unshares
|
||||
- unsponsored
|
||||
- unsquelch
|
||||
- unsquelched
|
||||
- unsquelching
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -143,8 +144,7 @@ addEmptyHolding(
|
||||
if (accountID == mptIssue.getIssuer())
|
||||
return tesSUCCESS;
|
||||
|
||||
return authorizeMPToken(
|
||||
{.view = ctx.view, .tx = ctx.tx}, priorBalance, mptID, accountID, journal);
|
||||
return authorizeMPToken(ctx, priorBalance, mptID, accountID, journal, 0, std::nullopt);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
@@ -193,9 +193,14 @@ authorizeMPToken(
|
||||
// - add the new mptokenKey to the owner directory
|
||||
// - create the MPToken object for the holder
|
||||
|
||||
auto const sponsorSle = getTxReserveSponsor({.view = ctx.view, .tx = ctx.tx});
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
SLE::pointer sponsorSle;
|
||||
if (account == tx[sfAccount])
|
||||
{
|
||||
auto sle = getTxReserveSponsor(ctx);
|
||||
if (!sle)
|
||||
return sle.error(); // LCOV_EXCL_LINE
|
||||
sponsorSle = std::move(*sle);
|
||||
}
|
||||
|
||||
// The reserve that is required to create the MPToken. Note
|
||||
// that although the reserve increases with every item
|
||||
@@ -205,10 +210,10 @@ authorizeMPToken(
|
||||
// The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on
|
||||
// the tx — the sponsor must always cover the reserve (via balance or prefunded
|
||||
// budget), so this check always runs for sponsored transactions.
|
||||
if (*sponsorSle || ownerCount(sleAcct, journal) >= 2)
|
||||
if (sponsorSle || ownerCount(sleAcct, journal) >= 2)
|
||||
{
|
||||
if (auto const ret = checkInsufficientReserve(
|
||||
view, ctx.tx, sleAcct, priorBalance, *sponsorSle, 1, 0, journal);
|
||||
view, ctx.tx, sleAcct, priorBalance, sponsorSle, 1, 0, journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -235,8 +240,8 @@ authorizeMPToken(
|
||||
view.insert(mptoken);
|
||||
|
||||
// Update owner count.
|
||||
adjustOwnerCount(view, sleAcct, *sponsorSle, 1, journal);
|
||||
addSponsorToLedgerEntry(mptoken, *sponsorSle);
|
||||
adjustOwnerCount(view, sleAcct, sponsorSle, 1, journal);
|
||||
addSponsorToLedgerEntry(mptoken, sponsorSle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -664,7 +664,9 @@ addEmptyHolding(
|
||||
return tecDUPLICATE;
|
||||
|
||||
SLE::pointer sponsorSle;
|
||||
if (!isPseudoAccount(sleDst))
|
||||
|
||||
// A reserve sponsor only covers tx.Account's own objects.
|
||||
if (!isPseudoAccount(sleDst) && accountID == tx[sfAccount])
|
||||
{
|
||||
auto sle = getTxReserveSponsor({.view = ctx.view, .tx = ctx.tx});
|
||||
if (!sle)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
@@ -202,6 +203,46 @@ preflight1Sponsor(PreflightContext const& ctx, AccountID const& id)
|
||||
JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
// Reserve sponsorship is only permitted for an explicit allow-list of
|
||||
// transaction types, for v1. All other tx types reject spfSponsorReserve here.
|
||||
if ((sponsorFlags & spfSponsorReserve) != 0u)
|
||||
{
|
||||
static std::unordered_set<TxType> const kReserveSponsorAllowed = {
|
||||
// Explicitly allow-listed for v1.
|
||||
ttDELEGATE_SET,
|
||||
ttDEPOSIT_PREAUTH,
|
||||
ttPAYMENT,
|
||||
ttSIGNER_LIST_SET,
|
||||
ttCHECK_CANCEL,
|
||||
ttCHECK_CASH,
|
||||
ttCHECK_CREATE,
|
||||
ttESCROW_CANCEL,
|
||||
ttESCROW_CREATE,
|
||||
ttESCROW_FINISH,
|
||||
ttPAYCHAN_CLAIM,
|
||||
ttPAYCHAN_CREATE,
|
||||
ttPAYCHAN_FUND,
|
||||
ttCLAWBACK,
|
||||
ttMPTOKEN_AUTHORIZE,
|
||||
ttMPTOKEN_ISSUANCE_CREATE,
|
||||
ttMPTOKEN_ISSUANCE_DESTROY,
|
||||
ttMPTOKEN_ISSUANCE_SET,
|
||||
ttTRUST_SET,
|
||||
ttCREDENTIAL_ACCEPT,
|
||||
ttCREDENTIAL_CREATE,
|
||||
ttCREDENTIAL_DELETE,
|
||||
ttACCOUNT_SET,
|
||||
ttREGULAR_KEY_SET,
|
||||
ttSPONSORSHIP_TRANSFER,
|
||||
};
|
||||
if (!kReserveSponsorAllowed.contains(ctx.tx.getTxnType()))
|
||||
{
|
||||
JLOG(ctx.j.debug())
|
||||
<< "preflight1: spfSponsorReserve not allowed for this transaction type";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/transactors/oracle/OracleSet.h>
|
||||
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
@@ -123,29 +122,15 @@ getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account
|
||||
{
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltNFTOKEN_OFFER:
|
||||
case ltORACLE:
|
||||
case ltPERMISSIONED_DOMAIN:
|
||||
case ltVAULT:
|
||||
case ltLOAN_BROKER:
|
||||
return sle->getAccountID(sfOwner);
|
||||
case ltCHECK:
|
||||
case ltDID:
|
||||
case ltTICKET:
|
||||
case ltOFFER:
|
||||
case ltXCHAIN_OWNED_CLAIM_ID:
|
||||
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
|
||||
case ltESCROW:
|
||||
case ltPAYCHAN:
|
||||
case ltMPTOKEN:
|
||||
case ltDELEGATE:
|
||||
case ltBRIDGE:
|
||||
case ltDEPOSIT_PREAUTH:
|
||||
return sle->getAccountID(sfAccount);
|
||||
case ltMPTOKEN_ISSUANCE:
|
||||
return sle->getAccountID(sfIssuer);
|
||||
case ltLOAN:
|
||||
return sle->getAccountID(sfBorrower);
|
||||
case ltSIGNER_LIST: {
|
||||
auto const signerList = view.read(keylet::signers(account));
|
||||
if (!signerList)
|
||||
@@ -159,12 +144,6 @@ getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account
|
||||
return sle->getAccountID(sfSubject);
|
||||
return sle->getAccountID(sfIssuer);
|
||||
}
|
||||
case ltNFTOKEN_PAGE: {
|
||||
// the upper 20 bytes of the index of ltNFTokenPage are the Owner's
|
||||
// AccountID
|
||||
uint256 const& key = sle->key();
|
||||
return AccountID::fromVoid(key.data());
|
||||
}
|
||||
case ltRIPPLE_STATE: {
|
||||
if (sle->isFlag(lsfHighReserve))
|
||||
{
|
||||
@@ -180,39 +159,12 @@ getLedgerEntryOwner(ReadView const& view, T const& sle, AccountID const& account
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
case ltACCOUNT_ROOT: {
|
||||
// AccountRoot is not supported for object sponsorship
|
||||
return std::nullopt;
|
||||
}
|
||||
case ltNEGATIVE_UNL:
|
||||
case ltDIR_NODE:
|
||||
case ltAMENDMENTS:
|
||||
case ltLEDGER_HASHES:
|
||||
case ltFEE_SETTINGS:
|
||||
case ltAMM:
|
||||
return std::nullopt;
|
||||
default:
|
||||
UNREACHABLE("Object is not supported by sponsorship.");
|
||||
return std::nullopt;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline std::uint32_t
|
||||
getLedgerEntryOwnerCount(T const& sle)
|
||||
{
|
||||
switch (sle->getType())
|
||||
{
|
||||
case ltORACLE: {
|
||||
return OracleSet::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)
|
||||
@@ -265,7 +217,30 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
|
||||
if (!sle)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
auto const ownerCountDelta = getLedgerEntryOwnerCount(sle);
|
||||
// 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)
|
||||
@@ -450,7 +425,7 @@ SponsorshipTransfer::doApply()
|
||||
if (!ownerSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
std::int64_t const ownerCountDelta = getLedgerEntryOwnerCount(objSle);
|
||||
std::int64_t const ownerCountDelta = 1;
|
||||
|
||||
auto const& sponsorField = getLedgerEntrySponsorField(objSle, *ownerID);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/LendingHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
@@ -56,6 +57,12 @@ LoanSet::preflight(PreflightContext const& ctx)
|
||||
|
||||
auto const& tx = ctx.tx;
|
||||
|
||||
if (tx.isFieldPresent(sfSponsorFlags) && isReserveSponsored(tx))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "LoanSet: reserve sponsorship is not allowed.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
// Special case for Batch inner transactions
|
||||
if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) &&
|
||||
!tx.isFieldPresent(sfCounterparty))
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <test/jtx/permissioned_domains.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/trust.h>
|
||||
@@ -4436,11 +4437,12 @@ protected:
|
||||
Account const lender{"lender"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const borrower{"borrower"};
|
||||
Account const sponsor{"sponsor"};
|
||||
auto const iou = issuer["IOU"];
|
||||
|
||||
auto testWrapper = [&](auto&& test) {
|
||||
Env env(*this);
|
||||
env.fund(XRP(1'000), lender, issuer, borrower);
|
||||
env.fund(XRP(1'000), lender, issuer, borrower, sponsor);
|
||||
env(trust(lender, iou(10'000'000)));
|
||||
env(pay(issuer, lender, iou(5'000'000)));
|
||||
BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
|
||||
@@ -4455,6 +4457,15 @@ protected:
|
||||
BrokerInfo const& brokerInfo,
|
||||
jtx::Fee const& loanSetFee,
|
||||
Number const& debtMaximumRequest) {
|
||||
for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee})
|
||||
{
|
||||
env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
|
||||
sponsor::As(sponsor, sponsorFlags),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
loanSetFee,
|
||||
Ter(temINVALID_FLAG));
|
||||
}
|
||||
|
||||
// first temBAD_SIGNER: TODO
|
||||
// invalid grace period
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/multisign.h>
|
||||
#include <test/jtx/noop.h>
|
||||
#include <test/jtx/offer.h>
|
||||
#include <test/jtx/paths.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/sendmax.h>
|
||||
@@ -22,10 +23,12 @@
|
||||
#include <test/jtx/sponsor.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/ticket.h>
|
||||
#include <test/jtx/token.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
@@ -404,8 +407,6 @@ public:
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const sponsor("sponsor");
|
||||
Account const invalid("invalid");
|
||||
|
||||
Account const signer1("signer1");
|
||||
Account const signer2("signer2");
|
||||
|
||||
@@ -679,10 +680,11 @@ public:
|
||||
using namespace test::jtx;
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const charlie("charlie");
|
||||
Account const sponsor("sponsor");
|
||||
|
||||
{
|
||||
// both pre-funded and co-signed,pre-funded value is used
|
||||
// Both pre-funded and co-signed; the pre-funded value is used.
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
@@ -710,17 +712,17 @@ public:
|
||||
|
||||
sle = env.le(keylet::sponsorship(sponsor, alice));
|
||||
BEAST_EXPECT(sle);
|
||||
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not paybacked
|
||||
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); // not restored
|
||||
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99));
|
||||
}
|
||||
|
||||
{
|
||||
// if pre-funded value is not enough, error
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.fund(XRP(10000), alice, bob, charlie, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::set(sponsor, 0, 10, XRP(10), XRP(100)),
|
||||
env(sponsor::set(sponsor, 0, 1, XRP(10), XRP(100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
@@ -799,8 +801,7 @@ public:
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const sponsor1("sponsor1");
|
||||
Account const sponsor2("sponsor2");
|
||||
env.fund(XRP(10000), alice, bob, sponsor1, sponsor2);
|
||||
env.fund(XRP(10000), alice, bob, sponsor1);
|
||||
env.close();
|
||||
|
||||
env(sponsor::transfer(
|
||||
@@ -850,19 +851,21 @@ public:
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const charlie("charlie");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
{
|
||||
// sponsor object
|
||||
env(did::set(alice),
|
||||
did::Uri("uri"),
|
||||
env.fund(XRP(1000), charlie);
|
||||
env.close();
|
||||
env(deposit::auth(alice, charlie),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor));
|
||||
env.close();
|
||||
|
||||
auto const keylet = keylet::did(alice);
|
||||
auto const keylet = keylet::depositPreauth(alice, charlie);
|
||||
env(sponsor::transfer(bob, tfSponsorshipEnd, keylet.key),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tecNO_PERMISSION));
|
||||
@@ -1454,6 +1457,73 @@ public:
|
||||
Ter(tecNO_PERMISSION));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// existing owner objects that are outside the v1 SponsorshipTransfer
|
||||
// object allow-list
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, sponsor);
|
||||
env.close();
|
||||
|
||||
auto const checkBlocked = [&](Account const& account, uint256 const& objectID) {
|
||||
env(sponsor::transfer(account, tfSponsorshipCreate, objectID),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
};
|
||||
|
||||
auto const ticketSeq = env.seq(alice);
|
||||
env(ticket::create(alice, 1));
|
||||
env.close();
|
||||
auto const ticketID = keylet::TicketT()(alice, ticketSeq + 1).key;
|
||||
BEAST_EXPECT(env.le(keylet::unchecked(ticketID)));
|
||||
checkBlocked(alice, ticketID);
|
||||
|
||||
env(did::setValid(alice));
|
||||
env.close();
|
||||
auto const didKeylet = keylet::did(alice.id());
|
||||
BEAST_EXPECT(env.le(didKeylet));
|
||||
checkBlocked(alice, didKeylet.key);
|
||||
|
||||
env(token::mint(alice, 0u));
|
||||
env.close();
|
||||
auto const nftPageKeylet = keylet::nftpageMax(alice);
|
||||
BEAST_EXPECT(env.le(nftPageKeylet));
|
||||
checkBlocked(alice, nftPageKeylet.key);
|
||||
|
||||
Account const borrower("borrower");
|
||||
env.fund(XRP(1000000), borrower);
|
||||
env.close();
|
||||
|
||||
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
|
||||
Vault const vault{env};
|
||||
auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset});
|
||||
env(vaultTx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit(
|
||||
{.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(1000)}));
|
||||
env.close();
|
||||
|
||||
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
|
||||
env(loanBroker::set(alice, vaultKeylet.key),
|
||||
loanBroker::kDebtMaximum(xrpAsset(1000).value()),
|
||||
loanBroker::kManagementFeeRate(TenthBips16{0}),
|
||||
loanBroker::kCoverRateMinimum(TenthBips32{0}),
|
||||
loanBroker::kCoverRateLiquidation(TenthBips32{0}));
|
||||
env.close();
|
||||
|
||||
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
|
||||
env(loan::set(borrower, brokerKeylet.key, xrpAsset(100).value()),
|
||||
Sig(sfCounterpartySignature, alice),
|
||||
Fee(env.current()->fees().base * 2));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(loanKeylet));
|
||||
checkBlocked(borrower, loanKeylet.key);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1473,10 +1543,8 @@ public:
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Fee should be checked before permission check,
|
||||
// otherwise tecNO_SPONSOR_PERMISSION returned when permission
|
||||
// check fails could cause context reset to pay Fee because it
|
||||
// is tec error
|
||||
// Fee should be checked before sponsor permission, otherwise a tec
|
||||
// result from a later check could cause context reset to pay Fee.
|
||||
auto aliceBalance = env.balance(alice);
|
||||
auto bobBalance = env.balance(bob);
|
||||
auto sponsorBalance = env.balance(sponsor);
|
||||
@@ -1553,8 +1621,6 @@ public:
|
||||
{
|
||||
// below reserve
|
||||
adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve);
|
||||
env.close();
|
||||
auto const feeAmt = XRP(4);
|
||||
|
||||
env(noop(alice),
|
||||
Fee(env.current()->fees().base),
|
||||
@@ -1588,10 +1654,8 @@ public:
|
||||
};
|
||||
|
||||
{
|
||||
// Fee should be checked before permission check,
|
||||
// otherwise tecNO_SPONSOR_PERMISSION returned when permission
|
||||
// check fails could cause context reset to pay Fee because it
|
||||
// is tec error
|
||||
// Fee should be checked before sponsor permission, otherwise a tec
|
||||
// result from a later check could cause context reset to pay Fee.
|
||||
auto aliceBalance = env.balance(alice);
|
||||
auto bobBalance = env.balance(bob);
|
||||
auto sponsorBalance = env.balance(sponsor);
|
||||
@@ -2050,7 +2114,6 @@ public:
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// test Sufficient sponsor balance
|
||||
if (cosigning)
|
||||
{
|
||||
adjustAccountXRPBalance(env, sponsor, reserve(env, 1) - drops(1));
|
||||
@@ -2227,7 +2290,7 @@ public:
|
||||
env.fund(XRP(10000), alice, bob, sponsor, sponsor2);
|
||||
env.close();
|
||||
|
||||
// CheckCreate -> Check = 0Cancel
|
||||
// CheckCreate -> Check -> CheckCancel
|
||||
|
||||
uint32_t seq = 0;
|
||||
testEachSponsorship(
|
||||
@@ -2293,7 +2356,7 @@ public:
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// CheckCreate -> = 0 CheckCash
|
||||
// CheckCreate -> CheckCash
|
||||
uint32_t seq2 = 0;
|
||||
testEachSponsorship(
|
||||
env,
|
||||
@@ -2334,7 +2397,7 @@ public:
|
||||
env(pay(gw, alice, usd(100)));
|
||||
env.close();
|
||||
|
||||
// CheckCreat = 0e -> CheckCash
|
||||
// CheckCreate -> CheckCash
|
||||
uint32_t seq2 = 0;
|
||||
testEachSponsorship(
|
||||
env,
|
||||
@@ -2476,8 +2539,7 @@ public:
|
||||
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor2));
|
||||
sponsor::As(sponsor2, spfSponsorReserve));
|
||||
env.close();
|
||||
}
|
||||
|
||||
@@ -2485,6 +2547,13 @@ public:
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
||||
if (!cosigning)
|
||||
{
|
||||
auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor2, alice));
|
||||
BEAST_EXPECT(sponsorshipSle);
|
||||
if (sponsorshipSle)
|
||||
BEAST_EXPECT(sponsorshipSle->getFieldU32(sfRemainingOwnerCount) == 0);
|
||||
}
|
||||
|
||||
// DepositPreauthDelete
|
||||
env(deposit::unauth(alice, sponsor));
|
||||
@@ -2828,14 +2897,11 @@ public:
|
||||
env(jv);
|
||||
env.close();
|
||||
|
||||
// for free mptoken checks
|
||||
// adjustAccountXRPBalance(env, sponsor, reserve(env, 2));
|
||||
// Create tickets so the sponsor is past free-tier reserve behavior.
|
||||
std::uint32_t const ticketSeq{env.seq(sponsor) + 1};
|
||||
env(ticket::create(sponsor, 2));
|
||||
env.close();
|
||||
|
||||
// adjustAccountXRPBalance(env, sponsor, reserve(env, 3) -
|
||||
// drops(1));
|
||||
jv = {};
|
||||
jv[sfTransactionType] = jss::MPTokenAuthorize;
|
||||
jv[sfAccount] = bob.human();
|
||||
@@ -3022,6 +3088,105 @@ public:
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testSponsoredTrustLineNoFreeReserve()
|
||||
{
|
||||
// An account with ownerCount < 2 may create its first trust lines even
|
||||
// without meeting the reserve. In any case, the sponsor pays the full
|
||||
// reserve in all cases, even for the sponsee's very first trust line.
|
||||
testcase("Sponsored trust line gets no free-reserve exception");
|
||||
using namespace test::jtx;
|
||||
|
||||
Account const issuer("issuer");
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), issuer, alice, sponsor);
|
||||
env.close();
|
||||
|
||||
auto const usd = issuer["usd"];
|
||||
auto const lineKeylet = keylet::line(alice, issuer, usd.currency);
|
||||
|
||||
// Sponsor funded for exactly its base reserve
|
||||
adjustAccountXRPBalance(env, sponsor, reserve(env, 0));
|
||||
|
||||
// alice's ownerCount is 0, so an unsponsored first trust line would be
|
||||
// free; but because it is sponsored, the reserve check is enforced
|
||||
// against the sponsor, which is one increment short.
|
||||
env(trust(alice, usd(100)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tecNO_LINE_INSUF_RESERVE));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(lineKeylet));
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
|
||||
// Give the sponsor has exactly one owner-reserve increment; the same
|
||||
// sponsored first trust line now succeeds and the sponsor pays for it.
|
||||
adjustAccountXRPBalance(env, sponsor, reserve(env, 1));
|
||||
|
||||
env(trust(alice, usd(100)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(lineKeylet));
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testCoSignReserveBoundedBySponsorshipBudget()
|
||||
{
|
||||
// sponsor co-signs, so a fee-only object (ReserveCount == 0) makes a co-signed
|
||||
// reserve sponsorship fail -- with no fallback to the sponsor's balance.
|
||||
testcase("Co-signed reserve sponsorship is bounded by Sponsorship budget");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const sponsor("sponsor");
|
||||
Account const sponsee("sponsee");
|
||||
env.fund(XRP(10000), sponsor, sponsee);
|
||||
env.close();
|
||||
|
||||
// Prefund a FEE-only Sponsorship for the sponsee; ReserveCount
|
||||
// defaults to 0.
|
||||
env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(sponsee));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(keylet::sponsorship(sponsor, sponsee)));
|
||||
|
||||
// Sponsee creates a Check with the sponsor co-signing the reserve. The
|
||||
// fee-only Sponsorship's has ReserveCount (0), so this fails
|
||||
// with tecINSUFFICIENT_RESERVE
|
||||
env(check::create(sponsee, sponsor, XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, sponsee) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 0);
|
||||
|
||||
// Bumping the Sponsorship's ReserveCount budget makes the same
|
||||
// co-signed reserve sponsorship succeed, the budget is what gates it.
|
||||
env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(sponsee));
|
||||
env.close();
|
||||
|
||||
env(check::create(sponsee, sponsor, XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, sponsee) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, sponsee) == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testTrustSet(bool cosigning)
|
||||
{
|
||||
@@ -3649,7 +3814,7 @@ public:
|
||||
jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = "";
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
// should fail BatchSigners does have signer for SponsorSignature
|
||||
// should fail because BatchSigners does not have signer for SponsorSignature
|
||||
env(batch::outer(alice, seq, XRP(1), tfAllOrNothing),
|
||||
batch::Inner(jt.jv, seq + 1),
|
||||
batch::Inner(ticket::create(alice, 1), seq + 2),
|
||||
@@ -3734,6 +3899,40 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the central allow-list in preflight1Sponsor rejects
|
||||
// spfSponsorReserve for transaction types that v1 does not permit.
|
||||
void
|
||||
testReserveSponsorGate()
|
||||
{
|
||||
testcase("Reserve sponsor allow-list gate");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
env(sponsor::set(sponsor, 0, 10, XRP(10)), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
|
||||
auto checkBlocked = [&](json::Value const& jv) {
|
||||
env(jv,
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(temINVALID_FLAG));
|
||||
};
|
||||
|
||||
checkBlocked(ticket::create(alice, 1));
|
||||
checkBlocked(offer(alice, XRP(100), bob["USD"](100)));
|
||||
checkBlocked(did::setValid(alice));
|
||||
checkBlocked(token::mint(alice, 0u));
|
||||
checkBlocked(sponsor::set(alice, 0, 10, XRP(10)));
|
||||
checkBlocked(acctdelete(alice, bob));
|
||||
checkBlocked(loan::set(alice, uint256(1), Number{1}));
|
||||
}
|
||||
|
||||
void
|
||||
testSponsorReserve(bool cosigning)
|
||||
{
|
||||
@@ -3775,6 +3974,10 @@ protected:
|
||||
|
||||
testDelegatePermission();
|
||||
testBatch();
|
||||
|
||||
testSponsoredTrustLineNoFreeReserve();
|
||||
testCoSignReserveBoundedBySponsorshipBudget();
|
||||
testReserveSponsorGate();
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1535,86 +1535,43 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// A Sponsorship object is visible to both sides, but its reserve side
|
||||
// belongs only to sfOwner.
|
||||
// A Sponsorship object is visible to both sides.
|
||||
{
|
||||
Env env(*this, testableAmendments());
|
||||
Account const owner("owner");
|
||||
Account const sponsee("sponsee");
|
||||
Account const sponsor("sponsor");
|
||||
|
||||
env.fund(XRP(10000), owner, sponsee, sponsor);
|
||||
env.fund(XRP(10000), owner, sponsee);
|
||||
env.close();
|
||||
|
||||
env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(owner));
|
||||
env(sponsor::set(owner, 0, 100, XRP(100)), sponsor::SponseeAcc(sponsee));
|
||||
env.close();
|
||||
|
||||
env(sponsor::set(owner, 0, 100, XRP(100)),
|
||||
sponsor::SponseeAcc(sponsee),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor));
|
||||
env.close();
|
||||
|
||||
auto const sponsorship = env.le(keylet::sponsorship(owner, sponsee));
|
||||
if (!BEAST_EXPECT(sponsorship))
|
||||
auto const sponsorshipKeylet = keylet::sponsorship(owner, sponsee);
|
||||
if (!BEAST_EXPECT(env.le(sponsorshipKeylet)))
|
||||
return;
|
||||
BEAST_EXPECT(sponsorship->isFieldPresent(sfSponsor));
|
||||
|
||||
{
|
||||
auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship);
|
||||
auto const resp = acctObjsSponsored(env, owner.id(), false, jss::sponsorship);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
if (BEAST_EXPECT(objs.size() == 1))
|
||||
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship);
|
||||
}
|
||||
{
|
||||
auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
BEAST_EXPECT(objs.size() == 0);
|
||||
}
|
||||
{
|
||||
auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
if (BEAST_EXPECT(objs.size() == 1))
|
||||
BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship);
|
||||
}
|
||||
}
|
||||
|
||||
// NFT page sponsored filter
|
||||
{
|
||||
// Mint an NFT for bob (creates NFT page)
|
||||
env(token::mint(bob, 0));
|
||||
env.close();
|
||||
|
||||
auto const nftPageKeylet = keylet::nftpageMax(bob);
|
||||
if (!BEAST_EXPECT(env.le(nftPageKeylet)))
|
||||
return;
|
||||
|
||||
// Sponsor the NFT page
|
||||
env(sponsor::transfer(bob, tfSponsorshipCreate, nftPageKeylet.key),
|
||||
sponsor::As(sponsor1, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor1));
|
||||
env.close();
|
||||
|
||||
// Verify NFT page has sponsor field
|
||||
auto const nftPage = env.le(nftPageKeylet);
|
||||
if (!BEAST_EXPECT(nftPage))
|
||||
return;
|
||||
BEAST_EXPECT(nftPage->isFieldPresent(sfSponsor));
|
||||
|
||||
// sponsored=true should include the sponsored NFT page
|
||||
// sponsored=false should NOT include the sponsored NFT page
|
||||
for (auto const sponsored : {true, false})
|
||||
{
|
||||
auto const resp = acctObjsSponsored(env, bob.id(), sponsored);
|
||||
auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
bool foundNFTPage = false;
|
||||
for (auto const& obj : objs)
|
||||
{
|
||||
if (obj[sfLedgerEntryType.jsonName] == jss::NFTokenPage &&
|
||||
obj.isMember(sfSponsor.jsonName))
|
||||
foundNFTPage = true;
|
||||
}
|
||||
BEAST_EXPECT(foundNFTPage == sponsored);
|
||||
BEAST_EXPECT(objs.size() == 0);
|
||||
}
|
||||
{
|
||||
auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship);
|
||||
auto const& objs = resp[jss::result][jss::account_objects];
|
||||
BEAST_EXPECT(objs.size() == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user