test: Add more tests for sponsor edge cases (#7746)

This commit is contained in:
Mayukha Vadari
2026-07-07 12:53:17 -04:00
committed by GitHub
parent a2f1418aa8
commit 5283e9dba1

View File

@@ -2608,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());
});
}
}
@@ -2640,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),
@@ -2663,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, {}));
@@ -2700,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),
@@ -2732,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));
@@ -2769,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);
@@ -3162,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()};
@@ -3187,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
@@ -3478,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)
{
@@ -3500,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)
@@ -3580,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)
{
@@ -3615,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()));