CAP: 0033
Title: Sponsored Reserve
Author: Jonathan Jove
Status: Accepted
Created: 2020-05-18
Discussion: https://groups.google.com/forum/#!msg/stellar-dev/E_tDs17mkJw/DmGXVY-QBAAJ
Protocol version: 14
This proposal makes it possible to pay reserves for another account.
This proposal seeks to solve the following problem: an entity should be able to provide the reserve for accounts controlled by other parties without giving those parties control of the reserve.
Consider, for example, an issuer that is willing to pay the reserve for trust lines to the asset it issues. With the current version of the protocol, the reserve must be part of the balance of an account. This means the issuer can only pay the reserve by sending native asset to accounts that create a trust line to the asset it issues. But this leaves the issuer vulnerable to attack because an attacker can extract funds from the issuer by creating new accounts, creating the trust line, waiting for the native asset to arrive, then removing the trust line and merging the account.
This proposal is in many ways analogous to CAP-0015:
- CAP-0015 makes it possible to pay transaction fees for other accounts without giving control of the underlying funds
- CAP-0033 makes it possible to pay reserves for other accounts without giving control of the underlying funds
The combination of these two proposals should greatly facilitate the development of non-custodial uses of the Stellar Network.
This proposal is aligned with the following Stellar Network Goal:
- The Stellar Network should make it easy for developers of Stellar projects to create highly usable products.
We introduce the relation is-sponsoring-future-reserves-for, in which an account
(the sponsoring account) pays any reserve that another account (the sponsored
account) would have to pay. This relation is initiated by
SponsorFutureReservesOp
, where the sponsoring account is the source account,
and is terminated by ConfirmAndClearSponsorOp
, where the sponsored account is
the source account. Both operations must appear in a single transaction, which
guarantees that both the sponsoring and sponsored accounts agree to every
sponsorship. We also introduce UpdateSponsorshipOp
, which can be used to
modify the sponsorship of existing ledger entries. To support this, we add new
extensions to AccountEntry
and LedgerEntry
which record pertinent
information about sponsorships.
This specification assumes CAP-0023, in order to show how sponsorships would work for claimable balance entries.
typedef AccountID* SponsorshipDescriptor;
struct AccountEntryExtensionV2
{
uint32 numSponsored; // Number of reserves sponsored for this account
// Reduces the reserve requirement for this account
uint32 numSponsoring; // Number of reserves sponsored by this account
// Increases the reserve requirement for this account
// signerSponsoringIDs are in 1-to-1 correspondence with signers, so
// the account sponsoring signer[i] is signerSponsoringIDs[i]. If
// signerSponsoringIDs[i] is empty, then there is no sponsor
SponsorshipDescriptor signerSponsoringIDs<20>;
union switch (int v)
{
case 0:
void;
}
ext;
};
struct AccountEntryExtensionV1
{
Liabilities liabilities;
union switch (int v)
{
case 0:
void;
case 2:
AccountEntryExtensionV2 v2;
}
ext;
};
struct AccountEntry
{
// ... unchanged ...
union switch (int v)
{
case 0:
void;
case 1:
AccountEntryExtensionV1 v1;
}
ext;
};
Note that ClaimableBalanceEntry
is not in the current protocol, so the XDR can
still be modified. reserve
has been removed, and sponsoringID
has been
replaced by sponsoringID
in LedgerEntryExtensionV1
.
struct ClaimableBalanceEntry
{
// Unique identifier for this ClaimableBalanceEntry
ClaimableBalanceID balanceID;
// List of claimants with associated predicate
Claimant claimants<10>;
// Any asset including native
Asset asset;
// Amount of asset
int64 amount;
// reserved for future use
union switch (int v)
{
case 0:
void;
}
ext;
};
struct LedgerEntryExtensionV1
{
// The account sponsoring this ledger entry. If sponsoringID is empty then
// there is no sponsor
SponsorshipDescriptor sponsoringID;
union switch (int v)
{
case 0:
void;
}
ext;
};
struct LedgerEntry
{
// ... unchanged ...
union switch (int v)
{
case 0:
void;
case 1:
LedgerEntryExtensionV1 v1;
}
ext;
};
enum OperationType
{
// ... CREATE_ACCOUNT, ..., MANAGE_BUY_OFFER unchanged ...
PATH_PAYMENT_STRICT_SEND = 13,
SPONSOR_FUTURE_RESERVES = 14,
CONFIRM_AND_CLEAR_SPONSOR = 15,
UPDATE_SPONSORSHIP = 16
};
struct SponsorFutureReservesOp
{
// sponsoredID identifies the account for which future reserves will be
// sponsored by the source account of this operation
AccountID sponsoredID;
};
enum UpdateSponsorshipType
{
UPDATE_SPONSORSHIP_LEDGER_ENTRY = 0,
UPDATE_SPONSORSHIP_SIGNER = 1
};
union UpdateSponsorshipOp switch (UpdateSponsorshipType type)
{
case UPDATE_SPONSORSHIP_LEDGER_ENTRY:
// Uniquely identifies a non-signer ledger entry
LedgerKey ledgerKey;
case UPDATE_SPONSORSHIP_SIGNER:
// Uniquely identifies a signer on a specific account
struct
{
AccountID accountID;
SignerKey signerKey;
}
signer;
};
struct Operation
{
// sourceAccount is the account used to run the operation
// if not set, the runtime defaults to "sourceAccount" specified at
// the transaction level
MuxedAccount* sourceAccount;
union switch (OperationType type)
{
// ... CREATE_ACCOUNT, ..., PATH_PAYMENT_STRICT_SEND unchanged ...
case SPONSOR_FUTURE_RESERVES:
SponsorFutureReservesOp sponsorFutureReservesOp;
case CONFIRM_AND_CLEAR_SPONSOR:
void;
case UPDATE_SPONSORSHIP:
UpdateSponsorshipOp updateSponsorshipOp;
}
body;
};
enum AccountMergeResultCode
{
// ... ACCOUNT_MERGE_SUCCESS, ..., ACCOUNT_MERGE_SEQNUM_TOO_FAR unchanged ...
ACCOUNT_MERGE_DEST_FULL = -6, // can't add source balance to
// destination balance
ACCOUNT_MERGE_IS_SPONSOR = -7 // can't merge account that is a sponsor
};
enum SponsorFutureReservesResultCode
{
// codes considered as "success" for the operation
SPONSOR_FUTURE_RESERVES_SUCCESS = 0,
// codes considered as "failure" for the operation
SPONSOR_FUTURE_RESERVES_MALFORMED = -1, // can't sponsor self
SPONSOR_FUTURE_RESERVES_ALREADY_SPONSORED = -2, // can't sponsor an account
// that is already sponsored
SPONSOR_FUTURE_RESERVES_RECURSIVE = -3 // can't sponsor an account
// that is itself sponsoring
// an account
};
union SponsorFutureReservesResult switch (SponsorFutureReservesResultCode code)
{
case SPONSOR_FUTURE_RESERVES_SUCCESS:
void;
default:
void;
};
enum ConfirmAndClearSponsorResultCode
{
// codes considered as "success" for the operation
CONFIRM_AND_CLEAR_SPONSOR_SUCCESS = 0,
// codes considered as "failure" for the operation
CONFIRM_AND_CLEAR_SPONSOR_NOT_SPONSORED = -1 // must be sponsored to confirm
};
union ConfirmAndClearSponsorResult switch (ConfirmAndClearSponsorResultCode code)
{
case CONFIRM_AND_CLEAR_SPONSOR_SUCCESS:
void;
default:
void;
};
enum UpdateSponsorshipResultCode
{
// codes considered as "success" for the operation
UPDATE_SPONSORSHIP_SUCCESS = 0,
// codes considered as "failure" for the operation
UPDATE_SPONSORSHIP_DOES_NOT_EXIST = -1, // specified entry does not exist
UPDATE_SPONSORSHIP_NOT_SPONSOR = -2, // not sponsor of specified entry
UPDATE_SPONSORSHIP_LOW_RESERVE = -3, // new reserve payor cannot afford
// this entry
UPDATE_SPONSORSHIP_ONLY_TRANSFERABLE = -4 // sponsorship of ClaimableBalance
// must be transferred to another
// account
};
union UpdateSponsorshipResult switch (UpdateSponsorshipResultCode code)
{
case UPDATE_SPONSORSHIP_SUCCESS:
void;
default:
void;
};
union OperationResult switch (OperationResultCode code)
{
case opINNER:
union switch (OperationType type)
{
// ... CREATE_ACCOUNT, ..., PATH_PAYMENT_STRICT_SEND unchanged ...
case SPONSOR_FUTURE_RESERVES:
SponsorFutureReservesResult sponsorFutureReservesResult;
case CONFIRM_AND_CLEAR_SPONSOR:
ConfirmAndClearSponsorResult confirmAndClearSponsorResult;
case UPDATE_SPONSORSHIP:
UpdateSponsorshipResult updateSponsorshipResult;
}
tr;
default:
void;
};
enum TransactionResultCode
{
// ... txFEE_BUMP_INNER_SUCCESS, ..., txNOT_SUPPORTED unchanged ...
txFEE_BUMP_INNER_FAILED = -13, // fee bump inner transaction failed
txBAD_SPONSORSHIP = -14 // sponsorship not confirmed and cleared
};
struct InnerTransactionResult
{
// Always 0. Here for binary compatibility.
int64 feeCharged;
union switch (TransactionResultCode code)
{
// ... txFEE_BUMP_INNER_SUCCESS, ..., txFEE_BUMP_INNER_FAILED unchanged ...
case txBAD_SPONSORSHIP:
void;
}
result;
// ext unchanged
};
No operation can take the native balance of an account below
(2 + numSubEntries + numSponsoring - numSponsored) * baseReserve + liabilities.selling
.
When account A
is-sponsoring-future-reserves-for account B
, any reserve
requirements that would normally accumulate on B
will instead accumulate on
A
as reflected in numSponsoring
. Both accounts are guaranteed to exist, but
this can fail. If such a change would cause A
to pass below the reserve
requirement, then the operation fails. The fact that these reserves are being
provided by another account will be reflected on B
in numSponsored
.
When a sponsored ledger entry or sub-entry is removed, numSponsoring
is
decreased on the sponsoring account and numSponsored
is decreased on the
sponsored account. Both accounts are guaranteed to exist, so this cannot fail.
It is an invariant that, after every operation, the sum of all changes in
numSponsoring
exactly equals the sum of all changes in numSponsored
.
Within any transaction, every SponsorFutureReservesOp s
must be paired with a
subsequent ConfirmAndClearSponsorOp c
with c.sourceAccount
equal to
s.sponsoredID
. If this constraint is not met, a transaction that would
otherwise have succeeded will fail with txBAD_SPONSORSHIP
.
SponsorFutureReservesOp
is the only operation that can initiate the
is-sponsoring-future-reserves-for relation. To check validity of
SponsorFutureReservesOp op
:
If op.sponsoredID = op.sourceAccount
Invalid with SPONSOR_FUTURE_RESERVES_MALFORMED
The behavior of SponsorFutureReservesOp op
is:
If an account is sponsoring future reserves for op.sponsoredID
Fail with SPONSOR_FUTURE_RESERVES_ALREADY_SPONSORED
If an account is sponsoring future reserves for op.sourceAccount
Fail with SPONSOR_FUTURE_RESERVES_RECURSIVE
If op.sponsoredID is sponsoring future reserves for any account
Fail with SPONSOR_FUTURE_RESERVES_RECURSIVE
Record that op.sourceAccount is sponsoring future reserves for op.sponsoredID
Succeed with SPONSOR_FUTURE_RESERVES_SUCCESS
SponsorFutureReservesOp
requires medium threshold.
ConfirmAndClearSponsorOp
is the only operation that can terminate the
is-sponsoring-future-reserves-for relation. ConfirmAndClearSponsorOp
is always
valid.
The behavior of ConfirmAndClearSponsorOp op
is:
If an account is not sponsoring future reserves for op.sourceAccount
Fail with CONFIRM_AND_CLEAR_SPONSOR_NOT_SPONSORED
Record that no account is sponsoring future reserves for op.sourceAccount
Succeed with CONFIRM_AND_CLEAR_SPONSOR_SUCCESS
ConfirmAndClearSponsorOp
requires medium threshold.
UpdateSponsorshipOp
allows the sponsor of existing reserves to transfer
sponsorship to another account. If an account is-sponsoring-future-reserves-for
the source account, then the new sponsor will be that account. If no account
is-sponsoring-future-reserves-for the source account, then there will be no
sponsor. Note that UpdateSponsorshipOp
does not obey the typical rules of
sponsorship. UpdateSponsorshipOp
is always valid.
The behavior of UpdateSponsorshipOp op
is as follows:
If op.type() == UPDATE_SPONSORSHIP_LEDGER_ENTRY
mult = op.ledgerKey().type() != ACCOUNT ? 1 : 2
requiredReserve = mult * baseReserve
If no LedgerEntry le with LedgerKey(le) == op.ledgerKey() exists
Fail with UPDATE_SPONSORSHIP_DOES_NOT_EXIST
wasSponsored = false
Load LedgerEntry le with LedgerKey(le) == op.ledgerKey()
If le.ext.v() == 0 || !le.ext.v1().sponsoringID
If Account(le) != op.sourceAccount
Fail with UPDATE_SPONSORSHIP_NOT_SPONSOR
Else
If le.ext.v1().sponsoringID != op.sourceAccount
Fail with UPDATE_SPONSORSHIP_NOT_SPONSOR
wasSponsored = true
Load AccountEntry source with source.accountID == op.sourceAccount
source.ext.v1().ext.v2().numSponsoring -= mult
If an account sID is sponsoring future reserves for op.sourceAccount
Load AccountEntry acc with acc.accountID == sID
If AvailableBalance(acc, NATIVE) < requiredReserve
Fail with UPDATE_SPONSORSHIP_LOW_RESERVE
acc.ext.v1().ext.v2().numSponsoring += mult
le.ext.v(1)
le.ext.v1().sponsoringID = sID
Else
If op.ledgerKey().type() == CLAIMABLE_BALANCE
Fail with UPDATE_SPONSOR_ONLY_TRANSFERABLE
Load AccountEntry acc with acc.accountID == Account(le)
If AvailableBalance(acc, NATIVE) < requiredReserve
Fail with UPDATE_SPONSORSHIP_LOW_RESERVE
If wasSponsored
acc.ext.v1().ext.v2().numSponsored -= mult
Clear le.ext.v1().sponsoringID
Else if op.type() == UPDATE_SPONSORSHIP_SIGNER
If no AccountEntry acc with acc.accountID == op.signer().accountID exists
Fail with UPDATE_SPONSORSHIP_DOES_NOT_EXIST
Load AccountEntry a with acc.accountID == op.signer().accountID
If op.signer().signerKey not in acc.signers
Fail with UPDATE_SPONSORSHIP_DOES_NOT_EXIST
Let i be such that a.signers[i].signerKey == op.signer().signerKey
wasSponsored = false
If a.ext.v() == 0 || a.ext.v1().ext.v() == 0
If op.signer().accountID != op.sourceAccount
Fail with UPDATE_SPONSORSHIP_NOT_SPONSOR
Else if a.ext.v1().ext.v() == 2
If a.ext.v1().ext.v2().signerSponsoringIDs[i] != op.sourceAccount
Fail with UPDATE_SPONSORSHIP_NOT_SPONSOR
wasSponsored = true
Load AccountEntry source with source.accountID == op.sourceAccount
source.ext.v1().ext.v2().numSponsoring--
If an account sID is sponsoring future reserves for op.sourceAccount
Load AccountEntry acc with acc.accountID == sID
If AvailableBalance(acc, NATIVE) < baseReserve
Fail with UPDATE_SPONSORSHIP_LOW_RESERVE
acc.ext.v1().ext.v2().numSponsoring++
Set a.ext.v(1)
Set a.ext.v1().ext.v(2)
a.ext.v1().ext.v2().signerSponsoringIDs[i].sponsoringID = sID
Else
If AvailableBalance(a, NATIVE) < requiredReserve
Fail with UPDATE_SPONSORSHIP_LOW_RESERVE
If wasSponsored
a.ext.v1().ext.v2().numSponsoring--
Clear a.ext.v1().ext.v2().signerSponsoringIDs[i].sponsoringID
Succeed with UPDATE_SPONSORSHIP_SUCCESS
UpdateSponsorshipOp
requires medium threshold.
AccountMergeOp
will fail with ACCOUNT_MERGE_IS_SPONSOR
if attempting to
merge an account that is-sponsoring-future-reserves-for another account. This
guarantees that the sponsoring account always exists.
Similarly, AccountMergeOp
will fail with ACCOUNT_MERGE_IS_SPONSOR
if
attempting to merge an account that has non-zero numSponsoring
. This is
required for sponsorship bookkeeping.
CreateAccountOp
will now be valid if startingBalance >= 0
, whereas prior to
this proposal it was valid if startingBalance > 0
.
Other operations will need updated semantics in order to behave correctly with this proposal. Specifically, changes will be required to handle sponsorship wherever ledger entries are created or removed.
In CAP-0031, an alternative approach to sponsorship, the logic for determining what can be sponsored is stored on the ledger. Not only is this complicated to implement and reason about, but it also introduces a variety of limitations in terms of what logic is supported. Moving sponsorship logic off-chain through the "sandwich approach", analogous to what is done in CAP-0018, eliminates all of these disadvantages.
There are a variety of reasons that the is-sponsoring-future-reserves-for relation is ephemeral (by which I mean contained within a single transaction). From a practical perspective, it would be deeply unwise to delegate to another party the right to make decisions about how your funds can be used to pay reserves. If you were to do this, then the other party could drain your entire balance.
But the technical reasons are far more compelling. Ephemeral sponsorship
guarantees that both the sponsoring and sponsored accounts must sign any
transaction that establishes a sponsorship relation. This applies even in the
case of sponsorship for CreateAccountOp
, whereas CreateAccountOp
can
usually be used without a signature from the created account. As a consequence,
sponsorship introduces absolutely no backwards compatibility issues with regard
to pre-signed transactions or pre-authorized transactions.
Because every sponsorship requires agreement from the sponsoring and sponsored accounts, it is safe to allow sponsorship revocation when the sponsored account can afford the reserve itself. That is part of the contract of the sponsorship relation, and if you didn't want revocation to occur then you shouldn't have accepted the sponsorship in the first place.
An account that is-sponsoring-future-reserves-for another account cannot be
merged. This does not constrain functionality at all, but simplifies the
implementation and reduces the number of possible errors that can be encountered
by downstream systems. Any transaction where this requirement would have been
the difference between success and failure necessarily contains both a
SponsorFutureReservesOp
and a ConfirmAndClearSponsorOp
. These operations
could simply be rearranged to permit the merge.
An account that is the current sponsor for any ledger entry or sub-entry cannot
be merged. If you want to merge such an account, then you can use
UpdateSponsorshipOp
to transfer the sponsorships to another account. Again,
this restriction greatly simplifies the implementation and semantics.
A typical use case for sponsorship is an enterprise sponsoring the reserves for customers. Because sponsorship requires signatures from both the sponsoring account and the sponsored account, this requires multi-signature coordination. But an enterprise may be coordinating many such signatures at once. Each coordination requires a sequence number. When sponsoring sub-entries such as signers and trust lines, it is possible to use the customer's sequence number. But sponsoring account creation requires the enterprise to provide the sequence number. This will likely require a pool of channel accounts, which is a source of complexity.
Despite the complexity that will be required for large-scale creation of sponsored accounts, we still believe that this approach is justified by its many benefits. Furthermore, the need for channel accounts during multi-signature coordination is an area in which the Stellar protocol can be generally improved. Already there are discussions of ideas which may mitigate some of these issues such as David Mazieres' proposal for Authenticated Operations, which can be found at https://groups.google.com/forum/#!msg/stellar-dev/zpO0Eppn8ks/e1ULbV_lAgAJ.
For ledger entries, the sponsoring ID is stored within the ledger entry. But for
signers, the sponsoring ID is stored separately. Why take different approaches?
The reason is that LedgerEntry
has an extension point but Signer
does not.
So if we wanted to store the sponsoring ID in the signer, then we would need to
extend the SignerKey
union. But this would force any downstream system that
interacts with signers to update as well, introducing complexity throughout the
ecosystem. Storing the sponsoring IDs for signers separately avoids this problem
entirely, but it is perhaps slightly less elegant. Still, the trade-off is more
than justified.
The relationship between sponsoring account and sponsored account can only be created by mutual agreement. When this relationship is established, the sponsoring account is endowed with the right to unilaterally end the relationship (ie. revoke the sponsorship) as long as doing so will not leave the sponsored account below the reserve requirement. But revoking the sponsorship is strictly worse for the sponsored account than transferring the sponsorship, because the worst thing that the new sponsoring account can do is revoke the sponsorship. Therefore, the sponsoring account should be able to transfer the sponsorship with the consent of the new sponsoring account but unilaterally with respect to the sponsored account.
So how would a transfer work if UpdateSponsorshipOp
were to obey the typical
rules of sponsorship? Let S1
be the current sponsor, S2
be the new sponsor,
K
be the LedgerKey
for an entry sponsored by S1
, and A
be the account
which controls the entry identified by K
. We would need a transaction with
S2
is source forSponsorFutureReservesOp
withsponsoredID = A
S1
is source forUpdateSponsorshipOp
withledgerKey = K
A
is source forConfirmAndClearSponsorshipOp
But this clearly requires a signature from the sponsored account, so if we are to satisfy the "unilateral" semantics described above then this approach cannot work.
This also resolves an issue for ClaimableBalanceEntry
, because there is no
notion of an account that controls a ClaimableBalanceEntry
.
In this example, we demonstrate how an account can be sponsored upon creation.
Let S
be the sponsoring account, C
be the creating account, and A
the
newly created account (S
and C
may be the same account). Then the following
transaction achieves the desired goal:
sourceAccount: C
fee: <FEE>
seqNum: <SEQ_NUM>
timeBounds: <TIME_BOUNDS>
memo:
type: MEMO_NONE
operations[0]:
sourceAccount: S
body:
type: SPONSOR_FUTURE_RESERVES
sponsoredID: A
operations[1]:
sourceAccount: C
body:
type: CREATE_ACCOUNT
destination: A
startingBalance: <STARTING_BALANCE>
operations[2]:
sourceAccount: A
body:
type: CONFIRM_AND_CLEAR_SPONSOR
where <FEE>
, <SEQ_NUM>
, <TIME_BOUNDS>
, and <STARTING_BALANCE>
should all
be substituted appropriately. Note that this requires a signature from A
even
though that account is being created.
In this example, we demonstrate how a single account can create two trust lines
which are sponsored by different accounts in a single transaction. Let S1
and
S2
be the sponsoring accounts. Let A
be the sponsored account. Then the
following transaction achieves the desired goal:
sourceAccount: A
fee: <FEE>
seqNum: <SEQ_NUM>
timeBounds: <TIME_BOUNDS>
memo:
type: MEMO_NONE
operations[0]:
sourceAccount: S1
body:
type: SPONSOR_FUTURE_RESERVES
sponsoredID: A
operations[1]:
sourceAccount: A
body:
type: CHANGE_TRUST
line: X
limit: INT64_MAX
operations[2]:
sourceAccount: A
body:
type: CONFIRM_AND_CLEAR_SPONSOR
operations[3]:
sourceAccount: S2
body:
type: SPONSOR_FUTURE_RESERVES
sponsoredID: A
operations[4]:
sourceAccount: A
body:
type: CHANGE_TRUST
line: Y
limit: INT64_MAX
operations[5]:
sourceAccount: A
body:
type: CONFIRM_AND_CLEAR_SPONSOR
where <FEE>
, <SEQ_NUM>
, and <TIME_BOUNDS>
should all be substituted
appropriately.
In this example, we demonstrate how a sponsorship can be revoked. Let S
be the sponsoring account. Let K
be the LedgerKey
for an entry which
is currently sponsored by S
. Then the following transaction achieves the
desired goal:
sourceAccount: S
fee: <FEE>
seqNum: <SEQ_NUM>
timeBounds: <TIME_BOUNDS>
memo:
type: MEMO_NONE
operations[0]:
sourceAccount: S
body:
type: UPDATE_SPONSORSHIP
ledgerKey: K
where <FEE>
, <SEQ_NUM>
, and <TIME_BOUNDS>
should all be substituted
appropriately.
In this example, we demonstrate how a sponsorship can be transferred. Let S1
and S2
be the sponsoring accounts. Let K
be the LedgerKey
for an entry
which is currently sponsored by S1
. Then the following transaction achieves
the desired goal:
sourceAccount: S1
fee: <FEE>
seqNum: <SEQ_NUM>
timeBounds: <TIME_BOUNDS>
memo:
type: MEMO_NONE
operations[0]:
sourceAccount: S2
body:
type: SPONSOR_FUTURE_RESERVES
sponsoredID: S1
operations[1]:
sourceAccount: S1
body:
type: UPDATE_SPONSORSHIP
ledgerKey: K
operations[2]:
sourceAccount: S1
body:
type: CONFIRM_AND_CLEAR_SPONSOR
where <FEE>
, <SEQ_NUM>
, and <TIME_BOUNDS>
should all be substituted
appropriately.
All downstream systems will need updated XDR in order to recognize the new operations and the modified ledger entries.
Any downstream system relying on the fact that CreateAccountOp
is invalid if
startingBalance = 0
will be affected.
This proposal will slightly reduce the efficacy of base reserve changes, because sponsored ledger entries cannot cause an account to pass below the reserve requirement.
None yet.
None yet.