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() == TRUSTLINE
and any ofdescriptor.trustLine().asset
is of typeASSET_TYPE_NATIVE
descriptor.trustLine().asset
is invalid
descriptor.type() == OFFER
and any ofdescriptor.offer().buying
is invaliddescriptor.offer().selling
is invaliddescriptor.offer().buying == descriptor.offer().selling
descriptor.type() == DATA
anddescriptor.data().dataName
is empty or invalid
The behavior of CreateSponsorshipOp
is as follows:
- Calculate
Multiplier
as- 3 if
descriptor.type() == ACCOUNT
- 2 otherwise
- 3 if
- Fail with
CREATE_SPONSORSHIP_LOW_RESERVE
if thesourceAccount
does not have at leastMultiplier * baseReserve
available balance of native asset - Deduct
Multiplier * baseReserve
of native asset fromsourceAccount
- Create a
SponsorshipEntry
assponsorship
with the following properties:sponsorship.createdBy = sourceAccount
sponsorship.sponsorshipID
as the next availablesponsorship.descriptor = descriptor
sponsorship.reserve = Multiplier * baseReserve
- Count the number of existing
LedgerEntry
described bydescriptor
asSponsorable
- Count the number of existing
SponsorshipEntry
withdescriptor = sponsorship.descriptor
andsponsorshipID < sponsorship.sponsorshipID
asSponsors
- Succeed with
CREATE_SPONSORSHIP_SUCCESS
ifSponsors >= Sponsorable
- Let
SponsoredAccount
bedescriptor.account().accountID
ifdescriptor.type() == ACCOUNT
descriptor.signer().accountID
ifdescriptor.type() == SIGNER
descriptor.trustLine().accountID
ifdescriptor.type() == TRUSTLINE
descriptor.offer().sellerID
ifdescriptor.type() == OFFER
descriptor.data().accountID
ifdescriptor.type() == DATA
- Succeed with
CREATE_SPONSORSHIP_SUCCESS
ifSponsoredAccount
does not exist - Load
SponsoredAccount
and incrementSponsoredAccount.sponsoredReserves
byMultiplier - 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_EXIST
if there is noSponsorshipEntry
with the specifiedsponsorshipID
- Load the
SponsorshipEntry
assponsorship
- Fail with
REMOVE_SPONSORSHIP_NOT_CREATOR
ifsponsorship.createdBy != sourceAccount
- Count the number of existing
LedgerEntry
described bydescriptor
asSponsorable
- Count the number of existing
SponsorshipEntry
withdescriptor = sponsorship.descriptor
andsponsorshipID < sponsorship.sponsorshipID
asSponsors
- Fail with
REMOVE_SPONSORSHIP_IN_USE
ifSponsorable > Sponsors
- Fail with
REMOVE_SPONSORSHIP_LINE_FULL
if thesourceAccount
does not havereserve
available limit of native asset - Add
reserve
of 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
SponsorshipEntry
withdescriptor.type() == ACCOUNT
anddescriptor.account().accountID = destination
exists - Fail with
CREATE_ACCOUNT_LOW_RESERVE
ifstartingBalance < 2 * baseReserve
- ...
When creating the account, we now
- ...
- Set
destination.sponsoredReserves = 2
if aSponsorshipEntry
withdescriptor.type() == ACCOUNT
anddescriptor.account().accountID = destination
exists - ...
We now return SET_OPTIONS_LOW_RESERVE
conditionally
- ...
- Count the number of signers on
sourceAccount
asSigners
- Count the number of
SponsorshipEntry
withdescriptor.type() == SIGNER
anddescriptor.signer().accountID = sourceAccount
asSponsors
- Skip to step 7 if
Signers >= Sponsors
- Increment
sourceAccount.sponsoredReserves
- Skip to step 8
- Fail with
SET_OPTIONS_LOW_RESERVE
if thesourceAccount
does not have at leastbaseReserve
available balance of native asset - Increment
sourceAccount.numSubEntries
- ...
When deleting a signer, we now
- ...
- Count the number of signers on
sourceAccount
asSigners
- Count the number of
SponsorshipEntry
withdescriptor.type() == SIGNER
anddescriptor.signer().accountID = sourceAccount
asSponsors
- Decrement
sourceAccount.sponsoredReserves
ifSigners <= Sponsors
- Decrement
sourceAccount.numSubEntries
- ...
When deleting a signer, we now
- ...
- Count the number of signers on
sourceAccount
asSigners
- Count the number of
SponsorshipEntry
withdescriptor.type() == SIGNER
anddescriptor.signer().accountID = sourceAccount
asSponsors
- Decrement
sourceAccount.sponsoredReserves
ifSigners <= Sponsors
- Decrement
sourceAccount.numSubEntries
- ...
We now return CHANGE_TRUST_LOW_RESERVE
conditionally
- ...
- Skip to step 5 if a
SponsorshipEntry
withdescriptor.type() == TRUSTLINE
,descriptor.trustLine().accountID = sourceAccount
, anddescriptor.trustLine().asset = asset
does not exist - Increment
sourceAccount.sponsoredReserves
- Skip to step 6
- Fail with
CHANGE_TRUST_LOW_RESERVE
if thesourceAccount
does not have at leastbaseReserve
available balance of native asset - Increment
sourceAccount.numSubEntries
- ...
When deleting a trust line, we now
- ...
- Decrement
sourceAccount.sponsoredReserves
if aSponsorshipEntry
withdescriptor.type() == TRUSTLINE
,descriptor.trustLine().accountID = sourceAccount
, anddescriptor.trustLine().asset = asset
exists - 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 asOffersDeleted
b. Count the number ofSponsorshipEntry
withdescriptor.type() == OFFER
,descriptor.offer().sellerID = trustor
,descriptor.offer().buying = Buying
, anddescriptor.offer().selling = Selling
asSponsors
c. Decrementtrustor.sponsoredReserves
bymin(OffersDeleted, Sponsors)
d. Decrementtrustor.numSubEntries
byOffersDeleted
- ...
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 = Selling
asSponsorable
- Count the number of
SponsorshipEntry
withdescriptor.type() == OFFER
,descriptor.offer().sellerID = sourceAccount
,descriptor.offer().buying = Buying
, anddescriptor.offer().selling = Selling
asSponsors
- Decrement
sourceAccount.sponsoredReserve
ifSponsorable <= 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 = Selling
asSponsorable
- Count the number of
SponsorshipEntry
withdescriptor.type() == OFFER
,descriptor.offer().sellerID = sourceAccount
,descriptor.offer().buying = Buying
, anddescriptor.offer().selling = Selling
asSponsors
- Increment
sourceAccount.sponsoredReserve
ifSponsorable < Sponsors
- Skip to step 7
- Fail with
MANAGE_OFFER_LOW_RESERVE
if thesourceAccount
does not have at leastbaseReserve
available 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
, andselling
asSponsorable
- Count the number of
SponsorshipEntry
withdescriptor.type() == OFFER
,descriptor.offer().sellerID = sellerID
,descriptor.offer().buying = buying
, anddescriptor.offer().selling = selling
asSponsors
- Decrement
sourceAccount.sponsoredReserve
ifSponsorable <= 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
, andselling
asSponsorable
- Count the number of
SponsorshipEntry
withdescriptor.type() == OFFER
,descriptor.offer().sellerID = sellerID
,descriptor.offer().buying = buying
, anddescriptor.offer().selling = selling
asSponsors
- Decrement
sourceAccount.sponsoredReserve
ifSponsorable <= 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
, andselling
asSponsorable
- Count the number of
SponsorshipEntry
withdescriptor.type() == OFFER
,descriptor.offer().sellerID = sellerID
,descriptor.offer().buying = buying
, anddescriptor.offer().selling = selling
asSponsors
- Decrement
sourceAccount.sponsoredReserve
ifSponsorable <= Sponsors
- Decrement
sourceAccount.numSubEntries
- ...
We now return MANAGE_DATA_LOW_RESERVE
conditionally
- ...
- Skip to step 5 if a
SponsorshipEntry
withdescriptor.type() == DATA
,descriptor.data().accountID = sourceAccount
, anddescriptor.data().dataName = dataName
does not exist - Increment
sourceAccount.sponsoredReserves
- Skip to step 6
- Fail with
MANAGE_DATA_LOW_RESERVE
if thesourceAccount
does not have at leastbaseReserve
available balance of native asset - Increment
sourceAccount.numSubEntries
- ...
When deleting data, we now
- ...
- Decrement
sourceAccount.sponsoredReserves
if aSponsorshipEntry
withdescriptor.type() == DATA
,descriptor.data().accountID = sourceAccount
, anddescriptor.data().dataName = dataName
exists - 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.