Skip to content

Latest commit

 

History

History
684 lines (578 loc) · 23.5 KB

cap-0031.md

File metadata and controls

684 lines (578 loc) · 23.5 KB

Preamble

CAP: 0031
Title: Sponsored Reserve
Author: Jonathan Jove
Status: Rejected (In favor of CAP-0033)
Created: 2020-03-31
Discussion: https://groups.google.com/forum/#!msg/stellar-dev/E_tDs17mkJw/DmGXVY-QBAAJ
Protocol version: TBD

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-0031 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 SponsorshipEntry as a new type of LedgerEntry which represents an offer to pay the reserve for a LedgerEntry described by descriptor. The operation CreateSponsorshipOp makes it possible to create a SponsorshipEntry whereas the operation RemoveSponsorshipOp makes it possible to remove a SponsorshipEntry. These operations are the only ways in which a SponsorshipEntry can be created or removed, and they are otherwise immutable.

Specification

XDR

AccountEntry

struct AccountEntry
{
    // ... accountID, ..., signers unchanged ...

    union switch (int v)
    {
    // ... v0, v1 unchanged ...
    case 2:
        struct
        {
            Liabilities liabilities;

            // The number of reserves sponsored for this ledger entry
            uint32 sponsoredReserves;

            union switch (int v)
            {
            case 0:
                void;
            }
            ext;
        } v2;
    }
    ext;
};

SponsorshipEntry

struct LedgerEntryType
{
    // ... ACCOUNT, TRUSTLINE, OFFER unchanged ...
    DATA = 3,
    SPONSORSHIP = 4
};

enum SponsorshipType
{
    ACCOUNT_SPONSORSHIP = 0,
    SIGNER_SPONSORSHIP = 1,
    TRUSTLINE_SPONSORSHIP = 2,
    OFFER_SPONSORSHIP = 3,
    DATA_SPONSORSHIP = 4
};

struct AccountSponsorship
{
    // Account to sponsor
    AccountID accountID;
};

struct SignerSponsorship
{
    // Account for which to sponsor the signer
    AccountID accountID;
};

struct TrustLineSponsorship
{
    // Account for which to sponsor the trust line
    AccountID accountID;

    // Trust line asset
    Asset asset;
};

struct OfferSponsorship
{
    // Account for which to sponsor the offer
    AccountID sellerID;

    // Offer must be buying this asset
    Asset buying;

    // Offer must be selling this asset
    Asset selling;
};

struct DataSponsorship
{
    // Account for which to sponsor the data
    AccountID accountID;

    // Name of the data entry to sponsor
    string64 dataName;
};

union SponsorshipDescriptor switch (SponsorshipType type)
{
case ACCOUNT_SPONSORSHIP:
    AccountSponsorship account;
case SIGNER_SPONSORSHIP:
    SignerSponsorship signer;
case TRUSTLINE_SPONSORSHIP:
    TrustLineSponsorship trustLine;
case OFFER_SPONSORSHIP:
    OfferSponsorship offer;
case DATA_SPONSORSHIP:
    DataSponsorship data;
};

struct SponsorshipEntry
{
    // Account that created this sponsorship
    AccountID createdBy;

    // Global ordered identifier for sponsorships
    int64 sponsorshipID;

    // Describe the ledger entries being sponsored
    SponsorshipDescriptor descriptor;

    // Reserve stored in this sponsorship
    int64 reserve;

    // reserved for future use
    union switch (int v)
    {
    case 0:
        void;
    }
    ext;
};

struct LedgerEntry
{
    uint32 lastModifiedLedgerSeq; // ledger the LedgerEntry was last changed

    union switch (LedgerEntryType type)
    {
    // ... ACCOUNT, TRUSTLINE, OFFER, DATA unchanged ...
    case SPONSORSHIP:
        SponsorshipEntry sponsorship;
    }
    data;

    // reserved for future use
    union switch (int v)
    {
    case 0:
        void;
    }
    ext;
};

struct LedgerKey
{
    // ... ACCOUNT, TRUSTLINE, OFFER, DATA unchanged ...
case SPONSORSHIP:
    struct
    {
        AccountID createdBy;
        int64 sponsorshipID;
    } sponsorship;
};

Operations

enum OperationType
{
    // ... CREATE_ACCOUNT, ..., MANAGE_BUY_OFFER unchanged ...
    PATH_PAYMENT_STRICT_SEND = 13,
    CREATE_SPONSORSHIP = 14,
    REMOVE_SPONSORSHIP = 15
};

struct CreateSponsorshipOp
{
    // Describe the ledger entries being sponsored
    SponsorshipDescriptor descriptor;
};

struct RemoveSponsorshipOp
{
    // The ID for the sponsorship to remove
    int64 sponsorshipID;
};

struct Operation
{
    // sourceAccount is the account used to run the operation
    // if not set, the runtime defaults to "sourceAccount" specified at
    // the transaction level
    AccountID* sourceAccount;

    union switch (OperationType type)
    {
    // ... CREATE_ACOUNT, ..., PATH_PAYMENT_STRICT_SEND unchanged ...
    case CREATE_SPONSORSHIP:
        CreateSponsorshipOp createSponsorshipOp;
    case REMOVE_SPONSORSHIP:
        RemoveSponsorshipOp removeSponsorshipOp;
    }
    body;
};

Operation Results

enum CreateSponsorshipResultCode
{
    CREATE_SPONSORSHIP_SUCCESS = 0,
    CREATE_SPONSORSHIP_MALFORMED = -1,
    CREATE_SPONSORSHIP_LOW_RESERVE = -2
};

union CreateSponsorshipResult switch (CreateSponsorshipResultCode code)
{
case CREATE_SPONSORSHIP_SUCCESS:
    void;
default:
    void;
};

enum RemoveSponsorshipResultCode
{
    REMOVE_SPONSORSHIP_SUCCESS = 0,
    REMOVE_SPONSORSHIP_DOES_NOT_EXIST = -1,
    REMOVE_SPONSORSHIP_NOT_CREATOR = -2,
    REMOVE_SPONSORSHIP_IN_USE = -3,
    REMOVE_SPONSORSHIP_LINE_FULL = -4
};

union RemoveSponsorshipResult switch (RemoveSponsorshipResultCode code)
{
case REMOVE_SPONSORSHIP_SUCCESS:
    void;
default:
    void;
};

struct OperationResult
{
    // sourceAccount is the account used to run the operation
    // if not set, the runtime defaults to "sourceAccount" specified at
    // the transaction level
    AccountID* sourceAccount;

    union switch (OperationType type)
    {
    // ... CREATE_ACOUNT, ..., PATH_PAYMENT_STRICT_SEND unchanged ...
    case CREATE_SPONSORSHIP:
        CreateSponsorshipResult createSponsorshipResult;
    case REMOVE_SPONSORSHIP:
        RemoveSponsorshipResult removeSponsorshipResult;
    }
    body;
};

Semantics

Available Balance and Limit of Native Asset

This proposal changes the definition of available balance of native asset to: balance - (2 + numSubEntries - sponsoredReserves) * baseReserve. The definition of available limit of native asset is unchanged, and remains INT64_MAX - balance.

CreateSponsorshipOp

A SponsorshipEntry can only be created by the CreateSponsorshipOp operation. CreateSponsorshipOp is invalid with CREATE_SPONSORSHIP_MALFORMED if

  • descriptor.type() == TRUSTLINE and any of
    • descriptor.trustLine().asset is of type ASSET_TYPE_NATIVE
    • descriptor.trustLine().asset is invalid
  • descriptor.type() == OFFER and any of
    • descriptor.offer().buying is invalid
    • descriptor.offer().selling is invalid
    • descriptor.offer().buying == descriptor.offer().selling
  • descriptor.type() == DATA and
    • descriptor.data().dataName is empty or invalid

The behavior of CreateSponsorshipOp is as follows:

  1. Calculate Multiplier as
    • 3 if descriptor.type() == ACCOUNT
    • 2 otherwise
  2. Fail with CREATE_SPONSORSHIP_LOW_RESERVE if the sourceAccount does not have at least Multiplier * baseReserve available balance of native asset
  3. Deduct Multiplier * baseReserve of native asset from sourceAccount
  4. Create a SponsorshipEntry as sponsorship with the following properties:
    • sponsorship.createdBy = sourceAccount
    • sponsorship.sponsorshipID as the next available
    • sponsorship.descriptor = descriptor
    • sponsorship.reserve = Multiplier * baseReserve
  5. Count the number of existing LedgerEntry described by descriptor as Sponsorable
  6. Count the number of existing SponsorshipEntry with descriptor = sponsorship.descriptor and sponsorshipID < sponsorship.sponsorshipID as Sponsors
  7. Succeed with CREATE_SPONSORSHIP_SUCCESS if Sponsors >= Sponsorable
  8. Let SponsoredAccount be
    • descriptor.account().accountID if descriptor.type() == ACCOUNT
    • descriptor.signer().accountID if descriptor.type() == SIGNER
    • descriptor.trustLine().accountID if descriptor.type() == TRUSTLINE
    • descriptor.offer().sellerID if descriptor.type() == OFFER
    • descriptor.data().accountID if descriptor.type() == DATA
  9. Succeed with CREATE_SPONSORSHIP_SUCCESS if SponsoredAccount does not exist
  10. Load SponsoredAccount and increment SponsoredAccount.sponsoredReserves by Multiplier - 1
  11. Succeed with CREATE_SPONSORSHIP_SUCCESS

CreateSponsorshipOp requires medium threshold because it can be used to send funds.

RemoveSponsorshipOp

A SponsorshipEntry can only be removed by the RemoveSponsorshipOp operation. RemoveSponsorshipOp is invalid with REMOVE_SPONSORSHIP_MALFORMED if

  • sponsorshipID <= 0

The behavior of RemoveSponsorshipOp is as follows:

  1. Fail with REMOVE_SPONSORSHIP_DOES_NOT_EXIST if there is no SponsorshipEntry with the specified sponsorshipID
  2. Load the SponsorshipEntry as sponsorship
  3. Fail with REMOVE_SPONSORSHIP_NOT_CREATOR if sponsorship.createdBy != sourceAccount
  4. Count the number of existing LedgerEntry described by descriptor as Sponsorable
  5. Count the number of existing SponsorshipEntry with descriptor = sponsorship.descriptor and sponsorshipID < sponsorship.sponsorshipID as Sponsors
  6. Fail with REMOVE_SPONSORSHIP_IN_USE if Sponsorable > Sponsors
  7. Fail with REMOVE_SPONSORSHIP_LINE_FULL if the sourceAccount does not have reserve available limit of native asset
  8. Add reserve of native asset to sourceAccount
  9. Remove sponsorship
  10. Succeed with REMOVE_SPONSORSHIP_SUCCESS

RemoveSponsorshipOp requires medium threshold because it is related to CreateSponsorshipOp.

CreateAccountOp

We now return CREATE_ACCOUNT_LOW_RESERVE conditionally

  1. ...
  2. Skip to step 4 if a SponsorshipEntry with descriptor.type() == ACCOUNT and descriptor.account().accountID = destination exists
  3. Fail with CREATE_ACCOUNT_LOW_RESERVE if startingBalance < 2 * baseReserve
  4. ...

When creating the account, we now

  1. ...
  2. Set destination.sponsoredReserves = 2 if a SponsorshipEntry with descriptor.type() == ACCOUNT and descriptor.account().accountID = destination exists
  3. ...

SetOptionsOp

We now return SET_OPTIONS_LOW_RESERVE conditionally

  1. ...
  2. Count the number of signers on sourceAccount as Signers
  3. Count the number of SponsorshipEntry with descriptor.type() == SIGNER and descriptor.signer().accountID = sourceAccount as Sponsors
  4. Skip to step 7 if Signers >= Sponsors
  5. Increment sourceAccount.sponsoredReserves
  6. Skip to step 8
  7. Fail with SET_OPTIONS_LOW_RESERVE if the sourceAccount does not have at least baseReserve available balance of native asset
  8. Increment sourceAccount.numSubEntries
  9. ...

When deleting a signer, we now

  1. ...
  2. Count the number of signers on sourceAccount as Signers
  3. Count the number of SponsorshipEntry with descriptor.type() == SIGNER and descriptor.signer().accountID = sourceAccount as Sponsors
  4. Decrement sourceAccount.sponsoredReserves if Signers <= Sponsors
  5. Decrement sourceAccount.numSubEntries
  6. ...

Removing Used One-Time Signers

When deleting a signer, we now

  1. ...
  2. Count the number of signers on sourceAccount as Signers
  3. Count the number of SponsorshipEntry with descriptor.type() == SIGNER and descriptor.signer().accountID = sourceAccount as Sponsors
  4. Decrement sourceAccount.sponsoredReserves if Signers <= Sponsors
  5. Decrement sourceAccount.numSubEntries
  6. ...

ChangeTrustOp

We now return CHANGE_TRUST_LOW_RESERVE conditionally

  1. ...
  2. Skip to step 5 if a SponsorshipEntry with descriptor.type() == TRUSTLINE, descriptor.trustLine().accountID = sourceAccount, and descriptor.trustLine().asset = asset does not exist
  3. Increment sourceAccount.sponsoredReserves
  4. Skip to step 6
  5. Fail with CHANGE_TRUST_LOW_RESERVE if the sourceAccount does not have at least baseReserve available balance of native asset
  6. Increment sourceAccount.numSubEntries
  7. ...

When deleting a trust line, we now

  1. ...
  2. Decrement sourceAccount.sponsoredReserves if a SponsorshipEntry with descriptor.type() == TRUSTLINE, descriptor.trustLine().accountID = sourceAccount, and descriptor.trustLine().asset = asset exists
  3. Decrement sourceAccount.numSubEntries
  4. ...

AllowTrustOp

When deleting offers after revoking authorization, we now

  1. ...
  2. For each asset pair (Buying, Selling) of an offer that was deleted a. Count the number of offers that were deleted as OffersDeleted b. Count the number of SponsorshipEntry with descriptor.type() == OFFER, descriptor.offer().sellerID = trustor, descriptor.offer().buying = Buying, and descriptor.offer().selling = Selling as Sponsors c. Decrement trustor.sponsoredReserves by min(OffersDeleted, Sponsors) d. Decrement trustor.numSubEntries by OffersDeleted
  3. ...

ManageSellOfferOp, ManageBuyOfferOp, and CreatePassiveSellOfferOp

When releasing liabilities before modifying an existing offer with asset pair (Buying, Selling), we now

  1. ...
  2. Count the number of offers with sellerID = sourceAccount, buying = Buying, and selling = Selling as Sponsorable
  3. Count the number of SponsorshipEntry with descriptor.type() == OFFER, descriptor.offer().sellerID = sourceAccount, descriptor.offer().buying = Buying, and descriptor.offer().selling = Selling as Sponsors
  4. Decrement sourceAccount.sponsoredReserve if Sponsorable <= Sponsors
  5. Decrement sourceAccount.numSubEntries
  6. ...

When computing the amount of Buying that can be bought and the amount of Selling that can be sold (with Buying and Selling not necessarily equal to the above), we now

  1. ...
  2. Count the number of offers with sellerID = sourceAccount, buying = Buying, and selling = Selling as Sponsorable
  3. Count the number of SponsorshipEntry with descriptor.type() == OFFER, descriptor.offer().sellerID = sourceAccount, descriptor.offer().buying = Buying, and descriptor.offer().selling = Selling as Sponsors
  4. Increment sourceAccount.sponsoredReserve if Sponsorable < Sponsors
  5. Skip to step 7
  6. Fail with MANAGE_OFFER_LOW_RESERVE if the sourceAccount does not have at least baseReserve available balance of native asset
  7. Increment sourceAccount.numSubEntries
  8. ...

When erasing an offer that was either taken entirely or partially taken and adjusted to 0, we now

  1. ...
  2. Count the number of offers with matching sellerID, buying, and selling as Sponsorable
  3. Count the number of SponsorshipEntry with descriptor.type() == OFFER, descriptor.offer().sellerID = sellerID, descriptor.offer().buying = buying, and descriptor.offer().selling = selling as Sponsors
  4. Decrement sourceAccount.sponsoredReserve if Sponsorable <= Sponsors
  5. Decrement sourceAccount.numSubEntries
  6. ...

When creating the offer, we now follow the same process as for computing the amount that can be bought and sold. Note that the failure in step 6 should never happen in this case.

PathPaymentStrictSend and PathPaymentStrictReceive

When erasing an offer that was either taken entirely or partially taken and adjusted to 0, we now

  1. ...
  2. Count the number of offers with matching sellerID, buying, and selling as Sponsorable
  3. Count the number of SponsorshipEntry with descriptor.type() == OFFER, descriptor.offer().sellerID = sellerID, descriptor.offer().buying = buying, and descriptor.offer().selling = selling as Sponsors
  4. Decrement sourceAccount.sponsoredReserve if Sponsorable <= Sponsors
  5. Decrement sourceAccount.numSubEntries
  6. ...

Increasing the Base Reserve

When preparing to update offers, the balance above reserve should be calculated as balance - (2 + numSubEntries - sponsoredReserves) * baseReserve.

When erasing an offer or adjusting an offer to 0, we now

  1. ...
  2. Count the number of offers with matching sellerID, buying, and selling as Sponsorable
  3. Count the number of SponsorshipEntry with descriptor.type() == OFFER, descriptor.offer().sellerID = sellerID, descriptor.offer().buying = buying, and descriptor.offer().selling = selling as Sponsors
  4. Decrement sourceAccount.sponsoredReserve if Sponsorable <= Sponsors
  5. Decrement sourceAccount.numSubEntries
  6. ...

ManageDataOp

We now return MANAGE_DATA_LOW_RESERVE conditionally

  1. ...
  2. Skip to step 5 if a SponsorshipEntry with descriptor.type() == DATA, descriptor.data().accountID = sourceAccount, and descriptor.data().dataName = dataName does not exist
  3. Increment sourceAccount.sponsoredReserves
  4. Skip to step 6
  5. Fail with MANAGE_DATA_LOW_RESERVE if the sourceAccount does not have at least baseReserve available balance of native asset
  6. Increment sourceAccount.numSubEntries
  7. ...

When deleting data, we now

  1. ...
  2. Decrement sourceAccount.sponsoredReserves if a SponsorshipEntry with descriptor.type() == DATA, descriptor.data().accountID = sourceAccount, and descriptor.data().dataName = dataName exists
  3. Decrement sourceAccount.numSubEntries
  4. ...

Design Rationale

How are sponsorship entries paired with the ledger entries they sponsor?

Sponsorship entries are paired with the ledger entries they sponsor implicitly. Sponsorship entries do not record what ledger entry they are currently sponsoring, nor do ledger entries record what sponsorship entry is currently sponsoring them. This is a consequence of the fact that there is no natural pairing between individual entries. To understand this, consider the situation where an account has two signers and there is a single sponsorship entry to sponsor those signers. Obviously, only one of the signers can be sponsored at any given time. But does it matter which one is sponsored? The answer is no:

  • Regardless of which signer is sponsored, the sponsorship will contribute one sponsored reserve
  • Regardless of which signer is sponsored, if either signer is removed then the remaining signer will be sponsored

Clearly, it is not meaningful to assign a sponsorship entry to a ledger entry. But what if you want to know which sponsorship entries are actually sponsoring a ledger entry? The answer to this question does matter, because it is forbidden to delete a sponsorship entry that is providing reserves. The sponsorship entries have an identifying characteristic, the sponsorshipID, which makes it possible to unambiguously answer this question. If there are N ledger entries that can be sponsored by a group of sponsorship entries with the same descriptor, then the sponsorship entries among that group that are actually sponsoring a ledger entry have the N lowest sponsorshipID. Because sponsorshipID is monotonically increasing in the number of SponsorshipEntry created, this is equivalent to the N oldest sponsorship entries that still exist.

Why do sponsorship entries sponsor reserve for only a single ledger entry?

It is reasonable to think that a single sponsorship entry might be able to sponsor reserves for multiple ledger entries, such as a single sponsorship entry sponsoring up to 5 signers. In fact, the earliest drafts of this proposal had that feature. The feature was removed because the costs outweighed the benefits. It considerably increased the complexity of certain aspects of this proposal.

First, it is clear that not every type of sponsorship entry actually could sponsor reserves for multiple ledger entries. Consider, for example, a sponsorship entry for trust lines. There can only exist at most one trust line that actually matches the descriptor, so there is no situation in which this sponsorship entry can actually sponsor reserves for multiple ledger entries. This led conceptually to two categories of sponsorship entries, which increased the complexity of both the mental model and the implementation.

Second, it led to the introduction of a ManageSponsorshipOp operation that was able to modify the number of reserves that a given sponsorship entry could sponsor. This was required because it is not permitted to delete a sponsorship entry that is providing reserves, so without the operation a sponsorship entry that was configured to provide 10 reserves but was actually only providing 1 could not be modified to release the other 9 reserves. The semantics of this operation were quite complex, since it contained aspects of both CreateSponsorshipOp and RemoveSponsorshipOp.

Sponsorship entries always provide a full reserve

Suppose there exists an entity that creates accounts and trust lines for its clients, using sponsorship entry to retain control of the reserve. At some point in the future, the base reserve is increased. If the sponsorship entry provided an actual amount of reserve based on the base reserve when it was created, rather than a full reserve unconditionally, then the entity would now need to increase the sponsorship for every client. For a large entity, this could be many thousands of sponsorships to manage which would take time. In the interim, their service would be degraded and the user experience for their clients greatly harmed. The solution in this proposal avoids this issue entirely.

Accounts record the total number of sponsored reserves

Theoretically, it is not necessary for accounts to record the total number of sponsored reserves. The data is purely derived and can always be calculated at any time. That being said, calculating it on demand is not particularly easy because it requires iterating over all of the sub-entries of the account. It is therefore likely that any performant implementation would store this data either persistently or in a large-scale cache. But if the data is going to be stored anyway, then it might as well be recorded in the ledger where it can be utilized by downstream systems. Exactly the same arguments apply, for example, to liabilities and numSubEntries.

Backwards Incompatibilities

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

Downstream systems which calculate the required reserve for an account will now potentially calculate a value that is too high. While inconvenient, this should not cause any systems to fail.

Security Concerns

None.

Test Cases

None yet.

Implementation

None yet.