CAP: 0033
Title: Sponsored Reserve
Author: Jonathan Jove
Status: Final
Created: 2020-05-18
Updated: 2020-08-13
Discussion: https://groups.google.com/forum/#!msg/stellar-dev/E_tDs17mkJw/DmGXVY-QBAAJ
Protocol version: 14/15
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 sponsoring-future-reserves relation, 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 BeginSponsoringFutureReservesOp
,
where the sponsoring account is the source account, and is terminated by
EndSponsoringFutureReservesOp
, 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 RevokeSponsorshipOp
, which can be used to modify the
sponsorship of existing ledger entries and signers. 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,
BEGIN_SPONSORING_FUTURE_RESERVES = 14,
END_SPONSORING_FUTURE_RESERVES = 15,
REVOKE_SPONSORSHIP = 16
};
struct BeginSponsoringFutureReservesOp
{
// sponsoredID identifies the account for which future reserves will be
// sponsored by the source account of this operation
AccountID sponsoredID;
};
enum RevokeSponsorshipType
{
REVOKE_SPONSORSHIP_LEDGER_ENTRY = 0,
REVOKE_SPONSORSHIP_SIGNER = 1
};
union RevokeSponsorshipOp switch (RevokeSponsorshipType type)
{
case REVOKE_SPONSORSHIP_LEDGER_ENTRY:
// Uniquely identifies a non-signer ledger entry
LedgerKey ledgerKey;
case REVOKE_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 BEGIN_SPONSORING_FUTURE_RESERVES:
BeginSponsoringFutureReservesOp beginSponsoringFutureReservesOp;
case END_SPONSORING_FUTURE_RESERVES:
void;
case REVOKE_SPONSORSHIP:
RevokeSponsorshipOp revokeSponsorshipOp;
}
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 BeginSponsoringFutureReservesResultCode
{
// codes considered as "success" for the operation
BEGIN_SPONSORING_FUTURE_RESERVES_SUCCESS = 0,
// codes considered as "failure" for the operation
BEGIN_SPONSORING_FUTURE_RESERVES_MALFORMED = -1, // can't sponsor self
BEGIN_SPONSORING_FUTURE_RESERVES_ALREADY_SPONSORED = -2, // can't sponsor an account
// that is already sponsored
BEGIN_SPONSORING_FUTURE_RESERVES_RECURSIVE = -3 // can't sponsor an account
// that is itself sponsoring
// an account
};
union BeginSponsoringFutureReservesResult switch (BeginSponsoringFutureReservesResultCode code)
{
case BEGIN_SPONSORING_FUTURE_RESERVES_SUCCESS:
void;
default:
void;
};
enum EndSponsoringFutureReservesResultCode
{
// codes considered as "success" for the operation
END_SPONSORING_FUTURE_RESERVES_SUCCESS = 0,
// codes considered as "failure" for the operation
END_SPONSORING_FUTURE_RESERVES_NOT_SPONSORED = -1 // must be sponsored to end
};
union EndSponsoringFutureReservesResult switch (EndSponsoringFutureReservesResultCode code)
{
case END_SPONSORING_FUTURE_RESERVES_SUCCESS:
void;
default:
void;
};
enum RevokeSponsorshipResultCode
{
// codes considered as "success" for the operation
REVOKE_SPONSORSHIP_SUCCESS = 0,
// codes considered as "failure" for the operation
REVOKE_SPONSORSHIP_DOES_NOT_EXIST = -1, // specified entry does not exist
REVOKE_SPONSORSHIP_NOT_SPONSOR = -2, // not sponsor of specified entry
REVOKE_SPONSORSHIP_LOW_RESERVE = -3, // new reserve payor cannot afford
// this entry
REVOKE_SPONSORSHIP_ONLY_TRANSFERABLE = -4 // sponsorship of ClaimableBalance
// must be transferred to another
// account
};
union RevokeSponsorshipResult switch (RevokeSponsorshipResultCode code)
{
case REVOKE_SPONSORSHIP_SUCCESS:
void;
default:
void;
};
union OperationResult switch (OperationResultCode code)
{
case opINNER:
union switch (OperationType type)
{
// ... CREATE_ACCOUNT, ..., PATH_PAYMENT_STRICT_SEND unchanged ...
case BEGIN_SPONSORING_FUTURE_RESERVES:
BeginSponsoringFutureReservesResult beginSponsoringFutureReservesResult;
case END_SPONSORING_FUTURE_RESERVES:
EndSponsoringFutureReservesResult endSponsoringFutureReservesResult;
case REVOKE_SPONSORSHIP:
RevokeSponsorshipResult revokeSponsorshipResult;
}
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 ended
};
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 cause an account to have
balance < (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
. This is achieved by updating A.numSponsoring
and B.numSponsored
(unless
the reserve requirement is accumulating due to a ClaimableBalance
entry).
An account A
begins sponsoring-future-reserves for an account B
upon a
successful BeginSponsoringFutureReservesOp
with sourceAccount = A
and
sponsoredID = B
. A
stops sponsoring-future-reserves for B
upon a
successful EndSponsoringFutureReserves
with sourceAccount = B
. These are the
only two operations which can impact the sponsoring-future-reserves state of an
account.
There are two invariants related to sponsoring-future-reserves:
- If an account
A
is sponsoring-future-reserves for an accountB
, thenB
is not sponsoring-future-reserves for any account. Specifically, this prevents an account from sponsoring-future-reserves for itself. - If an account
A
is sponsoring-future-reserves for an accountB
, thenA
exists. The converse need not be true (this facilitates creating a sponsored account).
There are three invariants related to sponsorships:
- (Global) The sum of all
numSponsoring
is equal to the sum of allnumSponsored
plus the sum of allClaimableBalance.claimants.size()
. - (Local) For an account
A
,numSponsoring
is equal to the count of allLedgerEntry le
withle.ext.v1().sponsoringID == A
weighted by the number of reserves required for thatLedgerEntry
(2 forACCOUNT
,claimants.size()
forCLAIMABLE_BALANCE
, 1 otherwise) plus the count of allA
insignerSponsoringIDs
. - (Local) For an account
A
,numSponsored
is equal to the count of allLedgerEntry le
which are sub-entries ofA
(includingA
itself) that havele.ext.v1().sponsoringID != null
weighted by the number of reserves required for thatLedgerEntry
(2 forACCOUNT
, 1 otherwise) plus the count of alls
inA.ext.v1().ext.v2().signerSponsoringIDs
withs != null
. Note that Claimable balances are not sub-entries, so they do not affectnumSponsored
.
BeginSponsoringFutureReservesOp
is the only operation that can initiate the
sponsoring-future-reserves relation. It is forbidden for A to be
sponsoring-future-reserves for B, and B to be sponsoring-future-reserves for C,
and this operation enforces this constraint.
To check validity of BeginSponsoringFutureReservesOp op
:
If op.sponsoredID == op.sourceAccount
Invalid with BEGIN_SPONSORING_FUTURE_RESERVES_MALFORMED
The behavior of BeginSponsoringFutureReservesOp op
is:
If an account is sponsoring future reserves for op.sponsoredID
Fail with BEGIN_SPONSORING_FUTURE_RESERVES_ALREADY_SPONSORED
If an account is sponsoring future reserves for op.sourceAccount
Fail with BEGIN_SPONSORING_FUTURE_RESERVES_RECURSIVE
If op.sponsoredID is sponsoring future reserves for any account
Fail with BEGIN_SPONSORING_FUTURE_RESERVES_RECURSIVE
Record that op.sourceAccount is sponsoring future reserves for op.sponsoredID
Succeed with BEGIN_SPONSORING_FUTURE_RESERVES_SUCCESS
BeginSponsoringFutureReservesOp
requires medium threshold.
EndSponsoringFutureReservesOp
is the only operation that can terminate the
sponsoring-future-reserves relation. This can only be done if some account is
sponsoring-future-reserves for the source account.
EndSponsoringFutureReservesOp
is always valid.
The behavior of EndSponsoringFutureReservesOp op
is:
If an account is not sponsoring future reserves for op.sourceAccount
Fail with END_SPONSORING_FUTURE_RESERVES_NOT_SPONSORED
Record that no account is sponsoring future reserves for op.sourceAccount
Succeed with END_SPONSORING_FUTURE_RESERVES_SUCCESS
EndSponsoringFutureReservesOp
requires medium threshold.
RevokeSponsorshipOp
allows a ledger entry or signer
- that is not sponsored to be sponsored,
- that is sponsored to change sponsors, or
- that is sponsored to no longer be sponsored.
To check validity of RevokeSponsorshipOp op
:
If ledgerVersion != 14
If op.type() == REVOKE_SPONSORSHIP_LEDGER_ENTRY
If op.ledgerKey().type() == TRUSTLINE
Let tl = op.ledgerKey().trustLine()
If tl.asset is not valid
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
If tl.asset.type() == ASSET_TYPE_NATIVE
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
If tl.accountID == getIssuer(tl.asset)
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
If op.ledgerKey().type() == OFFER
If op.ledgerKey().offer().offerID <= 0
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
If op.ledgerKey().type() == DATA
Let de = op.ledgerKey().data()
If de.dataName is empty
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
If de.dataName is not valid
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
The behavior of RevokeSponsorshipOp op
is as follows if
op.type() == REVOKE_SPONSORSHIP_LEDGER_ENTRY
:
Load op.ledgerKey() as le
If le does not exist
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
wasSponsored = false
If le.ext.v() == 0 or !le.ext.v1().sponsoringID
If getAccount(le) != op.sourceAccount
Fail with REVOKE_SPONSORSHIP_NOT_SPONSOR
Else if le.ext.v1().sponsoringID != op.sourceAccount
Fail with REVOKE_SPONSORSHIP_NOT_SPONSOR
Else
wasSponsored = true
// If the new sponsor would be the owner, this is equivalent to no
// sponsorship at all
willBeSponsored = false
If an account A is sponsoring future reserves for op.sourceAccount
If getAccount(le) != A
willBeSponsored = true
// Claimable balances must be sponsored
If !willBeSponsored and le.type() == CLAIMABLE_BALANCE
Fail with REVOKE_SPONSORSHIP_ONLY_TRANSFERABLE
Let mult be the number of base reserves required for le
If wasSponsored and willBeSponsored
// Change sponsors
Load account sponsoring future reserves for op.sourceAccount as newSponsor
If getAvailableBalance(newSponsor) < mult * baseReserve
Fail with REVOKE_SPONSORSHIP_LOW_RESERVE
If getNumSponsoring(newSponsor) > UINT32_MAX - mult
Fail with opTOO_MANY_SPONSORING
getNumSponsoring(newSponsor) += mult
Load le.ext.v1().sponsoringID as oldSponsor
getNumSponsoring(oldSponsor) -= mult
le.ext.v1().sponsoringID = newSponsor.accountID
Else if wasSponsored and !willBeSponsored
// Stop sponsoring something that was sponsored
Load getAccount(le) as owner
If getAvailableBalance(owner) < mult * baseReserve
Fail with REVOKE_SPONSORSHIP_LOW_RESERVE
getNumSponsored(owner) -= mult
Load le.ext.v1().sponsoringID as oldSponsor
getNumSponsoring(oldSponsor) -= mult
Clear le.ext.v1().sponsoringID
Else if !wasSponsored and willBeSponsored
// Sponsor something that wasn't sponsored
Load account sponsoring future reserves for op.sourceAccount as newSponsor
If getAvailableBalance(newSponsor) < mult * baseReserve
Fail with REVOKE_SPONSORSHIP_LOW_RESERVE
If getNumSponsoring(newSponsor) > UINT32_MAX - mult
Fail with opTOO_MANY_SPONSORING
getNumSponsoring(newSponsor) += mult
Load getAccount(le) as owner
getNumSponsored(owner) += mult
le.ext.v1().sponsoringID = newSponsor.accountID
Else
// This is a no-op
Succeed with REVOKE_SPONSORSHIP_SUCCESS
The behavior of RevokeSponsorshipOp op
is as follows if
op.type() == REVOKE_SPONSORSHIP_SIGNER
:
Load op.signer().accountID as le
If le does not exist
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
Let acc = le.data.account()
If op.signer().signerKey is not in acc.signers
Fail with REVOKE_SPONSORSHIP_DOES_NOT_EXIST
wasSponsored = false
If acc.ext.v() == 0 or (acc.ext.v() == 1 and acc.ext.v1().ext.v() == 0)
If acc.accountID != op.sourceAccount
Fail with REVOKE_SPONSORSHIP_NOT_SPONSOR
Else
Let i be the index of op.signer().signerKey in acc.signers
Let sid = acc.ext.v1().ext.v2().signerSponsoringIDs[i]
If !sid
If acc.accountID != op.sourceAccount
Fail with REVOKE_SPONSORSHIP_NOT_SPONSOR
Else if sid != op.sourceAccount
Fail with REVOKE_SPONSORSHIP_NOT_SPONSOR
Else
wasSponsored = true
// If the new sponsor would be the owner, this is equivalent to no
// sponsorship at all
willBeSponsored = false
If an account A is sponsoring future reserves for op.sourceAccount
If acc.accountID != A
willBeSponsored = true
If wasSponsored and willBeSponsored
// Change sponsors
Load account sponsoring future reserves for op.sourceAccount as newSponsor
If getAvailableBalance(newSponsor) < baseReserve
Fail with REVOKE_SPONSORSHIP_LOW_RESERVE
If getNumSponsoring(newSponsor) > UINT32_MAX - 1
Fail with opTOO_MANY_SPONSORING
getNumSponsoring(newSponsor) += 1
Let i be the index of op.signer().signerKey in acc.signers
Let sid = acc.ext.v1().ext.v2().signerSponsoringIDs[i]
Load sid as oldSponsor
getNumSponsoring(oldSponsor) -= 1
acc.ext.v1().ext.v2().signerSponsoringIDs[i] = newSponsor.accountID
Else if wasSponsored and !willBeSponsored
// Stop sponsoring something that was sponsored
If getAvailableBalance(acc) < baseReserve
Fail with REVOKE_SPONSORSHIP_LOW_RESERVE
getNumSponsored(acc) -= 1
Let i be the index of op.signer().signerKey in acc.signers
Let sid = acc.ext.v1().ext.v2().signerSponsoringIDs[i]
Load sid as oldSponsor
getNumSponsoring(oldSponsor) -= 1
Clear acc.ext.v1().ext.v2().signerSponsoringIDs[i]
Else if !wasSponsored and willBeSponsored
// Sponsor something that wasn't sponsored
Load account sponsoring future reserves for op.sourceAccount as newSponsor
If getAvailableBalance(newSponsor) < baseReserve
Fail with REVOKE_SPONSORSHIP_LOW_RESERVE
If getNumSponsoring(newSponsor) > UINT32_MAX - 1
Fail with opTOO_MANY_SPONSORING
getNumSponsoring(newSponsor) += 1
getNumSponsored(acc) += 1
Let i be the index of op.signer().signerKey in acc.signers
acc.ext.v1().ext.v2().signerSponsoringIDs[i] = newSponsor.accountID
Else
// This is a no-op
Succeed with REVOKE_SPONSORSHIP_SUCCESS
RevokeSponsorshipOp
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
.
Starting in protocol version 15, ManageSellOfferOp
and ManageBuyOfferOp
will
be invalid if offerID < 0
.
Any operation that can change numSubEntries
can now fail with
opTOO_MANY_SPONSORING
, if any numSponsoring
or
numSponsored
would exceed UINT32_MAX
.
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 sponsoring-future-reserves 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 BeginSponsoringFutureReservesOp
and a EndSponsoringFutureReservesOp
.
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
RevokeSponsorshipOp
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 RevokeSponsorshipOp
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 forBeginSponsoringFutureReservesOp
withsponsoredID = A
S1
is source forRevokeSponsorshipOp
withledgerKey = K
A
is source forEndSponsoringFutureReservesOp
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
.
It initially seems strange that there is no opTOO_MANY_SPONSORED
in analogy to
opTOO_MANY_SPONSORING
. But this case is already covered by the constraint that
numSponsored <= numSubEntries + 2 < UINT32_MAX
.
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: BEGIN_SPONSORING_FUTURE_RESERVES
sponsoredID: A
operations[1]:
sourceAccount: C
body:
type: CREATE_ACCOUNT
destination: A
startingBalance: <STARTING_BALANCE>
operations[2]:
sourceAccount: A
body:
type: END_SPONSORING_FUTURE_RESERVES
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: BEGIN_SPONSORING_FUTURE_RESERVES
sponsoredID: A
operations[1]:
sourceAccount: A
body:
type: CHANGE_TRUST
line: X
limit: INT64_MAX
operations[2]:
sourceAccount: A
body:
type: END_SPONSORING_FUTURE_RESERVES
operations[3]:
sourceAccount: S2
body:
type: BEGIN_SPONSORING_FUTURE_RESERVES
sponsoredID: A
operations[4]:
sourceAccount: A
body:
type: CHANGE_TRUST
line: Y
limit: INT64_MAX
operations[5]:
sourceAccount: A
body:
type: END_SPONSORING_FUTURE_RESERVES
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: REVOKE_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: BEGIN_SPONSORING_FUTURE_RESERVES
sponsoredID: S1
operations[1]:
sourceAccount: S1
body:
type: REVOKE_SPONSORSHIP
ledgerKey: K
operations[2]:
sourceAccount: S1
body:
type: END_SPONSORING_FUTURE_RESERVES
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 any of the following facts will be affected:
CreateAccountOp
is invalid ifstartingBalance = 0
- Manage offer operations can be valid if
offerID < 0
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.