## 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 ```c++ 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`. ```c++ 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 ```c++ 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 ```c++ 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 ```c++ 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 ```c++ 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: seqNum: timeBounds: 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: operations[2]: sourceAccount: A body: type: END_SPONSORING_FUTURE_RESERVES ``` where ``, ``, ``, and `` 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: seqNum: timeBounds: 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 ``, ``, and `` 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: seqNum: timeBounds: memo: type: MEMO_NONE operations[0]: sourceAccount: S body: type: REVOKE_SPONSORSHIP ledgerKey: K ``` where ``, ``, and `` 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: seqNum: timeBounds: 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 ``, ``, and `` 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.