Skip to main content

Sponsored Reserves

Sponsored reserves were introduced in CAP-0033 and allow an account (sponsoring account) to pay the base reserves for another account (sponsored account). While this relationship exists, base reserve requirements that would normally accumulate on the sponsored account now accumulate on the sponsoring account.

Both the Begin Sponsoring Future Reserves and the End Sponsoring Future Reserves operations must appear in the sponsorship transaction, guaranteeing that both accounts agree to the sponsorship.

Anything that increases the minimum balance can be sponsored (account creation, offers, trustlines, data entries, signers, claimable balances).

To learn about base reserves, see our section on Lumens.

Begin and end sponsorships

To create a sponsored reserve, you have to use a sandwich transaction that includes three operations.

  • The first operation: Begin Sponsoring Future Reserves initiates the sponsorship and requires the sponsoring account's signature.
  • The second operation: specifies what is being sponsored.
  • The third operation: End Sponsoring Future Reserves, allows the sponsored account to accept the sponsorship and requires the sponsored account’s signature.

Begin Sponsoring Future Reserves establishes the is-sponsoring-future-reserves-for relationship where the sponsoring account is the source account of the operation. The account specified in the operation is the sponsored account.

End Sponsoring Future Reserves ends the current is-sponsoring-future-reserves-for relationship for the source account of the operation.

At the end of any transaction, there must be no ongoing is-sponsoring-future-reserves-for relationships, which is why these two operations must be used together in a single transaction.

View operation details in our List of Operations section.

Revoke sponsorship

Allows the sponsoring account to remove or transfer sponsorships of existing ledgerEntries and signers. If the ledgerEntry or signer is not sponsored, the owner of the ledgerEntry or signer can establish a sponsorship if it is the beneficiary of an is-sponsoring-future-reserves-for relationship.

Operation logic

  • Entry/signer is sponsored
    • Source account is currently the beneficiary of a is-sponsoring-future-reserves-for relationship
      • Transfer sponsorship of entry/signer from source account to the account that is-sponsoring-future-reserves-for source account
    • Source account is not the beneficiary of a is-sponsoring-future-reserves-for relationship
      • Remove the sponsorship from the entry/signer
  • Entry/signer is not sponsored
    • Source account is currently the beneficiary of a is-sponsoring-future-reserves-for relationship
      • Establish sponsorship between entry/signer and the account that is-sponsoring-future-reserves-for source account
    • Source account is not the beneficiary of a is-sponsoring-future-reserves-for relationship
      • No-Op

View operation details in our List of Operations section.

Effect on minimum balance

Once sponsorships are introduced, the minimum balance calculation is: (2 base reserves + numSubEntries + numSponsoring - numSponsored) * baseReserve + liabilities.selling.

When account A is sponsoring future reserves for account B, any reserve requirements that would normally accumulate on B will instead accumulate on A, shown in numSponsoring. The fact that these reserves are being provided by another account will be reflected on B in numSponsored, which cancels out the increase in numSubEntries, keeping the minimum balance unchanged for B.

When a sponsored entry or subentry is removed, numSponsoring is decreased on the sponsoring account and numSponsored is decreased on the sponsored account.

To learn more about minimum balance requirements, see our section on Lumens.

Effect on claimable balances

All claimable balances are sponsored through built-in logic in the claimable balance operations. The account that creates the claimable balance pays the base reserve to get the claimable balance on the ledger. When the claimable balance is claimed by the claimant(s), the claimable balance is removed from the ledger, and the account that created it gets the base reserve back.

Read more about claimable balances in our Claimable Balances Encyclopedia Entry.

Examples

Each of the following examples builds on itself, referencing variables from previous snippets. The following examples will demonstrate:

  1. Sponsor creation of a trustline for another account
  2. Sponsor two trustlines for an account via two different sponsors
  3. Transfer the sponsorship responsibility from one account to another
  4. Revoke the sponsorship by an account entirely

For brevity in the Golang examples, we’ll assume the existence of a SignAndSend(...) method (defined below) which creates and submits a transaction with the proper parameters and basic error-checking.

Preamble

We’ll start by including the boilerplate of account and asset creation.

const sdk = require("stellar-sdk");
const http = require("got");

let server = new sdk.Server("https://horizon-testnet.stellar.org");

async function main() {
// Create & fund the new accounts.
let keypairs = [
sdk.Keypair.random(),
sdk.Keypair.random(),
sdk.Keypair.random(),
];

for (const keypair of keypairs) {
const base = "https://friendbot.stellar.org/?";
const path = base + "addr=" + encodeURIComponent(keypair.publicKey());

console.log(`Funding:\n ${keypair.secret()}\n ${keypair.publicKey()}`);

// We use the "got" library here to do the HTTP request synchronously, but
// you can obviously use any method you'd like for this.
const response = await http(path).catch(function (error) {
console.error(" failed:", error.response.body);
});
}

// Arbitrary assets to sponsor trustlines for. Let's assume they make sense.
let S1 = keypairs[0], A = keypairs[1], S2 = keypairs[2];
let assets = [
new sdk.Asset("ABCD", S1.publicKey()),
new sdk.Asset("EFGH", S1.publicKey()),
new sdk.Asset("IJKL", S2.publicKey()),
];

// ...

1. Sponsoring trustlines

Now, let’s sponsor trustlines for Account A. Notice how the CHANGE_TRUST operation is sandwiched between the begin and end sponsoring operations and that all relevant accounts need to sign the transaction.

//
// 1. S1 will sponsor a trustline for Account A.
//
let s1Account = await server.loadAccount(S1.publicKey()).catch(accountFail);
let tx = new sdk.TransactionBuilder(s1Account, { fee: sdk.BASE_FEE })
.addOperation(
sdk.Operation.beginSponsoringFutureReserves({
sponsoredId: A.publicKey(),
}),
)
.addOperation(
sdk.Operation.changeTrust({
source: A.publicKey(),
asset: assets[0],
limit: "1000", // This limit can vary according with your application;
// if left empty, it defaults to the max limit.
}),
)
.addOperation(
sdk.Operation.endSponsoringFutureReserves({
source: A.publicKey(),
}),
)
.setNetworkPassphrase(sdk.Networks.TESTNET)
.setTimeout(180)
.build();

// Note that while either can submit this transaction, both must sign it.
tx.sign(S1, A);
let txResponse = await server.submitTransaction(tx).catch(txCheck);
if (!txResponse) {
return;
}

console.log("Sponsored a trustline of", A.publicKey());

//
// 2. Both S1 and S2 sponsor trustlines for Account A for different assets.
//
let aAccount = await server.loadAccount(A.publicKey()).catch(accountFail);
let tx = new sdk.TransactionBuilder(aAccount, { fee: sdk.BASE_FEE })
.addOperation(
sdk.Operation.beginSponsoringFutureReserves({
source: S1.publicKey(),
sponsoredId: A.publicKey(),
}),
)
.addOperation(
sdk.Operation.changeTrust({
asset: assets[1],
limit: "5000",
}),
)
.addOperation(sdk.Operation.endSponsoringFutureReserves())

.addOperation(
sdk.Operation.beginSponsoringFutureReserves({
source: S2.publicKey(),
sponsoredId: A.publicKey(),
}),
)
.addOperation(
sdk.Operation.changeTrust({
asset: assets[2],
limit: "2500",
}),
)
.addOperation(sdk.Operation.endSponsoringFutureReserves())
.setNetworkPassphrase(sdk.Networks.TESTNET)
.setTimeout(180)
.build();

// Note that all 3 accounts must approve/sign this transaction.
tx.sign(S1, S2, A);
let txResponse = await server.submitTransaction(tx).catch(txCheck);
if (!txResponse) {
return;
}

console.log("Sponsored two trustlines of", A.publicKey());

2. Transferring sponsorship

Suppose that now Signer 1 wants to transfer the responsibility of sponsoring reserves for the trustline to Sponsor 2. This is accomplished by sandwiching the transfer between the BEGIN/END_SPONSORING_FUTURE_RESERVES operations. Both of the participants must sign the transaction, though either can submit it.

An intuitive way to think of a sponsorship transfer is that the very act of sponsorship is being sponsored by a new account. That is, the new sponsor takes over the responsibilities of the old sponsor by sponsoring a revocation.

//
// 3. Transfer sponsorship of B's second trustline from S1 to S2.
//
let tx = new sdk.TransactionBuilder(s1Account, { fee: sdk.BASE_FEE })
.addOperation(
sdk.Operation.beginSponsoringFutureReserves({
source: S2.publicKey(),
sponsoredId: S1.publicKey(),
}),
)
.addOperation(
sdk.Operation.revokeTrustlineSponsorship({
account: A.publicKey(),
asset: assets[1],
}),
)
.addOperation(sdk.Operation.endSponsoringFutureReserves())
.setNetworkPassphrase(sdk.Networks.TESTNET)
.setTimeout(180)
.build();

// Notice that while the old sponsor *sends* the transaction, both sponsors
// must *approve* the transfer.
tx.sign(S1, S2);
let txResponse = await server.submitTransaction(tx).catch(txCheck);
if (!txResponse) {
return;
}

console.log("Transferred sponsorship for", A.publicKey());

At this point, Signer 1 is only sponsoring the first asset (arbitrarily coded as ABCD), while Signer 2 is sponsoring the other two assets. (Recall that initially Signer 1 was also sponsoring EFGH.)

3. Sponsorship revocation

Finally, we can demonstrate complete revocation of sponsorships. Below, Signer 2 removes themselves from all responsibility over the two asset trustlines. Notice that Account A is not involved at all, since revocation should be performable purely at the sponsor’s discretion.

  //
// 4. S2 revokes sponsorship of B's trustlines entirely.
//
let s2Account = await server.loadAccount(S2.publicKey()).catch(accountFail);
let tx = new sdk.TransactionBuilder(s2Account, {fee: sdk.BASE_FEE})
.addOperation(sdk.Operation.revokeTrustlineSponsorship({
account: A.publicKey(),
asset: assets[1],
}))
.addOperation(sdk.Operation.revokeTrustlineSponsorship({
account: A.publicKey(),
asset: assets[2],
}))
.setNetworkPassphrase(sdk.Networks.TESTNET)
.setTimeout(180)
.build();

tx.sign(S2);
let txResponse = await server.submitTransaction(tx).catch(txCheck);
if (!txResponse) { return; }

console.log("Revoked sponsorship for", A.publicKey());
} // ends main()

Sponsorship Source Accounts

When it comes to the SourceAccount fields of the sponsorship sandwich, it's important to refer to the wisdom of CAP-33:

This relation is initiated by BeginSponsoringFutureReservesOp, where the sponsoring account is the source account, and is terminated by EndSponsoringFutureReserveOp, where the sponsored account is the source account.

Since the source account defaults to the transaction submitter when omitted, this field needs always needs to be set for either the Begin or the End.

For example, the following is an identical expression of the earlier Golang example of sponsoring a trustline, just submitted by the sponsor (Sponsor 1) rather than the sponsored account (Account A). Notice the differences in where SourceAccount is set:

    sponsorTrustline := []txnbuild.Operation{
&txnbuild.BeginSponsoringFutureReserves{
SponsoredID: addressA,
},
&txnbuild.ChangeTrust{
SourceAccount: aAccount.AccountID,
Line: &assets[0],
Limit: txnbuild.MaxTrustlineLimit,
},
&txnbuild.EndSponsoringFutureReserves{
SourceAccount: aAccount.AccountID,
},
}

// Again, both participants must still sign the transaction: the sponsored
// account must consent to the sponsorship.
SignAndSend(client, s1Account.AccountID, []*keypair.Full{S1, A}, sponsorTrustline...)

Other examples

If you’d like other examples or want to view a more-generic pseudo-code breakdown of these sponsorship scenarios, you can refer to CAP-0033 directly.

Footnote

For the above examples, an implementation of SignAndSend (Golang) and some (very) rudimentary error checking code (all languages) might look something like this:

function txCheck(err) {
console.error("Transaction submission failed:", err);
if (err.response != null && err.response.data != null) {
console.error("More details:", err.response.data.extras);
} else {
console.error("Unknown reason:", err);
}
}

function accountFail(err) {
console.error(" Failed to load account:", err.response.body);
}