mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 15:10:34 +00:00
Merge branch 'xrplf/sponsor' of https://github.com/XRPLF/rippled into mvadari/sponsor/reserve-context
This commit is contained in:
@@ -1170,7 +1170,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback,
|
||||
TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer,
|
||||
Delegation::NotDelegable,
|
||||
featureSponsor,
|
||||
MayModifyVault,
|
||||
NoPriv,
|
||||
({
|
||||
{sfObjectID, SoeOptional},
|
||||
{sfSponsee, SoeOptional},
|
||||
|
||||
@@ -21,7 +21,7 @@ class SponsorshipTransferBuilder;
|
||||
* Type: ttSPONSORSHIP_TRANSFER (90)
|
||||
* Delegable: Delegation::NotDelegable
|
||||
* Amendment: featureSponsor
|
||||
* Privileges: MayModifyVault
|
||||
* Privileges: NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use SponsorshipTransferBuilder to construct new transactions.
|
||||
|
||||
@@ -151,18 +151,6 @@ ValidPermissionedDomain::finalize(
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case ttSPONSORSHIP_TRANSFER: {
|
||||
if (sleStatus_.empty())
|
||||
return true;
|
||||
|
||||
if (sleStatus_[0].isDelete)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: domain object "
|
||||
"deleted by SponsorshipTransfer";
|
||||
return false;
|
||||
}
|
||||
return check(sleStatus_[0], j);
|
||||
}
|
||||
default: {
|
||||
if (!sleStatus_.empty())
|
||||
{
|
||||
|
||||
@@ -222,7 +222,10 @@ ValidVault::deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const
|
||||
if (!ret.has_value() || !vaultAsset.native())
|
||||
return ret;
|
||||
|
||||
if (auto const delegate = tx[~sfDelegate]; delegate.has_value() && *delegate != tx[sfAccount])
|
||||
// Only add the fee back if tx[sfAccount] actually paid it. When the fee is
|
||||
// paid by someone else (a delegate or a fee sponsor), the
|
||||
// account's XRP balance moved only by the vault amount.
|
||||
if (tx.getFeePayerID() != tx[sfAccount])
|
||||
return ret;
|
||||
|
||||
ret->delta += fee.drops();
|
||||
@@ -1045,8 +1048,6 @@ ValidVault::finalize(
|
||||
case ttLOAN_SET:
|
||||
case ttLOAN_MANAGE:
|
||||
case ttLOAN_PAY:
|
||||
case ttSPONSORSHIP_TRANSFER:
|
||||
// TBD for loans; SponsorshipTransfer may update a vault's sfSponsor
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
||||
@@ -396,9 +396,6 @@ AccountDelete::doApply()
|
||||
if (!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (src->isFieldPresent(sfSponsoredOwnerCount))
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Transfer any XRP remaining after the fee is paid to the destination:
|
||||
auto const remainingBalance = src->getFieldAmount(sfBalance).xrp();
|
||||
(*dst)[sfBalance] = (*dst)[sfBalance] + remainingBalance;
|
||||
@@ -410,11 +407,14 @@ AccountDelete::doApply()
|
||||
auto const sponsorID = src->getAccountID(sfSponsor);
|
||||
auto sponsorSle = view().peek(keylet::account(sponsorID));
|
||||
|
||||
if (!sponsorSle || !sponsorSle->isFieldPresent(sfSponsoringAccountCount))
|
||||
if (!sponsorSle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sponsoringAccountCount = sponsorSle->getFieldU32(sfSponsoringAccountCount);
|
||||
|
||||
XRPL_ASSERT(
|
||||
sponsoringAccountCount != 0,
|
||||
"xrpl::AccountDelete::doApply : sponsoring account count is present");
|
||||
if (sponsoringAccountCount == 0)
|
||||
{
|
||||
// sanity check
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
@@ -93,12 +94,6 @@ CredentialAccept::doApply()
|
||||
if (!sleSubject || !sleIssuer)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
{
|
||||
STAmount const reserve{accountReserve(view(), sleSubject, j_, {.ownerCountDelta = 1})};
|
||||
if (preFeeBalance_ < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
auto const credType(ctx_.tx[sfCredentialType]);
|
||||
Keylet const credentialKey = keylet::credential(accountID_, issuer, credType);
|
||||
auto const sleCred = view().peek(credentialKey); // Checked in preclaim()
|
||||
@@ -114,10 +109,26 @@ CredentialAccept::doApply()
|
||||
}
|
||||
|
||||
sleCred->setFieldU32(sfFlags, lsfAccepted);
|
||||
view().update(sleCred);
|
||||
|
||||
decreaseOwnerCount(view(), ReserveContext::makeFromAccount(view(), sleIssuer, nullptr), 1, j_);
|
||||
increaseOwnerCount(view(), ReserveContext::makeFromAccount(view(), sleSubject, nullptr), 1, j_);
|
||||
auto const txReserveContext = ctx_.getApplyViewContext().txReserveContext;
|
||||
|
||||
// Release the original creation sponsor from the credential (it covered
|
||||
// the issuer's reserve), then assign the accept tx's sponsor (if any) so
|
||||
// the credential reflects whoever is now covering the subject's reserve.
|
||||
decreaseOwnerCountForObject(view(), sleIssuer, sleCred, 1, j_);
|
||||
removeSponsorFromLedgerEntry(sleCred);
|
||||
|
||||
if (view().rules().enabled(featureSponsor) || view().rules().enabled(fixCleanup3_3_0))
|
||||
{
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sleSubject, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
addSponsorToLedgerEntry(sleCred, txReserveContext.sponsorSle);
|
||||
increaseOwnerCount(view(), txReserveContext, 1, j_);
|
||||
view().update(sleCred);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h> // IWYU pragma: keep
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
@@ -130,11 +131,10 @@ CredentialCreate::doApply()
|
||||
if (!sleIssuer)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
{
|
||||
STAmount const reserve{accountReserve(view(), sleIssuer, j_, {.ownerCountDelta = 1})};
|
||||
if (preFeeBalance_ < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
if (auto const ret = checkReserve(
|
||||
ctx_.getApplyViewContext(), sleIssuer, preFeeBalance_, {.ownerCountDelta = 1}, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
sleCred->setAccountID(sfSubject, subject);
|
||||
sleCred->setAccountID(sfIssuer, accountID_);
|
||||
@@ -152,8 +152,9 @@ CredentialCreate::doApply()
|
||||
return tecDIR_FULL;
|
||||
sleCred->setFieldU64(sfIssuerNode, *page);
|
||||
|
||||
increaseOwnerCount(
|
||||
view(), ReserveContext::makeFromAccount(view(), sleIssuer, nullptr), 1, j_);
|
||||
auto const txReserveContext = ctx_.getApplyViewContext().txReserveContext;
|
||||
increaseOwnerCount(view(), txReserveContext, 1, j_);
|
||||
addSponsorToLedgerEntry(sleCred, txReserveContext.sponsorSle);
|
||||
}
|
||||
|
||||
if (subject == accountID_)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/batch.h>
|
||||
#include <test/jtx/check.h>
|
||||
#include <test/jtx/credentials.h>
|
||||
#include <test/jtx/delegate.h>
|
||||
#include <test/jtx/deposit.h>
|
||||
#include <test/jtx/did.h>
|
||||
@@ -2607,6 +2608,76 @@ public:
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
|
||||
// the newly-created trust line carries the sponsor on
|
||||
// bob's side, and no sponsor on the counterparty side
|
||||
bool const bobLow = bob.id() < gw.id();
|
||||
auto const lineSle = env.le(keylet::trustLine(bob, gw, usd.currency));
|
||||
BEAST_EXPECT(lineSle);
|
||||
if (lineSle)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
lineSle->getAccountID(bobLow ? sfLowSponsor : sfHighSponsor) ==
|
||||
sponsor.id());
|
||||
BEAST_EXPECT(
|
||||
!lineSle->isFieldPresent(bobLow ? sfHighSponsor : sfLowSponsor));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// MPT sponsor: cashing an MPT check creates a sponsored MPToken for
|
||||
// the casher. Unlike the trust-line path, this returns
|
||||
// tecINSUFFICIENT_RESERVE (not tecNO_LINE_INSUF_RESERVE) on shortfall.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(10000), bob, sponsor, sponsor2);
|
||||
env.close();
|
||||
|
||||
MPTTester mptGw(env, gw, {.holders = {alice}});
|
||||
mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
||||
mptGw.authorize({.account = alice});
|
||||
auto const mpt = mptGw["MPT"];
|
||||
env(pay(gw, alice, mpt(10'000)));
|
||||
env.close();
|
||||
|
||||
// CheckCreate (alice -> bob) paying the MPT
|
||||
uint32_t seq2 = 0;
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
alice,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) {
|
||||
seq2 = env.seq(alice);
|
||||
submit(check::create(alice, bob, mpt(1)));
|
||||
});
|
||||
|
||||
auto const checkKeylet = keylet::check(alice, seq2);
|
||||
BEAST_EXPECT(env.le(checkKeylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
|
||||
// CheckCash by bob (no MPToken yet) creates a sponsored MPToken
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
bob,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) {
|
||||
submit(check::cash(bob, checkKeylet.key, mpt(1)));
|
||||
},
|
||||
[&]() {
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1); // MPToken
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) ==
|
||||
sponsor.id());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2639,8 +2710,11 @@ public:
|
||||
submit(delegate::set(alice, bob, {"Payment"}));
|
||||
});
|
||||
|
||||
// transfer sponsor
|
||||
// the created Delegate object carries the sponsor
|
||||
auto const keylet = keylet::delegate(alice, bob);
|
||||
BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
// transfer sponsor
|
||||
if (cosigning)
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
|
||||
@@ -2662,6 +2736,7 @@ public:
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
||||
BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id());
|
||||
|
||||
// delete
|
||||
env(delegate::set(alice, bob, {}));
|
||||
@@ -2699,8 +2774,11 @@ public:
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) { submit(deposit::auth(alice, sponsor)); });
|
||||
|
||||
// transfer sponsor
|
||||
// the created DepositPreauth object carries the sponsor
|
||||
auto const keylet = keylet::depositPreauth(alice, sponsor);
|
||||
BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
// transfer sponsor
|
||||
if (cosigning)
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
|
||||
@@ -2731,6 +2809,7 @@ public:
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
||||
BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id());
|
||||
|
||||
// DepositPreauthDelete
|
||||
env(deposit::unauth(alice, sponsor));
|
||||
@@ -2768,6 +2847,7 @@ public:
|
||||
|
||||
// Cover sfUnauthorizeCredentials cleanup for a sponsored preauth object.
|
||||
BEAST_EXPECT(env.le(preauthKeylet));
|
||||
BEAST_EXPECT(env.le(preauthKeylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
@@ -2782,6 +2862,171 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCredential(bool cosigning)
|
||||
{
|
||||
testcase("Credential");
|
||||
using namespace test::jtx;
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const sponsor("sponsor");
|
||||
Account const sponsor2("sponsor2");
|
||||
auto const credType = std::string("test");
|
||||
|
||||
// Self-issued credential: alice creates for herself, sponsor covers reserve
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
||||
env.close();
|
||||
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
alice,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) {
|
||||
submit(credentials::create(alice, alice, credType));
|
||||
});
|
||||
|
||||
auto const credKeylet = credentials::keylet(alice, alice, credType);
|
||||
BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
|
||||
// Transfer sponsor
|
||||
if (cosigning)
|
||||
{
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, credKeylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor2));
|
||||
env.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
|
||||
env.close();
|
||||
env(sponsor::transfer(alice, tfSponsorshipReassign, credKeylet.key),
|
||||
sponsor::As(sponsor2, spfSponsorReserve));
|
||||
env.close();
|
||||
}
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
||||
BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor2.id());
|
||||
|
||||
// CredentialDelete
|
||||
env(credentials::deleteCred(alice, alice, alice, credType));
|
||||
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);
|
||||
}
|
||||
|
||||
// Issuer creates for subject; sponsorship transfers to subject on accept
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// alice (issuer) creates credential for bob (subject), sponsor covers alice's reserve
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
alice,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) {
|
||||
submit(credentials::create(bob, alice, credType));
|
||||
});
|
||||
|
||||
// Before accept: alice (issuer) owns the credential
|
||||
auto const credKeylet = credentials::keylet(bob, alice, credType);
|
||||
BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
|
||||
|
||||
// Bob accepts: issuer's sponsorship ends, bob becomes unsponsored owner
|
||||
env(credentials::accept(bob, alice, credType));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); // original sponsorship ended
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); // bob owns it without a sponsor
|
||||
|
||||
// CredentialDelete by subject
|
||||
env(credentials::deleteCred(bob, bob, alice, credType));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
}
|
||||
|
||||
// Issuer creates for subject; accept tx itself is reserve-sponsored
|
||||
// This exercises the checkReserve() call in CredentialAccept::doApply()
|
||||
// that guards the sponsor's reserve when featureSponsor is enabled.
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// alice (issuer) creates credential for bob (subject) without sponsor
|
||||
env(credentials::create(bob, alice, credType));
|
||||
env.close();
|
||||
|
||||
auto const credKeylet = credentials::keylet(bob, alice, credType);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
|
||||
// Bob accepts with a sponsored reserve; the first attempt uses an
|
||||
// undercapitalized sponsor and must fail with tecINSUFFICIENT_RESERVE;
|
||||
// the second attempt uses a properly funded sponsor and must succeed.
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
bob,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) {
|
||||
submit(credentials::accept(bob, alice, credType));
|
||||
});
|
||||
|
||||
// After successful accept: alice (issuer) no longer owns the credential,
|
||||
// bob owns it and sponsor covers his reserve.
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
// Cleanup
|
||||
env(credentials::deleteCred(bob, bob, alice, credType));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDID(bool cosigning)
|
||||
{
|
||||
@@ -2996,6 +3241,80 @@ public:
|
||||
env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) ==
|
||||
sponsor2.id());
|
||||
}
|
||||
{
|
||||
// IOU Escrow cancel re-creates the owner's trust line, and the
|
||||
// cancel transaction's sponsor can cover that new line's reserve.
|
||||
// Creating an IOU escrow moves the sender's balance to the issuer,
|
||||
// so the sender can delete their now-zero trust line while the
|
||||
// escrow is pending. Cancelling returns the funds, re-creating the
|
||||
// line.
|
||||
Env env{*this, testableAmendments()};
|
||||
|
||||
env.fund(XRP(1000000), alice, bob, gw, sponsor, sponsor2);
|
||||
env.close();
|
||||
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env.close();
|
||||
|
||||
env.trust(usd(1000000), alice);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(10000)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
|
||||
// alice escrows her entire balance so the trust line can be removed
|
||||
auto const cancelAfter = env.now() + 100s;
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, bob, usd(10000)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(cancelAfter));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 2); // trust line + escrow
|
||||
|
||||
// alice deletes her now-zero trust line while the escrow is pending
|
||||
env(trust(alice, usd(0)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency)));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1); // just the escrow
|
||||
|
||||
// advance the ledger past the cancel time
|
||||
for (; env.now() < cancelAfter; env.close())
|
||||
{
|
||||
}
|
||||
|
||||
// EscrowCancel by alice re-creates her trust line; the sponsor
|
||||
// covers the new line's reserve. On the insufficient-reserve
|
||||
// attempt the whole transaction rolls back, leaving the escrow
|
||||
// intact to be cancelled on the success attempt.
|
||||
bool const aliceLow = alice.id() < gw.id();
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
alice,
|
||||
1,
|
||||
1,
|
||||
tecNO_LINE_INSUF_RESERVE,
|
||||
[&](Env& env, auto const& submit) { submit(escrow::cancel(alice, alice, seq)); },
|
||||
[&]() {
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice, seq)));
|
||||
auto const trustSle = env.le(keylet::trustLine(alice, gw, usd.currency));
|
||||
BEAST_EXPECT(trustSle);
|
||||
if (trustSle)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
trustSle->getAccountID(aliceLow ? sfLowSponsor : sfHighSponsor) ==
|
||||
sponsor.id());
|
||||
BEAST_EXPECT(
|
||||
!trustSle->isFieldPresent(aliceLow ? sfHighSponsor : sfLowSponsor));
|
||||
}
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1); // re-created trust line
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
});
|
||||
}
|
||||
{
|
||||
// MPT Escrow
|
||||
Env env{*this, testableAmendments()};
|
||||
@@ -3021,19 +3340,33 @@ public:
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
|
||||
// finish Escrow
|
||||
env(escrow::finish(bob, alice, seq),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kFulfillment(escrow::kFb1),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Fee(XRP(1)));
|
||||
env.close();
|
||||
// finish Escrow: bob has no MPToken, so finishing creates one and
|
||||
// the finish transaction's sponsor covers its reserve. The MPT
|
||||
// create-token path returns tecINSUFFICIENT_RESERVE (not
|
||||
// tecNO_LINE_INSUF_RESERVE) when the sponsor is underfunded.
|
||||
testEachSponsorship(
|
||||
env,
|
||||
cosigning,
|
||||
sponsor,
|
||||
bob,
|
||||
1,
|
||||
1,
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) {
|
||||
submit(
|
||||
escrow::finish(bob, alice, seq),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kFulfillment(escrow::kFb1),
|
||||
Fee(XRP(1)));
|
||||
});
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::mptoken(mptGw.issuanceID(), bob))->getAccountID(sfSponsor) ==
|
||||
sponsor.id());
|
||||
}
|
||||
|
||||
// A sponsored EscrowCreate must still verify that the source
|
||||
@@ -3312,6 +3645,9 @@ public:
|
||||
submit(paychan::create(alice, bob, XRP(100), settleDelay, pk));
|
||||
});
|
||||
|
||||
// the created PayChannel object carries the sponsor
|
||||
BEAST_EXPECT(env.le(Keylet(ltPAYCHAN, chan))->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
// transfer sponsor
|
||||
if (cosigning)
|
||||
{
|
||||
@@ -3334,6 +3670,7 @@ public:
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
||||
BEAST_EXPECT(env.le(Keylet(ltPAYCHAN, chan))->getAccountID(sfSponsor) == sponsor2.id());
|
||||
|
||||
env.close(env.now() + settleDelay);
|
||||
// PayChanClaim (delete PayChan)
|
||||
@@ -3414,6 +3751,9 @@ public:
|
||||
tecINSUFFICIENT_RESERVE,
|
||||
[&](Env& env, auto const& submit) { submit(signers(alice, 1, {{bob, 1}})); });
|
||||
|
||||
// the created SignerList object carries the sponsor
|
||||
BEAST_EXPECT(env.le(keylet::signerList(alice))->getAccountID(sfSponsor) == sponsor.id());
|
||||
|
||||
// transfer sponsor
|
||||
if (cosigning)
|
||||
{
|
||||
@@ -3449,6 +3789,7 @@ public:
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
||||
BEAST_EXPECT(env.le(keylet::signerList(alice))->getAccountID(sfSponsor) == sponsor2.id());
|
||||
|
||||
// Delete
|
||||
env(signers(alice, NoneT()));
|
||||
@@ -3897,12 +4238,6 @@ public:
|
||||
|
||||
auto const usd = gw["usd"];
|
||||
|
||||
// Create sponsorship allowing reserve sponsoring
|
||||
env(sponsor::set(sponsor, 0, 100, XRP(100)),
|
||||
sponsor::SponseeAcc(alice),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Create a trust line for alice
|
||||
env(trust(alice, usd(1000)));
|
||||
env.close();
|
||||
@@ -3962,6 +4297,37 @@ public:
|
||||
BEAST_EXPECT(
|
||||
sponsorSleAfter->getFieldU32(sfSponsoringAccountCount) == sponsoringAccountCount);
|
||||
}
|
||||
|
||||
{
|
||||
// Account with sponsored objects should be deletable
|
||||
Env env{*this, testableAmendments()};
|
||||
env.fund(XRP(1000000), alice, bob, sponsor);
|
||||
env.close();
|
||||
|
||||
// Create sponsored delegate (a non-deletion-blocker)
|
||||
env(deposit::auth(alice, bob),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor));
|
||||
|
||||
{
|
||||
auto const sponsorSle = env.le(keylet::account(sponsor));
|
||||
auto const aliceSle = env.le(keylet::account(alice));
|
||||
BEAST_EXPECT(sponsorSle->at(sfSponsoringOwnerCount) == 1);
|
||||
BEAST_EXPECT(aliceSle->at(sfOwnerCount) == 1);
|
||||
BEAST_EXPECT(aliceSle->at(sfSponsoredOwnerCount) == 1);
|
||||
}
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
|
||||
// AccountDelete should succeed
|
||||
{
|
||||
auto const requiredFee = drops(env.current()->fees().increment);
|
||||
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tesSUCCESS));
|
||||
BEAST_EXPECT(!env.le(keylet::account(alice)));
|
||||
auto const sponsorSle = env.le(keylet::account(sponsor));
|
||||
BEAST_EXPECT(sponsorSle->at(sfSponsoringOwnerCount) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4411,6 +4777,7 @@ public:
|
||||
testRequireFlag();
|
||||
testSponsorReserveSimple(cosigning);
|
||||
testCheck(cosigning);
|
||||
testCredential(cosigning);
|
||||
testDelegate(cosigning);
|
||||
testDepositPreauth(cosigning);
|
||||
testEscrow(cosigning);
|
||||
@@ -4565,6 +4932,56 @@ public:
|
||||
BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore);
|
||||
}
|
||||
|
||||
void
|
||||
testFeeSponsoredVaultInvariant()
|
||||
{
|
||||
// The ValidVault invariant checks that the vault's balance and the
|
||||
// depositor's balance change by equal amounts. For XRP vaults it adds the
|
||||
// fee back into the depositor's balance change: normally the depositor
|
||||
// pays the fee, so their balance drops by (deposit amount + fee), and
|
||||
// adding the fee back leaves just the deposit amount to compare against
|
||||
// the vault. But when a fee sponsor pays, the depositor's balance drops
|
||||
// by only the deposit amount, so the fee must NOT be added back or the
|
||||
// equal-amount check fails.
|
||||
testcase("Fee-sponsored VaultDeposit/VaultWithdraw pass ValidVault invariant");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const alice("alice");
|
||||
Account const sponsor("sponsor");
|
||||
env.fund(XRP(10000), alice, sponsor);
|
||||
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();
|
||||
|
||||
// Control: the same deposit shape, unsponsored, succeeds.
|
||||
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(100)}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Fee-sponsored (co-signed) deposit succeeds.
|
||||
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(100)}),
|
||||
Fee(XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// The same helper (deltaAssetsTxAccount) drives the withdraw path, so a
|
||||
// fee-sponsored withdrawal back to the depositor's own account also
|
||||
// passes on the destination side.
|
||||
env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(50)}),
|
||||
Fee(XRP(1)),
|
||||
sponsor::As(sponsor, spfSponsorFee),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
|
||||
protected:
|
||||
void
|
||||
testSponsor()
|
||||
@@ -4602,6 +5019,8 @@ protected:
|
||||
|
||||
testZeroBalanceSponsoredPaymentFeePayerCheck();
|
||||
testTrustSetCounterpartySponsorMisroute();
|
||||
|
||||
testFeeSponsoredVaultInvariant();
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -562,19 +562,19 @@ public:
|
||||
ownerCount(Account const& account) const;
|
||||
|
||||
/** Return the number of sponsored objects owned by an account.
|
||||
* Returns 0 if the account does not exist.
|
||||
* Throws if the account does not exist.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
sponsoredOwnerCount(Account const& account) const;
|
||||
|
||||
/** Return the number of sponsoring objects owned by an account.
|
||||
* Returns 0 if the account does not exist.
|
||||
* Throws if the account does not exist.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
sponsoringOwnerCount(Account const& account) const;
|
||||
|
||||
/** Return the number of sponsoring accounts owned by an account.
|
||||
* Returns 0 if the account does not exist.
|
||||
* Throws if the account does not exist.
|
||||
*/
|
||||
[[nodiscard]] std::uint32_t
|
||||
sponsoringAccountCount(Account const& account) const;
|
||||
|
||||
@@ -33,8 +33,7 @@ namespace xrpl {
|
||||
@param dirIndex Begin gathering account objects from this directory.
|
||||
@param entryIndex Begin gathering objects from this directory node.
|
||||
@param limit Maximum number of objects to find.
|
||||
@param hasSponsoredFilter Whether to filter by sponsored objects.
|
||||
@param sponsored Whether filtered objects should be sponsored.
|
||||
@param sponsoredFilter If set, only return objects whose sponsored state matches the value.
|
||||
@param jvResult A JSON result that holds the request objects.
|
||||
*/
|
||||
bool
|
||||
@@ -45,8 +44,7 @@ getAccountObjects(
|
||||
uint256 dirIndex,
|
||||
uint256 entryIndex,
|
||||
std::uint32_t const limit,
|
||||
bool const hasSponsoredFilter,
|
||||
bool const sponsored,
|
||||
std::optional<bool> const sponsoredFilter,
|
||||
json::Value& jvResult)
|
||||
{
|
||||
// check if dirIndex is valid
|
||||
@@ -59,11 +57,10 @@ getAccountObjects(
|
||||
return it != typeFilter.end();
|
||||
};
|
||||
|
||||
auto sponsoredMatchesFilter = [](bool const sponsored,
|
||||
std::optional<AccountID> const& sponsor) {
|
||||
if (sponsored)
|
||||
return sponsor.has_value();
|
||||
return !sponsor.has_value();
|
||||
auto sponsoredMatchesFilter = [&sponsoredFilter](std::optional<AccountID> const& sponsor) {
|
||||
if (!sponsoredFilter.has_value())
|
||||
return true;
|
||||
return sponsor.has_value() == *sponsoredFilter;
|
||||
};
|
||||
|
||||
// if dirIndex != 0, then all NFTs have already been returned. only
|
||||
@@ -104,15 +101,10 @@ getAccountObjects(
|
||||
|
||||
while (currentPage)
|
||||
{
|
||||
bool canAppendNFT = true;
|
||||
if (hasSponsoredFilter)
|
||||
{
|
||||
std::optional<AccountID> const nftSponsor = currentPage->isFieldPresent(sfSponsor)
|
||||
? currentPage->getAccountID(sfSponsor)
|
||||
: std::optional<AccountID>(std::nullopt);
|
||||
if (!sponsoredMatchesFilter(sponsored, nftSponsor))
|
||||
canAppendNFT = false;
|
||||
}
|
||||
std::optional<AccountID> const nftSponsor = currentPage->isFieldPresent(sfSponsor)
|
||||
? currentPage->getAccountID(sfSponsor)
|
||||
: std::optional<AccountID>(std::nullopt);
|
||||
bool const canAppendNFT = sponsoredMatchesFilter(nftSponsor);
|
||||
if (canAppendNFT)
|
||||
jvObjects.append(currentPage->getJson(JsonOptions::Values::None));
|
||||
auto const npm = (*currentPage)[~sfNextPageMin];
|
||||
@@ -232,7 +224,7 @@ getAccountObjects(
|
||||
};
|
||||
std::optional<AccountID> const sponsor = getSponsor();
|
||||
|
||||
if (hasSponsoredFilter && !sponsoredMatchesFilter(sponsored, sponsor))
|
||||
if (!sponsoredMatchesFilter(sponsor))
|
||||
canAppend = false;
|
||||
|
||||
if (canAppend)
|
||||
@@ -383,27 +375,18 @@ doAccountObjects(RPC::JsonContext& context)
|
||||
return RPC::invalidFieldError(jss::marker);
|
||||
}
|
||||
|
||||
bool const hasSponsoredFilter = params.isMember(jss::sponsored);
|
||||
bool sponsored = false;
|
||||
if (hasSponsoredFilter)
|
||||
std::optional<bool> sponsoredFilter;
|
||||
if (params.isMember(jss::sponsored))
|
||||
{
|
||||
auto const& sponsoredJv = params[jss::sponsored];
|
||||
if (!sponsoredJv.isBool())
|
||||
return RPC::expectedFieldError(jss::sponsored, "boolean");
|
||||
|
||||
sponsored = sponsoredJv.asBool();
|
||||
sponsoredFilter = sponsoredJv.asBool();
|
||||
}
|
||||
|
||||
if (!getAccountObjects(
|
||||
*ledger,
|
||||
accountID,
|
||||
typeFilter,
|
||||
dirIndex,
|
||||
entryIndex,
|
||||
limit,
|
||||
hasSponsoredFilter,
|
||||
sponsored,
|
||||
result))
|
||||
*ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsoredFilter, result))
|
||||
return RPC::invalidFieldError(jss::marker);
|
||||
|
||||
result[jss::account] = toBase58(accountID);
|
||||
|
||||
Reference in New Issue
Block a user