Skip to content

Latest commit

 

History

History
917 lines (781 loc) · 31.7 KB

cap-0033.md

File metadata and controls

917 lines (781 loc) · 31.7 KB

Preamble

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

Simple Summary

This proposal makes it possible to pay reserves for another account.

Motivation

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.

Goals Alignment

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.

Abstract

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.

Specification

This specification assumes CAP-0023, in order to show how sponsorships would work for claimable balance entries.

XDR

AccountEntry

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;
};

ClaimableBalanceEntry

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;
};

LedgerEntry

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;
};

Operations

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;
};

Operation Results

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;
};

Transaction Results

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
};

Semantics

Reserve Requirement

No operation can cause an account to have balance < (2 + numSubEntries + numSponsoring - numSponsored) * baseReserve + liabilities.selling.

Sponsoring-Future-Reserves

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.

Sponsoring-Future-Reserves Invariants

There are two invariants related to sponsoring-future-reserves:

  • If an account A is sponsoring-future-reserves for an account B, then B 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 account B, then A exists. The converse need not be true (this facilitates creating a sponsored account).

Sponsorship Invariants

There are three invariants related to sponsorships:

  • (Global) The sum of all numSponsoring is equal to the sum of all numSponsored plus the sum of all ClaimableBalance.claimants.size().
  • (Local) For an account A, numSponsoring is equal to the count of all LedgerEntry le with le.ext.v1().sponsoringID == A weighted by the number of reserves required for that LedgerEntry (2 for ACCOUNT, claimants.size() for CLAIMABLE_BALANCE, 1 otherwise) plus the count of all A in signerSponsoringIDs.
  • (Local) For an account A, numSponsored is equal to the count of all LedgerEntry le which are sub-entries of A (including A itself) that have le.ext.v1().sponsoringID != null weighted by the number of reserves required for that LedgerEntry (2 for ACCOUNT, 1 otherwise) plus the count of all s in A.ext.v1().ext.v2().signerSponsoringIDs with s != null. Note that Claimable balances are not sub-entries, so they do not affect numSponsored.

BeginSponsoringFutureReservesOp

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

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

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

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

CreateAccountOp will now be valid if startingBalance >= 0, whereas prior to this proposal it was valid if startingBalance > 0.

ManageSellOfferOp and ManageBuyOfferOp

Starting in protocol version 15, ManageSellOfferOp and ManageBuyOfferOp will be invalid if offerID < 0.

Operations that can change numSubEntries

Any operation that can change numSubEntries can now fail with opTOO_MANY_SPONSORING, if any numSponsoring or numSponsored would exceed UINT32_MAX.

Design Rationale

Sponsorship Logic is Off-Chain

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.

Why Should Sponsorship be Ephemeral?

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.

Sponsoring Accounts Cannot be Merged

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.

Sequence Number Utilization and Sponsoring Account Creation

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.

Why are Signer Sponsoring IDs Stored Separately from Signers?

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.

Why Doesn't RevokeSponsorshipOp Obey Typical Rules of Sponsorship?

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

  1. S2 is source for BeginSponsoringFutureReservesOp with sponsoredID = A
  2. S1 is source for RevokeSponsorshipOp with ledgerKey = K
  3. A is source for EndSponsoringFutureReservesOp

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.

There is no opTOO_MANY_SPONSORED

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.

Example: Sponsoring Account Creation

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.

Example: Two Trust Lines with Different Sponsors

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.

Example: Revoke Sponsorship

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.

Example: Transfer Sponsorship

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.

Backwards Incompatibilities

New XDR

All downstream systems will need updated XDR in order to recognize the new operations and the modified ledger entries.

Operation Validity Changes

Any downstream system relying on any of the following facts will be affected:

  • CreateAccountOp is invalid if startingBalance = 0
  • Manage offer operations can be valid if offerID < 0

Security Concerns

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.

Test Cases

None yet.

Implementation

None yet.