CAP: 0024
Title: Make PathPayment Symmetrical
Author: Jed McCaleb, Jonathan Jove
Status: Final
Created: 2019-07-08
Discussion: https://github.com/stellar/stellar-protocol/issues/49
Protocol version: 12
Right now PathPaymentOp
lets you specify how much the recipient will receive
while the amount sent can vary. There are use cases where you really want to
specify the amount send while the amount received can vary.
The motivation to add PathPaymentStrictSendOp
is analogous to that for adding
ManageBuyOfferOp
: many financial institutions have an obligation to faithfully
carry out customer orders. As a concrete example, suppose that an individual
whose native currency is X is selling a house. A potential buyer whose native
currency is Y approaches the seller. They agree to a contract where the buyer
must deposit a certain amount of currency X with an escrow agent chosen by the
seller. The escrowed amount will contribute to the purchase price if the
transaction is completed, will be returned to the buyer (in currency Y) in the
event that seller defaults, and will be credited to the seller as damages in the
event that buyer defaults. The buyer can easily use PathPaymentOp
to deposit
the agreed amount of currency X with the escrow agent. If the escrow agent needs
to transfer the escrowed funds to the seller, this can be done with PaymentOp
.
But if the escrow agent needs to transfer the escrowed funds to the buyer (in
currency Y) then PathPaymentOp
will not be effective because the escrow agent
would be required to guess the destAmount
in order to send the entire escrowed
amount. In general, there is no value for the destAmount
that guarantees that
the entire maxSend
will be sent, so the escrow agent will not be able to meet
his obligations.
This proposal is aligned with the following Stellar Network Goal: 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.
This proposal introduces a new operation PathPaymentStrictSendOp
that allows
a form of path payment with a strict equality constraint on the send amount
and an inequality constraint on the destination amount. The existing
PathPayment
operation is renamed to PathPaymentStrictReceiveOp
in order to
reflect that it allows a form of path payment with a strict equality constraint
on the destination amount and an inequality constraint on the send amount.
We first introduce the XDR for operations PathPaymentStrictReceiveOp
and
PathPaymentStrictSendOp
, as well as the corresponding changes to
OperationType
and Operation
.
enum OperationType
{
CREATE_ACCOUNT = 0,
PAYMENT = 1,
PATH_PAYMENT_STRICT_RECEIVE = 2,
MANAGE_SELL_OFFER = 3,
CREATE_PASSIVE_SELL_OFFER = 4,
SET_OPTIONS = 5,
CHANGE_TRUST = 6,
ALLOW_TRUST = 7,
ACCOUNT_MERGE = 8,
INFLATION = 9,
MANAGE_DATA = 10,
BUMP_SEQUENCE = 11,
MANAGE_BUY_OFFER = 12,
PATH_PAYMENT_STRICT_SEND = 13
};
struct PathPaymentStrictReceiveOp
{
Asset sendAsset; // asset we pay with
int64 sendMax; // the maximum amount of sendAsset to
// send (excluding fees).
// The operation will fail if can't be met
AccountID destination; // recipient of the payment
Asset destAsset; // what they end up with
int64 destAmount; // amount they end up with
Asset path<5>; // additional hops it must go through to get there
};
struct PathPaymentStrictSendOp
{
Asset sendAsset; // asset we pay with
int64 sendAmount; // amount of sendAsset to send (excluding fees)
AccountID destination; // recipient of the payment
Asset destAsset; // what they end up with
int64 destMin; // the minimum amount of dest asset to
// be received
// The operation will fail if it can't be met
Asset path<5>; // additional hops it must go through to get there
};
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, PAYMENT unchanged ...
case PATH_PAYMENT_STRICT_RECEIVE:
PathPaymentStrictReceiveOp pathPaymentStrictReceiveOp;
// ... MANAGE_SELL_OFFER, ..., MANAGE_BUY_OFFER unchanged ...
case PATH_PAYMENT_STRICT_SEND:
PathPaymentStrictSendOp pathPaymentStrictSendOp;
}
body;
};
We next introduce the result types PathPaymentStrictReceiveResult
and
PathPaymentStrictSendResult
, as well as the corresponding changes to
OperationResult
.
enum PathPaymentStrictReceiveResultCode
{
// codes considered as "success" for the operation
PATH_PAYMENT_STRICT_RECEIVE_SUCCESS = 0, // success
// codes considered as "failure" for the operation
PATH_PAYMENT_STRICT_RECEIVE_MALFORMED = -1, // bad input
PATH_PAYMENT_STRICT_RECEIVE_UNDERFUNDED = -2, // not enough funds in source account
PATH_PAYMENT_STRICT_RECEIVE_SRC_NO_TRUST = -3, // no trust line on source account
PATH_PAYMENT_STRICT_RECEIVE_SRC_NOT_AUTHORIZED = -4, // source not authorized to transfer
PATH_PAYMENT_STRICT_RECEIVE_NO_DESTINATION = -5, // destination account does not exist
PATH_PAYMENT_STRICT_RECEIVE_NO_TRUST = -6, // dest missing a trust line for asset
PATH_PAYMENT_STRICT_RECEIVE_NOT_AUTHORIZED = -7, // dest not authorized to hold asset
PATH_PAYMENT_STRICT_RECEIVE_LINE_FULL = -8, // dest would go above their limit
PATH_PAYMENT_STRICT_RECEIVE_NO_ISSUER = -9, // missing issuer on one asset
PATH_PAYMENT_STRICT_RECEIVE_TOO_FEW_OFFERS = -10, // not enough offers to satisfy path
PATH_PAYMENT_STRICT_RECEIVE_OFFER_CROSS_SELF = -11, // would cross one of its own offers
PATH_PAYMENT_STRICT_RECEIVE_OVER_SENDMAX = -12 // could not satisfy sendmax
};
union PathPaymentStrictReceiveResult switch (PathPaymentStrictReceiveResultCode code)
{
case PATH_PAYMENT_STRICT_RECEIVE_SUCCESS:
struct
{
ClaimOfferAtom offers<>;
SimplePaymentResult last;
} success;
case PATH_PAYMENT_STRICT_RECEIVE_NO_ISSUER:
Asset noIssuer; // the asset that caused the error
default:
void;
};
enum PathPaymentStrictSendResultCode
{
// codes considered as "success" for the operation
PATH_PAYMENT_STRICT_SEND_SUCCESS = 0, // success
// codes considered as "failure" for the operation
PATH_PAYMENT_STRICT_SEND_MALFORMED = -1, // bad input
PATH_PAYMENT_STRICT_SEND_UNDERFUNDED = -2, // not enough funds in source account
PATH_PAYMENT_STRICT_SEND_SRC_NO_TRUST = -3, // no trust line on source account
PATH_PAYMENT_STRICT_SEND_SRC_NOT_AUTHORIZED = -4, // source not authorized to transfer
PATH_PAYMENT_STRICT_SEND_NO_DESTINATION = -5, // destination account does not exist
PATH_PAYMENT_STRICT_SEND_NO_TRUST = -6, // dest missing a trust line for asset
PATH_PAYMENT_STRICT_SEND_NOT_AUTHORIZED = -7, // dest not authorized to hold asset
PATH_PAYMENT_STRICT_SEND_LINE_FULL = -8, // dest would go above their limit
PATH_PAYMENT_STRICT_SEND_NO_ISSUER = -9, // missing issuer on one asset
PATH_PAYMENT_STRICT_SEND_TOO_FEW_OFFERS = -10, // not enough offers to satisfy path
PATH_PAYMENT_STRICT_SEND_OFFER_CROSS_SELF = -11, // would cross one of its own offers
PATH_PAYMENT_STRICT_SEND_UNDER_DESTMIN = -12 // could not satisfy destMin
};
union PathPaymentStrictSendResult switch (PathPaymentStrictSendResultCode code)
{
case PATH_PAYMENT_STRICT_SEND_SUCCESS:
struct
{
ClaimOfferAtom offers<>;
SimplePaymentResult last;
} success;
case PATH_PAYMENT_STRICT_SEND_NO_ISSUER:
Asset noIssuer; // the asset that caused the error
default:
void;
};
union OperationResult switch (OperationResultCode code)
{
case opINNER:
union switch (OperationType type)
{
// ... CREATE_ACOUNT, PAYMENT unchanged ...
case PATH_PAYMENT_STRICT_RECEIVE:
PathPaymentStrictReceiveResult pathPaymentStrictReceiveResult;
// ... MANAGE_SELL_OFFER, ..., MANAGE_BUY_OFFER unchanged ...
case PATH_PAYMENT_STRICT_SEND:
PathPaymentStrictSendResult pathPaymentStrictSendResult;
}
tr;
default:
void;
};
PathPaymentStrictSendOp
returns
PATH_PAYMENT_STRICT_SEND_NO_DESTINATION
if issuer checks are not bypassed anddestAccount
does not existPATH_PAYMENT_STRICT_SEND_NO_ISSUER
if issuer checks are not bypassed and the issuer ofsendAsset
does not existPATH_PAYMENT_STRICT_SEND_SRC_NO_TRUST
ifsendAsset
is not native andsourceAccount
does not own a trust line forsendAsset
PATH_PAYMENT_STRICT_SEND_SRC_NOT_AUTHORIZED
ifsendAsset
is not native and the trust line forsendAsset
owned bysourceAccount
is not authorizedPATH_PAYMENT_STRICT_SEND_UNDERFUNDED
ifsourceAccount
does not have sufficient available balance ofsendAsset
after accounting for selling liabilities and base reserve (ifsendAsset
is native)- For each
(S, R)
in(sendAsset, path[0]), (path[0], path[1]), ..., (path[n-1], path[n]), (path[n], destAsset)
withS != R
PATH_PAYMENT_STRICT_SEND_NO_ISSUER
ifR
is not native and the issuer ofR
does not existPATH_PAYMENT_STRICT_SEND_OFFER_CROSS_SELF
if an offer owned bysourceAccount
was encountered betweenS
andR
before sending the required amount ofS
PATH_PAYMENT_STRICT_SEND_TOO_FEW_OFFERS
if less than the required amount ofS
was sent before exhausting all offers betweenS
andR
PATH_PAYMENT_STRICT_SEND_UNDER_DESTMIN
if the amount sent to the destination does not exceeddestMin
PATH_PAYMENT_STRICT_SEND_NO_ISSUER
if issuer checks are not bypassed and the issuer ofdestAsset
does not existPATH_PAYMENT_STRICT_SEND_NO_TRUST
ifdestAsset
is not native anddestAccount
does not own a trust line fordestAsset
PATH_PAYMENT_STRICT_SEND_NOT_AUTHORIZED
ifdestAsset
is not native and the trust line fordestAsset
owned bydestAccount
is not authorizedPATH_PAYMENT_STRICT_SEND_LINE_FULL
ifdestAccount
does not have sufficient available limit ofdestAsset
after accounting for buying liabilitiesPATH_PAYMENT_STRICT_SEND_SUCCESS
otherwise
If PathPaymentStrictSendOp
succeeds then the source account is debited exactly
sendAmount
of sendAsset
and the destAccount
is credited at least destMin
of destAsset
. In order to achieve this goal, it was required to introduce a
new rounding mode (see CAP-0004 for context). We provide an informal description
of what occurs and why; for a detailed proof please refer to the implementation.
This rounding mode may only differ from the typical rounding behavior if the offer
is larger than the remnants of the conversion (this can only occur for the last
offer crossed in each step of the path). In this case, the remaining quantity of
S
will be converted into the maximum amount of R
that is possible (given
limits on the conversion and the offer) such that the price remains favorable to
the owner of the offer. This is the correct behavior because
- The entire amount available to be sent is in fact sent if it is possible to do so
- The received amount is the maximum possible while continuing to meet the requirement that it must favor the owner of the offer
Every design decision concerning PathPaymentStrictSendOp
was decided to make the
operation "dual" to PathPaymentStrictReceiveOp
in as many ways as possible. With
that context in mind, the central design decision for PathPaymentStrictSendOp
was enforcing the requirement that it must send exactly the specified sendAmount
.
This decision has the following advantages, analogous to the advantages of having
PathPaymentStrictReceiveOp
receive exactly the specified destAmount
- The semantics of the operation are simple to explain
- The state of the
sourceAccount
anddestAccount
is predictable after the operation executes
All downstream systems will need updated XDR in order to recognize the new operation.
None yet.
None yet.
None yet.