Skip to content

Latest commit

 

History

History
256 lines (224 loc) · 10.6 KB

cap-0006.md

File metadata and controls

256 lines (224 loc) · 10.6 KB

Preamble

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

Simple Summary

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.

Abstract

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.

Motivation

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.

Specification

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 to MANAGE_SELL_OFFER
  • ManageOfferOp will be renamed to ManageSellOfferOp
  • ManageOfferResult will be renamed to ManageSellOfferResult

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;
};

Rationale

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.

Why is the price inverted?

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:

  1. When making a market, ManageSellOfferOp and ManageBuyOfferOp 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"
  2. 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 submitting ManageBuyOfferOp{selling=Y, buying=X, buyAmount=A, price=P}
  3. Converting the sell amount in ManageSellOfferOp to an equivalent buy amount is accomplished by computing amount * price; converting the buy amount in ManageBuyOfferOp to an equivalent sell amount is accomplished by computing buyAmount * price

Validation result codes

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.

Backwards Compatibility

This proposal is fully backward compatible.

Test Cases

Some test cases that must be considered include:

  • If ManageBuyOffer and ManageSellOfferOp 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

Implementation

No implementation yet.