CAP: 0023
Title: Two-Part Payments with ClaimableBalanceEntry
Author: Jonathan Jove
Status: Final
Created: 2019-06-04
Updated: 2020-02-05
Discussion: https://github.com/stellar/stellar-protocol/issues/303
Protocol version: 14
Payments can fail depending on the state of the destination account. This proposal introduces new operations that separate sending a payment from receiving the payment. Then the success of sending depends only on the state of the sending account and success of receiving depends only on the state of the receiving account.
This proposal seeks to solve the following problem: it should be easy to send a payment to an account that is not necessarily prepared to receive the payment. There are several manifestations of this problem, the two most important being
- it should be easy for protocols (like an implementation of payment channels) to pay out to participants, and
- it should be easy for issuers to issue assets non-interactively.
This proposal is aligned with several Stellar Network Goals, among them:
- The Stellar Network should facilitate simplicity and interoperability with other protocols and networks.
- The Stellar Network should enable cross-border payments, i.e. payments via
exchange of assets, throughout the globe, enabling users to make payments
between assets in a manner that is fast, cheap, and highly usable.
- In support of this, the Stellar Network should enable asset issuance, but as a means of enabling cross-border payments.
We introduce ClaimableBalanceEntry
as a new type of LedgerEntry
which
represents the transfer of ownership of some amount of an asset. The operations
CreateClaimableBalanceOp
and ClaimClaimableBalanceOp
allow the creation and
consumption of claimable balance entries, which permits temporal separation of
initiating and reciving a payment. Existing proposals, such as those for
deterministic accounts, can provide a similar mechanism but are not able to
handle authorization restricted assets as easily. A specific and simple protocol
that will be facilitated is the asset issuance protocol that issues an asset to
a given account, regardless of whether it exists or is prepared to receive the
funds.
First, we introduce ClaimableBalanceEntry
(Note that the XDR was updated in https://github.com/stellar/stellar-protocol/blob/master/core/cap-0033.md#claimablebalanceentry) and the corresponding changes for
LedgerEntryType
and LedgerEntry
.
enum LedgerEntryType
{
// ... ACCOUNT, TRUSTLINE, OFFER, unchanged ...
DATA = 3,
CLAIMABLE_BALANCE = 4
};
enum ClaimPredicateType
{
CLAIM_PREDICATE_UNCONDITIONAL = 0,
CLAIM_PREDICATE_AND = 1,
CLAIM_PREDICATE_OR = 2,
CLAIM_PREDICATE_NOT = 3,
CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME = 4,
CLAIM_PREDICATE_BEFORE_RELATIVE_TIME = 5
};
union ClaimPredicate switch (ClaimPredicateType type)
{
case CLAIM_PREDICATE_UNCONDITIONAL:
void;
case CLAIM_PREDICATE_AND:
ClaimPredicate andPredicates<2>;
case CLAIM_PREDICATE_OR:
ClaimPredicate orPredicates<2>;
case CLAIM_PREDICATE_NOT:
ClaimPredicate* notPredicate;
case CLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME:
int64 absBefore; // Will return true if closeTime < absBefore
case CLAIM_PREDICATE_BEFORE_RELATIVE_TIME:
int64 relBefore; // Seconds since closeTime of the ledger in which the
// ClaimableBalanceEntry was created
};
enum ClaimantType
{
CLAIMANT_TYPE_V0 = 0
};
union Claimant switch (ClaimantType type)
{
case CLAIMANT_TYPE_V0:
struct {
AccountID destination; // The account that can use this condition
ClaimPredicate predicate; // Claimable if predicate is true
} v0;
};
enum ClaimableBalanceIDType
{
CLAIMABLE_BALANCE_ID_TYPE_V0 = 0
};
union ClaimableBalanceID switch (ClaimableBalanceIDType type)
{
case CLAIMABLE_BALANCE_ID_TYPE_V0:
Hash v0;
};
struct ClaimableBalanceEntry
{
// Unique identifier for this ClaimableBalanceEntry
ClaimableBalanceID balanceID;
// Account that created this ClaimableBalanceEntry
AccountID createdBy;
// List of claimants with associated predicate
Claimant claimants<10>;
// Any asset including native
Asset asset;
// Amount of asset
int64 amount;
// Amount of native asset to pay the reserve
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 CLAIMABLE_BALANCE:
ClaimableBalanceEntry claimableBalance;
}
data;
// reserved for future use
union switch (int v)
{
case 0:
void;
}
ext;
};
struct LedgerKey
{
// ... ACCOUNT, TRUSTLINE, OFFER, DATA unchanged ...
case CLAIMABLE_BALANCE:
struct
{
ClaimableBalanceID balanceID;
} claimableBalance;
};
Second, we introduce the new operations CreateClaimableBalanceOp
and
ClaimClaimableBalanceOp
as well as the corresponding changes to
OperationType
and Operation
. We also introduce the type OperationID
to represent the hash preimage for ClaimableBalanceID
, along with a new EnvelopeType
.
enum OperationType
{
// ... CREATE_ACCOUNT, ..., MANAGE_BUY_OFFER unchanged ...
PATH_PAYMENT_STRICT_SEND = 13,
CREATE_CLAIMABLE_BALANCE = 14,
CLAIM_CLAIMABLE_BALANCE = 15
};
struct CreateClaimableBalanceOp
{
Asset asset;
int64 amount;
Claimant claimants<10>;
};
struct ClaimClaimableBalanceOp
{
ClaimableBalanceID balanceID;
};
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_CLAIMABLE_BALANCE:
CreateClaimableBalanceOp createClaimableBalanceOp;
case CLAIM_CLAIMABLE_BALANCE:
ClaimClaimableBalanceOp claimClaimableBalanceOp;
}
body;
};
union OperationID switch (EnvelopeType type)
{
case ENVELOPE_TYPE_OP_ID:
struct
{
AccountID sourceAccount;
SequenceNumber seqNum;
uint32 opNum;
} id;
};
enum EnvelopeType
{
// ... ENVELOPE_TYPE_TX_V0, ..., ENVELOPE_TYPE_TX_FEE_BUMP unchanged ...
ENVELOPE_TYPE_OP_ID = 6
};
Third, we introduce the result types CreateClaimableBalanceResult
and
ClaimClaimableBalanceResult
as well as the corresponding changes to
OperationResult
.
enum CreateClaimableBalanceResultCode
{
CREATE_CLAIMABLE_BALANCE_SUCCESS = 0,
CREATE_CLAIMABLE_BALANCE_MALFORMED = -1,
CREATE_CLAIMABLE_BALANCE_LOW_RESERVE = -2,
CREATE_CLAIMABLE_BALANCE_NO_TRUST = -3,
CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED = -4,
CREATE_CLAIMABLE_BALANCE_UNDERFUNDED = -5
};
union CreateClaimableBalanceResult switch (CreateClaimableBalanceResultCode code)
{
case CREATE_CLAIMABLE_BALANCE_SUCCESS:
ClaimableBalanceID balanceID;
default:
void;
};
enum ClaimClaimableBalanceResultCode
{
CLAIM_CLAIMABLE_BALANCE_SUCCESS = 0,
CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST = -1,
CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM = -2,
CLAIM_CLAIMABLE_BALANCE_LINE_FULL = -3,
CLAIM_CLAIMABLE_BALANCE_NO_TRUST = -4,
CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED = -5
};
union ClaimClaimableBalanceResult switch (ClaimClaimableBalanceResultCode code)
{
case CLAIM_CLAIMABLE_BALANCE_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_CLAIMABLE_BALANCE:
CreateClaimableBalanceResult createClaimableBalanceResult;
case CLAIM_CLAIMABLE_BALANCE:
ClaimClaimableBalanceResult claimClaimableBalanceResult;
}
body;
};
A ClaimableBalanceEntry
can only be created by the CreateClaimableBalanceOp
operation. CreateClaimableBalanceOp
is invalid with
CREATE_CLAIMABLE_BALANCE_MALFORMED
if
asset
is invalidamount <= 0
claimants
has length 0claimants[i].destination = claimants[j].destination
(for anyi != j
)claimants[i].predicate
has depth greater than 4 (for anyi
)claimants[i].predicate
contains a predicate of typeCLAIM_PREDICATE_AND
withandPredicates.size() != 2
,CLAIM_PREDICATE_OR
withorPredicates.size() != 2
, orCLAIM_PREDICATE_NOT
with a nullnotPredicate
(for anyi
)claimants[i].predicate
contains a predicate of typeCLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME
orCLAIM_PREDICATE_BEFORE_RELATIVE_TIME
withabsBefore < 0
orrelBefore < 0
(for anyi
)
The behavior of CreateClaimableBalanceOp
is as follows:
- Fail with
CREATE_CLAIMABLE_BALANCE_LOW_RESERVE
if thesourceAccount
does not have at leastclaimants.size() * baseReserve
available balance of native asset - Deduct
claimants.size() * baseReserve
of native asset fromsourceAccount
- Fail with
CREATE_CLAIMABLE_BALANCE_NO_TRUST
if thesourceAccount
does not have a trust line forasset
- Fail with
CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED
if thesourceAccount
trust line forasset
does not haveAUTHORIZED_FLAG
set - Fail with
CREATE_CLAIMABLE_BALANCE_UNDERFUNDED
if thesourceAccount
does not have at leastamount
available balance ofasset
- Deduct
amount
ofasset
fromsourceAccount
- Create a claimable balance entry with the following properties:
balanceID
of typeCLAIMABLE_BALANCE_ID_TYPE_V0
.balanceID.v0()
equal to the SHA256 hash of theOperationID
, which is created with thetype
ENVELOPE_TYPE_OP_ID
,sourceAccount
of the transaction (not the operation), theseqNum
of the transaction, and the index of this operation in the transaction asopNum
.createdBy = sourceAccount
claimants
as specified, with the exception thatCLAIM_PREDICATE_BEFORE_RELATIVE_TIME
will be converted toCLAIM_PREDICATE_BEFORE_ABSOLUTE_TIME
by addingrelBefore
to thecloseTime
in theLedgerHeader
. If this addition exceedsINT64_MAX
then useINT64_MAX
.
asset
as specified in the operationamount
as specified in the operationreserve
equal toclaimants.size() * baseReserve
- Succeed with
CREATE_CLAIMABLE_BALANCE_SUCCESS
and thebalanceID
from the previous step.
CreateClaimableBalanceOp
requires medium threshold because it can be used to
send funds.
A ClaimableBalanceEntry
can only be deleted by the ClaimClaimableBalanceOp
operation. ClaimClaimableBalanceOp
cannot be invalid.
The behavior of ClaimClaimableBalanceOp
is as follows:
- Fail with
CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST
if there is noClaimableBalanceEntry
matchingbalanceID
. - Fail with
CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM
if there is noi
such thatclaimants[i].destination = sourceAccount
or ifclaimants[i].predicate
is not satisfied - Skip to step 7 if
createdBy
does not exist - Skip to step 7 if
createdBy
does not have at leastreserve
available limit of native asset - Add
reserve
of native asset tocreatedBy
- Skip to step 9
- Fail with
CLAIM_CLAIMABLE_BALANCE_LINE_FULL
if thesourceAccount
does not have at leastreserve
available limit of native asset - Add
reserve
of native asset tosourceAccount
- Fail with
CLAIM_CLAIMABLE_BALANCE_NO_TRUST
ifasset
is not of typeASSET_TYPE_NATIVE
and thesourceAccount
trust line forasset
does not exist - Fail with
CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED
ifasset
is not of typeASSET_TYPE_NATIVE
and thesourceAccount
trust line forasset
does not have theAUTHORIZED_FLAG
flag set - Fail with
CLAIM_CLAIMABLE_BALANCE_LINE_FULL
if thesourceAccount
does not have at leastamount
available limit ofasset
- Add
amount
ofasset
to thesourceAccount
- Delete the
ClaimableBalanceEntry
- Succeed with
CLAIM_CLAIMABLE_BALANCE_SUCCESS
ClaimClaimableBalanceOp
requires low threshold because it can only be used to
transfer funds from a ClaimableBalanceEntry
to a trust line.
Each ClaimableBalanceEntry
exists as an independent entity on the ledger. It
is clear that a ClaimableBalanceEntry
cannot be a sub-entry of any its
claimants
, because it is a security risk for accounts to be able to add
sub-entries to other accounts. But why should these entries be independent
entities on the ledger rather than sub-entries of the accounts that created
them? There are two main benefits of this design:
- Sending accounts are not limited in the number of claimable balance entries they can create
- Sending accounts can be merged even if they created claimable balance entries that have not yet been claimed
For each ClaimableBalanceEntry
, claimants
contains a finite and immutable
list of accounts that could potentially claim the ClaimableBalanceEntry
. Even
if the conditions are satisfiable (which is not guaranteed), it is still
possible for the ClaimableBalanceEntry
to become stranded. If all of the
accounts listed in claimants
are merged and none of the private keys are
known, then the ClaimableBalanceEntry
will no longer be claimable.
Suppose that we try to relax this requirement in order to avoid this downside.
We could instead make claimants
contain a finite and immutable list of public
keys. The operation to claim the ClaimableBalanceEntry
could then contain a
signature over the tuple (sourceAccount, balanceID)
. If the signature was not
from one of the public keys that satisfy the conditions, then the operation
would fail. This would allow the appropriate party to claim the
ClaimableBalanceEntry
into any account that they control. But this would also
make it considerably easier to circumvent authorization restrictions on assets.
For instance, an authorized account could create a ClaimableBalanceEntry
with
a recipient public key whose private key is known only to some other party. That
party would then control the funds in the ClaimableBalanceEntry
and could
claim them into any account that is authorized. A similar scheme could be
executed today by changing the signers on an account, but this would only be
possible once per authorized account and cannot separate out a fraction of the
funds. In summary, an approach that could allow ClaimableBalanceEntry
to be
claimable into any account would significantly weaken the strength of
authorization restrictions.
One issue which has been discussed during the development of this proposal is
the absence of a mechanism to increase the amount
of a
ClaimableBalanceEntry
. The specific scenario which would warrant this
functionality is when a single account sends many identical payments to a single
account that is not prepared to receive them and does not claim any of the
payments. However, this case is sufficiently specific that we recommend pursuing
it in a separate proposal once this proposal has been implemented. Delaying this
feature presents minimal additional difficulty because ClaimableBalanceEntry
has an extension point.
This issue has also been slightly mitigated relative to earlier versions of this
proposal because ClaimableBalanceEntry
now returns the reserve to the sending
account, whenever possible.
Everything proposed in this document takes the same stance as existing features of the protocol with regard to memo: memo is a property of a transaction, not of an operation or a ledger entry.
All downstream systems will need updated XDR in order to recognize the new operations and ledger entries.
This proposal will slightly reduce the efficacy of base reserve changes, because
a ClaimableBalanceEntry
that has insufficient reserve is still usable.
None yet.