Market orders
A market order is Mangrove's simplest way of buying or selling assets. Such (taker) orders are run against a specific offer list with its associated %%outbound|outbound%% token and %%inbound|inbound%% token. The liquidity taker specifies a max %%ratio|ratio%% she's willing to accept and how much she wishes to trade: Either how many outbound tokens she wants or how many inbound tokens she wishes to pay.
Mangrove's market orders are DeFi market orders - which are different from market orders in TradFi:
In TradFi, a market order is an order to buy or sell immediately at the best available price.
In DeFi, where transactions can be front-run or sandwiched, adversaries may manipulate the best available price and thus extract value from a market order as there is no limit on the price. TradFi market orders are therefore unsafe for fully on-chain DEX'es like Mangrove.
To protect the user, Mangrove's market order therefore corresponds to a TradFi limit order: An order to buy or sell at given price or better.
More precisely, Mangrove ensures that the "price" %%tick|tick%% of the offers matched with the order does not exceed the specified max tick.
Market order executionβ
When a market order is processed by Mangrove's matching engine, it consumes the offers on the selected offer list one by one, in order, and starting from lowest tick (offers with the same tick are executed in FIFO order).
Offers match if their %%tick|tick%% is below or equal to the specified max tick.
The market order stops when one of these conditions are met:
- only offers with tick > max tick are left,
- the end of the offer list has been reached, or
- the taker has received or paid the amount they specified.
For each matched offer, Mangrove takes the following steps:
- Mangrove determines the amount of outbound tokens the offer should deliver and how much inbound token it should be paid:
- outbound amount: Either all of what the offer gives or, if that exceeds what the taker wants, the residual amount needed to fill the market order.
- inbound amount: Determined by the offer's tick: .
- Mangrove sends inbound tokens to the offer maker (EOA or maker contract).
- Mangrove then executes the offer logic's
makerExecute
function (a no-op for an EOA). - If the
makerExecute
call is successful, Mangrove sends outbound tokens to the taker. If the call or the transfer fail, Mangrove reverts the effects of steps 2. and 3.
Any failed offer execution results in a bounty being sent to the caller as compensation for the wasted gas.
Market order functionsβ
Mangrove provides two functions for executing market orders which differ in how the "price" limit is specified:
marketOrderByTick
: The limit is specified as amaxTick
which matched offers should not exceed.marketOrderByVolume
: The limit is specified as the ratio between two volumes,takerGives/takerWants
, which offers should not exceed.
The *ByVolume
variant is a convenience wrapper for the *ByTick
variant: The provided volumes are converted to a corresponding maxTick
, rounding down to the nearest tick allowed on the offer list, such that the taker does not pay more than specified.
The output from the two functions is the same.
- Signature
- Events
- Revert strings
- Solidity
function marketOrderByTick(
OLKey memory olKey,
Tick maxTick,
uint fillVolume,
bool fillWants,
) external returns (uint takerGot, uint takerGave, uint bounty, uint feePaid);
function marketOrderByVolume(
OLKey memory olKey,
uint takerWants,
uint takerGives,
bool fillWants
) external returns (uint takerGot, uint takerGave, uint bounty, uint feePaid);
// Since the contracts that are called during the order may be partly reentrant, more logs could be emitted by Mangrove.
// We list here only the main expected logs.
// For each successful offer taken during the market order:
event OfferSuccess(
bytes32 indexed olKeyHash, // hash of the OLKey (inbound token, outbound token and tickSpacing)
address indexed taker, // address of the market order call
uint indexed id,
uint takerWants, // original wants of the order
uint takerGives // original gives of the order
);
// For each offer cleaned during the market order:
event OfferFail(
bytes32 indexed olKeyHash, // hash of the OLKey (inbound token, outbound token and tickSpacing)
address indexed taker, // address of the market order call
uint indexed id,
uint takerWants, // original wants of the order
uint takerGives // original gives of the order
uint penalty,
// `mgvData` is either:
// * `"mgv/makerRevert"` if `makerExecute` call reverted
// * `"mgv/makerTransferFail"` if `outbound_tkn` transfer from the maker contract failed after `makerExecute`
// * `"mgv/makerReceiveFail"` if `inbound_tkn` transfer to maker contract failed (e.g. contract's address is not allowed to receive `inbound_tkn`)
bytes32 mgvData
);
// For each offer whose posthook reverted during second callback:
// 1. Loging offer failure
event OfferFailWithPosthookData(
bytes32 indexed olKeyHash,
address indexed taker,
uint indexed id,
uint takerWants,
uint takerGives,
uint penalty,
bytes32 mgvData,
// `posthookData` contains the first 32 bytes of the posthook revert reason
// e.g the complete reason if posthook reverted with a string small enough.
bytes32 posthookData
);
// 2. Debiting maker from Offer Bounty
event Debit(address indexed maker, uint amount);
// Logging at the end of Market Order:
event OrderComplete(
bytes32 indexed olKeyHash,
address indexed taker,
uint fee, // the fee paid by the taker
);
// Gatekeeping
"mgv/dead" // Trying to take offers on a terminated Mangrove
"mgv/inactive" // Trying to take offers on an inactive offer list
// Overflow
"mgv/mOrder/takerWants/160bits" // taker wants too much of a market Order
"mgv/mOrder/takerGives/160bits" // taker gives too much in the market order
// Panic reverts
"mgv/sendPenaltyReverted" // Mangrove could not send the offer bounty to taker
"mgv/MgvFailToPayTaker" // Mangrove was unable to transfer outbound_tkn to taker (Taker blacklisted?)
import {IMangrove} from "@mgv/src/IMangrove.sol";
import "@mgv/src/core/MgvLib.sol";
// context of the call
// IMangrove mgv = IMangrove(payable(<address of Mangrove>));
// Mangrove contract
IMangrove mgv = IMangrove(payable(mgv));
// OLKey olkey = OLKey(<address of outbound token>, <address of inbound token>, <tick spacing>);
// struct containing outbound_tkn, inbound_tkn and tickSpacing
OLKey memory olkey = OLKey(address(base), address(quote), 1);
// Tick maxTick = TickLib.tickFromVolumes(<raw amount of inbound token>, <raw amount of outbound token>);
// constructs max tick from a ratio between inbound and outbound token amounts
Tick maxTick = TickLib.tickFromVolumes(1_000_000, 1_000);
// marketOrderByTick
(uint takerGot, uint takerGave, uint bounty, uint feePaid) = mgv.marketOrderByTick(olKey, maxTick, 1 ether, true);
// marketOrderByVolume
(uint takerGot, uint takerGave, uint bounty, uint feePaid) = mgv.marketOrderByVolume(olKey, 1.1 ether, 1.9 ether, true);
Inputsβ
marketOrderByTick(olKey, maxTick, fillVolume, fillWants)
β
olKey
: identifies the offer list and consists of:outbound_tkn
: address of the outbound token (that the taker will buy).inbound_tkn
: address of the inbound token (that the taker will spend).tickSpacing
: specifies the space between allowed ticks (see Ticks, ratios, and prices for details)
maxTick
: specifies the max tick that can be matched with this orderfillVolume
: the desired volume of tokens in eitherolKey.outbound_tkn
orolKey.inbound_tkn
.- If
fillWants
istrue
,fillVolume
is inolKey.outbound_tkn
. This means the taker specified how much they wish to receive ("buy"). - If
fillWants
isfalse
,fillVolume
is inolKey.inbound_tkn
. This means the taker specified how much they wish to send ("sell").
- If
fillWants
: Whether to stop when the taker has received or spent a specified amount:- If
true
, the order is full when taker has receivedfillVolume
ofolKey.outbound_tkn
. - If
false
, the order is full when taker has sentfillVolume
ofolKey.inbound_tkn
.
- If
marketOrderByVolume(olKey, takerWants, takerGives, fillWants)
β
olKey
: identifies the offer list and consists of:outbound_tkn
: address of the outbound token (that the taker will buy).inbound_tkn
: address of the inbound token (that the taker will spend).tickSpacing
: specifies the space between allowed ticks (see Ticks, ratios, and prices for details)
takerWants
: amount of outbound token the taker wants. Must fit on 127 bits.takerGives
: amount of inbound token the taker gives. Must fit on 127 bits.- The ratio
takerGives/takerWants
specifies the max ratio (and thus tick) that can be matched with this order.
- The ratio
fillWants
: Whether to stop when the taker has receivedtakerWants
or spenttakerGives
:- If
true
, the order is full when taker has receivedtakerWants
ofolKey.outbound_tkn
. - If
false
, the order is full when taker has senttakerGives
ofolKey.inbound_tkn
.
- If
Outputsβ
takerGot
is the net amount of outbound tokens the taker has received (i.e., after applying the offer list fee if any).takerGave
is the amount of inbound tokens the taker has sent.bounty
is the amount of native tokens (in units of wei) the taker received in compensation for cleaning failing offersfeePaid
is the amount ofoutbound_tkn
that was sent to Mangrove's vault in payment of the potential %%fee|taker-fee%% configured for theolKey
offer list.
Exampleβ
Let's consider the following DAI-WETH offer list with no fee:
Tick | Ratio (WETH/DAI) | Offer ID | Gives (DAI) |
---|---|---|---|
-75103 | 0.0005476 | 77 | 925.26 |
-75103 | 0.0005476 | 177 | 916.47 |
-75041 | 0.0005510 | 42 | 871.76 |
The following two examples illustrate the difference between fillWants = true
or false
:
A taker calls marketOrderByTick
on the offer list with:
maxTick
= -75000- corresponds to a max ratio of 0.0005539
fillVolume
= 2,500 * 10**18fillWants
=true
Since fillWants = true
, we have:
fillVolume
is inolKey.outbound_tkn
and corresponds to 2,500 DAI- the market order will be considered filled once 2,500 DAI has been received.
In summary, this corresponds to a buy order for 2,500 DAI and the taker is willing to pay up to 2,500 * 0.0005539 = 1.3832
WETH.
The market order will execute as follows:
- Get 925.26 DAI for
925.26 * 0.0005476 = 0.5067
WETH from offer #77 - Get 916.47 DAI for
916.47 * 0.0005476 = 0.5019
WETH from offer #177 - Get the remaining
2500 - 925.26 - 916.47 = 658.27
DAI for658.27 * 0.0005510 = 0.3627
WETH from offer #42.
In total, the taker gets 2,500 DAI and sends 0.5067 + 0.5019 + 0.3627 = 1.3713
WETH.
A taker calls marketOrderByTick
on the offer list with:
maxTick
= -75000- corresponds to a max ratio of 0.0005539
fillVolume
= 1.2 * 10**18fillWants
=false
Since fillWants = false
, we have:
fillVolume
is inolKey.inbound_tkn
and corresponds to 1.2 WETH- the market order will be considered filled once 1.2 WETH has been sent.
In summary, this corresponds to a sell order of 1.2 WETH and the taker wants to receive at least 1.2 / 0.0005539 = 2168.84
DAI.
- Sell
925.26 * 0.0005476 = 0.5067
WETH for 925.26 DAI to offer #77 - Sell
916.47 * 0.0005476 = 0.5019
WETH for 916.47 DAI to offer #177 - Sell the remaining
1.2 - 0.5067 - 0.5019 = 0.1914
WETH for0.1914 / 0.0005510 = 347.37
DAI to offer #42.
In total, the taker gets 925.26 + 916.47 + 347.37 = 2,189.10
DAI and sends 1.2 WETH.
More on market order behaviourβ
Suppose one wants to buy or sell some token B
(base), using token Q
(quote) as payment, on a market with tick spacing T
.
- Market buy: A limit buy order for
x
B
tokens, corresponds to amarketOrderByTick
on the (B
,Q
,T
) offer list withfillWants
set totrue
,fillVolume = x
(the volume one wishes to buy), andmaxTick
= . - Market sell: A limit sell order for
x
B
tokens, corresponds to amarketOrderByTick
on the (Q
,B
,T
) offer list withfillWants
set tofalse
,fillVolume = x
(the volume one wishes to sell), andmaxTick
= .
Contrary to GTC orders on regular order book based exchanges, the residual of your order (i.e., the volume you were not able to buy/sell due to hitting your price limit) will not be put on the market as an offer. Instead, the market order will simply end partially filled.
It is possible to implement GTC orders through a %%maker contract|maker-contract%%. MangroveOrder in the Strat Lib implements GTC and other advanced order types.
Bounties for taking failing offersβ
If an offer fails to deliver, the taker gets a %%bounty|bounty%% in native token to compensate for the gas spent on executing the offer. The bounty is paid by the %%offer owner|offer-owner%% and is taken from the %%provision|provision%% they deposited with Mangrove when posting the offer.
Refer to Offer provisions for details on how provisions and bounties work and are calculated.
Token allowanceβ
ERC20 tokens transfers are initiated by Mangrove using transferFrom
. If Mangrove's allowance
on the taker's address (for tokens to be spent) is too low, the order will revert.
Active offer listsβ
Every Mangrove offer list can be either active or inactive, and Mangrove itself can be either alive or dead. Taking offers is only possible when Mangrove is alive and on offer lists that are active.