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
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-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.
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 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.
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;
};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;
};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;
};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;
};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.
A SponsorshipEntry can only be created by the CreateSponsorshipOp operation.
CreateSponsorshipOp is invalid with CREATE_SPONSORSHIP_MALFORMED if
descriptor.type() == TRUSTLINEand any ofdescriptor.trustLine().assetis of typeASSET_TYPE_NATIVEdescriptor.trustLine().assetis invalid
descriptor.type() == OFFERand any ofdescriptor.offer().buyingis invaliddescriptor.offer().sellingis invaliddescriptor.offer().buying == descriptor.offer().selling
descriptor.type() == DATAanddescriptor.data().dataNameis empty or invalid
The behavior of CreateSponsorshipOp is as follows:
- Calculate
Multiplieras- 3 if
descriptor.type() == ACCOUNT - 2 otherwise
- 3 if
- Fail with
CREATE_SPONSORSHIP_LOW_RESERVEif thesourceAccountdoes not have at leastMultiplier * baseReserveavailable balance of native asset - Deduct
Multiplier * baseReserveof native asset fromsourceAccount - Create a
SponsorshipEntryassponsorshipwith the following properties:sponsorship.createdBy = sourceAccountsponsorship.sponsorshipIDas the next availablesponsorship.descriptor = descriptorsponsorship.reserve = Multiplier * baseReserve
- Count the number of existing
LedgerEntrydescribed bydescriptorasSponsorable - Count the number of existing
SponsorshipEntrywithdescriptor = sponsorship.descriptorandsponsorshipID < sponsorship.sponsorshipIDasSponsors - Succeed with
CREATE_SPONSORSHIP_SUCCESSifSponsors >= Sponsorable - Let
SponsoredAccountbedescriptor.account().accountIDifdescriptor.type() == ACCOUNTdescriptor.signer().accountIDifdescriptor.type() == SIGNERdescriptor.trustLine().accountIDifdescriptor.type() == TRUSTLINEdescriptor.offer().sellerIDifdescriptor.type() == OFFERdescriptor.data().accountIDifdescriptor.type() == DATA
- Succeed with
CREATE_SPONSORSHIP_SUCCESSifSponsoredAccountdoes not exist - Load
SponsoredAccountand incrementSponsoredAccount.sponsoredReservesbyMultiplier - 1 - Succeed with
CREATE_SPONSORSHIP_SUCCESS
CreateSponsorshipOp requires medium threshold because it can be used to send
funds.
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:
- Fail with
REMOVE_SPONSORSHIP_DOES_NOT_EXISTif there is noSponsorshipEntrywith the specifiedsponsorshipID - Load the
SponsorshipEntryassponsorship - Fail with
REMOVE_SPONSORSHIP_NOT_CREATORifsponsorship.createdBy != sourceAccount - Count the number of existing
LedgerEntrydescribed bydescriptorasSponsorable - Count the number of existing
SponsorshipEntrywithdescriptor = sponsorship.descriptorandsponsorshipID < sponsorship.sponsorshipIDasSponsors - Fail with
REMOVE_SPONSORSHIP_IN_USEifSponsorable > Sponsors - Fail with
REMOVE_SPONSORSHIP_LINE_FULLif thesourceAccountdoes not havereserveavailable limit of native asset - Add
reserveof native asset tosourceAccount - Remove
sponsorship - Succeed with
REMOVE_SPONSORSHIP_SUCCESS
RemoveSponsorshipOp requires medium threshold because it is related to
CreateSponsorshipOp.
We now return CREATE_ACCOUNT_LOW_RESERVE conditionally
- ...
- Skip to step 4 if a
SponsorshipEntrywithdescriptor.type() == ACCOUNTanddescriptor.account().accountID = destinationexists - Fail with
CREATE_ACCOUNT_LOW_RESERVEifstartingBalance < 2 * baseReserve - ...
When creating the account, we now
- ...
- Set
destination.sponsoredReserves = 2if aSponsorshipEntrywithdescriptor.type() == ACCOUNTanddescriptor.account().accountID = destinationexists - ...
We now return SET_OPTIONS_LOW_RESERVE conditionally
- ...
- Count the number of signers on
sourceAccountasSigners - Count the number of
SponsorshipEntrywithdescriptor.type() == SIGNERanddescriptor.signer().accountID = sourceAccountasSponsors - Skip to step 7 if
Signers >= Sponsors - Increment
sourceAccount.sponsoredReserves - Skip to step 8
- Fail with
SET_OPTIONS_LOW_RESERVEif thesourceAccountdoes not have at leastbaseReserveavailable balance of native asset - Increment
sourceAccount.numSubEntries - ...
When deleting a signer, we now
- ...
- Count the number of signers on
sourceAccountasSigners - Count the number of
SponsorshipEntrywithdescriptor.type() == SIGNERanddescriptor.signer().accountID = sourceAccountasSponsors - Decrement
sourceAccount.sponsoredReservesifSigners <= Sponsors - Decrement
sourceAccount.numSubEntries - ...
When deleting a signer, we now
- ...
- Count the number of signers on
sourceAccountasSigners - Count the number of
SponsorshipEntrywithdescriptor.type() == SIGNERanddescriptor.signer().accountID = sourceAccountasSponsors - Decrement
sourceAccount.sponsoredReservesifSigners <= Sponsors - Decrement
sourceAccount.numSubEntries - ...
We now return CHANGE_TRUST_LOW_RESERVE conditionally
- ...
- Skip to step 5 if a
SponsorshipEntrywithdescriptor.type() == TRUSTLINE,descriptor.trustLine().accountID = sourceAccount, anddescriptor.trustLine().asset = assetdoes not exist - Increment
sourceAccount.sponsoredReserves - Skip to step 6
- Fail with
CHANGE_TRUST_LOW_RESERVEif thesourceAccountdoes not have at leastbaseReserveavailable balance of native asset - Increment
sourceAccount.numSubEntries - ...
When deleting a trust line, we now
- ...
- Decrement
sourceAccount.sponsoredReservesif aSponsorshipEntrywithdescriptor.type() == TRUSTLINE,descriptor.trustLine().accountID = sourceAccount, anddescriptor.trustLine().asset = assetexists - Decrement
sourceAccount.numSubEntries - ...
When deleting offers after revoking authorization, we now
- ...
- For each asset pair
(Buying, Selling)of an offer that was deleted a. Count the number of offers that were deleted asOffersDeletedb. Count the number ofSponsorshipEntrywithdescriptor.type() == OFFER,descriptor.offer().sellerID = trustor,descriptor.offer().buying = Buying, anddescriptor.offer().selling = SellingasSponsorsc. Decrementtrustor.sponsoredReservesbymin(OffersDeleted, Sponsors)d. Decrementtrustor.numSubEntriesbyOffersDeleted - ...
When releasing liabilities before modifying an existing offer with asset pair
(Buying, Selling), we now
- ...
- Count the number of offers with
sellerID = sourceAccount,buying = Buying, andselling = SellingasSponsorable - Count the number of
SponsorshipEntrywithdescriptor.type() == OFFER,descriptor.offer().sellerID = sourceAccount,descriptor.offer().buying = Buying, anddescriptor.offer().selling = SellingasSponsors - Decrement
sourceAccount.sponsoredReserveifSponsorable <= Sponsors - Decrement
sourceAccount.numSubEntries - ...
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
- ...
- Count the number of offers with
sellerID = sourceAccount,buying = Buying, andselling = SellingasSponsorable - Count the number of
SponsorshipEntrywithdescriptor.type() == OFFER,descriptor.offer().sellerID = sourceAccount,descriptor.offer().buying = Buying, anddescriptor.offer().selling = SellingasSponsors - Increment
sourceAccount.sponsoredReserveifSponsorable < Sponsors - Skip to step 7
- Fail with
MANAGE_OFFER_LOW_RESERVEif thesourceAccountdoes not have at leastbaseReserveavailable balance of native asset - Increment
sourceAccount.numSubEntries - ...
When erasing an offer that was either taken entirely or partially taken and adjusted to 0, we now
- ...
- Count the number of offers with matching
sellerID,buying, andsellingasSponsorable - Count the number of
SponsorshipEntrywithdescriptor.type() == OFFER,descriptor.offer().sellerID = sellerID,descriptor.offer().buying = buying, anddescriptor.offer().selling = sellingasSponsors - Decrement
sourceAccount.sponsoredReserveifSponsorable <= Sponsors - Decrement
sourceAccount.numSubEntries - ...
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.
When erasing an offer that was either taken entirely or partially taken and adjusted to 0, we now
- ...
- Count the number of offers with matching
sellerID,buying, andsellingasSponsorable - Count the number of
SponsorshipEntrywithdescriptor.type() == OFFER,descriptor.offer().sellerID = sellerID,descriptor.offer().buying = buying, anddescriptor.offer().selling = sellingasSponsors - Decrement
sourceAccount.sponsoredReserveifSponsorable <= Sponsors - Decrement
sourceAccount.numSubEntries - ...
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
- ...
- Count the number of offers with matching
sellerID,buying, andsellingasSponsorable - Count the number of
SponsorshipEntrywithdescriptor.type() == OFFER,descriptor.offer().sellerID = sellerID,descriptor.offer().buying = buying, anddescriptor.offer().selling = sellingasSponsors - Decrement
sourceAccount.sponsoredReserveifSponsorable <= Sponsors - Decrement
sourceAccount.numSubEntries - ...
We now return MANAGE_DATA_LOW_RESERVE conditionally
- ...
- Skip to step 5 if a
SponsorshipEntrywithdescriptor.type() == DATA,descriptor.data().accountID = sourceAccount, anddescriptor.data().dataName = dataNamedoes not exist - Increment
sourceAccount.sponsoredReserves - Skip to step 6
- Fail with
MANAGE_DATA_LOW_RESERVEif thesourceAccountdoes not have at leastbaseReserveavailable balance of native asset - Increment
sourceAccount.numSubEntries - ...
When deleting data, we now
- ...
- Decrement
sourceAccount.sponsoredReservesif aSponsorshipEntrywithdescriptor.type() == DATA,descriptor.data().accountID = sourceAccount, anddescriptor.data().dataName = dataNameexists - Decrement
sourceAccount.numSubEntries - ...
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.
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.
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.
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.
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.
None.
None yet.
None yet.