CAP: 0006
Title: Add ManageBuyOffer Operation
Author: Jonathan Jove
Status: Final
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_OFFERwill be renamed toMANAGE_SELL_OFFERManageOfferOpwill be renamed toManageSellOfferOpManageOfferResultwill 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,
ManageSellOfferOpandManageBuyOfferOpwill 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
ManageSellOfferOpto an equivalent buy amount is accomplished by computingamount * price; converting the buy amount inManageBuyOfferOpto 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
ManageBuyOfferandManageSellOfferOphave the same max send and max receive after crossing offers, then the same offer is added to the ledger ManageBuyOfferproperly accounts for liabilities
No implementation yet.