CAP: 0006
Title: Add ManageBuyOffer Operation
Author: Jonathan Jove
Status: Implemented
Created: 2018-10-24
Discussion: https://github.com/stellar/stellar-protocol/issues/180
Protocol version: 11
We introduce the ManageBuyOffer
operation with functionality similar to the
ManageOffer
operation except that the amount is specified in terms of the
buying
asset instead of the selling
asset.
The ManageOffer
operation specifies the maximum amount of the selling
asset
that should be sold by the offer. It is not, however, possible to express the
maximum amount of the buying
asset that should be bought by the offer. These
constraints are not equivalent because at the time of offer submission it is not
known at what price the offer will execute. We propose to add a new operation
called ManageBuyOffer
which specifies the maximum amount of the buying
asset
that should be bought by the offer. The price will be the "price of thing being
bought in terms of what you are selling" rather than the "price of thing being
sold in terms of what you are buying". The behavior is otherwise analogous to
the extant ManageOffer
operation.
Many financial institutions have an obligation to faithfully execute customer
orders. Customer orders to sell a certain quantity of an asset in exchange for
the maximum quantity of a different asset are already easily expressed in terms
of the ManageOffer
operation. In contrast, customer orders to buy a certain
quantity of an asset in exchange for the minimum quantity of a different asset
are not expressible in terms of the ManageOffer
operation. We introduce the
ManageBuyOffer
operation to facilitate the execution of the latter kind of
order.
ManageBuyOfferOp
specification:
struct ManageBuyOfferOp
{
Asset selling;
Asset buying;
int64 buyAmount; // amount being bought. if set to 0, delete the offer
Price price; // price of thing being bought in terms of what you are
// selling
// 0=create a new offer, otherwise edit an existing offer
int64 offerID;
};
/******* ManageBuyOffer Result ********/
enum ManageBuyOfferResultCode
{
// codes considered as "success" for the operation
MANAGE_BUY_OFFER_SUCCESS = 0,
// codes considered as "failure" for the operation
MANAGE_BUY_OFFER_MALFORMED = -1, // generated offer would be invalid
MANAGE_BUY_OFFER_SELL_NO_TRUST = -2, // no trust line for what we're selling
MANAGE_BUY_OFFER_BUY_NO_TRUST = -3, // no trust line for what we're buying
MANAGE_BUY_OFFER_SELL_NOT_AUTHORIZED = -4, // not authorized to sell
MANAGE_BUY_OFFER_BUY_NOT_AUTHORIZED = -5, // not authorized to buy
MANAGE_BUY_OFFER_LINE_FULL = -6, // can't receive more of what it's buying
MANAGE_BUY_OFFER_UNDERFUNDED = -7, // doesn't hold what it's trying to sell
MANAGE_BUY_OFFER_CROSS_SELF = -8, // would cross an offer from the same user
MANAGE_BUY_OFFER_SELL_NO_ISSUER = -9, // no issuer for what we're selling
MANAGE_BUY_OFFER_BUY_NO_ISSUER = -10, // no issuer for what we're buying
// update errors
MANAGE_BUY_OFFER_NOT_FOUND = -11, // offerID does not match an existing offer
MANAGE_BUY_OFFER_LOW_RESERVE = -12 // not enough funds to create a new Offer
};
union ManageBuyOfferResult switch (ManageBuyOfferResultCode code)
{
case MANAGE_BUY_OFFER_SUCCESS:
ManageOfferSuccessResult success;
default:
void;
};
Name changes are binary compatible, so for better naming consistency:
MANAGE_OFFER
will be renamed toMANAGE_SELL_OFFER
ManageOfferOp
will be renamed toManageSellOfferOp
ManageOfferResult
will be renamed toManageSellOfferResult
Additionally, we will update naming for ManageOfferResultCode
to be
enum ManageSellOfferResultCode
{
// codes considered as "success" for the operation
MANAGE_SELL_OFFER_SUCCESS = 0,
// codes considered as "failure" for the operation
MANAGE_SELL_OFFER_MALFORMED = -1, // generated offer would be invalid
MANAGE_SELL_OFFER_SELL_NO_TRUST = -2, // no trust line for what we're selling
MANAGE_SELL_OFFER_BUY_NO_TRUST = -3, // no trust line for what we're buying
MANAGE_SELL_OFFER_SELL_NOT_AUTHORIZED = -4, // not authorized to sell
MANAGE_SELL_OFFER_BUY_NOT_AUTHORIZED = -5, // not authorized to buy
MANAGE_SELL_OFFER_LINE_FULL = -6, // can't receive more of what it's buying
MANAGE_SELL_OFFER_UNDERFUNDED = -7, // doesn't hold what it's trying to sell
MANAGE_SELL_OFFER_CROSS_SELF = -8, // would cross an offer from the same user
MANAGE_SELL_OFFER_SELL_NO_ISSUER = -9, // no issuer for what we're selling
MANAGE_SELL_OFFER_BUY_NO_ISSUER = -10, // no issuer for what we're buying
// update errors
MANAGE_SELL_OFFER_NOT_FOUND = -11, // offerID does not match an existing offer
MANAGE_SELL_OFFER_LOW_RESERVE = -12 // not enough funds to create a new Offer
};
Updated Operation
specification:
enum OperationType
{
CREATE_ACCOUNT = 0,
PAYMENT = 1,
PATH_PAYMENT = 2,
MANAGE_SELL_OFFER = 3,
CREATE_PASSIVE_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
};
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)
{
case CREATE_ACCOUNT:
CreateAccountOp createAccountOp;
case PAYMENT:
PaymentOp paymentOp;
case PATH_PAYMENT:
PathPaymentOp pathPaymentOp;
case MANAGE_SELL_OFFER:
ManageSellOfferOp manageSellOfferOp;
case CREATE_PASSIVE_OFFER:
CreatePassiveOfferOp createPassiveOfferOp;
case SET_OPTIONS:
SetOptionsOp setOptionsOp;
case CHANGE_TRUST:
ChangeTrustOp changeTrustOp;
case ALLOW_TRUST:
AllowTrustOp allowTrustOp;
case ACCOUNT_MERGE:
AccountID destination;
case INFLATION:
void;
case MANAGE_DATA:
ManageDataOp manageDataOp;
case BUMP_SEQUENCE:
BumpSequenceOp bumpSequenceOp;
case MANAGE_BUY_OFFER:
ManageBuyOfferOp manageBuyOfferOp;
}
body;
};
Adding ManageBuyOffer
will not require modifying what data is contained in the
ledger. This can be understood by considering offer execution as two distinct
processes. The first process begins when an offer is submitted. If this offer
matches against an existing offer, then those offers must execute at the price
of the existing offer. This repeats until either the submitted offer has
executed entirely or the submitted offer does not match against any existing
offer. During this first process, limits on the buying amount are not equivalent
to limits on the selling amount since the execution price is variable.
The second process begins with adding to the offer book the remainder of the offer submitted at the start of the first process, if that offer was not already executed entirely. Any subsequent execution of this offer will occur at the price of this offer, unless the offer is modified (in which case the first process begins anew). Therefore, a limit on the buying amount is equivalent to a limit on the selling amount during the second process.
At this point, it is clear what the semantics of the ManageBuyOffer
operation
should be. During the first process, which is a subset of the apply-phase of the
operation, the total amount that can be executed is limited by the buyAmount
specified in the ManageBuyOfferOp
. At the start of the second process, the
remaining buyAmount
is converted into a sell amount and stored in the ledger,
analogous to what would be done with the remaining amount
at the end of
ManageOffer
. The price
must also be inverted before it is stored in the
ledger.
There is, however, one important caveat to all of the above. At the end of the
day, offers are stored on the ledger as sell offers which means that the amount
is a sell amount. When rounding occurs in favor of an offer, it may receive more
of the asset that is not limited than would be otherwise expected. During the
first process, this does not cause an issue for either ManageBuyOffer
or
ManageSellOffer
as each has a limit in terms of the appropriate asset. But, as
noted, in the ledger all limits are on the selling asset so it is possible for
ManageBuyOffer
to buy more than expected during the second process.
As noted in the abstract, the price will be the "price of thing being bought in terms of what you are selling" rather than the "price of thing being sold in terms of what you are buying". There are three main reasons for this interface:
- When making a market,
ManageSellOfferOp
andManageBuyOfferOp
will have prices that appear in the same units:- The price in
ManageSellOfferOp{selling=X, buying=Y, amount=A, price=P}
is the "price of thing being sold in terms of what you are buying" so it is the "price of X in terms of Y" - The price in
ManageBuyOfferOp{selling=Y, buying=X, buyAmount=A, price=P}
is the "price of thing being bought in terms of what you are selling" so it is the "price of X in terms of Y"
- The price in
- As an extension of (1), if
{selling=X, buying=Y, amount=A, price=P}
represents the best offer in a given market, then it can be exactly crossed by submittingManageBuyOfferOp{selling=Y, buying=X, buyAmount=A, price=P}
- Converting the sell amount in
ManageSellOfferOp
to an equivalent buy amount is accomplished by computingamount * price
; converting the buy amount inManageBuyOfferOp
to an equivalent sell amount is accomplished by computingbuyAmount * price
In implementing ManageBuyOffer
, it was observed that ManageOffer
does not
respect the convention that failure to validate should only return an error code
labeled MALFORMED
. ManageBuyOffer
should respect this convention, and for
consistency ManageSellOffer
should also respect this convention starting in
the protocol version which implements this proposal. Specifically validation of
ManageSellOffer
will return MANAGE_SELL_OFFER_MALFORMED
instead of
MANAGE_SELL_OFFER_NOT_FOUND
.
This proposal is fully backward compatible.
Some test cases that must be considered include:
- If
ManageBuyOffer
andManageSellOfferOp
have the same max send and max receive after crossing offers, then the same offer is added to the ledger ManageBuyOffer
properly accounts for liabilities
No implementation yet.