mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
- Add accountSet() jtx helper and use it (plus existing helpers: sponsor::As, Oracle, MPTSet, xrpLiquid/accountReserve) instead of hand-built JSON where a suitable helper already exists - Add missing coverage: equality at the count() maxU32 boundary, sb2-side assertions in PaymentSandbox owner-count merge tests, sequential fee/reserve sponsorship exhaustion across transactions, reserve-sponsored queue exhaustion in TxQ, and indirect deletion of an expired sponsored credential - Drop the redundant self-compare in OwnerCounts equality test - Move testFeeOnlySponsorshipObjectRouting next to testCosignedTransferConsumesPrefundedBudget (same precedence rule) - Add missing check.h include in TxQ_test.cpp
6833 lines
265 KiB
C++
6833 lines
265 KiB
C++
#include <test/jtx/AMM.h>
|
|
#include <test/jtx/Account.h>
|
|
#include <test/jtx/Env.h>
|
|
#include <test/jtx/TestHelpers.h>
|
|
#include <test/jtx/acctdelete.h>
|
|
#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>
|
|
#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>
|
|
#include <test/jtx/offer.h>
|
|
#include <test/jtx/paths.h>
|
|
#include <test/jtx/pay.h>
|
|
#include <test/jtx/permissioned_domains.h>
|
|
#include <test/jtx/regkey.h>
|
|
#include <test/jtx/sendmax.h>
|
|
#include <test/jtx/seq.h>
|
|
#include <test/jtx/sig.h>
|
|
#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 <test/jtx/xchain_bridge.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>
|
|
#include <xrpl/beast/unit_test/suite.h>
|
|
#include <xrpl/beast/utility/instrumentation.h>
|
|
#include <xrpl/core/ServiceRegistry.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#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>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#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 {
|
|
|
|
static STAmount
|
|
accountReserve(jtx::Env& env, std::uint32_t count = 1)
|
|
{
|
|
return env.current()->fees().reserve * count;
|
|
}
|
|
|
|
static STAmount
|
|
reserve(jtx::Env& env, std::uint32_t count)
|
|
{
|
|
return env.current()->fees().accountReserve(count, 1);
|
|
}
|
|
|
|
static void
|
|
adjustAccountXRPBalance(jtx::Env& env, jtx::Account const& account, STAmount const& balanceTo)
|
|
{
|
|
using namespace test::jtx;
|
|
XRPL_ASSERT(isXRP(balanceTo), "adjustAccountXRPBalance: balanceTo must be XRP");
|
|
auto const currentBalance = env.balance(account);
|
|
if (currentBalance == balanceTo)
|
|
return;
|
|
|
|
auto const baseFee = env.current()->fees().base;
|
|
if (currentBalance > balanceTo)
|
|
{
|
|
env(pay(account, env.master, currentBalance - balanceTo),
|
|
Fee(XRP(1)),
|
|
sponsor::As(env.master, spfSponsorFee),
|
|
Sig(sfSponsorSignature, env.master));
|
|
}
|
|
else
|
|
{
|
|
env(pay(env.master, account, balanceTo - currentBalance), Fee(baseFee));
|
|
}
|
|
|
|
env.close();
|
|
}
|
|
|
|
class Sponsor_test : public beast::unit_test::Suite
|
|
{
|
|
public:
|
|
void
|
|
testDisabled()
|
|
{
|
|
testcase("Disabled");
|
|
using namespace test::jtx;
|
|
Env env{*this, testableAmendments() - featureSponsor};
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, sponsor);
|
|
|
|
// check Sponsor fields
|
|
auto const jt = noop(alice);
|
|
auto jt1 = jt;
|
|
jt1[sfSponsor.jsonName] = sponsor.human();
|
|
env(jt1, Ter(temDISABLED));
|
|
env(jt, Sig(sfSponsorSignature, sponsor), Ter(temDISABLED));
|
|
|
|
auto jt2 = jt;
|
|
jt2[sfSponsorFlags.jsonName] = spfSponsorFee | spfSponsorReserve;
|
|
env(jt2, Ter(temDISABLED));
|
|
|
|
// check Sponsor transactions
|
|
env(sponsor::transfer(alice, 0), Ter(temDISABLED));
|
|
env(sponsor::set(sponsor, 0), Ter(temDISABLED));
|
|
}
|
|
|
|
void
|
|
testInvalidSponsorshipSet()
|
|
{
|
|
testcase("Invalid SponsorshipSet");
|
|
using namespace test::jtx;
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
Account const noFunded("noFunded");
|
|
Account const gw("gw");
|
|
|
|
auto const usd = gw["usd"];
|
|
env.fund(XRP(10000), alice, sponsor, gw);
|
|
env.close();
|
|
|
|
//
|
|
// preflight
|
|
//
|
|
|
|
// Invalid flags
|
|
{
|
|
env(sponsor::set(sponsor, ~tfSponsorshipSetMask - tfInnerBatchTxn),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temINVALID_FLAG));
|
|
|
|
env(sponsor::set(
|
|
sponsor,
|
|
tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temINVALID_FLAG));
|
|
|
|
env(sponsor::set(
|
|
sponsor,
|
|
tfSponsorshipSetRequireSignForReserve |
|
|
tfSponsorshipClearRequireSignForReserve),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temINVALID_FLAG));
|
|
|
|
for (auto flag :
|
|
{tfSponsorshipSetRequireSignForFee,
|
|
tfSponsorshipClearRequireSignForFee,
|
|
tfSponsorshipSetRequireSignForReserve,
|
|
tfSponsorshipClearRequireSignForReserve})
|
|
{
|
|
env(sponsor::set(sponsor, tfDeleteObject | flag),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temINVALID_FLAG));
|
|
}
|
|
}
|
|
|
|
// invalid SponsorAccount / Sponsee
|
|
// Neither Sponsor nor Sponsee is specified (amendment enabled)
|
|
env(sponsor::set(sponsor, 0), Ter(temMALFORMED));
|
|
// Account = Sponsor
|
|
env(sponsor::set(alice, tfDeleteObject),
|
|
sponsor::CounterpartySponsor(alice),
|
|
Ter(temMALFORMED));
|
|
// Account = Sponsee
|
|
env(sponsor::set(alice, tfDeleteObject), sponsor::SponseeAcc(alice), Ter(temMALFORMED));
|
|
// Both Sponsor and Sponsee are specified
|
|
env(sponsor::set(alice, 0),
|
|
sponsor::CounterpartySponsor(sponsor),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temMALFORMED));
|
|
|
|
// Invalid feeAmount
|
|
for (auto const& amt : {XRP(-1), usd(1)})
|
|
{
|
|
env(sponsor::set_fee(sponsor, 0, amt), sponsor::SponseeAcc(alice), Ter(temBAD_AMOUNT));
|
|
}
|
|
// Invalid MaxFee
|
|
for (auto const& amt : {XRP(-1), usd(1)})
|
|
{
|
|
env(sponsor::set_fee(sponsor, 0, XRP(1), amt),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temBAD_AMOUNT));
|
|
}
|
|
|
|
// Invalid Delete operation
|
|
env(sponsor::set_reserve(sponsor, tfDeleteObject, 1),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temMALFORMED));
|
|
env(sponsor::set_fee(sponsor, tfDeleteObject, XRP(1)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temMALFORMED));
|
|
env(sponsor::set_max_fee(sponsor, tfDeleteObject, XRP(1)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temMALFORMED));
|
|
|
|
// Invalid SponsorAccount with non-Delete operation
|
|
env(sponsor::set_reserve(sponsor, 0, 100),
|
|
sponsor::CounterpartySponsor(alice),
|
|
Ter(temMALFORMED));
|
|
env(sponsor::set_fee(sponsor, 0, XRP(1), XRP(1)),
|
|
sponsor::CounterpartySponsor(alice),
|
|
Ter(temMALFORMED));
|
|
|
|
//
|
|
// preclaim
|
|
//
|
|
|
|
// Invalid Sponsee
|
|
env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(noFunded), Ter(tecNO_DST));
|
|
env.close();
|
|
|
|
// Invalid Sponsor
|
|
env(sponsor::set(sponsor, tfDeleteObject),
|
|
sponsor::CounterpartySponsor(noFunded),
|
|
Ter(tecNO_DST));
|
|
env.close();
|
|
|
|
// Invalid Delete operation (sponsorship not found)
|
|
env(sponsor::set(sponsor, tfDeleteObject), sponsor::SponseeAcc(alice), Ter(tecNO_ENTRY));
|
|
env.close();
|
|
|
|
// insufficient balance to sponsor Fee
|
|
adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve);
|
|
env(sponsor::set_fee(sponsor, 0, XRP(4)), sponsor::SponseeAcc(alice), Ter(tecUNFUNDED));
|
|
env.close();
|
|
|
|
// insufficient reserve to create sponsorship
|
|
adjustAccountXRPBalance(env, sponsor, XRP(100) + XRP(1) + reserve(env, 1) - drops(1));
|
|
env(sponsor::set(sponsor, 0, 100, XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tecUNFUNDED));
|
|
env.close();
|
|
|
|
// FeeAmount + Fee > Balance
|
|
// Balance = 1000XRP, FeeAmount = 1001XRP
|
|
adjustAccountXRPBalance(env, sponsor, XRP(1000));
|
|
env(sponsor::set_fee(sponsor, 0, XRP(1001)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tecUNFUNDED));
|
|
env.close();
|
|
// Balance = 1000XRP, FeeAmount = 999XRP, Fee=2XRP
|
|
adjustAccountXRPBalance(env, sponsor, XRP(1000));
|
|
env(sponsor::set_fee(sponsor, 0, XRP(999)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(2)),
|
|
Ter(tecUNFUNDED));
|
|
env.close();
|
|
|
|
// create sponsor to use above tests
|
|
// need feeAmount(1000) + Fee(1) + reserve(~250) = ~1251
|
|
adjustAccountXRPBalance(env, sponsor, XRP(1000) + XRP(1) + reserve(env, 1));
|
|
env(sponsor::set(sponsor, 0, 100, XRP(1000)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// delta-based balance check
|
|
// After create: sponsor balance ~ 0, feeAmount = XRP(1000)
|
|
|
|
// Decreasing feeAmount should succeed (refund, negative delta)
|
|
adjustAccountXRPBalance(env, sponsor, XRP(500));
|
|
env(sponsor::set_fee(sponsor, 0, XRP(800)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
// balance was 500, delta = 800-1000 = -200 (refund), balance = 500+200-1 = 699
|
|
|
|
// Increasing feeAmount within delta budget should succeed
|
|
adjustAccountXRPBalance(env, sponsor, XRP(500));
|
|
env(sponsor::set_fee(sponsor, 0, XRP(850)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
// balance was 500, delta = 850-800 = 50, balance = 500-50-1 = 449
|
|
|
|
// Increasing feeAmount where delta exceeds balance should fail
|
|
adjustAccountXRPBalance(env, sponsor, XRP(310));
|
|
env(sponsor::set_fee(sponsor, 0, XRP(1200)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tecUNFUNDED));
|
|
env.close();
|
|
|
|
// Increasing feeAmount to reach insufficient reserve
|
|
auto const currentFeeAmount = env.le(keylet::sponsorship(sponsor.id(), alice.id()))
|
|
->getFieldAmount(sfFeeAmount)
|
|
.xrp();
|
|
adjustAccountXRPBalance(env, sponsor, XRP(310));
|
|
env(sponsor::set_fee(sponsor, 0, currentFeeAmount + XRP(309)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tecUNFUNDED));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testPseudoAccountSponsorship()
|
|
{
|
|
testcase("Pseudo account sponsorship");
|
|
using namespace test::jtx;
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const gw("gw");
|
|
Account const sp("sponsor");
|
|
|
|
Asset const asset = gw["IOU"].asset();
|
|
|
|
env.fund(XRP(1000000), alice, bob, gw, sp);
|
|
env.close();
|
|
|
|
// Create a vault to get a pseudo account
|
|
Vault const vault{env};
|
|
auto [tx, keylet] = vault.create({.owner = alice, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
auto const vaultSle = env.le(keylet);
|
|
BEAST_EXPECT(vaultSle);
|
|
Account const pseudoAcc("vault", vaultSle->getAccountID(sfAccount));
|
|
env.memoize(pseudoAcc);
|
|
|
|
// Sponsee is a pseudo account -> tecNO_PERMISSION
|
|
env(sponsor::set(sp, 0, 100, XRP(100)),
|
|
sponsor::SponseeAcc(pseudoAcc),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Sponsor is a pseudo account -> tecNO_PERMISSION
|
|
// (submitted by bob with counterpartySponsor pointing to pseudo account)
|
|
env(sponsor::set(bob, tfDeleteObject),
|
|
sponsor::CounterpartySponsor(pseudoAcc),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testSingleSigning()
|
|
{
|
|
testcase("Single signing");
|
|
using namespace test::jtx;
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
Account const invalid("invalid");
|
|
Account const regular("regular");
|
|
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.memoize(regular);
|
|
env.close();
|
|
|
|
// Signature doesn't exist
|
|
auto tx = noop(alice);
|
|
tx[sfSponsor.jsonName] = sponsor.human();
|
|
tx[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = strHex(sponsor.pk().slice());
|
|
|
|
env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED));
|
|
|
|
// Invalid signature
|
|
tx[sfSponsorSignature.jsonName][sfTxnSignature.jsonName] = "DEADBEEF";
|
|
env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED));
|
|
|
|
// Signer account doesn't exist
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(invalid, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, invalid),
|
|
Ter(terNO_ACCOUNT));
|
|
|
|
// Success
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// The SponsorSignature is a cryptographically valid signature, but it
|
|
// is bob's, not the named sponsor's. It passes the local crypto-only
|
|
// check (STTx::checkSign verifies the signature against its own
|
|
// SigningPubKey), and the engine then rejects it in
|
|
// Transactor::checkSingleSign ("signed with any other key") with
|
|
// tefBAD_AUTH.
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, bob),
|
|
Ter(tefBAD_AUTH));
|
|
|
|
// Give the sponsor a regular key.
|
|
env(regkey(sponsor, regular));
|
|
env.close();
|
|
|
|
// A sponsor signature made with the regular key is accepted
|
|
// (Transactor::checkSingleSign: RegularKey == idSigner).
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, regular),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// The master key still works while it is enabled.
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// Disable the sponsor's master key. Sig(sponsor) forces the master
|
|
// key: with a regular key set, Env::autofill_sig signs with the
|
|
// regular key, and AccountSet would return tecNEED_MASTER_KEY.
|
|
env(fset(sponsor, asfDisableMaster), Sig(sponsor));
|
|
env.close();
|
|
|
|
// A master-key sponsor signature is now rejected
|
|
// (Transactor::checkSingleSign: lsfDisableMaster ->
|
|
// tefMASTER_DISABLED).
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tefMASTER_DISABLED));
|
|
|
|
// ... but the regular key still works.
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, regular),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testMultiSigning()
|
|
{
|
|
testcase("Multi signing");
|
|
using namespace test::jtx;
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
Account const signer1("signer1");
|
|
Account const signer2("signer2");
|
|
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
env(signers(sponsor, 1, {{signer1, 1}, {signer2, 1}}));
|
|
env.close();
|
|
|
|
// Invalid signature
|
|
auto tx = noop(alice);
|
|
auto& signers1 = tx[sfSponsorSignature.jsonName][sfSigners.jsonName][0U][sfSigner.jsonName];
|
|
signers1[sfAccount.jsonName] = signer1.human();
|
|
signers1[sfSigningPubKey.jsonName] = strHex(signer1.pk().slice());
|
|
signers1[sfTxnSignature.jsonName] = "DEADBEEF";
|
|
env(tx, Fee(XRP(1)), sponsor::As(sponsor, spfSponsorReserve), Ter(telENV_RPC_FAILED));
|
|
|
|
// bob is not a multi-signing account.
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(bob, spfSponsorReserve),
|
|
Msig(sfSponsorSignature, {signer1}),
|
|
Ter(tefNOT_MULTI_SIGNING));
|
|
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Msig(sfSponsorSignature, {signer1}),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env(signers(sponsor, 2, {{signer1, 1}, {signer2, 1}}));
|
|
env.close();
|
|
|
|
// test calculateBaseFee for multisigned sponsor
|
|
auto const baseFee = env.current()->fees().base;
|
|
env(noop(alice),
|
|
Fee(baseFee + 2 * baseFee - 1),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Msig(sfSponsorSignature, {signer1, signer2}),
|
|
Ter(telINSUF_FEE_P));
|
|
|
|
env(noop(alice),
|
|
Fee(baseFee + 2 * baseFee),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Msig(sfSponsorSignature, {signer1, signer2}),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// Below quorum: Transactor::checkMultiSign -> tefBAD_QUORUM.
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Msig(sfSponsorSignature, {signer1}),
|
|
Ter(tefBAD_QUORUM));
|
|
|
|
// A signer that is not on the sponsor's signer list:
|
|
// Transactor::checkMultiSign ("The SigningAccount is not in the
|
|
// SignerEntries") -> tefBAD_SIGNATURE.
|
|
Account const outsider("outsider");
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Msig(sfSponsorSignature, {signer1, outsider}),
|
|
Ter(tefBAD_SIGNATURE));
|
|
|
|
// Both the sponsee transaction and the sponsor are multisigned. The
|
|
// required fee is
|
|
// (1 + |tx.Signers| + |SponsorSignature.Signers|) * baseFee
|
|
// (Transactor::calculateBaseFee).
|
|
env(signers(alice, 1, {{signer1, 1}, {signer2, 1}}));
|
|
env.close();
|
|
|
|
env(noop(alice),
|
|
Msig({signer1}),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Msig(sfSponsorSignature, {signer1, signer2}),
|
|
Fee(baseFee + 3 * baseFee - 1),
|
|
Ter(telINSUF_FEE_P));
|
|
|
|
env(noop(alice),
|
|
Msig({signer1}),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Msig(sfSponsorSignature, {signer1, signer2}),
|
|
Fee(baseFee + 3 * baseFee),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testSponsoredTicketUse()
|
|
{
|
|
testcase("Sponsored transaction consuming a Ticket");
|
|
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();
|
|
|
|
std::uint32_t const ticketSeq{env.seq(alice) + 1};
|
|
env(ticket::create(alice, 2));
|
|
env.close();
|
|
env.require(tickets(alice, 2));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
auto const aliceSeq = env.seq(alice);
|
|
|
|
// Fee-sponsored (co-signed) transaction consuming a ticket.
|
|
auto const feeAmt = XRP(1);
|
|
auto const sponsorBalance = env.balance(sponsor);
|
|
env(noop(alice),
|
|
ticket::Use(ticketSeq),
|
|
Fee(feeAmt),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env.require(tickets(alice, 1));
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt);
|
|
// Consuming a ticket does not advance the account sequence.
|
|
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
|
|
|
// Reserve-sponsored (co-signed) transaction consuming a ticket.
|
|
env(check::create(alice, bob, XRP(10)),
|
|
ticket::Use(ticketSeq + 1),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env.require(tickets(alice, 0));
|
|
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
|
// The consumed ticket is replaced by the sponsored check.
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
}
|
|
|
|
void
|
|
testInvalidSponsorField()
|
|
{
|
|
testcase("Invalid Sponsor Field");
|
|
using namespace test::jtx;
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
Account const noFunded("noFunded");
|
|
env.fund(XRP(10000), alice, sponsor);
|
|
env.close();
|
|
|
|
// Invalid Sponsor Account (Account = Sponsor.Account)
|
|
env(noop(alice), sponsor::As(alice, spfSponsorFee), Ter(temMALFORMED));
|
|
|
|
// Invalid Sponsor Account
|
|
// (SponsorSignature is specified but Sponsor.Account is not specified)
|
|
env(noop(alice), Sig(sfSponsorSignature, sponsor), Ter(temMALFORMED));
|
|
|
|
// Invalid Sponsor Account (Sponsor.Account doesn't exist)
|
|
env(noop(alice), sponsor::As(noFunded, spfSponsorReserve), Ter(terNO_ACCOUNT));
|
|
env(noop(alice),
|
|
sponsor::As(noFunded, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, noFunded),
|
|
Ter(terNO_ACCOUNT));
|
|
|
|
// Invalid Flags
|
|
env(noop(alice),
|
|
sponsor::As(sponsor, (spfSponsorFee | spfSponsorReserve) + 1),
|
|
Ter(temINVALID_FLAG));
|
|
|
|
// SponsorFlags=0 with valid sponsor (no sponsorship purpose)
|
|
env(noop(alice), sponsor::As(sponsor, 0), Ter(temINVALID_FLAG));
|
|
|
|
// no SponsorFlag with valid sponsor
|
|
auto tx = noop(alice);
|
|
tx[sfSponsor.jsonName] = sponsor.human();
|
|
env(tx, Ter(temINVALID_FLAG));
|
|
|
|
// Invalid Flags without sponsor
|
|
tx = noop(alice);
|
|
tx[sfSponsorFlags.jsonName] = spfSponsorFee | spfSponsorReserve;
|
|
env(tx, Ter(temINVALID_FLAG));
|
|
}
|
|
|
|
void
|
|
testSimpleSponsorshipSet()
|
|
{
|
|
testcase("Simple SponsorshipSet");
|
|
using namespace test::jtx;
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, sponsor);
|
|
env.close();
|
|
|
|
{
|
|
// create sponsorship
|
|
env(sponsor::set(
|
|
sponsor,
|
|
tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve,
|
|
100,
|
|
XRP(100),
|
|
XRP(1)),
|
|
Fee(XRP(1)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
auto sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100);
|
|
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100));
|
|
BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1));
|
|
BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForFee));
|
|
BEAST_EXPECT(sle->isFlag(lsfSponsorshipRequireSignForReserve));
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(1));
|
|
|
|
// update sponsorship (decrement)
|
|
env(sponsor::set(sponsor, 0, 50, XRP(50), XRP(0.5)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 50);
|
|
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(50));
|
|
BEAST_EXPECT(sle->at(sfMaxFee) == XRP(0.5));
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(2));
|
|
|
|
// update sponsorship (increment)
|
|
env(sponsor::set(sponsor, 0, 200, XRP(200), XRP(2)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 200);
|
|
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(200));
|
|
BEAST_EXPECT(sle->at(sfMaxFee) == XRP(2));
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(3));
|
|
|
|
// delete from sponsor
|
|
env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - XRP(4));
|
|
|
|
env(sponsor::set(
|
|
sponsor,
|
|
tfSponsorshipSetRequireSignForFee | tfSponsorshipSetRequireSignForReserve,
|
|
100,
|
|
XRP(100),
|
|
XRP(1)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// delete from sponsee
|
|
env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS));
|
|
env.close();
|
|
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
|
|
|
// Cannot create sponsorship with no fee or reserve budget. MaxFee
|
|
// and flags do not make a sponsorship object useful by themselves.
|
|
env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
|
|
|
env(sponsor::set_max_fee(sponsor, 0, XRP(1)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
|
|
|
env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
|
|
|
// update sponsorship with non-zero value
|
|
env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100);
|
|
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100));
|
|
BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1));
|
|
|
|
// update sponsorship flags
|
|
auto testFlagUpdate = [&](auto setFlag, auto clearFlag, auto ledgerFlag) {
|
|
env(sponsor::set(sponsor, setFlag), sponsor::SponseeAcc(alice), Fee(XRP(1)));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->isFlag(ledgerFlag));
|
|
|
|
env(sponsor::set(sponsor, clearFlag), sponsor::SponseeAcc(alice), Fee(XRP(1)));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(!sle->isFlag(ledgerFlag));
|
|
};
|
|
|
|
testFlagUpdate(
|
|
tfSponsorshipSetRequireSignForFee,
|
|
tfSponsorshipClearRequireSignForFee,
|
|
lsfSponsorshipRequireSignForFee);
|
|
testFlagUpdate(
|
|
tfSponsorshipSetRequireSignForReserve,
|
|
tfSponsorshipClearRequireSignForReserve,
|
|
lsfSponsorshipRequireSignForReserve);
|
|
|
|
// Cannot update sponsorship so both fee and reserve budgets are absent.
|
|
env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100);
|
|
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100));
|
|
BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1));
|
|
}
|
|
|
|
{
|
|
// Removing one budget field while the other remains keeps the
|
|
// Sponsorship valid. Starting state (from above):
|
|
// RemainingOwnerCount = 100, FeeAmount = XRP(100).
|
|
|
|
// Remove only FeeAmount (set to 0); RemainingOwnerCount remains.
|
|
env(sponsor::set_fee(sponsor, 0, XRP(0)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
auto sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
|
|
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100);
|
|
|
|
// Re-add FeeAmount, then remove only RemainingOwnerCount;
|
|
// FeeAmount remains.
|
|
env(sponsor::set_fee(sponsor, 0, XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env(sponsor::set_reserve(sponsor, 0, 0),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
|
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100));
|
|
}
|
|
|
|
{
|
|
// Update Sponsorship (FeeAmount)
|
|
// set empty FeeAmount
|
|
env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// add FeeAmount
|
|
env(sponsor::set_fee(sponsor, 0, XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
{
|
|
// Update Sponsorship (ReserveCount)
|
|
// set empty ReserveCount
|
|
env(sponsor::set_fee(sponsor, 0, XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// add ReserveCount
|
|
env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
{
|
|
// delete Sponsorship (only with FeeAmount)
|
|
env(sponsor::set_fee(sponsor, 0, XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
{
|
|
// delete Sponsorship (only with ReserveCount)
|
|
env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env(sponsor::del(alice), sponsor::CounterpartySponsor(sponsor), Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testPreFundAndCosign()
|
|
{
|
|
testcase("PreFund and Cosign");
|
|
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; the pre-funded value is used.
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
auto const checkSeq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
auto sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99);
|
|
BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99));
|
|
|
|
env(check::cancel(alice, keylet::check(alice, checkSeq).key), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
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, charlie, sponsor);
|
|
env.close();
|
|
|
|
env(sponsor::set(sponsor, 0, 1, XRP(10), XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// Fee insufficient
|
|
env(check::create(alice, bob, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(XRP(11)),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
|
|
env(sponsor::set_reserve(sponsor, 0, 0), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// reserve insufficient
|
|
env(check::create(alice, bob, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(XRP(1)),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testSequentialSponsorshipExhaustion()
|
|
{
|
|
// A prefunded Sponsorship object's FeeAmount/RemainingOwnerCount is
|
|
// shared across however many sponsored transactions the sponsee
|
|
// submits over time, not just a single one. Submit a run of
|
|
// transactions against a budget that only covers a few of them, and
|
|
// confirm the early ones succeed while the ones submitted once the
|
|
// budget is drained fail, without corrupting the object.
|
|
testcase("Sequential sponsorship exhaustion across transactions");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
|
|
// Fee exhaustion: enough FeeAmount for 3 of 6 fee-sponsored
|
|
// transactions; RemainingOwnerCount is generous so reserve is never
|
|
// the limiting factor.
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
env(sponsor::set(sponsor, 0, 10, XRP(3)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
for (int i = 1; i <= 6; ++i)
|
|
{
|
|
auto const expectedTer = i <= 3 ? tesSUCCESS : TER{terINSUF_FEE_B};
|
|
env(check::create(alice, bob, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(XRP(1)),
|
|
Ter(expectedTer));
|
|
env.close();
|
|
}
|
|
|
|
auto const sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 3);
|
|
}
|
|
|
|
// Reserve exhaustion: enough RemainingOwnerCount for 3 of 6
|
|
// reserve-sponsored transactions; FeeAmount is generous so fee is
|
|
// never the limiting factor.
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
env(sponsor::set(sponsor, 0, 3, XRP(100)), sponsor::SponseeAcc(alice), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
for (int i = 1; i <= 6; ++i)
|
|
{
|
|
auto const expectedTer = i <= 3 ? tesSUCCESS : TER{tecINSUFFICIENT_RESERVE};
|
|
env(check::create(alice, bob, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(XRP(1)),
|
|
Ter(expectedTer));
|
|
env.close();
|
|
}
|
|
|
|
auto const sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 0);
|
|
BEAST_EXPECT(ownerCount(env, alice) == 3);
|
|
}
|
|
}
|
|
|
|
void
|
|
testSponsoredFreeTierReserve()
|
|
{
|
|
testcase("Sponsored Free-Tier Reserve");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const issuer("issuer");
|
|
Account const sponsor("sponsor");
|
|
|
|
// Trust lines and MPTokens normally skip the reserve check when the
|
|
// holder's ownerCount < 2 (the "free-tier" / first-two-items shortcut). When the
|
|
// tx is sponsored, that shortcut must not apply — the sponsor must
|
|
// still cover the reserve.
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, issuer);
|
|
// Sponsor is funded just below the reserve required to cover a single
|
|
// sponsored item.
|
|
env.fund(reserve(env, 1) - drops(1), sponsor);
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
|
|
MPTTester mptt(env, issuer, {.fund = false});
|
|
mptt.create();
|
|
|
|
// Free-tier trust line cosigned by an undercapitalized sponsor must
|
|
// fail — the holder's free-first-two-items shortcut does not let the
|
|
// sponsor skip the reserve check.
|
|
env(trust(alice, issuer["USD"](100)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tecNO_LINE_INSUF_RESERVE));
|
|
env.close();
|
|
|
|
// Free-tier MPTokenAuthorize must also fail for the same reason.
|
|
env(MPTTester::authorizeJV({.account = alice, .id = mptt.issuanceID()}),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testTransferSponsor()
|
|
{
|
|
testcase("Transfer Sponsor");
|
|
using namespace test::jtx;
|
|
|
|
// Verify preflight checks
|
|
{
|
|
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();
|
|
|
|
// Test invalid flags for SponsorshipTransfer
|
|
{
|
|
// Invalid flag is provided
|
|
env(sponsor::transfer(
|
|
alice,
|
|
(tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd) + 1),
|
|
Ter(temINVALID_FLAG));
|
|
|
|
// No SponsorshipTransfer flag is provided
|
|
env(sponsor::transfer(alice, 0), Ter(temINVALID_FLAG));
|
|
|
|
// Only one of the three valid flags can be set.
|
|
// Setting more than one flag is invalid
|
|
for (auto flag : {
|
|
tfSponsorshipCreate | tfSponsorshipReassign,
|
|
tfSponsorshipCreate | tfSponsorshipEnd,
|
|
tfSponsorshipReassign | tfSponsorshipEnd,
|
|
tfSponsorshipCreate | tfSponsorshipReassign | tfSponsorshipEnd,
|
|
})
|
|
env(sponsor::transfer(alice, flag), Ter(temINVALID_FLAG));
|
|
}
|
|
|
|
// Malformed tests for tfSponsorshipCreate
|
|
{
|
|
// No sponsor field present
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate), Ter(temMALFORMED));
|
|
|
|
// Sponsor field present without spfSponsorReserve
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(temINVALID_FLAG));
|
|
|
|
// Sponsee field present
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::SponseeAcc(bob),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Ter(temMALFORMED));
|
|
}
|
|
|
|
// Malformed tests for tfSponsorshipReassign
|
|
{
|
|
// No sponsor field present
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign), Ter(temMALFORMED));
|
|
|
|
// Sponsor field present without spfSponsorReserve
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(temINVALID_FLAG));
|
|
|
|
// Sponsee field present
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign),
|
|
sponsor::SponseeAcc(bob),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Ter(temMALFORMED));
|
|
}
|
|
|
|
// Malformed tests for tfSponsorshipEnd
|
|
{
|
|
// Sponsor field present
|
|
env(sponsor::transfer(alice, tfSponsorshipEnd),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Ter(temMALFORMED));
|
|
|
|
// SponsorFlags field present
|
|
auto tx = sponsor::transfer(alice, tfSponsorshipEnd);
|
|
tx[sfSponsorFlags.jsonName] = spfSponsorFee;
|
|
env(tx, Ter(temINVALID_FLAG));
|
|
|
|
// Account = Sponsee
|
|
env(sponsor::transfer(alice, tfSponsorshipEnd),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(temMALFORMED));
|
|
}
|
|
}
|
|
|
|
{
|
|
// Invalid SponsorshipEnd permission (sponsor object/sponsor account)
|
|
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.fund(XRP(1000), charlie);
|
|
env.close();
|
|
env(deposit::auth(alice, charlie),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
auto const keylet = keylet::depositPreauth(alice, charlie);
|
|
env(sponsor::transfer(bob, tfSponsorshipEnd, keylet.key),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tecNO_PERMISSION));
|
|
}
|
|
{
|
|
// sponsor object
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(bob, tfSponsorshipEnd),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tecNO_PERMISSION));
|
|
}
|
|
{
|
|
// The provided sfSponsee account does not exist
|
|
// when ending sponsorship.
|
|
Account const ghost("ghost"); // never funded, absent from ledger
|
|
env(sponsor::transfer(sponsor, tfSponsorshipEnd),
|
|
sponsor::SponseeAcc(ghost),
|
|
Ter(terNO_ACCOUNT));
|
|
}
|
|
}
|
|
|
|
{
|
|
// sponsor account
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor1("sponsor1");
|
|
Account const sponsor2("sponsor2");
|
|
env.fund(XRP(10000), alice, bob, sponsor1, sponsor2);
|
|
|
|
// sfSponsor provided but sfSponsorSignature not provided
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Ter(temMALFORMED));
|
|
env.close();
|
|
|
|
adjustAccountXRPBalance(env, sponsor1, accountReserve(env, 2) - drops(1));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
adjustAccountXRPBalance(env, sponsor1, accountReserve(env, 2));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 1);
|
|
auto const sle1 = env.le(keylet::account(alice));
|
|
BEAST_EXPECT(sle1->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id());
|
|
|
|
// transfer sponsor
|
|
adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 2) - drops(1));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 2));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1);
|
|
BEAST_EXPECT(
|
|
!env.le(keylet::account(sponsor1))->isFieldPresent(sfSponsoringAccountCount));
|
|
auto const sle2 = env.le(keylet::account(alice));
|
|
BEAST_EXPECT(sle2->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id());
|
|
|
|
// Reassign to the current sponsor is a no-op and is rejected
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1);
|
|
|
|
// sponsor 2 accounts
|
|
adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 3));
|
|
env(sponsor::transfer(bob, tfSponsorshipCreate),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2));
|
|
env.close();
|
|
|
|
// dissolve sponsors
|
|
adjustAccountXRPBalance(env, alice, accountReserve(env, 1) - drops(1));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipEnd), Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
adjustAccountXRPBalance(env, alice, accountReserve(env, 1));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipEnd));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1);
|
|
auto const sle3 = env.le(keylet::account(alice));
|
|
BEAST_EXPECT(!sle3->isFieldPresent(sfSponsor));
|
|
|
|
env(sponsor::transfer(bob, tfSponsorshipEnd));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(
|
|
!env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringAccountCount));
|
|
auto const sle4 = env.le(keylet::account(bob));
|
|
BEAST_EXPECT(!sle4->isFieldPresent(sfSponsor));
|
|
|
|
// not sponsored
|
|
env(sponsor::transfer(bob, tfSponsorshipEnd), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
}
|
|
{
|
|
// dissolve account sponsorship from sponsor
|
|
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::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.le(alice)->getAccountID(sfSponsor) == sponsor.id());
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1);
|
|
|
|
env(sponsor::transfer(sponsor, tfSponsorshipEnd), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0);
|
|
}
|
|
|
|
{
|
|
// sponsor object (co-signing)
|
|
Env env{*this, testableAmendments()};
|
|
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.close();
|
|
|
|
adjustAccountXRPBalance(env, sponsor1, reserve(env, 1) - drops(1));
|
|
adjustAccountXRPBalance(env, sponsor2, reserve(env, 1) - drops(1));
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
|
|
auto const checkId = keylet::check(alice, seq).key;
|
|
BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr);
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
env(pay(alice, sponsor1, drops(1)));
|
|
env.close();
|
|
|
|
// Invalid ObjectID (not found)
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, keylet::check(alice, 0).key),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1),
|
|
Ter(tecNO_ENTRY));
|
|
env.close();
|
|
|
|
// Invalid Owner
|
|
env(sponsor::transfer(bob, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Reassign an object that is not sponsored yet
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Valid Owner
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
auto const sle1 = env.le(keylet::unchecked(checkId));
|
|
BEAST_EXPECT(sle1->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id());
|
|
|
|
// Create on an object that is already sponsored
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Reassign to the current sponsor is a no-op and is rejected
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1);
|
|
|
|
// transfer sponsor
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
|
|
env(pay(alice, sponsor2, drops(1)));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0);
|
|
auto const sle2 = env.le(keylet::unchecked(checkId));
|
|
BEAST_EXPECT(sle2->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id());
|
|
|
|
// dissolve sponsor: ending an object sponsorship succeeds even
|
|
// when the sponsee lacks sufficient reserve to reclaim the object.
|
|
adjustAccountXRPBalance(env, alice, reserve(env, 1) - drops(1));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipEnd, checkId));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(
|
|
!env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount));
|
|
auto const sle3 = env.le(keylet::unchecked(checkId));
|
|
BEAST_EXPECT(!sle3->isFieldPresent(sfSponsor));
|
|
|
|
// Ending sponsorship on an object that is not sponsored (a ticket,
|
|
// which cannot be sponsored) is rejected.
|
|
adjustAccountXRPBalance(env, alice, reserve(env, 2));
|
|
auto const ticketSeq = env.seq(alice);
|
|
env(ticket::create(alice, 1));
|
|
env.close();
|
|
auto ticketId = keylet::ticket(alice, ticketSeq + 1).key;
|
|
BEAST_EXPECT(env.le(keylet::unchecked(ticketId)));
|
|
env(sponsor::transfer(alice, tfSponsorshipEnd, ticketId), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
env(noop(alice), ticket::Use(ticketSeq + 1));
|
|
env.close();
|
|
}
|
|
{
|
|
// sponsor object (pre-funded + no ltSponsorship entry)
|
|
Env env{*this, testableAmendments()};
|
|
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.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
|
|
auto const checkId = keylet::check(alice, seq).key;
|
|
BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr);
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Ter(terNO_PERMISSION));
|
|
env.close();
|
|
|
|
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor2, spfSponsorReserve));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Ter(terNO_PERMISSION));
|
|
env.close();
|
|
}
|
|
{
|
|
// sponsor object (pre-funded)
|
|
Env env{*this, testableAmendments()};
|
|
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.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
|
|
auto const checkId = keylet::check(alice, seq).key;
|
|
BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr);
|
|
|
|
// insufficient reserve count
|
|
env(sponsor::set_fee(sponsor1, 0, XRP(100)), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
env(sponsor::set_reserve(sponsor1, 0, 100), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
auto checkSle = env.le(keylet::unchecked(checkId));
|
|
BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor1.id());
|
|
auto sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice));
|
|
BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99);
|
|
|
|
// Reassign to the current sponsor is rejected and must not draw
|
|
// down the pre-funded reserve budget
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice));
|
|
BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1);
|
|
|
|
// transfer sponsor
|
|
env(sponsor::set_reserve(sponsor2, 0, 100), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
|
|
sponsor::As(sponsor2, spfSponsorReserve));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0);
|
|
checkSle = env.le(keylet::unchecked(checkId));
|
|
BEAST_EXPECT(checkSle->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(checkSle->getAccountID(sfSponsor) == sponsor2.id());
|
|
sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice));
|
|
BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99);
|
|
auto sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice));
|
|
BEAST_EXPECT(sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 99);
|
|
|
|
// dissolve sponsor
|
|
adjustAccountXRPBalance(env, alice, reserve(env, 1));
|
|
env(sponsor::transfer(alice, tfSponsorshipEnd, checkId));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0);
|
|
BEAST_EXPECT(
|
|
!env.le(keylet::account(sponsor2))->isFieldPresent(sfSponsoringOwnerCount));
|
|
checkSle = env.le(keylet::unchecked(checkId));
|
|
BEAST_EXPECT(!checkSle->isFieldPresent(sfSponsor));
|
|
sponsor2Sle = env.le(keylet::sponsorship(sponsor2, alice));
|
|
BEAST_EXPECT(sponsor2Sle->getFieldU32(sfRemainingOwnerCount) == 99);
|
|
}
|
|
|
|
{
|
|
// Dissolve object sponsorship from sponsor(no-ltSponsorship)
|
|
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();
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
|
|
auto const checkId = keylet::check(alice, seq).key;
|
|
BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr);
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::unchecked(checkId))->getAccountID(sfSponsor) == sponsor.id());
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
// not the owner of the object
|
|
env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(keylet::unchecked(checkId))->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
}
|
|
|
|
{
|
|
// Dissolve object sponsorship from sponsor (with ltSponsorship)
|
|
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();
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
|
|
auto const checkId = keylet::check(alice, seq).key;
|
|
BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr);
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::unchecked(checkId))->getAccountID(sfSponsor) == sponsor.id());
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) ==
|
|
100);
|
|
|
|
// not the owner of the object
|
|
env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(sponsor, tfSponsorshipEnd, checkId), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(keylet::unchecked(checkId))->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) ==
|
|
100);
|
|
}
|
|
|
|
{
|
|
// sponsor trustline
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
|
|
auto const& highAcc = alice > bob ? alice : bob;
|
|
auto const& lowAcc = alice > bob ? bob : alice;
|
|
|
|
for (bool const isIssuerHigh : {false, true})
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
auto const& issuer = isIssuerHigh ? highAcc : lowAcc;
|
|
auto const& user = isIssuerHigh ? lowAcc : highAcc;
|
|
|
|
auto const usd = issuer["usd"];
|
|
auto const currency = usd.currency;
|
|
|
|
env(trust(user, issuer["usd"](100)));
|
|
env.close();
|
|
|
|
auto const trustId = keylet::trustLine(user, issuer, currency);
|
|
BEAST_EXPECT(env.le(trustId));
|
|
|
|
// transfer sponsor
|
|
env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.le(trustId));
|
|
|
|
BEAST_EXPECT(
|
|
env.le(trustId)->getAccountID(isIssuerHigh ? sfLowSponsor : sfHighSponsor) ==
|
|
sponsor.id());
|
|
BEAST_EXPECT(
|
|
!env.le(trustId)->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor));
|
|
|
|
// dissolve sponsor
|
|
env(sponsor::transfer(user, tfSponsorshipEnd, trustId.key));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.le(trustId));
|
|
BEAST_EXPECT(
|
|
!env.le(trustId)->isFieldPresent(isIssuerHigh ? sfLowSponsor : sfHighSponsor));
|
|
BEAST_EXPECT(
|
|
!env.le(trustId)->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor));
|
|
}
|
|
}
|
|
|
|
{
|
|
// invalid transfer
|
|
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();
|
|
|
|
// create owner dir
|
|
env(ticket::create(alice, 1));
|
|
env.close();
|
|
|
|
// AccountRoot
|
|
// Amendments
|
|
// LedgerHashes
|
|
// FeeSettings
|
|
// NegativeUNL
|
|
// DirNode
|
|
auto const keylets = {
|
|
keylet::account(alice),
|
|
// keylet::amendments(),
|
|
keylet::skip(),
|
|
keylet::feeSettings(),
|
|
// keylet::negativeUNL(),
|
|
keylet::ownerDir(alice),
|
|
};
|
|
for (auto const& keylet : keylets)
|
|
{
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
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::ticket(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::nftokenPageMax(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
|
|
testSponsorFee()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
testcase("Sponsor Fee");
|
|
|
|
{
|
|
// co-signing
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
{
|
|
// 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);
|
|
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(2000)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(terNO_ACCOUNT));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
}
|
|
|
|
env.fund(XRP(1000), sponsor);
|
|
env.close();
|
|
|
|
{
|
|
// Sponsor pays the Fee
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto sponsorBalance = env.balance(sponsor);
|
|
|
|
auto const sendAmt = XRP(100);
|
|
auto const feeAmt = XRP(10);
|
|
env(pay(alice, bob, sendAmt),
|
|
Fee(feeAmt),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance + sendAmt);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt);
|
|
}
|
|
|
|
{
|
|
// insufficient balance to pay Fee
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto sponsorBalance = env.balance(sponsor);
|
|
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(2000)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
}
|
|
|
|
{
|
|
// Fee is paid by Sponsor
|
|
// on context reset (tec error)
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto sponsorBalance = env.balance(sponsor);
|
|
auto const feeAmt = XRP(10);
|
|
|
|
env(pay(alice, bob, XRP(20000)),
|
|
Fee(feeAmt),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tecUNFUNDED_PAYMENT));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt);
|
|
}
|
|
|
|
{
|
|
// A co-signed sponsor pays the fee from its own balance, but
|
|
// must never be charged into its own account reserve. With a
|
|
// balance of exactly reserve + fee the fee is still payable,
|
|
// charging the sponsor down to precisely its reserve.
|
|
auto const feeAmt = XRP(10);
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 0) + feeAmt);
|
|
auto const sponsorBalance = env.balance(sponsor);
|
|
|
|
env(noop(alice),
|
|
Fee(feeAmt),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt);
|
|
BEAST_EXPECT(env.balance(sponsor) == reserve(env, 0));
|
|
}
|
|
|
|
{
|
|
// below reserve
|
|
adjustAccountXRPBalance(env, sponsor, env.current()->fees().reserve);
|
|
|
|
env(noop(alice),
|
|
Fee(env.current()->fees().base),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
|
|
env(noop(alice),
|
|
Fee(XRP(10)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
{
|
|
// pre funded
|
|
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();
|
|
|
|
{
|
|
// 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);
|
|
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(2000)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
}
|
|
|
|
env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
{
|
|
// Sponsor pays the Fee
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto sponsorBalance = env.balance(sponsor);
|
|
auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice);
|
|
|
|
auto const sendAmt = XRP(100);
|
|
auto const feeAmt = XRP(10);
|
|
env(pay(alice, bob, sendAmt), Fee(feeAmt), sponsor::As(sponsor, spfSponsorFee));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance + sendAmt);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
BEAST_EXPECT(
|
|
sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee - feeAmt);
|
|
}
|
|
|
|
{
|
|
// insufficient balance to pay Fee
|
|
{
|
|
// > FeeAmount
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto sponsorBalance = env.balance(sponsor);
|
|
auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice);
|
|
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(90) + drops(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee);
|
|
}
|
|
// use all FeeAmount
|
|
{
|
|
// = FeeAmount
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto sponsorBalance = env.balance(sponsor);
|
|
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(90)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(100));
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance + XRP(100));
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
BEAST_EXPECT(
|
|
!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount));
|
|
}
|
|
|
|
// reset FeeAmount and MaxFee
|
|
env(sponsor::del(sponsor), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
env(sponsor::set_fee(sponsor, 0, XRP(10), XRP(1)), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
{
|
|
// > MaxFee
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto sponsorBalance = env.balance(sponsor);
|
|
auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice);
|
|
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(1) + drops(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee);
|
|
}
|
|
}
|
|
|
|
{
|
|
// Fee is paid by Sponsor
|
|
// on context reset (tec error)
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto sponsorBalance = env.balance(sponsor);
|
|
auto sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice);
|
|
auto const feeAmt = XRP(1);
|
|
|
|
env(pay(alice, bob, XRP(20000)),
|
|
Fee(feeAmt),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(tecUNFUNDED_PAYMENT));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
BEAST_EXPECT(
|
|
sponsor::sponsorshipFeeBalance(env, sponsor, alice) == sponsorFee - feeAmt);
|
|
}
|
|
|
|
// make sfFeeAmount absent if tec error and all Fee is paid
|
|
{
|
|
// reset FeeAmount and MaxFee
|
|
env(sponsor::del(sponsor), sponsor::SponseeAcc(alice));
|
|
env(sponsor::set_fee(sponsor, 0, XRP(10)), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount));
|
|
auto sponsorAvailableFee = sponsor::sponsorshipFeeBalance(env, sponsor, alice);
|
|
env(check::cancel(alice, uint256(1)),
|
|
Fee(sponsorAvailableFee),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(tecNO_ENTRY));
|
|
env.close();
|
|
BEAST_EXPECT(
|
|
!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount));
|
|
}
|
|
}
|
|
|
|
// MaxFee cap is enforced in reset() for tec-failing transactions.
|
|
// On a closed ledger view (!view.open()), checkFee returns tecINSUFF_FEE when
|
|
// Fee > MaxFee (not terINSUF_FEE_B), triggering reset()
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const carol("sponsor");
|
|
|
|
env.fund(XRP(10000), alice, carol);
|
|
env.close();
|
|
|
|
// FeeAmount=1000 drops, MaxFee=10 drops
|
|
env(sponsor::set_fee(carol, 0, drops(1000), drops(10)), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
// Apply directly against the closed ledger view (open_ = false) so that
|
|
// checkFee returns tecINSUFF_FEE and reset() is invoked.
|
|
OpenView overlay(&*env.closed());
|
|
|
|
auto jt = env.jt(
|
|
noop(alice),
|
|
Fee(drops(1000)),
|
|
Seq(env.seq(alice)),
|
|
sponsor::As(carol, spfSponsorFee));
|
|
|
|
auto const result = xrpl::apply(env.app(), overlay, *jt.stx, TapNone, env.journal);
|
|
BEAST_EXPECT(result.ter == tecINSUFF_FEE);
|
|
BEAST_EXPECT(result.applied);
|
|
|
|
// Only MaxFee (10 drops) must be deducted, not the full 1000 drops.
|
|
auto const sle = overlay.read(keylet::sponsorship(carol.id(), alice.id()));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->isFieldPresent(sfFeeAmount));
|
|
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()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
// set flag
|
|
env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(10)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terNO_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) ==
|
|
XRP(10));
|
|
|
|
// clear flag
|
|
env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
// Payment is re-applied
|
|
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount));
|
|
}
|
|
|
|
// RequireSignForFee: co-signing should succeed
|
|
{
|
|
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();
|
|
|
|
// set flag
|
|
env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
// pre-funded (no sig) should fail
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terNO_PERMISSION));
|
|
env.close();
|
|
|
|
// co-signing (with sig) should succeed
|
|
env(pay(alice, bob, XRP(100)),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) == XRP(9));
|
|
}
|
|
}
|
|
|
|
void
|
|
testSponsorAccount()
|
|
{
|
|
testcase("Sponsor Account");
|
|
using namespace test::jtx;
|
|
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
Account const sponsor3("sponsor3");
|
|
Account const bob("bob");
|
|
Account const charlie("charlie");
|
|
Account const dave("dave");
|
|
Account const gw("gw");
|
|
auto const usd = gw["usd"];
|
|
|
|
{
|
|
// Disabled
|
|
Env env{*this, testableAmendments() - featureSponsor};
|
|
env.fund(XRP(10000), alice, sponsor);
|
|
env.close();
|
|
env(pay(alice, bob, XRP(100)), Txflags(tfSponsorCreatedAccount), Ter(temDISABLED));
|
|
env.close();
|
|
}
|
|
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, sponsor, sponsor2, sponsor3);
|
|
env.close();
|
|
|
|
// Invalid flags
|
|
for (auto flag : {
|
|
tfNoRippleDirect,
|
|
tfPartialPayment,
|
|
tfLimitQuality,
|
|
})
|
|
{
|
|
env(pay(alice, bob, XRP(100)),
|
|
Txflags(tfSponsorCreatedAccount | flag),
|
|
Ter(temINVALID_FLAG));
|
|
env.close();
|
|
}
|
|
|
|
// Invalid amount(iou)
|
|
env(pay(alice, bob, usd(100)), Txflags(tfSponsorCreatedAccount), Ter(temBAD_AMOUNT));
|
|
env.close();
|
|
|
|
// Sponsored account creation is reserve sponsorship and is only supported for direct XRP
|
|
// payments.
|
|
env(pay(alice, bob, drops(1)),
|
|
Txflags(tfSponsorCreatedAccount),
|
|
Sendmax(usd(2)),
|
|
Ter(temINVALID));
|
|
env.close();
|
|
|
|
env(pay(alice, bob, drops(1)),
|
|
Txflags(tfSponsorCreatedAccount),
|
|
Path(~XRP),
|
|
Ter(temINVALID));
|
|
env.close();
|
|
|
|
// Account is not sponsored by normal Sponsor specification
|
|
{
|
|
env(pay(alice, bob, drops(env.current()->fees().accountReserve(0, 1))),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
auto const bobSle = env.le(keylet::account(bob));
|
|
BEAST_EXPECT(!bobSle->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0);
|
|
}
|
|
|
|
// Use tfSponsorCreatedAccount to sponsor an account
|
|
{
|
|
// to funded account
|
|
env(pay(sponsor2, bob, drops(1)),
|
|
Txflags(tfSponsorCreatedAccount),
|
|
Fee(XRP(1)),
|
|
Ter(tecNO_SPONSOR_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(sponsor2) == XRP(9999));
|
|
|
|
// to non-funded account / insufficient balance for reserve
|
|
env(pay(sponsor2, charlie, XRP(9999) - env.current()->fees().reserve + drops(1)),
|
|
Txflags(tfSponsorCreatedAccount),
|
|
Ter(tecUNFUNDED_PAYMENT));
|
|
env.close();
|
|
|
|
// to non-funded account
|
|
auto const sponsor2BalanceBefore = env.balance(sponsor2);
|
|
env(pay(sponsor2, charlie, drops(1)), Txflags(tfSponsorCreatedAccount), Fee(XRP(1)));
|
|
env.close();
|
|
|
|
auto const charlieSle = env.le(keylet::account(charlie));
|
|
BEAST_EXPECT(charlieSle->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(charlieSle->getAccountID(sfSponsor) == sponsor2.id());
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, charlie) == 0);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1);
|
|
// verify sponsor balance decreased by payment + Fee
|
|
BEAST_EXPECT(env.balance(sponsor2) == sponsor2BalanceBefore - drops(1) - XRP(1));
|
|
}
|
|
{
|
|
// insufficient reserve to sponsor acount
|
|
|
|
auto const sendAmount = drops(1);
|
|
// 2 account reserve + send amount
|
|
auto const requireBalance = accountReserve(env, 2) + sendAmount;
|
|
adjustAccountXRPBalance(env, sponsor3, requireBalance - drops(1));
|
|
env(pay(sponsor3, dave, sendAmount),
|
|
Txflags(tfSponsorCreatedAccount),
|
|
Fee(XRP(1)),
|
|
Ter(tecUNFUNDED_PAYMENT));
|
|
env.close();
|
|
|
|
adjustAccountXRPBalance(env, sponsor3, requireBalance);
|
|
env(pay(sponsor3, dave, sendAmount),
|
|
Txflags(tfSponsorCreatedAccount),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testRequireFlag()
|
|
{
|
|
using namespace test::jtx;
|
|
{
|
|
testcase("SponsorshipRequireSignForReserve");
|
|
|
|
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();
|
|
|
|
// set flag
|
|
env(sponsor::set_reserve(sponsor, tfSponsorshipSetRequireSignForReserve, 10),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
env(check::create(alice, bob, XRP(100)),
|
|
Fee(XRP(10)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Ter(terNO_PERMISSION));
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
|
|
// clear flag
|
|
env(sponsor::set_reserve(sponsor, tfSponsorshipClearRequireSignForReserve, 1),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
// CheckCreate is re-applied
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
}
|
|
|
|
{
|
|
testcase("SponsorshipRequireSignForFee");
|
|
|
|
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();
|
|
|
|
// set flag
|
|
env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
env(check::create(alice, bob, XRP(100)),
|
|
Fee(XRP(10)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terNO_PERMISSION));
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->getFieldAmount(sfFeeAmount) ==
|
|
XRP(10));
|
|
|
|
// clear flag
|
|
env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
// CheckCreate is re-applied
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))->isFieldPresent(sfFeeAmount));
|
|
}
|
|
|
|
{
|
|
testcase("RequireSign flags with prefunded SponsorshipTransfer");
|
|
|
|
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();
|
|
|
|
// alice owns an unsponsored check.
|
|
auto const checkSeq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
auto const checkId = keylet::check(alice, checkSeq).key;
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// Sponsorship with a reserve budget where prefunded (unsigned) use
|
|
// of the reserve budget requires a sponsor signature.
|
|
env(sponsor::set_reserve(sponsor, tfSponsorshipSetRequireSignForReserve, 10),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
// An unsigned SponsorshipTransfer(tfSponsorshipCreate) is rejected
|
|
// by Transactor::checkSponsor (lsfSponsorshipRequireSignForReserve).
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Ter(terNO_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == 10);
|
|
|
|
// With a sponsor signature the transfer succeeds and
|
|
// SponsorshipTransfer::doApply still draws down the prefunded
|
|
// budget (decrementPrefundedReserveCount).
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
auto const checkSle = env.le(keylet::check(alice, checkSeq));
|
|
BEAST_EXPECT(
|
|
checkSle && checkSle->isFieldPresent(sfSponsor) &&
|
|
checkSle->getAccountID(sfSponsor) == sponsor.id());
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == 9);
|
|
|
|
// Mirror for the fee flag on an ordinary prefunded fee-sponsored
|
|
// transaction: unsigned use is rejected by Transactor::checkSponsor
|
|
// (lsfSponsorshipRequireSignForFee), and a co-signed transaction
|
|
// still draws the fee from the prefunded FeeAmount because
|
|
// getFeePayer prefers the Sponsorship object when it exists.
|
|
env(sponsor::set_fee(sponsor, tfSponsorshipSetRequireSignForFee, XRP(10)),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, alice) == XRP(10));
|
|
|
|
env(noop(alice),
|
|
Fee(XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, alice) == XRP(9));
|
|
}
|
|
}
|
|
|
|
void
|
|
testSponsorReserveSimple(bool cosigning)
|
|
{
|
|
testcase("SponsorReserveSimple");
|
|
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();
|
|
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 1) - drops(1));
|
|
|
|
env(check::create(alice, bob, XRP(100)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 1));
|
|
|
|
env(check::create(alice, bob, XRP(100)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
else
|
|
{
|
|
env(sponsor::set_reserve(sponsor, 0, 250), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1));
|
|
|
|
env(check::create(alice, bob, XRP(100)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 2));
|
|
|
|
env(check::create(alice, bob, XRP(100)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
// test helper for both cosigning and pre-funded sponsorship
|
|
template <typename SubmitCallback>
|
|
void
|
|
testEachSponsorship(
|
|
test::jtx::Env& env,
|
|
bool cosigning,
|
|
jtx::Account const& sponsor,
|
|
jtx::Account const& sponsee,
|
|
uint32_t reserveCount,
|
|
uint32_t sponsorReserveCount,
|
|
TER insufficientReserveResult,
|
|
SubmitCallback callback,
|
|
std::optional<std::function<void()>> expected = std::nullopt)
|
|
{
|
|
using namespace test::jtx;
|
|
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 =
|
|
cosigning ? std::optional<Sig>(Sig(sfSponsorSignature, sponsor)) : std::nullopt;
|
|
|
|
auto const sponsorCurrentOwnerCount = ownerCount(env, sponsor) -
|
|
sponsoredOwnerCount(env, sponsor) + sponsoringOwnerCount(env, sponsor);
|
|
|
|
auto submit = [&](TER ter) {
|
|
return [&, ter](json::Value const& jv, auto const&... fN) {
|
|
if (sponsorSig)
|
|
{
|
|
env(jv, fN..., sponsor::As(sponsor, spfSponsorReserve), *sponsorSig, Ter(ter));
|
|
}
|
|
else
|
|
{
|
|
env(jv, fN..., sponsor::As(sponsor, spfSponsorReserve), Ter(ter));
|
|
}
|
|
};
|
|
};
|
|
|
|
// Insufficient Reserve
|
|
{
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(
|
|
env,
|
|
sponsor,
|
|
reserve(env, sponsorCurrentOwnerCount + sponsorReserveCount) - drops(1));
|
|
}
|
|
else
|
|
{
|
|
// cleanup previous sponsorship
|
|
if (env.le(keylet::sponsorship(sponsor, sponsee)))
|
|
{
|
|
env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee));
|
|
env.close();
|
|
}
|
|
|
|
if (sponsorReserveCount > 1)
|
|
{
|
|
env(sponsor::set(sponsor, 0, sponsorReserveCount - 1, XRP(1)),
|
|
sponsor::SponseeAcc(sponsee));
|
|
}
|
|
else
|
|
{
|
|
// just create sponsor object
|
|
env(sponsor::set(sponsor, 0, std::nullopt, XRP(1)),
|
|
sponsor::SponseeAcc(sponsee));
|
|
}
|
|
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
|
|
{
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(
|
|
env, sponsor, reserve(env, sponsorCurrentOwnerCount + sponsorReserveCount));
|
|
}
|
|
else
|
|
{
|
|
// reset sponsorship
|
|
env(sponsor::del(sponsor), sponsor::SponseeAcc(sponsee));
|
|
env(sponsor::set(sponsor, 0, sponsorReserveCount, XRP(1)),
|
|
sponsor::SponseeAcc(sponsee));
|
|
env.close();
|
|
}
|
|
callback(env, submit(tesSUCCESS));
|
|
env.close();
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
if (expected)
|
|
{
|
|
(*expected)();
|
|
}
|
|
else
|
|
{
|
|
BEAST_EXPECT(ownerCount(env, sponsee) - sponseeOwnerCountBefore == reserveCount);
|
|
BEAST_EXPECT(
|
|
sponsoredOwnerCount(env, sponsee) - sponseeSponsoredOwnerCountBefore ==
|
|
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);
|
|
}
|
|
};
|
|
|
|
void
|
|
testCheck(bool cosigning)
|
|
{
|
|
testcase("Check");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const gw("gw");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
|
|
auto const usd = gw["usd"];
|
|
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
// CheckCreate -> Check -> CheckCancel
|
|
|
|
uint32_t seq = 0;
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
seq = env.seq(alice);
|
|
submit(check::create(alice, bob, XRP(1)));
|
|
});
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1); // Check
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
auto const keylet = keylet::check(alice, seq);
|
|
BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
if (cosigning)
|
|
{
|
|
// transfer sponsor
|
|
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();
|
|
|
|
// transfer sponsor
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet.key),
|
|
sponsor::As(sponsor2, spfSponsorReserve));
|
|
env.close();
|
|
}
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1); // Check
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
|
|
|
BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id());
|
|
|
|
// CheckCancel
|
|
env(check::cancel(alice, keylet.key));
|
|
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(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
// CheckCreate -> CheckCash
|
|
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, XRP(1)));
|
|
});
|
|
|
|
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
|
|
|
|
// CheckCash
|
|
auto const checkId2 = keylet::check(alice, seq2).key;
|
|
env(check::cash(bob, checkId2, XRP(1)));
|
|
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);
|
|
}
|
|
|
|
// RippleState sponsor (CheckCashMakesTrustLine)
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, gw, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
env.trust(usd(100), alice);
|
|
env.close();
|
|
env(pay(gw, alice, usd(100)));
|
|
env.close();
|
|
|
|
// CheckCreate -> CheckCash
|
|
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, usd(1)));
|
|
});
|
|
|
|
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
|
|
|
|
auto const keylet = keylet::check(alice, seq2);
|
|
BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
// CheckCash
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
bob,
|
|
1,
|
|
1,
|
|
tecNO_LINE_INSUF_RESERVE,
|
|
[&](Env& env, auto const& submit) { submit(check::cash(bob, keylet.key, usd(1))); },
|
|
[&]() {
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1); // RippleState
|
|
BEAST_EXPECT(ownerCount(env, bob) == 1); // RippleState
|
|
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());
|
|
});
|
|
}
|
|
}
|
|
|
|
void
|
|
testDelegate(bool cosigning)
|
|
{
|
|
testcase("Delegate");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
// DelegateSet
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
submit(delegate::set(alice, bob, {"Payment"}));
|
|
});
|
|
|
|
// 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),
|
|
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, keylet.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(keylet)->getAccountID(sfSponsor) == sponsor2.id());
|
|
|
|
// delete
|
|
env(delegate::set(alice, bob, {}));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
testDepositPreauth(bool cosigning)
|
|
{
|
|
testcase("DepositPreauth");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
auto const credType = std::string("credType");
|
|
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
// DepositPreauthSet
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) { submit(deposit::auth(alice, 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),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2));
|
|
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);
|
|
BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor2.id());
|
|
|
|
// 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(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);
|
|
|
|
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
|
|
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
|
|
testEscrow(bool cosigning)
|
|
{
|
|
testcase("Escrow");
|
|
using namespace test::jtx;
|
|
using namespace std::chrono_literals;
|
|
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
{
|
|
// Native Escrow
|
|
Env env{*this, testableAmendments()};
|
|
auto const baseFee = env.current()->fees().base;
|
|
|
|
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
// EscrowCreate
|
|
uint32_t seq = 0;
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
seq = env.seq(alice);
|
|
submit(
|
|
escrow::create(alice, bob, XRP(100)),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kCancelTime(env.now() + 100s));
|
|
});
|
|
BEAST_EXPECT(
|
|
env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
// transfer sponsor
|
|
if (cosigning)
|
|
{
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).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, keylet::escrow(alice, seq).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(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor2.id());
|
|
|
|
// EscrowFinish
|
|
env(escrow::finish(bob, alice, seq),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kFulfillment(escrow::kFb1),
|
|
Fee(baseFee * 150));
|
|
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);
|
|
}
|
|
|
|
Account const gw("gw");
|
|
auto const usd = gw["usd"];
|
|
{
|
|
// IOU Escrow
|
|
Env env{*this, testableAmendments()};
|
|
auto const baseFee = env.current()->fees().base;
|
|
|
|
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);
|
|
|
|
// EscrowCreate
|
|
uint32_t seq = 0;
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
seq = env.seq(alice);
|
|
submit(
|
|
escrow::create(alice, bob, usd(100)),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kCancelTime(env.now() + 100s));
|
|
});
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
// EscrowFinish
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor2,
|
|
bob,
|
|
1,
|
|
1,
|
|
tecNO_LINE_INSUF_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
submit(
|
|
escrow::finish(bob, alice, seq),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kFulfillment(escrow::kFb1),
|
|
Fee(baseFee * 150));
|
|
});
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) ==
|
|
sponsor2.id());
|
|
}
|
|
{
|
|
// IOU EscrowFinish recycles reserve when the same sponsor backs
|
|
// the escrow being removed and the destination line being created.
|
|
Env env{*this, testableAmendments()};
|
|
auto const baseFee = env.current()->fees().base;
|
|
|
|
env.fund(XRP(1000000), alice, bob, gw, sponsor);
|
|
env.close();
|
|
|
|
env(fset(gw, asfAllowTrustLineLocking));
|
|
env.close();
|
|
|
|
env.trust(usd(1000000), alice);
|
|
env.close();
|
|
env(pay(gw, alice, usd(10000)));
|
|
env.close();
|
|
|
|
uint32_t seq = 0;
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
seq = env.seq(alice);
|
|
submit(
|
|
escrow::create(alice, bob, usd(100)),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kCancelTime(env.now() + 100s));
|
|
});
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 1));
|
|
}
|
|
else
|
|
{
|
|
env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob));
|
|
env.close();
|
|
}
|
|
|
|
if (cosigning)
|
|
{
|
|
env(escrow::finish(bob, alice, seq),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kFulfillment(escrow::kFb1),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(baseFee * 150),
|
|
Ter(tesSUCCESS));
|
|
}
|
|
else
|
|
{
|
|
env(escrow::finish(bob, alice, seq),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kFulfillment(escrow::kFb1),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Fee(baseFee * 150),
|
|
Ter(tesSUCCESS));
|
|
}
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::trustLine(bob, gw, usd.currency))->getAccountID(sfHighSponsor) ==
|
|
sponsor.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()};
|
|
env.fund(XRP(1000000), bob, sponsor);
|
|
env.close();
|
|
|
|
MPTTester mptGw(env, gw, {.holders = {alice}});
|
|
mptGw.create(
|
|
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
|
|
mptGw.authorize({.account = alice});
|
|
auto const mpt = mptGw["MPT"];
|
|
env(pay(gw, alice, mpt(10'000)));
|
|
env.close();
|
|
|
|
// create Escrow from alice to bob
|
|
auto const seq = env.seq(alice);
|
|
env(escrow::create(alice, bob, mpt(100)),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kCancelTime(env.now() + 100s));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
|
|
// 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());
|
|
}
|
|
{
|
|
// MPT EscrowFinish has the same reserve recycling behavior as IOU
|
|
// when it creates the destination MPToken.
|
|
Env env{*this, testableAmendments()};
|
|
auto const baseFee = env.current()->fees().base;
|
|
env.fund(XRP(1000000), bob, sponsor);
|
|
env.close();
|
|
|
|
MPTTester mptGw(env, gw, {.holders = {alice}});
|
|
mptGw.create(
|
|
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
|
|
mptGw.authorize({.account = alice});
|
|
auto const mpt = mptGw["MPT"];
|
|
env(pay(gw, alice, mpt(10'000)));
|
|
env.close();
|
|
|
|
uint32_t seq = 0;
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
seq = env.seq(alice);
|
|
submit(
|
|
escrow::create(alice, bob, mpt(100)),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kCancelTime(env.now() + 100s));
|
|
});
|
|
|
|
BEAST_EXPECT(
|
|
env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 1));
|
|
}
|
|
else
|
|
{
|
|
env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob));
|
|
env.close();
|
|
}
|
|
|
|
if (cosigning)
|
|
{
|
|
env(escrow::finish(bob, alice, seq),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kFulfillment(escrow::kFb1),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(baseFee * 150),
|
|
Ter(tesSUCCESS));
|
|
}
|
|
else
|
|
{
|
|
env(escrow::finish(bob, alice, seq),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kFulfillment(escrow::kFb1),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Fee(baseFee * 150),
|
|
Ter(tesSUCCESS));
|
|
}
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
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
|
|
// 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
|
|
testMPToken(bool cosigning)
|
|
{
|
|
testcase("MPToken");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
// MPTokenIssuanceCreate
|
|
json::Value jv = {};
|
|
jv[sfAccount] = alice.human();
|
|
jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
|
|
MPTID mptid;
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
mptid = makeMptID(env.seq(alice), alice.id());
|
|
submit(jv);
|
|
});
|
|
|
|
// transfer sponsor
|
|
auto const mptIssuanceKeylet = keylet::mptokenIssuance(mptid);
|
|
|
|
if (cosigning)
|
|
{
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, mptIssuanceKeylet.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, mptIssuanceKeylet.key),
|
|
sponsor::As(sponsor2, spfSponsorReserve));
|
|
env.close();
|
|
}
|
|
|
|
// MPTokenAuthorize
|
|
jv = {};
|
|
jv[sfTransactionType] = jss::MPTokenAuthorize;
|
|
jv[sfAccount] = bob.human();
|
|
jv[sfMPTokenIssuanceID] = to_string(mptid);
|
|
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 2));
|
|
env(ticket::create(sponsor, 2)); // adjust for free mptoken
|
|
env.close();
|
|
}
|
|
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
bob,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) { submit(jv); });
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
|
|
// transfer sponsor
|
|
auto const mptTokenKeylet = keylet::mptoken(mptid, bob);
|
|
if (cosigning)
|
|
{
|
|
env(sponsor::transfer(bob, tfSponsorshipReassign, mptTokenKeylet.key),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2));
|
|
env.close();
|
|
}
|
|
else
|
|
{
|
|
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(bob));
|
|
env.close();
|
|
|
|
env(sponsor::transfer(bob, tfSponsorshipReassign, mptTokenKeylet.key),
|
|
sponsor::As(sponsor2, spfSponsorReserve));
|
|
env.close();
|
|
}
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 2);
|
|
|
|
// MPTokenAuthorize Unauthorize
|
|
jv = {};
|
|
jv[sfTransactionType] = jss::MPTokenAuthorize;
|
|
jv[sfAccount] = bob.human();
|
|
jv[sfMPTokenIssuanceID] = to_string(mptid);
|
|
jv[sfFlags] = tfMPTUnauthorize;
|
|
env(jv);
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1);
|
|
|
|
// MPTokenIssuanceDestroy
|
|
jv = {};
|
|
jv[sfTransactionType] = jss::MPTokenIssuanceDestroy;
|
|
jv[sfAccount] = alice.human();
|
|
jv[sfMPTokenIssuanceID] = to_string(mptid);
|
|
env(jv);
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0);
|
|
}
|
|
{
|
|
// check INSUFFICIENT_RESERVE for MPToken
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
// MPTokenAuthorize
|
|
json::Value jv = {};
|
|
jv[sfAccount] = alice.human();
|
|
jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
|
|
auto const mptid = makeMptID(env.seq(alice), alice.id());
|
|
env(jv);
|
|
env.close();
|
|
|
|
// 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();
|
|
|
|
jv = {};
|
|
jv[sfTransactionType] = jss::MPTokenAuthorize;
|
|
jv[sfAccount] = bob.human();
|
|
jv[sfMPTokenIssuanceID] = to_string(mptid);
|
|
// error (non-free mptoken)
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 3) - drops(1));
|
|
env(jv,
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
}
|
|
else
|
|
{
|
|
env(sponsor::set(sponsor, 0, std::nullopt, XRP(1)), sponsor::SponseeAcc(bob));
|
|
env.close();
|
|
|
|
env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
}
|
|
|
|
env(noop(sponsor), ticket::Use(ticketSeq));
|
|
env.close();
|
|
|
|
// pass (free-tier mptoken for the holder, but the sponsor is still
|
|
// charged a reserve increment regardless of the ownerCount < 2 shortcut).
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 2));
|
|
env(jv,
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
else
|
|
{
|
|
env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob));
|
|
env.close();
|
|
env(jv, sponsor::As(sponsor, spfSponsorReserve), Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testPayChan(bool cosigning)
|
|
{
|
|
testcase("PayChan");
|
|
using namespace test::jtx;
|
|
using namespace std::literals::chrono_literals;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
// PayChanCreate
|
|
auto const pk = alice.pk();
|
|
auto const settleDelay = 10s;
|
|
uint256 chan;
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
tecINSUFFICIENT_RESERVE,
|
|
[&](Env& env, auto const& submit) {
|
|
chan = paychan::channel(alice, bob, env.seq(alice));
|
|
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)
|
|
{
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, chan),
|
|
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, chan),
|
|
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(Keylet(ltPAYCHAN, chan))->getAccountID(sfSponsor) == sponsor2.id());
|
|
|
|
env.close(env.now() + settleDelay);
|
|
// PayChanClaim (delete PayChan)
|
|
env(paychan::claim(bob, chan), Txflags(tfClose));
|
|
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);
|
|
}
|
|
|
|
// 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
|
|
testSignerList(bool cosigning)
|
|
{
|
|
testcase("SignerList");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
// SignerListSet
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
alice,
|
|
1,
|
|
1,
|
|
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)
|
|
{
|
|
// invalid signer list owner 1
|
|
// account doesn't have signer list but specified signer list exists
|
|
env(sponsor::transfer(bob, tfSponsorshipReassign, keylet::signerList(alice).key),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecNO_PERMISSION));
|
|
// invalid signer list owner 2
|
|
// account has signer list and specified signer list exists
|
|
env(signers(bob, 1, {{alice, 1}}));
|
|
env.close();
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signerList(bob).key),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecNO_PERMISSION));
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::signerList(alice).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, keylet::signerList(alice).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(keylet::signerList(alice))->getAccountID(sfSponsor) == sponsor2.id());
|
|
|
|
// Delete
|
|
env(signers(alice, NoneT()));
|
|
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);
|
|
}
|
|
|
|
// Legacy (pre-MultiSignReserve) SignerLists lack lsfOneOwnerCount and cost
|
|
// 2 + signer_count owner units, whereas modern lists cost 1.
|
|
void
|
|
testLegacySignerListReserve()
|
|
{
|
|
testcase("Legacy SignerList sponsorship reserve");
|
|
using namespace test::jtx;
|
|
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const carol("carol");
|
|
Account const dave("dave");
|
|
Account const sponsor("sponsor");
|
|
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, carol, dave, sponsor);
|
|
env.close();
|
|
|
|
// Modern 3-signer list: weight 1, lsfOneOwnerCount set.
|
|
env(signers(alice, 1, {{bob, 1}, {carol, 1}, {dave, 1}}));
|
|
env.close();
|
|
|
|
auto const signerListKeylet = keylet::signerList(alice.id());
|
|
auto const sponsorKeylet = keylet::sponsorship(sponsor.id(), alice.id());
|
|
std::uint32_t const legacyWeight = 5; // 2 + 3 signers
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
|
|
// Pre-fund exactly the legacy weight
|
|
env(sponsor::set_reserve(sponsor, 0, legacyWeight), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
|
|
BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == legacyWeight);
|
|
|
|
// Synthesize a pre-MultiSignReserve list: clear lsfOneOwnerCount and
|
|
// restore the owner's OwnerCount to the legacy weight.
|
|
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) -> bool {
|
|
auto signerList = std::make_shared<SLE>(*view.read(signerListKeylet));
|
|
auto account = std::make_shared<SLE>(*view.read(keylet::account(alice.id())));
|
|
signerList->clearFlag(lsfOneOwnerCount);
|
|
account->setFieldU32(sfOwnerCount, legacyWeight);
|
|
view.rawReplace(signerList);
|
|
view.rawReplace(account);
|
|
return true;
|
|
});
|
|
if (auto const sle = env.le(signerListKeylet); BEAST_EXPECT(sle))
|
|
BEAST_EXPECT((sle->getFlags() & lsfOneOwnerCount) == 0);
|
|
BEAST_EXPECT(ownerCount(env, alice) == legacyWeight);
|
|
|
|
// Create must charge the full legacy weight (5), not 1: the bug bumped
|
|
// the counters by 1 and left 4 pre-funded units unspent.
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, signerListKeylet.key),
|
|
sponsor::As(sponsor, spfSponsorReserve));
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == legacyWeight);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == legacyWeight);
|
|
// All pre-funded units consumed (drained to absent).
|
|
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
|
|
|
// Delete unwinds the legacy weight; create bumped by the same amount, so
|
|
// the counters return to 0 (the bug bumped by 1 -> underflow on delete).
|
|
env(signers(alice, NoneT()));
|
|
|
|
BEAST_EXPECT(!env.le(signerListKeylet));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
if (auto const sle = env.le(sponsorKeylet); BEAST_EXPECT(sle))
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount));
|
|
}
|
|
|
|
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::trustLine(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)
|
|
{
|
|
testcase("TrustSet");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const charlie("charlie");
|
|
Account const sponsor("sponsor");
|
|
Account const sponsor2("sponsor2");
|
|
|
|
auto const validateSponsoredTrustline =
|
|
[&](std::shared_ptr<const SLE> const& sle, bool isIssuerHigh, Account const& sponsor) {
|
|
BEAST_EXPECT(
|
|
sle->getAccountID(isIssuerHigh ? sfLowSponsor : sfHighSponsor) == sponsor.id());
|
|
BEAST_EXPECT(!sle->isFieldPresent(isIssuerHigh ? sfHighSponsor : sfLowSponsor));
|
|
};
|
|
|
|
auto const& highAcc = alice > bob ? alice : bob;
|
|
auto const& lowAcc = alice > bob ? bob : alice;
|
|
|
|
// create and delete
|
|
for (bool const isIssuerHigh : {false, true})
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, charlie, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
auto const& issuer = isIssuerHigh ? highAcc : lowAcc;
|
|
auto const& user = isIssuerHigh ? lowAcc : highAcc;
|
|
|
|
auto const usd = issuer["usd"];
|
|
auto const currency = usd.currency;
|
|
|
|
// create TrustLine
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 2));
|
|
env(ticket::create(sponsor, 2)); // adjust for free trustline
|
|
env.close();
|
|
}
|
|
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
user,
|
|
1,
|
|
1,
|
|
tecNO_LINE_INSUF_RESERVE,
|
|
[&](Env& env, auto const& submit) { submit(trust(user, usd(100))); });
|
|
|
|
auto const keylet = keylet::trustLine(user, issuer, currency);
|
|
|
|
if (cosigning)
|
|
{
|
|
// invalid owner
|
|
env(sponsor::transfer(charlie, tfSponsorshipReassign, keylet.key),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecNO_PERMISSION));
|
|
// invalid reserve owner
|
|
env(sponsor::transfer(issuer, tfSponsorshipReassign, keylet.key),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecNO_PERMISSION));
|
|
env(sponsor::transfer(user, tfSponsorshipReassign, keylet.key),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2));
|
|
env.close();
|
|
}
|
|
else
|
|
{
|
|
env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(user));
|
|
env.close();
|
|
env(sponsor::transfer(user, tfSponsorshipReassign, keylet.key),
|
|
sponsor::As(sponsor2, spfSponsorReserve));
|
|
env.close();
|
|
}
|
|
|
|
// delete TrustLine
|
|
env(trust(user, usd(0)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, user) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, user) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
|
|
BEAST_EXPECT(!env.le(keylet));
|
|
}
|
|
|
|
// update
|
|
for (bool const isIssuerHigh : {false, true})
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, sponsor, sponsor2);
|
|
env.close();
|
|
|
|
auto const& issuer = isIssuerHigh ? highAcc : lowAcc;
|
|
auto const& user = isIssuerHigh ? lowAcc : highAcc;
|
|
|
|
auto const usd = issuer["usd"];
|
|
auto const currency = usd.currency;
|
|
|
|
// create TrustLine from issuer
|
|
env(trust(issuer, user["usd"](100)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.le(keylet::trustLine(user, issuer, currency)));
|
|
|
|
if (cosigning)
|
|
{
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 2));
|
|
env(ticket::create(sponsor, 2)); // adjust for free trustline
|
|
env.close();
|
|
}
|
|
|
|
testEachSponsorship(
|
|
env,
|
|
cosigning,
|
|
sponsor,
|
|
user,
|
|
1,
|
|
1,
|
|
tecINSUF_RESERVE_LINE,
|
|
[&](Env& env, auto const& submit) { submit(trust(user, usd(100))); });
|
|
|
|
auto const line = env.le(keylet::trustLine(user, issuer, currency));
|
|
validateSponsoredTrustline(line, isIssuerHigh, sponsor);
|
|
|
|
// update TrustLine from user to clear reserve
|
|
env(trust(user, usd(0)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, user) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, user) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
BEAST_EXPECT(env.le(keylet::trustLine(user, issuer, currency)));
|
|
|
|
// remove TrustLine from issuer
|
|
env(trust(issuer, user["usd"](0)));
|
|
env.close();
|
|
BEAST_EXPECT(!env.le(keylet::trustLine(user, issuer, currency)));
|
|
}
|
|
|
|
// both High and Low sponsored
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
// create TrustLines
|
|
env(trust(alice, bob["usd"](100)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
env(trust(bob, alice["usd"](100)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
auto sle = env.le(keylet::trustLine(alice, bob, alice["usd"].currency));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle->isFlag(lsfHighReserve));
|
|
BEAST_EXPECT(sle->isFlag(lsfLowReserve));
|
|
BEAST_EXPECT(sle->getAccountID(sfHighSponsor) == sponsor.id());
|
|
BEAST_EXPECT(sle->getAccountID(sfLowSponsor) == sponsor.id());
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2);
|
|
|
|
// clear TrustLines
|
|
env(trust(alice, bob["usd"](0)));
|
|
env.close();
|
|
env(trust(bob, alice["usd"](0)));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::trustLine(alice, bob, alice["usd"].currency));
|
|
BEAST_EXPECT(!sle);
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
testAccountDelete()
|
|
{
|
|
testcase("AccountDelete");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
|
|
{
|
|
// Delete Sponsor/Sponsee Account with ltSponsorship (tecHAS_OBLIGATIONS)
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
// set sponsor
|
|
env(sponsor::set(sponsor, 0, 100, XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
incLgrSeqForAccDel(env, sponsor);
|
|
|
|
auto const keylet = keylet::sponsorship(sponsor, alice);
|
|
auto const sponsorObj = env.le(keylet);
|
|
BEAST_EXPECT(sponsorObj);
|
|
|
|
// AccountDelete
|
|
auto const requiredFee = drops(env.current()->fees().increment);
|
|
env(acctdelete(alice, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
|
|
env(acctdelete(sponsor, bob), Fee(requiredFee), Ter(tecHAS_OBLIGATIONS));
|
|
}
|
|
|
|
{
|
|
// Delete SponsoredAccount
|
|
Env env{*this, testableAmendments()};
|
|
env.memoize(alice);
|
|
env.fund(XRP(1000000), bob, sponsor);
|
|
env.close();
|
|
|
|
// create SponsoredAccount
|
|
env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount));
|
|
env.close();
|
|
|
|
incLgrSeqForAccDel(env, alice);
|
|
|
|
// AccountDelete: destination = non-sponsor
|
|
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);
|
|
|
|
incLgrSeqForAccDel(env, alice);
|
|
|
|
// AccountDelete: destination = sponsor
|
|
env(acctdelete(alice, sponsor), Fee(requiredFee), Ter(tesSUCCESS));
|
|
|
|
auto const sponsorSle2 = env.le(keylet::account(sponsor));
|
|
BEAST_EXPECT(!sponsorSle2->isFieldPresent(sfSponsoringAccountCount));
|
|
}
|
|
|
|
{
|
|
// Sponsor with sfSponsoringOwnerCount cannot delete (tecHAS_OBLIGATIONS)
|
|
Env env{*this, testableAmendments()};
|
|
Account const gw("gw");
|
|
env.fund(XRP(1000000), alice, bob, sponsor, gw);
|
|
env.close();
|
|
|
|
auto const usd = gw["usd"];
|
|
|
|
// Create a trust line for alice
|
|
env(trust(alice, usd(1000)));
|
|
env.close();
|
|
|
|
// Transfer reserve sponsorship of trust line to sponsor
|
|
auto const trustId = keylet::trustLine(alice, gw, usd.currency);
|
|
BEAST_EXPECT(env.le(trustId));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, trustId.key),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
// Verify sfSponsoringOwnerCount is set on sponsor
|
|
auto const sponsorSle = env.le(keylet::account(sponsor));
|
|
BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringOwnerCount));
|
|
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);
|
|
}
|
|
|
|
{
|
|
// Sponsor with sfSponsoringAccountCount cannot delete (tecHAS_OBLIGATIONS)
|
|
Env env{*this, testableAmendments()};
|
|
env.memoize(alice);
|
|
env.fund(XRP(1000000), bob, sponsor);
|
|
env.close();
|
|
|
|
// Create SponsoredAccount (sets sfSponsoringAccountCount on sponsor)
|
|
env(pay(sponsor, alice, XRP(10000)), Txflags(tfSponsorCreatedAccount));
|
|
env.close();
|
|
|
|
// Verify sfSponsoringAccountCount is set on sponsor
|
|
auto const sponsorSle = env.le(keylet::account(sponsor));
|
|
BEAST_EXPECT(sponsorSle->isFieldPresent(sfSponsoringAccountCount));
|
|
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);
|
|
}
|
|
|
|
{
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
testcase("Fee-sponsored AccountDelete");
|
|
|
|
{
|
|
// Co-signed: the sponsor pays the special owner-reserve fee
|
|
// (AccountDelete::calculateBaseFee = calculateOwnerReserveFee =
|
|
// fees().increment) from its own balance, floored at its own
|
|
// reserve (Transactor::checkFee / payFee for
|
|
// FeePayerType::SponsorCoSigned).
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
incLgrSeqForAccDel(env, alice);
|
|
|
|
STAmount const requiredFee = drops(env.current()->fees().increment);
|
|
// Sponsor balance = own reserve + the AccountDelete fee: paying
|
|
// the fee charges the sponsor down to exactly its reserve.
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 0) + requiredFee);
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const bobBalance = env.balance(bob);
|
|
env(acctdelete(alice, bob),
|
|
Fee(requiredFee),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(keylet::account(alice)));
|
|
// alice's entire balance went to bob; the fee came from the
|
|
// sponsor.
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance + aliceBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == reserve(env, 0));
|
|
}
|
|
|
|
{
|
|
// Co-signed: the sponsor may not dip below its own reserve to pay
|
|
// the fee (Transactor::checkFee: SponsorCoSigned spendable =
|
|
// balance - reserve).
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
incLgrSeqForAccDel(env, alice);
|
|
|
|
STAmount const requiredFee = drops(env.current()->fees().increment);
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 0) + requiredFee - drops(1));
|
|
|
|
env(acctdelete(alice, bob),
|
|
Fee(requiredFee),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
BEAST_EXPECT(env.le(keylet::account(alice)));
|
|
}
|
|
|
|
{
|
|
// Pre-funded: FeeAmount below the AccountDelete fee.
|
|
// Transactor::checkFee: prefunded maxSpendable = FeeAmount, which
|
|
// is less than the fee paid -> terINSUF_FEE_B on an open ledger.
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
STAmount const requiredFee = drops(env.current()->fees().increment);
|
|
env(sponsor::set_fee(sponsor, 0, requiredFee - drops(1)), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
incLgrSeqForAccDel(env, alice);
|
|
|
|
env(acctdelete(alice, bob),
|
|
Fee(requiredFee),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
BEAST_EXPECT(env.le(keylet::account(alice)));
|
|
}
|
|
|
|
{
|
|
// Pre-funded: MaxFee below the AccountDelete fee.
|
|
// Transactor::checkFee: prefunded maxSpendable =
|
|
// min(FeeAmount, MaxFee) < fee paid -> terINSUF_FEE_B.
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
STAmount const requiredFee = drops(env.current()->fees().increment);
|
|
env(sponsor::set_fee(sponsor, 0, requiredFee, requiredFee - drops(1)),
|
|
sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
incLgrSeqForAccDel(env, alice);
|
|
|
|
env(acctdelete(alice, bob),
|
|
Fee(requiredFee),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
BEAST_EXPECT(env.le(keylet::account(alice)));
|
|
}
|
|
|
|
{
|
|
// Pre-funded with a sufficient FeeAmount: the fee check passes,
|
|
// but the Sponsorship object itself is an obligation of the
|
|
// sponsee, so the delete fails with tecHAS_OBLIGATIONS
|
|
// (AccountDelete::preclaim) and the fee is still drawn from the
|
|
// pre-funded FeeAmount on context reset (Transactor::reset).
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
STAmount const requiredFee = drops(env.current()->fees().increment);
|
|
env(sponsor::set_fee(sponsor, 0, XRP(1000)), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
incLgrSeqForAccDel(env, alice);
|
|
|
|
env(acctdelete(alice, bob),
|
|
Fee(requiredFee),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(tecHAS_OBLIGATIONS));
|
|
env.close();
|
|
BEAST_EXPECT(env.le(keylet::account(alice)));
|
|
BEAST_EXPECT(
|
|
sponsor::sponsorshipFeeBalance(env, sponsor, alice) == XRP(1000) - requiredFee);
|
|
}
|
|
}
|
|
|
|
void
|
|
testDelegatePermission()
|
|
{
|
|
testcase("DelegatePermission");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const carol("carol");
|
|
|
|
//
|
|
// SponsorshipTransfer is not delegable.
|
|
//
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, carol);
|
|
env.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
|
|
auto const keylet = keylet::check(alice, seq);
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key),
|
|
sponsor::As(bob, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, bob),
|
|
delegate::As(carol),
|
|
Ter(temINVALID));
|
|
|
|
env(delegate::set(alice, carol, {"SponsorshipTransfer"}), Ter(temMALFORMED));
|
|
}
|
|
//
|
|
// test send SponsorshipSet on other's behalf
|
|
//
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, carol);
|
|
env.close();
|
|
|
|
env(sponsor::set(alice, 0, 100, XRP(100)),
|
|
sponsor::SponseeAcc(bob),
|
|
delegate::As(carol),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
|
|
env(delegate::set(alice, carol, {"SponsorshipSet"}));
|
|
env.close();
|
|
|
|
env(sponsor::set(alice, 0, 100, XRP(100)),
|
|
sponsor::SponseeAcc(bob),
|
|
delegate::As(carol),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testDelegateBlockReserveSponsor()
|
|
{
|
|
testcase("Delegate Block Reserve Sponsor");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const carol("carol");
|
|
Account const sponsor("sponsor");
|
|
|
|
// Co-signed reserve sponsorship is blocked for delegated transactions.
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, carol, sponsor);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"CheckCreate"}));
|
|
env.close();
|
|
|
|
env(check::create(alice, carol, XRP(1)),
|
|
delegate::As(bob),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(temINVALID));
|
|
}
|
|
|
|
// Pre-funded reserve sponsorship is blocked for delegated transactions.
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000000), alice, bob, carol, sponsor);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"CheckCreate"}));
|
|
env(sponsor::set_reserve(sponsor, 0, 1), sponsor::SponseeAcc(bob));
|
|
env.close();
|
|
|
|
env(check::create(alice, carol, XRP(1)),
|
|
delegate::As(bob),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Ter(temINVALID));
|
|
}
|
|
}
|
|
|
|
void
|
|
testDelegateSponsorFeePayer()
|
|
{
|
|
testcase("Delegate Sponsor Fee Payer");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const carol("carol");
|
|
Account const sponsor("sponsor");
|
|
|
|
// Co-signing: the sponsor account pays the delegated transaction fee.
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, carol, sponsor);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const carolBalance = env.balance(carol);
|
|
auto const sponsorBalance = env.balance(sponsor);
|
|
auto const sendAmt = XRP(100);
|
|
auto const feeAmt = XRP(10);
|
|
|
|
env(pay(alice, carol, sendAmt),
|
|
delegate::As(bob),
|
|
Fee(feeAmt),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt);
|
|
// sponsor pays the fee
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - feeAmt);
|
|
}
|
|
|
|
// Pre-funded: the Sponsorship object for sponsorship(sponsor, delegate)
|
|
// pays the fee.
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, bob, carol, sponsor);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env(sponsor::set_fee(sponsor, 0, XRP(100)), sponsor::SponseeAcc(bob));
|
|
env.close();
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const carolBalance = env.balance(carol);
|
|
auto const sponsorBalance = env.balance(sponsor);
|
|
auto const sponsorFee = sponsor::sponsorshipFeeBalance(env, sponsor, bob);
|
|
auto const sendAmt = XRP(100);
|
|
auto const feeAmt = XRP(10);
|
|
|
|
// verify sponsorship(sponsor, alice) is not present, because we are testing
|
|
// sponsorship(sponsor, bob) will pay the fee. bob is sfDelegate.
|
|
BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice)));
|
|
|
|
env(pay(alice, carol, sendAmt),
|
|
delegate::As(bob),
|
|
Fee(feeAmt),
|
|
sponsor::As(sponsor, spfSponsorFee));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
// sponsorship(sponsor, bob) pays the fee, bob is sfDelegate
|
|
BEAST_EXPECT(sponsor::sponsorshipFeeBalance(env, sponsor, bob) == sponsorFee - feeAmt);
|
|
}
|
|
}
|
|
|
|
void
|
|
testBatch()
|
|
{
|
|
testcase("Batch");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const sponsor("sponsor");
|
|
|
|
//
|
|
// outer transaction
|
|
//
|
|
{
|
|
// test outer transaction with co-signing sponsor
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(batch::outer(alice, seq, XRP(1), tfAllOrNothing),
|
|
batch::Inner(noop(alice), seq + 1),
|
|
batch::Inner(ticket::create(alice, 1), seq + 2),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// does not affect reserve
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
|
|
// Fee is paid by sponsor
|
|
BEAST_EXPECT(env.balance(alice) == XRP(1000));
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(1000 - 1));
|
|
}
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
// spfSponsorReserve on outer Batch is rejected
|
|
for (auto const flags : {spfSponsorReserve | spfSponsorFee, spfSponsorReserve})
|
|
{
|
|
auto const seq = env.seq(alice);
|
|
env(batch::outer(alice, seq, XRP(1), tfAllOrNothing),
|
|
batch::Inner(noop(alice), seq + 1),
|
|
batch::Inner(noop(alice), seq + 2),
|
|
sponsor::As(sponsor, flags),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(temINVALID_FLAG));
|
|
env.close();
|
|
}
|
|
}
|
|
{
|
|
// test outer transaction with prefunded sponsor
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob);
|
|
env.fund(XRP(1001), sponsor);
|
|
env.close();
|
|
|
|
env(sponsor::set(sponsor, 0, 100, XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(batch::outer(alice, seq, XRP(1), tfAllOrNothing),
|
|
batch::Inner(noop(alice), seq + 1),
|
|
batch::Inner(ticket::create(alice, 1), seq + 2),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// does not affect reserve
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
|
|
// Fee is paid by sponsor object
|
|
BEAST_EXPECT(env.balance(alice) == XRP(1000));
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(900));
|
|
|
|
auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sponsorshipSle);
|
|
BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100 - 1));
|
|
BEAST_EXPECT(sponsorshipSle->at(sfRemainingOwnerCount) == 100);
|
|
}
|
|
//
|
|
// Inner transaction
|
|
//
|
|
{
|
|
// test invalid Inner transaction with co-signing sponsor
|
|
Account const signerAccount("signer");
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor, signerAccount);
|
|
env.close();
|
|
|
|
env(signers(sponsor, 1, {Signer(signerAccount, 1)}));
|
|
env.close();
|
|
|
|
{
|
|
auto jt = env.jtnofill(
|
|
noop(alice),
|
|
sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
jt.jv.removeMember(sfTxnSignature.jsonName);
|
|
|
|
auto const seq = env.seq(alice);
|
|
// should fail because Inner transaction cannot include SponsorSignature with
|
|
// TxnSignature
|
|
BEAST_EXPECT(jt.jv[sfSponsorSignature.jsonName].isMember(sfTxnSignature.jsonName));
|
|
env(batch::outer(alice, seq, XRP(1), tfAllOrNothing),
|
|
batch::Inner(jt.jv, seq + 1),
|
|
batch::Inner(ticket::create(alice, 1), seq + 2),
|
|
Ter(temBAD_SIGNATURE));
|
|
}
|
|
|
|
{
|
|
auto jt = env.jtnofill(
|
|
noop(alice),
|
|
sponsor::As(sponsor, spfSponsorReserve | spfSponsorFee),
|
|
Msig(sfSponsorSignature, sponsor, signerAccount));
|
|
jt.jv.removeMember(sfTxnSignature.jsonName);
|
|
|
|
auto const seq = env.seq(alice);
|
|
// should fail because Inner transaction cannot include SponsorSignature with
|
|
// Signers
|
|
BEAST_EXPECT(jt.jv[sfSponsorSignature.jsonName].isMember(sfSigners.jsonName));
|
|
env(batch::outer(alice, seq, XRP(1), tfAllOrNothing),
|
|
batch::Inner(jt.jv, seq + 1),
|
|
batch::Inner(ticket::create(alice, 1), seq + 2),
|
|
Ter(temBAD_SIGNER));
|
|
}
|
|
|
|
{
|
|
auto jt = env.jtnofill(
|
|
noop(alice),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
jt.jv.removeMember(sfTxnSignature.jsonName);
|
|
jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName);
|
|
jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = "";
|
|
|
|
auto const seq = env.seq(alice);
|
|
// 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),
|
|
Ter(temBAD_SIGNER));
|
|
}
|
|
}
|
|
|
|
{
|
|
// test Inner transaction with prefunded sponsor
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob);
|
|
env.fund(XRP(1001), sponsor);
|
|
env.close();
|
|
|
|
env(sponsor::set(sponsor, 0, 100, XRP(100)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(XRP(1)),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(900));
|
|
|
|
auto jt = env.jtnofill(
|
|
check::create(alice, bob, XRP(1)), sponsor::As(sponsor, spfSponsorReserve));
|
|
// remove txn signature since it is filled by env.jtnofill()
|
|
jt.jv.removeMember(jss::TxnSignature);
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(batch::outer(alice, seq, XRP(1), tfAllOrNothing),
|
|
batch::Inner(noop(alice), seq + 1),
|
|
batch::Inner(jt.jv, seq + 2),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// affect sponsor reserve
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
// Fee is paid by outer transaction originator (alice)
|
|
BEAST_EXPECT(env.balance(alice) == XRP(999));
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(900));
|
|
|
|
// reserve count is decreased
|
|
auto const sponsorshipSle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sponsorshipSle);
|
|
BEAST_EXPECT(sponsorshipSle->at(sfFeeAmount) == XRP(100));
|
|
BEAST_EXPECT(sponsorshipSle->at(sfRemainingOwnerCount) == 99);
|
|
}
|
|
|
|
{
|
|
// test Inner transaction with co-signing sponsor
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
auto jt = env.jtnofill(
|
|
check::create(alice, bob, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
// remove txn signature since it is filled by env.jtnofill()
|
|
jt.jv.removeMember(sfTxnSignature.jsonName);
|
|
jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName);
|
|
jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = "";
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(batch::outer(alice, seq, XRP(1), tfAllOrNothing),
|
|
batch::Inner(noop(alice), seq + 1),
|
|
batch::Inner(jt.jv, seq + 2),
|
|
batch::Sig(sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// affect sponsor reserve
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
// Fee is paid by outer transaction originator (alice)
|
|
BEAST_EXPECT(env.balance(alice) == XRP(999));
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(1000));
|
|
}
|
|
|
|
// Inner tx with sfSponsor + spfSponsorFee (pre-funded) is rejected
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
// Create pre-funded sponsorship
|
|
env(sponsor::set(sponsor, 0, 0, XRP(1)), sponsor::SponseeAcc(alice), Fee(XRP(1)));
|
|
env.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
auto const batchFee = batch::calcBatchFee(env, 0, 2);
|
|
|
|
// Create inner transaction with sponsor fee
|
|
auto innerPay = pay(alice, bob, XRP(1));
|
|
innerPay[sfSponsor.jsonName] = sponsor.human();
|
|
innerPay[sfSponsorFlags.jsonName] = static_cast<std::uint32_t>(spfSponsorFee);
|
|
|
|
// Should be rejected with temBAD_FEE
|
|
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
|
|
batch::Inner(innerPay, seq + 1),
|
|
batch::Inner(noop(alice), seq + 2),
|
|
Ter(temINVALID_FLAG));
|
|
env.close();
|
|
}
|
|
|
|
// Inner tx with sfSponsor + spfSponsorFee (co-signed) is rejected
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
auto const batchFee = batch::calcBatchFee(env, 1, 2);
|
|
|
|
auto innerPay = pay(alice, bob, XRP(1));
|
|
innerPay[sfSponsor.jsonName] = sponsor.human();
|
|
innerPay[sfSponsorFlags.jsonName] = static_cast<std::uint32_t>(spfSponsorFee);
|
|
|
|
// Should be rejected with temBAD_FEE
|
|
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
|
|
batch::Inner(innerPay, seq + 1),
|
|
batch::Inner(noop(alice), seq + 2),
|
|
batch::Sig(sponsor),
|
|
Ter(temINVALID_FLAG));
|
|
env.close();
|
|
}
|
|
|
|
// Inner tx with spfSponsorFee + spfSponsorReserve is rejected
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
auto const batchFee = batch::calcBatchFee(env, 0, 2);
|
|
|
|
auto innerTx = check::create(alice, bob, XRP(1));
|
|
innerTx[sfSponsor.jsonName] = sponsor.human();
|
|
innerTx[sfSponsorFlags.jsonName] =
|
|
static_cast<std::uint32_t>(spfSponsorFee | spfSponsorReserve);
|
|
|
|
// Should be rejected with temBAD_FEE (fee sponsorship check comes first)
|
|
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
|
|
batch::Inner(innerTx, seq + 1),
|
|
batch::Inner(noop(alice), seq + 2),
|
|
Ter(temINVALID_FLAG));
|
|
env.close();
|
|
}
|
|
|
|
// Outer batch tx with sponsor fee is allowed
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
auto const seq = env.seq(bob);
|
|
auto const batchFee = batch::calcBatchFee(env, 0, 2);
|
|
|
|
// Outer batch with sponsor fee should work fine
|
|
env(batch::outer(bob, seq, batchFee, tfAllOrNothing),
|
|
batch::Inner(noop(bob), seq + 1),
|
|
batch::Inner(noop(bob), seq + 2),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// Sponsor paid the fee
|
|
BEAST_EXPECT(env.balance(bob) == XRP(1000));
|
|
}
|
|
|
|
// Inner tx co-signed by a sponsor who IS the outer Batch submitter:
|
|
// Batch::preflightSigValidated skips the sponsor when collecting
|
|
// requiredSigners (sponsor == outerAccount), so no BatchSigners entry
|
|
// is needed for the sponsor - it authorizes the batch by signing the
|
|
// outer transaction itself.
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
auto jt = env.jtnofill(
|
|
check::create(alice, bob, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
jt.jv.removeMember(sfTxnSignature.jsonName);
|
|
jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName);
|
|
jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = "";
|
|
|
|
auto const aliceSeq = env.seq(alice);
|
|
auto const seq = env.seq(sponsor);
|
|
auto const batchFee = batch::calcBatchFee(env, 1, 2);
|
|
|
|
// alice (the inner authorizer) is the only required batch signer.
|
|
env(batch::outer(sponsor, seq, batchFee, tfAllOrNothing),
|
|
batch::Inner(jt.jv, aliceSeq),
|
|
batch::Inner(noop(sponsor), seq + 1),
|
|
batch::Sig(alice),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
auto const checkSle = env.le(keylet::check(alice, aliceSeq));
|
|
BEAST_EXPECT(checkSle && checkSle->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
// The outer submitter (the sponsor) pays the batch fee.
|
|
BEAST_EXPECT(env.balance(alice) == XRP(1000));
|
|
BEAST_EXPECT(env.balance(sponsor) == XRP(1000) - drops(batchFee));
|
|
}
|
|
|
|
// Inner sponsor-signature object with a non-empty SigningPubKey (but
|
|
// otherwise batch-marker shaped) is rejected by Batch::preflight's
|
|
// checkSignatureFields with temBAD_REGKEY.
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(1000), alice, bob, sponsor);
|
|
env.close();
|
|
|
|
auto jt = env.jtnofill(
|
|
check::create(alice, bob, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
jt.jv.removeMember(sfTxnSignature.jsonName);
|
|
jt.jv[sfSponsorSignature.jsonName].removeMember(sfTxnSignature.jsonName);
|
|
// Leave the sponsor's SigningPubKey (filled by the Sig helper)
|
|
// in place: inner sponsor signatures must be empty markers.
|
|
BEAST_EXPECT(
|
|
!jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName].asString().empty());
|
|
|
|
auto const seq = env.seq(alice);
|
|
auto const batchFee = batch::calcBatchFee(env, 1, 2);
|
|
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
|
|
batch::Inner(jt.jv, seq + 1),
|
|
batch::Inner(noop(alice), seq + 2),
|
|
batch::Sig(sponsor),
|
|
Ter(temBAD_REGKEY));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
// 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}));
|
|
|
|
// AMM transactions are not in the allow-list.
|
|
checkBlocked(AMM::createJv(alice.id(), XRP(10), bob["USD"](10), 0));
|
|
|
|
// OracleSet is not in the allow-list. The transaction only needs to be
|
|
// well-formed enough to construct the STTx; preflight1Sponsor rejects
|
|
// it before the Oracle-specific preflight checks run.
|
|
{
|
|
json::Value oracleSet;
|
|
oracleSet[jss::TransactionType] = jss::OracleSet;
|
|
oracleSet[jss::Account] = alice.human();
|
|
oracleSet[jss::OracleDocumentID] = 1;
|
|
oracleSet[jss::LastUpdateTime] = 1'000'000u;
|
|
json::Value price;
|
|
price[jss::BaseAsset] = "XRP";
|
|
price[jss::QuoteAsset] = "USD";
|
|
price[jss::AssetPrice] = 740;
|
|
price[jss::Scale] = 1;
|
|
json::Value priceData;
|
|
priceData[jss::PriceData] = price;
|
|
oracleSet[jss::PriceDataSeries] = json::Value(json::ValueType::Array);
|
|
oracleSet[jss::PriceDataSeries].append(priceData);
|
|
checkBlocked(oracleSet);
|
|
}
|
|
|
|
// PermissionedDomainSet is not in the allow-list.
|
|
checkBlocked(pdomain::setTx(alice.id(), {{.issuer = bob, .credType = "termsofuse"}}));
|
|
|
|
// XChain transactions are not in the allow-list.
|
|
checkBlocked(bridgeCreate(alice, bridge(alice, xrpIssue(), bob, xrpIssue()), XRP(1)));
|
|
}
|
|
|
|
void
|
|
testSponsorReserve(bool cosigning)
|
|
{
|
|
testRequireFlag();
|
|
testSponsorReserveSimple(cosigning);
|
|
testCheck(cosigning);
|
|
testCredential(cosigning);
|
|
testDelegate(cosigning);
|
|
testDepositPreauth(cosigning);
|
|
testEscrow(cosigning);
|
|
testMPToken(cosigning);
|
|
testPayChan(cosigning);
|
|
testSignerList(cosigning);
|
|
testTrustSet(cosigning);
|
|
}
|
|
|
|
void
|
|
testZeroBalanceSponsoredPaymentFeePayerCheck()
|
|
{
|
|
// Zero-balance sponsored Payment: getFeePayer() consistency check
|
|
testcase("Sponsored Payment: minimal-balance account with sponsor-pays-fee");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
Account const dest("dest");
|
|
|
|
auto const baseFee = env.current()->fees().base;
|
|
auto const baseReserve = env.current()->fees().reserve;
|
|
|
|
// Fund sponsor and dest generously, alice with base reserve + 1 XRP for payment
|
|
env.fund(XRP(10000), sponsor, dest);
|
|
env.fund(baseReserve + XRP(1), alice);
|
|
env.close();
|
|
|
|
// Precondition: alice has base reserve + 1 XRP (enough for payment but not fee)
|
|
BEAST_EXPECT(env.balance(alice) == baseReserve + XRP(1));
|
|
|
|
// Alice tries to send a Payment to dest where sponsor pays the fee via spfSponsorFee.
|
|
// Passed even alice balance doesn't have enough to pay fee.
|
|
auto const preDest = env.balance(dest);
|
|
auto const preSponsor = env.balance(sponsor);
|
|
|
|
// Alice sends 1 XRP to dest, sponsor pays the fee
|
|
env(pay(alice, dest, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(baseFee));
|
|
env.close();
|
|
|
|
// Payment succeeded
|
|
// Alice's balance decreased by 1 XRP (the payment amount, NOT the fee)
|
|
BEAST_EXPECT(env.balance(alice) == baseReserve);
|
|
|
|
// Dest received 1 XRP
|
|
BEAST_EXPECT(env.balance(dest) == preDest + XRP(1));
|
|
|
|
// Sponsor paid the fee (NOT alice)
|
|
BEAST_EXPECT(env.balance(sponsor) == preSponsor - baseFee);
|
|
}
|
|
|
|
void
|
|
testTrustSetCounterpartySponsorMisroute()
|
|
{
|
|
// TrustSet's modify path applies the tx-level reserve sponsor to whichever
|
|
// side has its reserve gate trip on this update, regardless of whether that
|
|
// side belongs to the tx submitter. trustCreate only sets the submitter's
|
|
// reserve flag and snapshots the counterparty's asfDefaultRipple state into
|
|
// the line's NoRipple bit; if the counterparty later toggles asfDefaultRipple
|
|
// (the canonical issuer flow), the line and account flags disagree and on
|
|
// the submitter's next TrustSet the counterparty-side gate fires. Sponsor still will be
|
|
// checked if it can be applied to that end of the trustLine.
|
|
|
|
testcase("TrustSet modify with sponsor does not misroute onto counterparty side");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice{"alice_t2178"};
|
|
Account const bob{"bob_t2178"};
|
|
Account const carol{"carol_t2178"};
|
|
|
|
// Fund without auto-setting asfDefaultRipple
|
|
env.fund(XRP(100'000), alice, bob, carol);
|
|
env.close();
|
|
|
|
// Determine account ordering
|
|
bool const aliceIsHigh = alice.id() > bob.id();
|
|
|
|
// To trigger the bug, we need the COUNTERPARTY's reserve gate to trip
|
|
// We use issuer/holder terminology where:
|
|
// - holder creates the trust line (their reserve is set first)
|
|
// - issuer enables DefaultRipple after (creates flag mismatch)
|
|
// - holder's second TrustSet triggers issuer's reserve gate
|
|
|
|
auto const issuer = aliceIsHigh ? bob : alice;
|
|
auto const holder = aliceIsHigh ? alice : bob;
|
|
auto const usd = issuer["USD"];
|
|
|
|
// Issuer must NOT have DefaultRipple set initially
|
|
// Clear it explicitly (env.fund may have set it)
|
|
env(fclear(issuer, asfDefaultRipple));
|
|
env.close();
|
|
|
|
// Holder creates the trust line first (holder's reserve flag is set)
|
|
// At this point, issuer does NOT have DefaultRipple set, so
|
|
// the NoRipple bit on issuer's side is set according to issuer's current flag
|
|
env(trust(holder, usd(1'000)));
|
|
env.close();
|
|
|
|
// Issuer now enables asfDefaultRipple (canonical issuer flow)
|
|
// This creates a mismatch: issuer's account flag says DefaultRipple=true
|
|
// but the trust line's NoRipple bit on issuer's side is still set
|
|
env(fset(issuer, asfDefaultRipple));
|
|
env.close();
|
|
|
|
SF_ACCOUNT const& issuerSponsorField = aliceIsHigh ? sfLowSponsor : sfHighSponsor;
|
|
SF_ACCOUNT const& holderSponsorField = aliceIsHigh ? sfHighSponsor : sfLowSponsor;
|
|
|
|
auto const lineKey = keylet::trustLine(alice, bob, usd.currency);
|
|
auto const sleLineBefore = env.le(lineKey);
|
|
if (!BEAST_EXPECT(sleLineBefore))
|
|
return;
|
|
BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfLowSponsor));
|
|
BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfHighSponsor));
|
|
|
|
auto const carolBefore = sponsoringOwnerCount(env, carol);
|
|
BEAST_EXPECT(carolBefore == 0);
|
|
auto const issuerSponsoredBefore = sponsoredOwnerCount(env, issuer);
|
|
BEAST_EXPECT(issuerSponsoredBefore == 0);
|
|
|
|
// Holder modifies the trust line with Carol as sponsor
|
|
// This should trigger the issuer's reserve gate because of the DefaultRipple mismatch
|
|
// Carol (sponsor) should NOT be applied to issuer's side (issuer != tx submitter)
|
|
env(trust(holder, usd(2'000)),
|
|
sponsor::As(carol, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, carol),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
auto const sleLineAfter = env.le(lineKey);
|
|
if (!BEAST_EXPECT(sleLineAfter))
|
|
return;
|
|
|
|
// Carol only agreed to back the holder, not the issuer
|
|
BEAST_EXPECT(!sleLineAfter->isFieldPresent(issuerSponsorField));
|
|
|
|
// Holder's side also has no sponsor because holder's reserve flag was
|
|
// already set on the FIRST TrustSet (no sponsor in scope then)
|
|
BEAST_EXPECT(!sleLineAfter->isFieldPresent(holderSponsorField));
|
|
|
|
// Carol's sponsoring count should remain unchanged (no misroute)
|
|
auto const carolAfter = sponsoringOwnerCount(env, carol);
|
|
BEAST_EXPECT(carolAfter == carolBefore);
|
|
|
|
// Issuer's sponsored count should remain unchanged (no misroute)
|
|
auto const issuerSponsoredAfter = sponsoredOwnerCount(env, issuer);
|
|
BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore);
|
|
}
|
|
|
|
void
|
|
testSelfEscrowFinishReserveGate()
|
|
{
|
|
testcase("Self-escrow finish reserve order gated by amendment");
|
|
using namespace test::jtx;
|
|
using namespace std::chrono_literals;
|
|
|
|
// Finishing a self-escrow (source == destination) whose trust line
|
|
// was deleted while the escrow was outstanding auto-creates the line,
|
|
// and the outcome of that reserve check depends on whether the escrow
|
|
// reserve is released before delivery (Sponsor) or after (legacy).
|
|
// With the source's balance in the one-increment window
|
|
// [reserve(1), reserve(2)), the legacy order requires reserve(2) and
|
|
// fails, while the Sponsor order requires reserve(1) and succeeds.
|
|
auto runTest = [&](FeatureBitset features, TER expected) {
|
|
Account const alice("alice");
|
|
Account const gw("gw");
|
|
auto const usd = gw["usd"];
|
|
|
|
Env env{*this, features};
|
|
auto const baseFee = env.current()->fees().base;
|
|
|
|
env.fund(XRP(10000), alice, gw);
|
|
env.close();
|
|
|
|
env(fset(gw, asfAllowTrustLineLocking));
|
|
env.close();
|
|
|
|
env.trust(usd(1000), alice);
|
|
env.close();
|
|
env(pay(gw, alice, usd(100)));
|
|
env.close();
|
|
|
|
// Escrow alice's entire USD balance to herself. The escrowed
|
|
// IOUs return to the issuer, zeroing the line balance.
|
|
auto const seq = env.seq(alice);
|
|
env(escrow::create(alice, alice, usd(100)),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kCancelTime(env.now() + 100s));
|
|
env.close();
|
|
|
|
// Delete the now-empty trust line. Both accounts have
|
|
// DefaultRipple set (jtx fund does that), so a plain limit-0
|
|
// TrustSet returns the line to its default state.
|
|
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
|
|
|
|
// Put alice's balance in the window. Pay the excess away
|
|
// directly: adjustAccountXRPBalance needs the Sponsor amendment.
|
|
STAmount const target = reserve(env, 1) + XRP(1);
|
|
env(pay(alice, env.master, env.balance(alice) - target - baseFee), Fee(baseFee));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == target);
|
|
|
|
env(escrow::finish(alice, alice, seq),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kFulfillment(escrow::kFb1),
|
|
Fee(baseFee * 150),
|
|
Ter(expected));
|
|
env.close();
|
|
|
|
if (expected == tesSUCCESS)
|
|
{
|
|
BEAST_EXPECT(!env.le(keylet::escrow(alice, seq)));
|
|
BEAST_EXPECT(env.le(keylet::trustLine(alice, gw, usd.currency)));
|
|
BEAST_EXPECT(env.balance(alice, usd) == usd(100));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1); // the new line
|
|
}
|
|
else
|
|
{
|
|
BEAST_EXPECT(env.le(keylet::escrow(alice, seq)));
|
|
BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency)));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1); // still the escrow
|
|
}
|
|
};
|
|
|
|
// Pre-amendment: legacy order — the escrow still counts against the
|
|
// reserve while the auto-created line is checked.
|
|
runTest(testableAmendments() - featureSponsor, tecNO_LINE_INSUF_RESERVE);
|
|
|
|
// Post-amendment: the escrow reserve is recycled into the new line.
|
|
runTest(testableAmendments(), tesSUCCESS);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void
|
|
testSponsoredObjectDeletionRefund()
|
|
{
|
|
// Deleting a co-signed reserve-sponsored object must
|
|
// refund the sponsor's SponsoringOwnerCount back to zero.
|
|
testcase("Sponsored object deletion refunds sponsor owner count");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(100000), sponsor);
|
|
env.close();
|
|
|
|
// Sponsored Check deletion - verify decreaseOwnerCountForObject
|
|
// (used by CheckCancel) reads the object's sfSponsor field and refunds
|
|
// the sponsor's owner count.
|
|
{
|
|
testcase(" — Check deletion");
|
|
|
|
Account const checkOwner("check_owner");
|
|
Account const dest("check_dest");
|
|
env.fund(XRP(100000), checkOwner, dest);
|
|
env.close();
|
|
|
|
// Create a check with co-signed reserve sponsorship. This bumps the
|
|
// sponsor's sfSponsoringOwnerCount rather than the check owner's.
|
|
auto const checkSeq = env.seq(checkOwner);
|
|
env(check::create(checkOwner, dest, XRP(1)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
auto sponsorCountBefore = sponsoringOwnerCount(env, sponsor);
|
|
BEAST_EXPECT(sponsorCountBefore == 1); // check costs 1 owner count
|
|
|
|
// Cancel (delete) the check.
|
|
env(check::cancel(checkOwner, keylet::check(checkOwner, checkSeq).key));
|
|
env.close();
|
|
|
|
auto sponsorCountAfter = sponsoringOwnerCount(env, sponsor);
|
|
BEAST_EXPECT(sponsorCountAfter == 0); // fully refunded
|
|
}
|
|
|
|
// Sponsored TrustSet (trust line) deletion - verify the sponsor
|
|
// refund works when a sponsored trust line is deleted
|
|
{
|
|
testcase(" — TrustLine deletion");
|
|
|
|
Account const issuer("issuer");
|
|
Account const holder("holder");
|
|
env.fund(XRP(100000), issuer, holder);
|
|
env.close();
|
|
|
|
auto const usd = issuer["USD"];
|
|
|
|
// Create trust line with sponsorship on the holder side
|
|
env(trust(holder, usd(1000)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
auto sponsorCountBefore = sponsoringOwnerCount(env, sponsor);
|
|
BEAST_EXPECT(sponsorCountBefore == 1); // trust line costs 1
|
|
|
|
// Delete the trust line by clearing it to default
|
|
env(trust(holder, usd(0)));
|
|
env.close();
|
|
|
|
auto sponsorCountAfter = sponsoringOwnerCount(env, sponsor);
|
|
BEAST_EXPECT(sponsorCountAfter == 0); // fully refunded
|
|
}
|
|
}
|
|
|
|
void
|
|
testAccountSponsorshipTransferPermissions()
|
|
{
|
|
// Account-level (no sfObjectID) SponsorshipTransfer permission errors.
|
|
testcase("Account-level SponsorshipTransfer permissions");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const sponsor1("sponsor1");
|
|
Account const sponsor2("sponsor2");
|
|
env.fund(XRP(10000), alice, sponsor1, sponsor2);
|
|
env.close();
|
|
|
|
// Account-level Reassign without sfSponsorSignature: the
|
|
// isAccountReserveSponsorship branch in SponsorshipTransfer::preflight
|
|
// requires a co-signature for account-level create AND reassign.
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Ter(temMALFORMED));
|
|
|
|
// Account-level Reassign of an account that was never sponsored.
|
|
env(sponsor::transfer(alice, tfSponsorshipReassign),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0);
|
|
|
|
// sponsor1 account-sponsors alice.
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor1, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor1));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.le(alice)->getAccountID(sfSponsor) == sponsor1.id());
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 1);
|
|
|
|
// Account-level Create on an account that is already account-sponsored.
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor2, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor2),
|
|
Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// The failed transfer must not change the sponsorship state.
|
|
BEAST_EXPECT(env.le(alice)->getAccountID(sfSponsor) == sponsor1.id());
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor1) == 1);
|
|
BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 0);
|
|
}
|
|
|
|
void
|
|
testCosignedTransferConsumesPrefundedBudget()
|
|
{
|
|
// A co-signed object-level SponsorshipTransfer(tfSponsorshipCreate)
|
|
// still draws down an existing Sponsorship object's RemainingOwnerCount
|
|
// (SponsorshipTransfer::doApply calls decrementPrefundedReserveCount
|
|
// whenever the Sponsorship SLE exists, signature or not), and
|
|
// checkReserve gates the transfer on that budget.
|
|
testcase("Co-signed SponsorshipTransfer draws down prefunded budget");
|
|
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();
|
|
|
|
// An unsponsored check owned by alice.
|
|
auto const seq = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
auto const checkId = keylet::check(alice, seq).key;
|
|
|
|
// Budgeted sponsorship: RemainingOwnerCount = 2.
|
|
env(sponsor::set_reserve(sponsor, 0, 2), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
// Co-signed transfer succeeds and decrements the budget 2 -> 1.
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
BEAST_EXPECT(env.le(keylet::check(alice, seq))->getAccountID(sfSponsor) == sponsor.id());
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == 1);
|
|
|
|
// A second unsponsored check.
|
|
auto const seq2 = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
auto const checkId2 = keylet::check(alice, seq2).key;
|
|
|
|
// A second co-signed transfer exhausts the budget 1 -> 0 (a
|
|
// SponsorshipSet update to an all-zero-budget object is itself
|
|
// rejected by SponsorshipSet::preclaim's hasSponsorshipBudget check,
|
|
// so the only way to reach a zero RemainingOwnerCount here is by
|
|
// spending it down via SponsorshipTransfer).
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId2),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2);
|
|
BEAST_EXPECT(
|
|
env.le(keylet::sponsorship(sponsor, alice))->getFieldU32(sfRemainingOwnerCount) == 0);
|
|
|
|
// A third unsponsored check.
|
|
auto const seq3 = env.seq(alice);
|
|
env(check::create(alice, bob, XRP(1)));
|
|
env.close();
|
|
auto const checkId3 = keylet::check(alice, seq3).key;
|
|
|
|
// With the budget exhausted, the same co-signed transfer fails in
|
|
// checkReserve (RemainingOwnerCount 0 < ownerCountDelta 1) with
|
|
// tecINSUFFICIENT_RESERVE, even though the sponsor's own balance is
|
|
// ample: an existing Sponsorship object's budget always bounds
|
|
// reserve sponsorship for that sponsor/sponsee pair.
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId3),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(keylet::check(alice, seq3))->isFieldPresent(sfSponsor));
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 2);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 2);
|
|
}
|
|
|
|
void
|
|
testFeeOnlySponsorshipObjectRouting()
|
|
{
|
|
// "Object always wins" fee routing: getFeePayer prefers an existing
|
|
// Sponsorship object over the co-signed sponsor's own balance. A
|
|
// fee-only-empty object (created by set_reserve, so sfFeeAmount is
|
|
// absent) therefore makes fee sponsorship fail with terINSUF_FEE_B
|
|
// even when the sponsor validly co-signs and could easily pay from
|
|
// its own balance. This is the intended precedence: once a
|
|
// Sponsorship object exists, all fee sponsorship for that
|
|
// sponsor/sponsee pair is routed through its pre-funded FeeAmount.
|
|
testcase("Fee-only Sponsorship object outranks a valid co-sign");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, sponsor);
|
|
env.close();
|
|
|
|
// Reserve-only sponsorship: no sfFeeAmount on the object.
|
|
env(sponsor::set_reserve(sponsor, 0, 5), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
auto sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle && !sle->isFieldPresent(sfFeeAmount));
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const sponsorBalance = env.balance(sponsor);
|
|
|
|
env(noop(alice),
|
|
Fee(env.current()->fees().base),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(terINSUF_FEE_B));
|
|
|
|
// Nothing was charged anywhere and the object is untouched.
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance);
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle && !sle->isFieldPresent(sfFeeAmount));
|
|
BEAST_EXPECT(sle && sle->getFieldU32(sfRemainingOwnerCount) == 5);
|
|
}
|
|
|
|
void
|
|
testZeroValueUpdateOnAbsentFields()
|
|
{
|
|
// Updating a Sponsorship with all-zero FeeAmount/MaxFee when those
|
|
// optional fields are already absent must succeed: STObject::
|
|
// makeFieldAbsent early-returns on fields that are already not
|
|
// present. A positive RemainingOwnerCount keeps the object clear of
|
|
// SponsorshipSet::preclaim's hasSponsorshipBudget invariant, which
|
|
// rejects any create/update leaving both FeeAmount and
|
|
// RemainingOwnerCount non-positive.
|
|
testcase("Zero-value SponsorshipSet update on absent fields");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, sponsor);
|
|
env.close();
|
|
|
|
auto const baseFee = env.current()->fees().base;
|
|
|
|
// Create a minimal sponsorship: a positive reserve budget, with
|
|
// all-zero FeeAmount/MaxFee left absent.
|
|
env(sponsor::set(sponsor, 0, 5, XRP(0), XRP(0)), sponsor::SponseeAcc(alice));
|
|
env.close();
|
|
|
|
auto sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
if (!sle)
|
|
return;
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee));
|
|
BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == 5);
|
|
|
|
// Submit the identical all-zero FeeAmount/MaxFee update again (with
|
|
// the same reserve budget): those fields are already absent, so each
|
|
// makeFieldAbsent call is a no-op and the update succeeds.
|
|
auto const sponsorBalance = env.balance(sponsor);
|
|
env(sponsor::set(sponsor, 0, 5, XRP(0), XRP(0)),
|
|
sponsor::SponseeAcc(alice),
|
|
Fee(baseFee),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
sle = env.le(keylet::sponsorship(sponsor, alice));
|
|
BEAST_EXPECT(sle);
|
|
if (!sle)
|
|
return;
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount));
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfMaxFee));
|
|
BEAST_EXPECT(sle->getFieldU32(sfRemainingOwnerCount) == 5);
|
|
// Only the fee was charged.
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - drops(baseFee));
|
|
}
|
|
|
|
void
|
|
testEmptySponsorSignatureObject()
|
|
{
|
|
// A completely empty SponsorSignature object (empty SigningPubKey, no
|
|
// TxnSignature, no Signers) on a normal (non-batch) transaction. The
|
|
// empty SigningPubKey routes STTx::checkMultiSign, which throws on the
|
|
// missing sfSigners array; the signature check therefore fails at the
|
|
// RPC/signing layer before the engine ever sees the transaction, which
|
|
// jtx reports as telENV_RPC_FAILED.
|
|
testcase("Empty SponsorSignature object on a normal transaction");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, sponsor);
|
|
env.close();
|
|
|
|
env(
|
|
noop(alice),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
[](Env&, JTx& jt) {
|
|
jt.jv[sfSponsorSignature.jsonName][sfSigningPubKey.jsonName] = "";
|
|
},
|
|
Fee(XRP(1)),
|
|
Ter(telENV_RPC_FAILED));
|
|
}
|
|
|
|
void
|
|
testOuterMultisignedSponsoredTx()
|
|
{
|
|
// A fee-sponsored transaction whose OUTER signature is a multisig.
|
|
// STTx::checkMultiSign validates the outer Signers against
|
|
// getInitiator() (the account/delegate), not getFeePayerID(), so the
|
|
// sponsor may pay the fee for a transaction it did not initiate, and
|
|
// may even be a member of the initiator's signer list.
|
|
testcase("Outer-multisigned fee-sponsored transaction");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const carol("carol");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, bob, carol, sponsor);
|
|
env.close();
|
|
|
|
auto const baseFee = env.current()->fees().base;
|
|
|
|
{
|
|
env(signers(alice, 2, {{bob, 1}, {carol, 1}}));
|
|
env.close();
|
|
|
|
// calculateBaseFee: base + (outer signers + sponsor signers) *
|
|
// base. Two outer signers and a single-signed sponsor -> 3x base.
|
|
env(noop(alice),
|
|
Msig({bob, carol}),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(baseFee + 2 * baseFee - 1),
|
|
Ter(telINSUF_FEE_P));
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const sponsorBalance = env.balance(sponsor);
|
|
env(noop(alice),
|
|
Msig({bob, carol}),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(baseFee + 2 * baseFee),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
// The sponsor pays the multisig-scaled fee; alice pays nothing.
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - drops(3 * baseFee));
|
|
}
|
|
|
|
{
|
|
// The sponsor is itself a member of alice's signer list and signs
|
|
// both as an outer multisigner and as the fee sponsor. This is
|
|
// legal: the outer Signers are checked against the initiator
|
|
// (alice), which the sponsor is not.
|
|
env(signers(alice, 2, {{bob, 1}, {sponsor, 1}}));
|
|
env.close();
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const sponsorBalance = env.balance(sponsor);
|
|
env(noop(alice),
|
|
Msig({bob, sponsor}),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Fee(baseFee + 2 * baseFee),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(sponsor) == sponsorBalance - drops(3 * baseFee));
|
|
}
|
|
}
|
|
|
|
void
|
|
testSponsoredAccountBaseReserveExclusion()
|
|
{
|
|
// An account-sponsored account contributes 0 to its own account
|
|
// reserve (accountCountImpl: isSponsored -> 0), so xrpLiquid excludes
|
|
// the base reserve entirely. A sponsored account whose balance is
|
|
// below the base reserve can still fund an xrpLiquid-gated action
|
|
// (OfferCreate selling XRP) that an identical unsponsored account
|
|
// cannot.
|
|
testcase("Sponsored account base-reserve exclusion in xrpLiquid");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const gw("gw");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, bob, gw, sponsor);
|
|
env.close();
|
|
|
|
auto const usd = gw["USD"];
|
|
auto const fees = env.current()->fees();
|
|
|
|
// Target balance: covers one owner-reserve increment (the offer) but
|
|
// is below the base account reserve.
|
|
auto const target = drops(fees.accountReserve(1, 0)) + XRP(10);
|
|
BEAST_EXPECT(target < drops(fees.accountReserve(0, 1)));
|
|
|
|
// Account-sponsor alice; bob stays unsponsored as the control.
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
BEAST_EXPECT(env.le(alice)->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
// Drain both accounts below the base reserve by burning fees: an
|
|
// ordinary fee payer may dip below its reserve (a Payment could not
|
|
// take the unsponsored control below the base reserve).
|
|
env(noop(alice), Fee(env.balance(alice) - target));
|
|
env(noop(bob), Fee(env.balance(bob) - target));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == target);
|
|
BEAST_EXPECT(env.balance(bob) == target);
|
|
|
|
// Control: for the unsponsored account, xrpLiquid = balance - base
|
|
// reserve = 0, so OfferCreate's funding check fails.
|
|
beast::Journal const j{beast::Journal::getNullSink()};
|
|
BEAST_EXPECT(accountReserve(*env.current(), bob.id(), j) == fees.accountReserve(0, 1));
|
|
BEAST_EXPECT(xrpLiquid(*env.current(), bob.id(), 0, j) == XRPAmount(0));
|
|
env(offer(bob, usd(5), XRP(5)), Ter(tecUNFUNDED_OFFER));
|
|
env.close();
|
|
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
|
|
|
// The sponsored account contributes 0 to its own reserve, so its
|
|
// reserve is 0 (no owned objects yet), and its full balance is
|
|
// liquid.
|
|
BEAST_EXPECT(accountReserve(*env.current(), alice.id(), j) == fees.accountReserve(0, 0));
|
|
BEAST_EXPECT(xrpLiquid(*env.current(), alice.id(), 0, j) == env.balance(alice));
|
|
|
|
// The sponsored account's reserve is just the owner increment, so the
|
|
// same offer is funded and can even be placed on the book.
|
|
auto const offerSeq = env.seq(alice);
|
|
env(offer(alice, usd(5), XRP(5)), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.le(keylet::offer(alice, offerSeq)));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
// alice is still below the (unsponsored) base reserve.
|
|
BEAST_EXPECT(env.balance(alice) < drops(fees.accountReserve(0, 1)));
|
|
}
|
|
|
|
void
|
|
testThirdPartyEscrowCancelRefundsSponsor()
|
|
{
|
|
// EscrowCancel of a sponsored escrow submitted by a THIRD PARTY
|
|
// (neither the owner nor the sponsor) after CancelAfter: the deletion
|
|
// must refund the sponsor's SponsoringOwnerCount and the owner's
|
|
// SponsoredOwnerCount.
|
|
testcase("Third-party EscrowCancel refunds the sponsor");
|
|
using namespace test::jtx;
|
|
using namespace std::chrono_literals;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const carol("carol");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, bob, carol, sponsor);
|
|
env.close();
|
|
|
|
auto const cancelAfter = env.now() + 100s;
|
|
auto const seq = env.seq(alice);
|
|
env(escrow::create(alice, bob, XRP(100)),
|
|
escrow::kCondition(escrow::kCb1),
|
|
escrow::kCancelTime(cancelAfter),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
BEAST_EXPECT(env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id());
|
|
|
|
// Advance past the cancel time.
|
|
for (; env.now() <= cancelAfter; env.close())
|
|
{
|
|
}
|
|
|
|
// carol (a third party) cancels the escrow.
|
|
auto const aliceBalance = env.balance(alice);
|
|
env(escrow::cancel(carol, alice, seq));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(keylet::escrow(alice, seq)));
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance + XRP(100));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
}
|
|
|
|
void
|
|
testPaymentEngineTrustLineDeletion()
|
|
{
|
|
// A reserve-sponsored trust line deleted inside the payment engine
|
|
// (updateTrustLine/trustDelete via redeemIOU), not by TrustSet, must
|
|
// still refund the sponsor's SponsoringOwnerCount.
|
|
testcase("Payment-engine deletion of a sponsored trust line");
|
|
using namespace test::jtx;
|
|
|
|
Account const alice("alice");
|
|
Account const gw("gw");
|
|
Account const sponsor("sponsor");
|
|
|
|
{
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, gw, sponsor);
|
|
env.close();
|
|
|
|
auto const usd = gw["USD"];
|
|
auto const lineId = keylet::trustLine(alice, gw, usd.currency);
|
|
|
|
env(trust(alice, usd(100)));
|
|
env.close();
|
|
BEAST_EXPECT(env.le(lineId));
|
|
|
|
// Transfer the line's reserve to the sponsor.
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, lineId.key),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
env(pay(gw, alice, usd(50)));
|
|
env.close();
|
|
|
|
// Setting the limit back to 0 keeps the line alive because the
|
|
// balance is non-zero.
|
|
env(trust(alice, usd(0)));
|
|
env.close();
|
|
BEAST_EXPECT(env.le(lineId));
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
// Paying the full balance back to the issuer deletes the line
|
|
// inside the payment engine.
|
|
env(pay(alice, gw, usd(50)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(lineId));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
}
|
|
|
|
{
|
|
// AMM variant: a sponsored LP-token trust line is deleted by
|
|
// AMMWithdraw (withdraw-all) when the LP tokens are fully
|
|
// redeemed. Note: the AMM's own asset lines (owned by the AMM
|
|
// pseudo-account) cannot be sponsored - pseudo-accounts cannot
|
|
// submit SponsorshipTransfer and a third party gets
|
|
// tecNO_PERMISSION - so only the LP-side line is testable here.
|
|
Env env{*this, testableAmendments()};
|
|
env.fund(XRP(10000), alice, gw, sponsor);
|
|
env.close();
|
|
|
|
auto const usd = gw["USD"];
|
|
env(trust(alice, usd(1000)));
|
|
env.close();
|
|
env(pay(gw, alice, usd(500)));
|
|
env.close();
|
|
|
|
AMM amm(env, alice, XRP(100), usd(100));
|
|
|
|
auto const lpLine =
|
|
keylet::trustLine(alice.id(), amm.ammAccount(), amm.lptIssue().currency);
|
|
BEAST_EXPECT(env.le(lpLine));
|
|
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, lpLine.key),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
// Withdraw everything: the LP-token line is deleted as the LP
|
|
// balance is redeemed to zero.
|
|
amm.withdrawAll(alice);
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(lpLine));
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
testExpiredSponsoredCredentialIndirectDeletion()
|
|
{
|
|
// CredentialAccept on an already-expired sponsored credential fails
|
|
// with tecEXPIRED, but still deletes the credential as a side effect
|
|
// (removeExpired). That indirect deletion must still refund the
|
|
// sponsor's SponsoringOwnerCount and the issuer's SponsoredOwnerCount,
|
|
// just like the third-party EscrowCancel and payment-engine
|
|
// trust-line deletion cases above.
|
|
testcase("Indirect deletion of an expired sponsored credential");
|
|
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();
|
|
|
|
auto const credType = std::string("test");
|
|
|
|
// alice (issuer) creates a credential for bob (subject), with a
|
|
// short expiration.
|
|
auto const expiration =
|
|
env.current()->header().parentCloseTime.time_since_epoch().count() + 20;
|
|
auto jv = credentials::create(bob, alice, credType);
|
|
jv[sfExpiration.jsonName] = expiration;
|
|
env(jv);
|
|
env.close();
|
|
|
|
auto const credKeylet = credentials::keylet(bob, alice, credType);
|
|
env(sponsor::transfer(alice, tfSponsorshipCreate, credKeylet.key),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1);
|
|
|
|
// Advance past expiration.
|
|
for (; env.current()->header().parentCloseTime.time_since_epoch().count() <=
|
|
static_cast<std::int64_t>(expiration);
|
|
env.close())
|
|
{
|
|
}
|
|
|
|
// Bob's accept fails (expired), but removeExpired deletes the
|
|
// credential as a side effect.
|
|
env(credentials::accept(bob, alice, credType), Ter(tecEXPIRED));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.le(credKeylet));
|
|
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
|
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
|
}
|
|
|
|
void
|
|
testReserveSponsorAllowListNoOp()
|
|
{
|
|
// Allow-listed transaction types that create no owner-counted object:
|
|
// spfSponsorReserve with a valid co-sign is a harmless no-op. The
|
|
// transactions succeed and neither party gains any sponsorship count
|
|
// fields.
|
|
testcase("Reserve sponsor no-op on allow-listed non-creating txs");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const gw("gw");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, bob, gw, sponsor);
|
|
env.close();
|
|
|
|
auto const expectNoSponsorCounts = [&](Account const& acc) {
|
|
auto const sle = env.le(keylet::account(acc));
|
|
if (BEAST_EXPECT(sle))
|
|
{
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfSponsoredOwnerCount));
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfSponsoringOwnerCount));
|
|
BEAST_EXPECT(!sle->isFieldPresent(sfSponsoringAccountCount));
|
|
}
|
|
};
|
|
|
|
// AccountSet
|
|
env(noop(alice),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
expectNoSponsorCounts(alice);
|
|
expectNoSponsorCounts(sponsor);
|
|
|
|
// SetRegularKey
|
|
env(regkey(alice, bob),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
expectNoSponsorCounts(alice);
|
|
expectNoSponsorCounts(sponsor);
|
|
|
|
// Clawback (partial, so the trust line survives)
|
|
env(fset(gw, asfAllowTrustLineClawback));
|
|
env.close();
|
|
env(trust(alice, gw["USD"](100)));
|
|
env.close();
|
|
env(pay(gw, alice, gw["USD"](50)));
|
|
env.close();
|
|
env(claw(gw, alice["USD"](10)),
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
expectNoSponsorCounts(gw);
|
|
expectNoSponsorCounts(sponsor);
|
|
|
|
// MPTokenIssuanceSet
|
|
MPTTester mptt(env, alice, {.fund = false});
|
|
mptt.create({.flags = tfMPTCanLock});
|
|
json::Value jvSet = mptt.setJV({.account = alice, .id = mptt.issuanceID()});
|
|
jvSet[sfFlags.jsonName] = tfMPTLock;
|
|
env(jvSet,
|
|
sponsor::As(sponsor, spfSponsorReserve),
|
|
Sig(sfSponsorSignature, sponsor),
|
|
Ter(tesSUCCESS));
|
|
env.close();
|
|
expectNoSponsorCounts(alice);
|
|
expectNoSponsorCounts(sponsor);
|
|
}
|
|
|
|
void
|
|
testPseudoAccountAsSponsor()
|
|
{
|
|
// A pseudo-account (an AMM account) named as sfSponsor on a co-signed
|
|
// fee-sponsored transaction. The signature itself is cryptographically
|
|
// valid (made with an arbitrary real key), so it passes the local
|
|
// crypto check; Transactor::checkSign then rejects the sponsor because
|
|
// pseudo-accounts can never sign -> tefBAD_AUTH.
|
|
testcase("Pseudo account named as co-signing sponsor");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const gw("gw");
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.close();
|
|
|
|
auto const usd = gw["USD"];
|
|
env(trust(alice, usd(1000)));
|
|
env.close();
|
|
env(pay(gw, alice, usd(500)));
|
|
env.close();
|
|
|
|
AMM const amm(env, alice, XRP(100), usd(100));
|
|
|
|
// rogue is never funded; it only supplies a real signing keypair.
|
|
Account const rogue("rogue");
|
|
|
|
auto tx = noop(bob);
|
|
tx[sfSponsor.jsonName] = to_string(amm.ammAccount());
|
|
tx[sfSponsorFlags.jsonName] = static_cast<std::uint32_t>(spfSponsorFee);
|
|
|
|
auto const bobBalance = env.balance(bob);
|
|
env(tx, Fee(XRP(1)), Sig(sfSponsorSignature, rogue), Ter(tefBAD_AUTH));
|
|
env.close();
|
|
|
|
// tef results claim no fee; nothing changed.
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
}
|
|
|
|
void
|
|
testCosignedClosedLedgerFeeCap()
|
|
{
|
|
// Mirror of the pre-funded OpenView-overlay test in testSponsorFee,
|
|
// for the CO-SIGNED path: on a closed-ledger view, checkFee returns
|
|
// tecINSUFF_FEE when the sponsor's spendable balance (balance minus
|
|
// its own reserve) is positive but below the fee, and reset() caps
|
|
// the charged fee so the sponsor never dips below its reserve.
|
|
testcase("Co-signed sponsor fee on closed ledger caps at reserve");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testableAmendments()};
|
|
Account const alice("alice");
|
|
Account const sponsor("sponsor");
|
|
env.fund(XRP(10000), alice, sponsor);
|
|
env.close();
|
|
|
|
// Spendable above reserve: exactly 10 drops.
|
|
adjustAccountXRPBalance(env, sponsor, reserve(env, 0) + drops(10));
|
|
|
|
// Apply directly against the closed ledger view (open_ = false).
|
|
OpenView overlay(&*env.closed());
|
|
|
|
auto jt = env.jt(
|
|
noop(alice),
|
|
Fee(drops(1000)),
|
|
Seq(env.seq(alice)),
|
|
sponsor::As(sponsor, spfSponsorFee),
|
|
Sig(sfSponsorSignature, sponsor));
|
|
|
|
auto const result = xrpl::apply(env.app(), overlay, *jt.stx, TapNone, env.journal);
|
|
BEAST_EXPECT(result.ter == tecINSUFF_FEE);
|
|
BEAST_EXPECT(result.applied);
|
|
|
|
// Only the 10 spendable drops were charged: the co-signed sponsor is
|
|
// never charged into its own account reserve.
|
|
auto const sle = overlay.read(keylet::account(sponsor.id()));
|
|
BEAST_EXPECT(sle);
|
|
BEAST_EXPECT(sle && sle->getFieldAmount(sfBalance) == reserve(env, 0));
|
|
}
|
|
|
|
protected:
|
|
void
|
|
testSponsor()
|
|
{
|
|
testDisabled();
|
|
testInvalidSponsorshipSet();
|
|
testPseudoAccountSponsorship();
|
|
|
|
testSingleSigning();
|
|
testMultiSigning();
|
|
|
|
testInvalidSponsorField();
|
|
|
|
testSimpleSponsorshipSet();
|
|
|
|
testPreFundAndCosign();
|
|
testSequentialSponsorshipExhaustion();
|
|
testSponsoredFreeTierReserve();
|
|
testSponsoredTicketUse();
|
|
|
|
testTransferSponsor();
|
|
testLegacySignerListReserve();
|
|
testSponsorFee();
|
|
testSponsorAccount();
|
|
|
|
testAccountDelete();
|
|
|
|
testDelegatePermission();
|
|
testDelegateBlockReserveSponsor();
|
|
testDelegateSponsorFeePayer();
|
|
|
|
testBatch();
|
|
|
|
testSponsoredTrustLineNoFreeReserve();
|
|
testCoSignReserveBoundedBySponsorshipBudget();
|
|
testReserveSponsorGate();
|
|
|
|
testZeroBalanceSponsoredPaymentFeePayerCheck();
|
|
testTrustSetCounterpartySponsorMisroute();
|
|
testSelfEscrowFinishReserveGate();
|
|
|
|
testFeeSponsoredVaultInvariant();
|
|
testSponsoredObjectDeletionRefund();
|
|
|
|
testAccountSponsorshipTransferPermissions();
|
|
testCosignedTransferConsumesPrefundedBudget();
|
|
testFeeOnlySponsorshipObjectRouting();
|
|
testZeroValueUpdateOnAbsentFields();
|
|
testEmptySponsorSignatureObject();
|
|
testOuterMultisignedSponsoredTx();
|
|
testSponsoredAccountBaseReserveExclusion();
|
|
testThirdPartyEscrowCancelRefundsSponsor();
|
|
testPaymentEngineTrustLineDeletion();
|
|
testExpiredSponsoredCredentialIndirectDeletion();
|
|
testReserveSponsorAllowListNoOp();
|
|
testPseudoAccountAsSponsor();
|
|
testCosignedClosedLedgerFeeCap();
|
|
}
|
|
|
|
void
|
|
testTxSponsor(bool cosigning)
|
|
{
|
|
testSponsorReserve(cosigning);
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
testSponsor();
|
|
}
|
|
};
|
|
|
|
class SponsorTxCosigning_test : public Sponsor_test
|
|
{
|
|
void
|
|
run() override
|
|
{
|
|
testTxSponsor(true);
|
|
}
|
|
};
|
|
|
|
class SponsorTxPrefunded_test : public Sponsor_test
|
|
{
|
|
void
|
|
run() override
|
|
{
|
|
testTxSponsor(false);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(Sponsor, app, xrpl);
|
|
BEAST_DEFINE_TESTSUITE(SponsorTxCosigning, app, xrpl);
|
|
BEAST_DEFINE_TESTSUITE(SponsorTxPrefunded, app, xrpl);
|
|
|
|
} // namespace xrpl::test
|