structs.ts

        

Mangrove Summary

  • Mangrove holds offer lists for outbound_tkn,inbound_tkn pairs with a given tickSpacing.
  • Offers are sorted in a tree (the “tick tree”) where each available price point (a bin) holds a doubly linked list of offers.
  • Each offer promises outbound_tkn and requests inbound_tkn.
  • Each offer has an attached maker address.
  • When an offer is executed, Mangrove does the following:
    1. Flashloan some inbound_tkn to the offer’s maker.
    2. Call an arbitrary execute function on that address.
    3. Transfer back some outbound_tkn.
    4. Call back the maker so they can update their offers.

Let the devs know about any error, typo etc, by contacting devs@mangrove.exchange

More documentation / discussions can be found at https://docs.mangrove.exchange/.

There is one Mangrove contract that manages all tradeable offer lists. This reduces deployment costs for new offer lists and lets market makers have all their provision for all offer lists in the same place.

The interaction map between the different actors is as follows:

The sequence diagram of a market order is as follows:

Ratio and price

In the comments, we use the word ‘price’ to refer to the ratio between the amount promised by an offer and the amount it requests in return. In the code however, we use the generic word ratio to avoid confusion with notions of price based on concepts such as ‘quote’ and ‘base’ tokens, etc.

The unit of price (and ratio) is inbound_tkn/outbound_tkn.

tickSpacing

The granularity of available price points in an offer list is controlled by the tickSpacing parameter. With a high tickSpacing, fewer price points are available, but the gas cost of market orders is lower since a smaller part of the tick tree has to be explored.

The available prices in an offer list are 1.0001^i for all MIN_TICK <= i <= MAX_TICK such that i % tickSpacing = 0.

Tree storage

Offers are stored in a tree we call a “tick tree”. Thanks to this tree structure, offer operations (insert, update, and retract) take constant time (the height of the tree is fixed).

Bins

Below the bottom of the tree are bins. A bin is a doubly linked list of offers. All offers in a bin have the same tick. During a market order, offers in a bin are executed in order, from the first to the last. Inserted offers are always appended at the end of a bin.

Bins are laid in sequence. In the context of an offer list, each bin has an associated tick (and a tick determines a price). If a bin has tick t, the following bin has tick t+tickSpacing.

Bins are identified by their index in the bin sequence, from the first (MIN_BIN) to the last (MAX_BIN). The number of available bins is larger than the number of available ticks, so some bins will never be used.

Tree structure

The structure of the tick tree is as follows:

At the bottom of the tree, leaves contain information about 4 bins: their first and last offer. Offer ids use 32 bits, so leaves use 256 bits.

When a market order runs, execution starts with the first offer of the lowest-numbered nonempty bin and continues from there.

Once all the offers in the smallest bin have been executed, the next non-empty bin is found. If the leaf of the current bin now only has empty bins, the tree must be searched for the next non-empty bin, starting at the node above the leaf:

A non-leaf node contains a bit field with information about all its children: if the ith bit of the node is set, its ith child has at least one non-empty bin. Otherwise, its ith child only has empty bins.

To find the next non-empty bin, it may be necessary to keep going up the tree until the root is reached. At each level above, the bit field rule applies similarly: the ith bit of a node is set iff its ith child has at least one set bit.

Once a node with a set bit is found, its rightmost nonempty child is examined, and so on until the next nonempty bin is reached, and the first offer of that bin gets executed.

Caching

At any time, if there is at least one offer in the offer list, the best offer is the first offer of the bin containing the cheapest offers; and that bin is the best bin. Its parent is the best level3, whose parent is the best level2, and whose parent is the best level1 (whose parent is the root).

The root, the best level1, the best level2, and the best level3 are always stored in local. The position of the best bin in the best leaf is also stored in local.

This data is useful for two things:

  • Read and modify the tree branch of the best offer without additional storage reads and writes (except for modifying the best leaf).
  • Know the price of the best offer without an additional storage read (it can be computed using the set bit positions in each level, the position of the best bin in the best leaf, and the value of tickSpacing).

The structure of the local config is as follows:

This caching means that as the price oscillates within a more restricted range, fewer additional storage read/writes have to be performed (as most of the information is available in local) when there is a market order or an offer insertion/update.

Some numbers

Here are some useful numbers, for reference:


        

        

Preprocessing

The current file (structs.js) is used in Structs.pre.sol (not shown here) to generate the libraries in Struct.pre.sol. Here is an example of js struct specification and of a generated library:

struct_defs = {
universe: [
 {name: "serialnumber", bits: 16, type: "uint"},
 {name: "hospitable",bits: 8, type:"bool"}
]
}

The generated file will store all data in a single EVM stack slot (seen as an abstract type <TypeName> by Solidity); here is a simplified version:

struct UniverseUnpacked {
uint serialnumber;
bool hospitable;
}

library Library {
// use Solidity 0.8* custom types
type Universe is uint;

// test word equality
function eq(Universe ,Universe) returns (bool);

// word <-> struct conversion
function to_struct(Universe) returns (UniverseUnpacked memory);
function t_of_struct(UniverseUnpacked memory) returns (Universe);

// arguments <-> word conversion
function unpack(Universe) returns (uint serialnumber, bool hospitable);
function pack(uint serialnumber, bool hospitable) returns(Universe);

// read and write first property
function serialnumber(Universe) returns (uint);
function serialnumber(Universe,uint) returns (Universe);

// read and write second property
function hospitable(Universe) returns (bool);
function hospitable(Universe,bool) returns (Universe);
}

Then, in Solidity code, one can write:

Universe uni = UniverseLib.pack(32,false);
uint num = uni.serialnumber();
uni.hospitable(true);

        

Data structures


        

Struct-like data structures are stored in storage and memory as 256 bits words. We avoid using structs due to significant gas savings gained by extracting data from words only when needed. This is exacerbated by the fact that Mangrove uses one recursive call per executed offer; it is much cheaper to accumulate used stack space than memory space throughout the recursive calls.

The generation is defined in lib/preproc.ts.


        

Struct fields that are common to multiple structs are factored here. Multiple field names refer to offer identifiers, so the id_field is a function that takes a name as argument and returns a field with the right size & type.

154
155
156
157
158
159
160
161
162
163
164
165
const fields = {
  gives: { name: "gives", bits: 127, type: "uint" },
  gasprice: { name: "gasprice", bits: 26, type: "uint" },
  gasreq: { name: "gasreq", bits: 24, type: "uint" },
  kilo_offer_gasbase: { name: "kilo_offer_gasbase", bits: 9, type: "uint" },
};

const id_field = (name: string) => {
  return { name, bits: 32, type: "uint" };
};

Structs


        

Offer


        

        

Offers hold doubly linked list pointers to their prev and next offers, as well as price and volume information. 256 bits wide, so one storage read is enough. They have the following fields:


        
172
173
174
const struct_defs = {
  offer: {
    fields: [
  • prev points to immediately better offer at the same price point, if any, and 0 if there is none. 32 bits wide.
176
      id_field("prev"),
  • next points to immediately worse offer at the same price point, if any, and 0 if there is none. 32 bits wide.
178
      id_field("next"),
  • tick is the log base 1.0001 of the price of the offer. 21 bits wide.
180
      {name:"tick",bits:21,type:"Tick",underlyingType: "int"},
  • gives is the amount of outbound_tkn the offer will give if successfully executed. 127 bits wide.
182
183
184
185
186
187
188
189
190
191
192
      fields.gives,
    ],
    additionalDefinitions: `import {Bin} from "@mgv/lib/core/TickTreeLib.sol";
import {Tick} from "@mgv/lib/core/TickLib.sol";
import {OfferExtra,OfferUnpackedExtra} from "@mgv/lib/core/OfferExtra.sol";

using OfferExtra for Offer global;
using OfferUnpackedExtra for OfferUnpacked global;
`
  },

OfferDetail


        

        

OfferDetails hold the maker’s address and provision/penalty-related information. They have the following fields:

197
198
  offerDetail: {
    fields: [
  • maker is the address that created the offer. It will be called when the offer is executed and later during the posthook phase.
200
      { name: "maker", bits: 160, type: "address" },
  • gasreq gas will be provided to execute. 24 bits wide, i.e. around 16M gas. Note that if more room was needed, we could bring it down to 16 bits and have it represent 1k gas increments.
204
      fields.gasreq,
  • kilo_offer_gasbase represents the gas overhead used by processing the offer inside Mangrove + the overhead of initiating an entire order, in 1k gas increments.

The gas considered ‘used’ by an offer is the sum of

  • gas consumed during the call to the offer
  • kilo_offer_gasbase * 1e3

(There is an inefficiency here. The overhead could be split into an “offer-local overhead” and a “general overhead”. That general overhead gas penalty could be spread between all offers executed during an order, or all failing offers. It would still be possible for a cleaner to execute a failing offer alone and make them pay the entire general gas overhead. For the sake of simplicity we keep only one “offer overhead” value.)

If an offer fails, gasprice Mwei is taken from the provision per unit of gas used. gasprice should approximate the average gas price at offer creation time.

kilo_offer_gasbase is the actual field name, is 9 bits wide, and represents 1k gas increments. The accessor offer_gasbase returns kilo_offer_gasbase * 1e3.

kilo_offer_gasbase is also the name of a local Mangrove parameter. When an offer is created, its current value is copied from Mangrove local configuration. The maker does not choose it.

So, when an offer is created, the maker is asked to provision the following amount of wei:

(gasreq + offer_gasbase) * gasprice * 1e6

where offer_gasbase and gasprice are Mangrove’s current configuration values (or a higher value for gasprice if specified by the maker).

When an offer fails, the following amount is given to the taker as compensation:

(gasused + offer_gasbase) * gasprice * 1e6

where offer_gasbase and gasprice are Mangrove’s current configuration values. The rest is given back to the maker.

240
      fields.kilo_offer_gasbase,
  • gasprice is in Mwei/gas and 26 bits wide, which accommodates 0.001 to ~67k gwei / gas. gasprice is also the name of a global Mangrove parameter. When an offer is created, the offer’s gasprice is set to the max of the user-specified gasprice and Mangrove’s global gasprice.
242
243
244
245
246
247
248
249
      fields.gasprice,
    ],
    additionalDefinitions: (struct) => `import {OfferDetailExtra,OfferDetailUnpackedExtra} from "@mgv/lib/core/OfferDetailExtra.sol";
using OfferDetailExtra for OfferDetail global;
using OfferDetailUnpackedExtra for OfferDetailUnpacked global;
`,
  },

Global Configuration

Configuration information for an offer list is split between a global struct (common to all offer lists) and a local struct specific to each offer list. Global configuration fields are:

253
254
  global: {
    fields: [
  • The monitor can provide real-time values for gasprice and density to Mangrove. It can also receive liquidity event notifications.
256
      { name: "monitor", bits: 160, type: "address" },
  • If useOracle is true, Mangrove will use the monitor address as an oracle for gasprice and density, for every outbound_tkn/inbound_tkn pair, except if the oracle-provided values do not pass a check performed by Mangrove. In that case the oracle values are ignored.
258
      { name: "useOracle", bits: 1, type: "bool" },
  • If notify is true, Mangrove will notify the monitor address after every offer execution.
260
      { name: "notify", bits: 1, type: "bool" },
  • The gasprice is the amount of penalty paid by failed offers, in Mwei per gas used. gasprice should approximate the average gas price and will be subject to regular updates.
262
      fields.gasprice,
  • gasmax specifies how much gas an offer may ask for at execution time. An offer which asks for more gas than the block limit would live forever on the book. Nobody could take it or remove it, except its creator (who could cancel it). In practice, we will set this parameter to a reasonable limit taking into account both practical transaction sizes and the complexity of maker contracts.
265
      { name: "gasmax", bits: 24, type: "uint" },
  • dead: if necessary, Mangrove can be entirely deactivated by governance (offers can still be retracted and provisions can still be withdrawn). Once killed, Mangrove must be redeployed; It cannot be resurrected.
267
      { name: "dead", bits: 1, type: "bool" },
  • maxRecursionDepth is the maximum number of times a market order can recursively execute offers. This is a protection against stack overflows.
269
      { name: "maxRecursionDepth", bits: 8, type: "uint" },      
  • maxGasreqForFailingOffers is the maximum gasreq failing offers can consume in total. This is used in a protection against failing offers collectively consuming the block gas limit in a market order. Setting it too high would make it possible for successive failing offers to consume up to that limit then trigger a revert (thus the failing offer would not be removed). During a market order, Mangrove keeps a running sum of the gasreq of the failing offers it has executed and stops the market order when that sum exceeds maxGasreqForFailingOffers.
271
272
273
274
      { name: "maxGasreqForFailingOffers", bits: 32, type: "uint" },      
    ],
  },

Local configuration

276
277
  local: {
    fields: [
  • An offer list is not active by default, but may be activated/deactivated by governance.
279
      { name: "active", bits: 1, type: "bool" },
  • fee, in basis points, of outbound_tkn given to the taker. This fee is sent to Mangrove. Fee is capped to ~2.5%.
281
      { name: "fee", bits: 8, type: "uint" },
  • density is similar to a ‘dust’ parameter. We prevent spamming of low-volume offers by asking for a minimum ‘density’ in outbound_tkn per gas requested. For instance, if density is worth 10, offer_gasbase == 5000, an offer with gasreq == 30000 must promise at least 10 × (30000 + 5000) = 350000 outbound_tkn. 9 bits wide.

We store the density as a float with 2 bits for the mantissa, 7 for the exponent, and an exponent bias of 32, so that density ranges from 2322^{-32} to 1.75×2951.75 \times 2^{95}. For more information, see DensityLib.

287
      { name: "density", bits: 9, type: "Density", underlyingType: "uint"},

To save gas, Mangrove caches the entire tick tree branch of the bin that contains the best offer in each offer list’s local parameter. Taken together, binPosInLeaf, level3, level2, level1, and root provide the following info:

  • What the current bin is (see BinLib.bestBinFromLocal)
  • When a leaf is emptied and the next offer must be fetched, the information in the fields level3, level2, level1 and root avoid multiple storage reads
292
293
294
295
296
      { name: "binPosInLeaf", bits: 2, type: "uint" },
      { name: "level3", bits: 64, type: "Field", underlyingType: "uint" },
      { name: "level2", bits: 64, type: "Field", underlyingType: "uint" },
      { name: "level1", bits: 64, type: "Field", underlyingType: "uint" },
      { name: "root", bits: 2, type: "Field", underlyingType: "uint" },
  • offer_gasbase represents the gas overhead used by processing the offer inside Mangrove + the overhead of initiating an entire order. Mangrove considers that a failed offer has used at least offer_gasbase gas. The actual field name is kilo_offer_gasbase and the accessor offer_gasbase returns kilo_offer_gasbase*1e3. Local to an offer list, because the costs of calling outbound_tkn and inbound_tkn’s transferFrom are part of offer_gasbase. Should only be updated when ERC20 contracts change or when opcode prices change.
298
      fields.kilo_offer_gasbase,
  • If lock is true, orders may not be added nor executed, nor the offer list read by external contracts.

Reentrancy during offer execution is not considered safe:

  • during execution, an offer could consume other offers further up in the list, effectively front-running the taker currently executing the offer.
  • it could also cancel other offers, creating a discrepancy between the advertised and actual market price at no cost to the maker.
  • a maker could potentially distinguish between a clean and a market order based on the current state of the offer list

Note: An optimization in the marketOrder function relies on reentrancy being forbidden.

308
      { name: "lock", bits: 1, type: "bool" },
  • last is a counter for offer ids, incremented every time a new offer is created. It can’t go above 23212^{32}-1.
310
311
      id_field("last"),
    ],

Import additional libraries for Local and LocalExtra.

313
314
    additionalDefinitions: (struct) => `import {Density, DensityLib} from "@mgv/lib/core/DensityLib.sol";
import {Bin,TickTreeLib,Field} from "@mgv/lib/core/TickTreeLib.sol";

Globally enable global.method(…)

316
317
318
319
320
321
322
323
import {LocalExtra,LocalUnpackedExtra} from "@mgv/lib/core/LocalExtra.sol";
using LocalExtra for Local global;
using LocalUnpackedExtra for LocalUnpacked global;
`,
  }
};

export default struct_defs;
Constants.sol
2
3
pragma solidity ^0.8.17;

The constants below are written as literals to optimize gas. For all the relevant constants here, the non-literal expression that computes them is checked in Constants.t.sol.

5
6
7
8
9
10
uint constant ONE = 1;
uint constant ONES = type(uint).max;
uint constant TOPBIT = 0x8000000000000000000000000000000000000000000000000000000000000000;
uint constant NOT_TOPBIT = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;

sizes must match field sizes in structs.ts where relevant


        

Number of bits used to represent a tick

14
uint constant TICK_BITS = 21;

Number of bits used to represent an offer id

16
uint constant OFFER_BITS = 32;

Maximum possible size of a field – protects against wrong code changes. Constraint given by BitLib.ctz64.

18
19
uint constant MAX_FIELD_SIZE = 64;

X_SIZE_BITS is 1+log2 of the size of X, where size is the number of elements it holds.In a field, an element is a bit; in a leaf, an element is a pair of offer ids. For LEVELs and ROOT, the value must be exact, so only power-of-2 sizes are allowed.

21
22
23
24
uint constant LEAF_SIZE_BITS = 2; 
uint constant LEVEL_SIZE_BITS = 6;
uint constant ROOT_SIZE_BITS = 1;

X_SIZE is 2**X_SIZE_BITS

26
27
28
29
int constant LEAF_SIZE = 4;
int constant LEVEL_SIZE = 64;
int constant ROOT_SIZE = 2;

X_SIZE_MASK is 0...01...1 where the number of 1s is X_SIZE_BITS

31
32
33
uint constant LEAF_SIZE_MASK = 0x3;
uint constant LEVEL_SIZE_MASK = 0x3f;
uint constant ROOT_SIZE_MASK = 0x1;

0...01...1 with OFFER_BITS 1s at the end

35
36
uint constant OFFER_MASK = 0xffffffff;

Same as ROOT_SIZE

38
int constant NUM_LEVEL1 = 2;

Same as NUM_LEVEL1 * LEVEL_SIZE

40
int constant NUM_LEVEL2 = 128;

Same as NUM_LEVEL2 * LEVEL_SIZE

42
int constant NUM_LEVEL3 = 8192;

Same as NUM_LEVEL3 * LEVEL_SIZE

44
int constant NUM_LEAFS = 524288;

Same as NUM_LEAFS * LEAF

46
int constant NUM_BINS = 2097152;

min and max bins are defined like min and max int.

48
49
50
int constant MIN_BIN = -1048576;
int constant MAX_BIN = 1048575;

The tick range is the largest such that the mantissa of 1.0001^MAX_TICK fits on 128 bits (and thus can be multiplied by volumes).

52
53
int constant MIN_TICK = -887272;
int constant MAX_TICK = 887272;

These are reference values for what the function tickFromRatio function will return, not the most possible accurate values for the min and max tick.

55
56
57
58
uint constant MIN_RATIO_MANTISSA = 170153974464283981435225617938057077692;
int constant MIN_RATIO_EXP = 255;
uint constant MAX_RATIO_MANTISSA = 340256786836388094050805785052946541084;
int constant MAX_RATIO_EXP = 0;

MANTISSA_BITS is the number of bits used in the mantissa of normalized floats that represent ratios. 128 means we can multiply all allowed volumes by the mantissa and not overflow.

60
61
uint constant MANTISSA_BITS = 128;
uint constant MANTISSA_BITS_MINUS_ONE = 127;

With |tick|<=887272 and normalized mantissas on 128 bits, the maximum possible mantissa is 340282295208261841796968287475569060645, so the maximum safe volume before overflow is NO_OVERFLOW_AMOUNT = 340282438633630198193436196978374475856 (slightly above 2**128).

For ease of use, we could pick a simpler, slightly smaller max safe volume: (1<<128)-1.

However the *ByVolume functions get a price by (abstractly) performing outboundAmount/inboundAmount. If we limit all volumes to NO_OVERFLOW_AMOUNT but aren’t more restrictive than that, since type(uint128).max > 1.0001**MAX_TICK, we will get ratios that are outside the price boundaries.

We thus pick a uniform, easy to remember constraint on volumes that works everywhere: (1<<127)-1

71
72
uint constant MAX_SAFE_VOLUME = 170141183460469231731687303715884105727;

When a market order consumes offers, the implementation uses recursion consumes additional EVM stack space at each new offer. To avoid reverting due to stack overflow, Mangrove keeps a counter and stops the market order when it reaches a maximum recursion depth. INITIAL_MAX_RECURSION_DEPTH is the maximum recursion depth given at deployment time.

See maxRecursionDepth in structs.ts

Without optimizer enabled it fails above 79. With optimizer and 200 runs it fails above 80. Set default a bit lower to be safe.

78
79
uint constant INITIAL_MAX_RECURSION_DEPTH = 75;

When a market order consumes offers, it may use gas on offers that fail to deliver. To avoid reverts after a string of failing offers that consumes more gas than is available in a block, Mangrove stops a market order after it has gone through failing offers such that their cumulative gasreq is greater than the global maxGasreqForFailingOffers parameter. At deployment, maxGasreqForFailingOffers is set to:

INITIAL_MAX_GASREQ_FOR_FAILING_OFFERS_MULTIPLIER * gasmax
84
85
uint constant INITIAL_MAX_GASREQ_FOR_FAILING_OFFERS_MULTIPLIER = 3;

Those two constants are used in TickLib.ratioFromTick to convert a log base 1.0001 to a log base 2.

87
88
uint constant LOG_BP_SHIFT = 235;
uint constant LOG_BP_2X235 = 382733217082594961806491056566382061424140926068392360945012727618364717537;
MgvLib.sol

        

MgvLib contains data structures returned by external calls to Mangrove and the interfaces it uses for its own external calls.

4
5
6
7
8
9
10
11
12
pragma solidity ^0.8.10;

import "@mgv/src/preprocessed/Structs.post.sol";
import {IERC20} from "@mgv/lib/IERC20.sol";
import {Density, DensityLib} from "@mgv/lib/core/DensityLib.sol";
import "@mgv/lib/core/TickTreeLib.sol";
import "@mgv/lib/core/TickLib.sol";

OLKey (for “OfferListKey”) contains the information that characterizes an offer list:

  • outbound_tkn, token that goes from the maker to the taker (to remember the direction, imagine the token as going out of the Mangrove offer, towards the taker)
  • inbound_tkn, token that goes from the taker to the maker (to remember the direction, imagine the token as going into Mangrove from the taker)
  • tickSpacing, how many ticks should be jumped between available price points. More volatile outbound/inbound pairs should have a larger tickSpacing.
17
18
19
20
21
22
struct OLKey {
  address outbound_tkn;
  address inbound_tkn;
  uint tickSpacing;
}

Globally enable olKey.method(...)

24
25
26
using OLLib for OLKey global;

library OLLib {

The id of an OLKey should be keccak256(abi.encode(olKey)) To save gas, id() directly hashes the memory (which matches the ABI encoding; there is a fuzz test on that). If the memory layout changes, this function must be updated.

29
30
31
32
33
34
  function hash(OLKey memory olKey) internal pure returns (bytes32 _id) {
    assembly ("memory-safe") {
      _id := keccak256(olKey, 96)
    }
  }

Creates a flipped copy of the olKey with same tickSpacing.

36
37
38
39
  function flipped(OLKey memory olKey) internal pure returns (OLKey memory) {
    return OLKey(olKey.inbound_tkn, olKey.outbound_tkn, olKey.tickSpacing);
  }

Convert tick to bin according to olKey.tickSpacing

41
42
43
44
  function nearestBin(OLKey memory olKey, Tick _tick) internal pure returns (Bin) {
    return _tick.nearestBin(olKey.tickSpacing);
  }

Convert bin to tick according to olKey.tickSpacing

46
47
48
49
50
  function tick(OLKey memory olKey, Bin bin) internal pure returns (Tick) {
    return bin.tick(olKey.tickSpacing);
  }
}

Structs

The structs defined in structs.js have their counterpart as solidity structs that are easy to manipulate for outside contracts / callers of view functions.

53
54
library MgvLib {

Some miscellaneous data types useful to Mangrove and external contracts


        

        

SingleOrder holds data about an order-offer match in a struct. Used by marketOrder (and some of its nested functions) to avoid stack too deep errors. It is used in market order and cleaning.

60
61
62
  struct SingleOrder {
    OLKey olKey;
    uint offerId;

The offer given to the maker will be cleaned of prev/next pointers.

64
    Offer offer;

takerWants/takerGives mutate over execution. Initially the wants/gives from the taker’s pov, then actual wants/gives adjusted by offer’s price and volume.

66
67
    uint takerWants;
    uint takerGives;

offerDetail is only populated when necessary.

69
70
    OfferDetail offerDetail;
    Global global;

The local given to the maker will be cleaned of tick tree information.

72
73
74
    Local local;
  }

OrderResult holds additional data for the maker and is given to them after they fulfilled an offer. It gives them their own returned data from the previous call and an mgvData specifying whether Mangrove encountered an error.

76
77
  struct OrderResult {

makerData holds a message that was either returned by the maker or passed as revert message at the end of the trade execution

79
    bytes32 makerData;

mgvData is an internal Mangrove status code code.

81
82
83
    bytes32 mgvData;
  }

CleanTarget holds data about an offer that should be cleaned, i.e. made to fail by executing it with the specified volume. It is used in MgvOfferTaking.cleanByImpersonation.

85
86
87
88
89
90
91
92
  struct CleanTarget {
    uint offerId;
    Tick tick;
    uint gasreq;
    uint takerWants;
  }
}

Events

The events emitted are listed here:

95
interface HasMgvEvents {

Events in solidity is a hard thing to do in a optimal way. If you look at it as a purely gas efficient issue, you want to emit as few events as possible and with as few fields as possible. But as events also has to be usable for an off chain user, then doing this is not always the best solution.

We tried to list the main forces that drive which events and data to emit:

  1. Use as little gas as possible.
  2. An indexer should be able to keep track of the state of Mangrove.
  3. Doing RPC calls directly, should be able to find offers and other information based on offer list, maker, taker, offer id, etc.

These forces are in conflict and it is impossible to find a solution that is optimal for all three. The following events are therefore an attempt to balance the trade-offs.


        

Mangrove Creation

Emitted at the creation of the new Mangrove contract

112
113
  event NewMgv();

Mangrove adds or removes wei from maker’s account


        
  • Credit event occurs when an offer is removed from Mangrove or when the fund function is called This is emitted when a user’s account on Mangrove is credited with some native funds, to be used as provision for offers.

It emits the maker’s address and the amount credited

The maker address is indexed so that we can filter on it when doing RPC calls.

These are the scenarios where it can happen:

  • Fund Mangrove directly
  • Fund Mangrove when posting an offer
  • When updating an offer
    • Funding Mangrove or
    • the updated offer needs less provision, than it already has. Meaning the user gets credited the difference.
  • When retracting an offer and deprovisioning it. Meaning the user gets credited the provision that was locked by the offer.
  • When an offer fails. The remaining provision gets credited back to the maker

A challenge for an indexer is to know how much provision each offer locks. With the current events, an indexer is going to have to know the liveness, gasreq and gasprice of the offer. If the offer is not live, then also know if it has been deprovisioned. And know what the gasbase of the offer list was when the offer was posted. With this information an indexer can calculate the exact provision locked by the offer.

The indexer also cannot deduce what scenario the credit event happened. E.g., we don’t know if the credit event happened because an offer failed or because the user simply funded Mangrove.

137
  event Credit(address indexed maker, uint amount);
  • Debit event occurs when an offer is posted or when the withdraw function is called This is emitted when a user’s account on Mangrove is debited with some native funds.

It emits the maker’s address and the amount debited.

The maker address is indexed so that we can filter on it when doing RPC calls.

These are the scenarios where it can happen:

  • Withdraw funds from Mangrove directly
  • When posting an offer. The user gets debited the provision that the offer locks.
  • When updating an offer and it requires more provision that it already has. Meaning the user gets debited the difference.

Same challenges as for Credit

154
155
  event Debit(address indexed maker, uint amount);

Mangrove reconfiguration


        

This event is emitted when an offer list is activated or deactivated. Meaning one half of a market is opened.

It emits the olKeyHash and the boolean value. By emitting this, an indexer will be able to keep track of what offer lists are active and what their hash is.

The olKeyHash and both token addresses are indexed, so that we can filter on it when doing RPC calls.

164
165
166
167
  event SetActive(
    bytes32 indexed olKeyHash, address indexed outbound_tkn, address indexed inbound_tkn, uint tickSpacing, bool value
  );

This event is emitted when the fee of an offer list is changed.

It emits the olKeyHash and the value. By emitting this, an indexer will be able to keep track of what fee each offer list has.

The olKeyHash is indexed, so that we can filter on it when doing RPC calls.

175
176
177
  event SetFee(bytes32 indexed olKeyHash, uint value);

This event is emitted when the gasbase of an offer list is changed.

It emits the olKeyHash and the offer_gasbase. By emitting this, an indexer will be able to keep track of what gasbase each offer list has.

The olKeyHash is indexed, so that we can filter on it when doing RPC calls.

185
186
  event SetGasbase(bytes32 indexed olKeyHash, uint offer_gasbase);

This event is emitted when the governance of Mangrove is set.

It emits the governance address. By emitting this, an indexer will be able to keep track of what governance address Mangrove has.

No fields are indexed as there is no need for RPC calls to filter on this.

194
195
  event SetGovernance(address value);

This event is emitted when the monitor address of Mangrove is set. Be aware that the address for Monitor is also the address for the oracle.

It emits the monitor / oracle address. By emitting this, an indexer will be able to keep track of what monitor/oracle address Mangrove use.

No fields are indexed as there is no need for RPC calls to filter on this.

203
204
  event SetMonitor(address value);

This event is emitted when the configuration for whether to use an oracle or not is set.

It emits the useOracle, which is a boolean value, that controls whether or not Mangrove reads its gasprice and density from an oracle or uses its own local values. By emitting this, an indexer will be able to keep track of if Mangrove is using an oracle.

No fields are indexed as there is no need for RPC calls to filter on this.

212
213
  event SetUseOracle(bool value);

This event is emitted when the configuration for notify on Mangrove is set.

It emits a boolean value, to tell whether or not notify is active. By emitting this, an indexer will be able to keep track of whether or not Mangrove notifies the Monitor/Oracle when and offer is taken, either successfully or not.

No fields are indexed as there is no need for RPC calls to filter on this.

221
222
  event SetNotify(bool value);

This event is emitted when the gasmax of Mangrove is set.

It emits the gasmax. By emitting this, an indexer will be able to keep track of what gasmax Mangrove has. Read more about Mangroves gasmax on docs.mangrove.exchange

No fields are indexed as there is no need for RPC calls to filter on this.

230
231
  event SetGasmax(uint value);

This event is emitted when the density of an offer list is changed.

It emits the olKeyHash and the density. By emitting this, an indexer will be able to keep track of what density each offer list has.

The olKeyHash is indexed, so that we can filter on it when doing RPC calls.

239
240
  event SetDensity96X32(bytes32 indexed olKeyHash, uint value);

This event is emitted when the max recursion depth of Mangrove is set.

It emits the max depth value. By emitting this, an indexer will be able to keep track of what max recursion depth Mangrove has. Read more about Mangroves max recursion depth on docs.mangrove.exchange

246
247
248
  event SetMaxRecursionDepth(uint value);

This event is emitted when the max gasreq for failing offers of Mangrove is set.

It emits the max gasreq for failing offers value. By emitting this, an indexer will be able to keep track of what max gasreq for failing offers Mangrove has. Read more about Mangroves max gasreq for failing offers on docs.mangrove.exchange

254
255
  event SetMaxGasreqForFailingOffers(uint value);

This event is emitted when the gasprice of Mangrove is set.

It emits the gasprice. By emitting this, an indexer will be able to keep track of what gasprice Mangrove has. Read more about Mangroves gasprice on docs.mangrove.exchange

No fields are indexed as there is no need for RPC calls to filter on this.

263
  event SetGasprice(uint value);

Clean order execution


        

This event is emitted when a user tries to clean offers on Mangrove, using the build in clean functionality.

It emits the olKeyHash, the taker address and offersToBeCleaned, which is the number of offers that should be cleaned. By emitting this event, an indexer can save what offer list the user is trying to clean and what taker is being used. This way it can keep a context for the following events being emitted (Just like OrderStart). The offersToBeCleaned is emitted so that an indexer can keep track of how many offers the user tried to clean. Combining this with the amount of OfferFail events emitted, then an indexer can know how many offers the user actually managed to clean. This could be used for analytics.

The fields olKeyHash and taker are indexed, so that we can filter on them when doing RPC calls.

274
275
  event CleanStart(bytes32 indexed olKeyHash, address indexed taker, uint offersToBeCleaned);

This event is emitted when a Clean operation is completed.

It does not emit any fields. This event is only needed in order to know that the clean operation is completed. This way an indexer can know exactly in what context we are in. It could emit the total bounty received, but in order to know who got the bounty, an indexer would still be needed or we would have to emit the taker address as well, in order for an RPC call to find the data. But an indexer would still be able to find this info, by collecting all the previous events. So we do not emit the bounty.

283
284
  event CleanComplete();

Market order execution


        

This event is emitted when a market order is started on Mangrove.

It emits the olKeyHash, the taker, the maxTick, fillVolume and fillWants.

The fields olKeyHash and taker are indexed, so that we can filter on them when doing RPC calls.

By emitting this an indexer can keep track of what context the current market order is in. E.g. if a user starts a market order and one of the offers taken also starts a market order, then we can in an indexer have a stack of started market orders and thereby know exactly what offer list the order is running on and the taker.

By emitting maxTick, fillVolume and fillWants, we can now also know how much of the market order was filled and if it matches the price given. See OrderComplete for more.

298
299
  event OrderStart(bytes32 indexed olKeyHash, address indexed taker, Tick maxTick, uint fillVolume, bool fillWants);

This event is emitted when a market order is finished.

It emits olKeyHash, the taker and the total fee paid. We need this event, in order to know that a market order is completed. This way an indexer can know exactly in what context we are in. The total fee paid for that market order, is needed, as we do not emit this any other places. The fields olKeyHash and taker is not needed for an indexer, but they are emitted and indexed in order for RPC calls to filter and find the fee.

306
307
  event OrderComplete(bytes32 indexed olKeyHash, address indexed taker, uint fee);

Offer execution


        

This event is emitted when an offer is successfully taken. Meaning both maker and taker has gotten their funds.

It emits the olKeyHash, taker address, the offerId and the takerWants and takerGives that the offer was taken at. Just as for OfferFail, the olKeyHash and taker are not needed for an indexer to work, but are needed for RPC calls. olKeyHash taker and id are indexed so that we can filter on them when doing RPC calls. As maker can be a strategy and not the actual owner, then we chose not to emit it here and to mark the field id indexed, the strat should emit the relation between maker and offerId. This way you can still by RPC calls find the relevant offer successes So they are emitted and indexed for that reason. Just like OfferFail this event is emitted during posthook end, so it is emitted in reverse order compared to what order they are taken. This event could be emitted at offer execution, but for consistency we emit it at posthook end.

If the posthook of the offer fails. Then we emit OfferSuccessWithPosthookData instead of just OfferSuccess. This event has one extra field, which is the reason for the posthook failure. By emitting the posthook data, an indexer can keep track of the reason posthook fails, this could for example be used for analytics.

By emitting offerId, wants and gives, an indexer can keep track of whether the offer was partially or fully taken, by looking at the what the offer was posted at.

324
325
326
327
328
329
330
331
332
333
334
335
336
  event OfferSuccess(
    bytes32 indexed olKeyHash, address indexed taker, uint indexed id, uint takerWants, uint takerGives
  );

  event OfferSuccessWithPosthookData(
    bytes32 indexed olKeyHash,
    address indexed taker,
    uint indexed id,
    uint takerWants,
    uint takerGives,
    bytes32 posthookData
  );

This event is emitted when an offer fails, because of a maker error.

It emits olKeyHash, taker, the offerId, the offers takerWants, takerGives, penalty and the reason for failure. olKeyHash and taker are all fields that we do not need, in order for an indexer to work, as an indexer will be able the get that info from the former OrderStart and OfferWrite events. But in order for RPC call to filter on this, we need to emit them. olKeyHash taker and id are indexed so that we can filter on them when doing RPC calls. As maker can be a strategy and not the actual owner, then we chose to not emit it here and to mark the field id indexed, the strat should emit the relation between maker and offerId. This way you can still by RPC calls find the relevant offer successes

If the posthook of the offer fails. Then we emit OfferFailWithPosthookData instead of just OfferFail. This event has one extra field, which is the reason for the posthook failure. By emitting the posthook data, an indexer can keep track of the reason posthook fails, this could for example be used for analytics.

This event is emitted during posthook end, we wait to emit this event to the end, because we need the information of penalty, which is only available at the end of the posthook. This means that OfferFail events are emitted in reverse order, compared to what order they are taken. This is due to the way we handle posthooks. The same goes for OfferSuccess.

By emitting this event, an indexer can keep track of, if an offer failed and thereby if the offer is live. By emitting the takerWants and takerGives that the offer was taken with, then an indexer can keep track of these amounts, which could be useful for e.g. strategy manager, to know if their offers fail at a certain amount.

355
356
357
358
359
360
361
  event OfferFail(
    bytes32 indexed olKeyHash,
    address indexed taker,
    uint indexed id,
    uint takerWants,
    uint takerGives,
    uint penalty,

mgvData may only be "mgv/makerRevert", "mgv/makerTransferFail" or "mgv/makerReceiveFail"

363
364
365
366
367
368
369
370
371
372
    bytes32 mgvData
  );

  event OfferFailWithPosthookData(
    bytes32 indexed olKeyHash,
    address indexed taker,
    uint indexed id,
    uint takerWants,
    uint takerGives,
    uint penalty,

mgvData may only be "mgv/makerRevert", "mgv/makerTransferFail" or "mgv/makerReceiveFail"

374
375
376
377
    bytes32 mgvData,
    bytes32 posthookData
  );

After permit and approve

This is emitted when a user permits another address to use a certain amount of its funds to do market orders, or when a user revokes another address to use a certain amount of its funds.

Approvals are based on the pair of outbound and inbound token. Be aware that it is not offer list bases, as an offer list also holds the tick spacing.

We emit outbound token, inbound token, owner, msg.sender (spender), value. Where owner is the one who owns the funds, spender is the one who is allowed to use the funds and value is the amount of funds that is allowed to be used.

Outbound, inbound and owner is indexed, this way one can filter on these fields when doing RPC calls. If we could somehow combine outbound and inbound into one field, then we could also index the spender.

388
389
390
391
  event Approval(
    address indexed outbound_tkn, address indexed inbound_tkn, address indexed owner, address spender, uint value
  );

Mangrove closure

393
394
  event Kill();

An offer was created or updated.

This event is emitted when an offer is posted on Mangrove.

It emits the olKeyHash, the maker address, the tick, the gives, the gasprice, gasreq and the offers id.

By emitting the olKeyHash and id, an indexer will be able to keep track of each offer, because offer list and id together create a unique id for the offer. By emitting the maker address, we are able to keep track of who has posted what offer. The tick and gives, enables an indexer to know exactly how much an offer is willing to give and at what price, this could for example be used to calculate a return. The gasprice and gasreq, enables an indexer to calculate how much provision is locked by the offer, see Credit for more information.

The fields olKeyHash and maker are indexed, so that we can filter on them when doing RPC calls.

404
405
406
407
  event OfferWrite(
    bytes32 indexed olKeyHash, address indexed maker, int tick, uint gives, uint gasprice, uint gasreq, uint id
  );

This event is emitted when a user retracts his offer.

It emits the olKeyHash, the maker address, offerId and whether or not the user chose to deprovision the offer.

By emitting this event an indexer knows whether or not an offer is live. And whether or not an offer is deprovisioned. This is important because we need to know this, when we try to calculate how much an offer locks in provision. See the description of Credit for more info.

The maker is not needed for an indexer to work, but is needed for RPC calls, so it is emitted and indexed for that reason. The olKeyHash is only indexed because it is needed for RPC calls.

417
418
419
  event OfferRetract(bytes32 indexed olKeyHash, address indexed maker, uint id, bool deprovision);
}

IMaker interface

421
interface IMaker {

Called upon offer execution.

  • If the call throws, Mangrove will not try to transfer funds and the first 32 bytes of revert reason are passed to makerPosthook as makerData
  • If the call returns normally, returnData is passed to makerPosthook as makerData and Mangrove will attempt to transfer the funds.
426
427
  function makerExecute(MgvLib.SingleOrder calldata order) external returns (bytes32);

Called after all offers of an order have been executed. Posthook of the last executed order is called first and full reentrancy into Mangrove is enabled at this time. order recalls key arguments of the order that was processed and result recalls important information for updating the current offer. (see above)

429
430
431
  function makerPosthook(MgvLib.SingleOrder calldata order, MgvLib.OrderResult calldata result) external;
}

Monitor interface

If enabled, the monitor receives notification after each offer execution and is read for each offer list’s gasprice and density.

434
435
436
437
438
439
440
interface IMgvMonitor {
  function notifySuccess(MgvLib.SingleOrder calldata sor, address taker) external;

  function notifyFail(MgvLib.SingleOrder calldata sor, address taker) external;

  function read(OLKey memory olKey) external view returns (uint gasprice, Density density);
}
MgvCommon.sol

        

MgvCommon and its descendants describe an orderbook-based exchange (“Mangrove”) where market makers do not have to provision their offer. In a nutshell: each offer created by a maker specifies an address (maker) to call upon offer execution by a taker. When an offer is executed, Mangrove transfers the amount to be paid by the taker to the maker, calls the maker, attempts to transfer the amount promised by the maker to the taker, and reverts if it cannot.

5
6
7
8
9
pragma solidity ^0.8.10;

import "@mgv/src/core/MgvLib.sol";

MgvCommon contains state variables used everywhere in the operation of Mangrove and related gatekeeping functions. The main Mangrove contract inherits from MgvCommon, and the auxiliary MgvAppendix contract inherits from MgvCommon as well. This way, when Mangrove delegatecalls to MgvAppendix, the storage slots match.

11
contract MgvCommon is HasMgvEvents {

State variables


        

        

The governance address. Governance is the only address that can configure parameters.

16
17
  address internal _governance;

Global mgv configuration, encoded in a 256 bits word. The information encoded is detailed in structs.js.

19
20
  Global internal internal_global;

OfferData contains all the information related to an offer. Each field contains packed information such as the volumes and the gas required. See structs.js for more information.

22
23
24
25
26
  struct OfferData {
    Offer offer;
    OfferDetail detail;
  }

OfferList contains all data specific to an offer list.

28
  struct OfferList {

local is the Mangrove configuration specific to the outbound,inbound,tickSpacing offer list. It contains e.g. the minimum offer density. It contains packed information, see structs.js for more.

30
    Local local;

level1s maps a level1 index to a (dirty) field. Each field holds 64 bits marking the (non)empty state of 64 level2 fields.

32
    mapping(int => DirtyField) level1s;

level2s maps a level2 index to a (dirty) field. Each field holds 64 bits marking the (non)empty state of 64 level3 fields.

34
    mapping(int => DirtyField) level2s;

level3s maps a level3 index to a (dirty) field. Each field holds 64 bits marking the (non)empty state of 64 leaves.

36
    mapping(int => DirtyField) level3s;

leafs (intentionally not leaves for clarity) maps a leaf index to a leaf. Each leaf holds the first&last offer id of 4 bins.

38
    mapping(int => DirtyLeaf) leafs;

OfferData maps an offer id to a struct that holds the two storage words where the packed offer information resides. For more information see Offer and OfferDetail.

40
41
42
    mapping(uint => OfferData) offerData;
  }

OLKeys (see MgvLib.sol) are hashed to a bytes32 OLKey identifier, which get mapped to an OfferList struct. Having a single mapping instead of one mapping per field in OfferList means we can pass around a storage reference to that struct.

44
  mapping(bytes32 => OfferList) internal offerLists;

For convenience, and to enable future functions that access offer lists by directly supplying an OLKey identifier, Mangrove maintains an inverse id -> key mapping.

46
47
  mapping(bytes32 => OLKey) internal _olKeys;

Makers provision their possible penalties in the balanceOf mapping.

Offers specify the amount of gas they require for successful execution (gasreq). To minimize book spamming, market makers must provision an amount of native tokens that depends on their gasreq and on the offer list’s offer_gasbase. This provision is deducted from their balanceOf. If an offer fails, part of that provision is given to the taker as a penalty. The exact amount depends on the gas used by the offer before failing and during the execution of its posthook.

Mangrove keeps track of available balances in the balanceOf map, which is decremented every time a maker creates a new offer, and may be modified on offer updates/cancellations/takings.

54
55
  mapping(address maker => uint balance) internal _balanceOf;

Gatekeeping

Gatekeeping functions are safety checks called in various places.


        

unlockedOfferListOnly protects modifying the offer list while an order is in progress. Since external contracts are called during orders, allowing reentrancy would, for instance, let a market maker replace offers currently on the book with worse ones. Note that the external contracts will be called again after the order is complete, this time without any lock on the offer list.

63
64
65
66
  function unlockedOfferListOnly(Local local) internal pure {
    require(!local.lock(), "mgv/reentrancyLocked");
  }

In case of emergency, Mangrove can be killed. It cannot be resurrected. When a Mangrove is dead, the following operations are disabled :

  • Executing an offer
  • Sending ETH to Mangrove the normal way. Usual shenanigans are possible.
  • Creating a new offer
73
74
75
76
  function liveMgvOnly(Global _global) internal pure {
    require(!_global.dead(), "mgv/dead");
  }

When Mangrove is deployed, all offer lists are inactive by default (since locals[outbound_tkn][inbound_tkn] is 0 by default). Offers on inactive offer lists cannot be taken or created. They can be updated and retracted.

78
79
80
81
82
  function activeOfferListOnly(Global _global, Local _local) internal pure {
    liveMgvOnly(_global);
    require(_local.active(), "mgv/inactive");
  }

_config is the lower-level variant which opportunistically returns a pointer to the storage offer list induced by (outbound_tkn,inbound_tkn,tickSpacing).

84
85
86
87
88
89
90
91
92
93
94
  function _config(OLKey memory olKey)
    internal
    view
    returns (Global _global, Local _local, OfferList storage offerList)
  {
    unchecked {
      offerList = offerLists[olKey.hash()];
      _global = internal_global;
      _local = offerList.local;
      if (_global.useOracle()) {
        (uint gasprice, Density density) = IMgvMonitor(_global.monitor()).read(olKey);

Gas gasprice can be ignored by making sure the oracle’s set gasprice does not pass the check below.

96
97
98
        if (GlobalLib.gasprice_check(gasprice)) {
          _global = _global.gasprice(gasprice);
        }

Oracle density can be ignored by making sure the oracle’s set density does not pass the checks below.


        

Checking the size of density is necessary to prevent overflow when density is used in calculations.

102
103
104
105
106
107
108
        if (LocalLib.density_check(density)) {
          _local = _local.density(density);
        }
      }
    }
  }

Token transfer functions


        

transferTokenFrom is adapted from existing code and in particular avoids the “no return value” bug. It never throws and returns true iff the transfer was successful according to tokenAddress.

Note that any spurious exception due to an error in Mangrove code will be falsely blamed on from.

115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
  function transferTokenFrom(address tokenAddress, address from, address to, uint value) internal returns (bool) {
    unchecked {
      bytes memory cd = abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value);
      (bool noRevert, bytes memory data) = tokenAddress.call(cd);
      return (noRevert && (data.length == 0 || abi.decode(data, (bool))));
    }
  }

  function transferToken(address tokenAddress, address to, uint value) internal returns (bool) {
    unchecked {
      bytes memory cd = abi.encodeWithSelector(IERC20.transfer.selector, to, value);
      (bool noRevert, bytes memory data) = tokenAddress.call(cd);
      return (noRevert && (data.length == 0 || abi.decode(data, (bool))));
    }
  }

Permit-related functionality


        

Takers may provide allowances on specific offer lists, so other addresses can execute orders in their name. Allowance may be set using the usual approve function, or through an EIP712 permit.

The mapping is outbound_tkn => inbound_tkn => owner => spender => allowance. There is no tickSpacing specified since we assume the natural semantics of a permit are “spender has the right to trade token A against token B at any tickSpacing”.

137
138
139
140
  mapping(
    address outbound_tkn
      => mapping(address inbound_tkn => mapping(address owner => mapping(address spender => uint allowance)))
  ) internal _allowance;

Storing nonces avoids replay attacks.

142
143
  mapping(address owner => uint nonce) internal _nonces;

Following EIP712, structured data signing has keccak256("Permit(address outbound_tkn,address inbound_tkn,address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") in its prefix.

145
  bytes32 internal constant _PERMIT_TYPEHASH = 0xf0ea0a7146fb6eedb561d97b593d57d9b7df3c94d689372dc01302e5780248f4;

If you are looking for DOMAIN_SEPARATOR, it is defined in MgvOfferTakingWithPermit.

If you define an immutable C, you must initialize it in the constructor of C, unless you use solidity >= 0.8.21. Then you can initialize it in the constructor of a contract that inherits from C. At the time of the writing 0.8.21 is too recent so we move DOMAIN_SEPARATOR to MgvOfferTakingWithPermit, which has a constructor and also initializes DOMAIN_SEPARATOR.

149
}
MgvHasOffers.sol
2
3
4
5
6
pragma solidity ^0.8.10;

import "@mgv/src/core/MgvLib.sol";
import {MgvCommon} from "./MgvCommon.sol";

MgvHasOffers contains the state variables and functions common to both market-maker operations and market-taker operations. Mostly: storing offers, removing them, updating market makers’ provisions.

8
contract MgvHasOffers is MgvCommon {

Provision debit/credit utility functions


        

balanceOf is in wei of ETH.

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  function debitWei(address maker, uint amount) internal {
    unchecked {
      uint makerBalance = _balanceOf[maker];
      require(makerBalance >= amount, "mgv/insufficientProvision");
      _balanceOf[maker] = makerBalance - amount;
      emit Debit(maker, amount);
    }
  }

  function creditWei(address maker, uint amount) internal {
    unchecked {
      _balanceOf[maker] += amount;
      emit Credit(maker, amount);
    }
  }

Misc. low-level functions


        

Offer deletion


        

When an offer is deleted, it is marked as such by setting gives to 0 and leaving other fields intact. Note that provision accounting in Mangrove aims to minimize writes. Each maker funds Mangrove to increase its balance. When an offer is created/updated, we compute how much should be reserved to pay for possible penalties. That amount can always be recomputed with

offerDetail.gasprice * 1e6 * (offerDetail.gasreq + offerDetail.offer_gasbase)

The balance is updated to reflect the remaining available ethers.

Now, when an offer is deleted, the offer can stay provisioned, or be deprovisioned. In the latter case, we set gasprice to 0, which induces a provision of 0. All code calling dirtyDeleteOffer with deprovision set to true must be careful to correctly account for where that provision is going (back to the maker’s balanceOf, or sent to a taker as compensation).

40
41
42
43
44
45
46
47
48
49
50
51
52
  function dirtyDeleteOffer(OfferData storage offerData, Offer offer, OfferDetail offerDetail, bool deprovision)
    internal
  {
    unchecked {
      offer = offer.gives(0);
      if (deprovision) {
        offerDetail = offerDetail.gasprice(0);
      }
      offerData.offer = offer;
      offerData.detail = offerDetail;
    }
  }

Removing an offer from the tick tree


        

To remove an offer from the tick tree, we do the following:

  • Remove the offer from the bin
  • If the bin is now empty, mark it as empty in its leaf
  • If the leaf is now empty, mark it as empty in its level3
  • If the level3 is now empty, mark it as empty in its level2
    • If the level2 is now empty, mark it as empty in its level1
      • If the level1 is now empty, mark it as empty in the root
        • If the root is now empty, return
  • Once we are done marking leaves/fields as empty, if the removed offer was the best there is at least one remaining offer, and if the caller requested it by setting shouldUpdateBranch=true, go down the tree and find the new best offer.

Each step must take into account the fact that the branch of the best offer is cached in local and that loading a new best offer requires caching a different branch in local.

The reason why the caller might set shouldUpdateBranch=false is that it is itself about to insert an offer better than the current best offer. In that case, it will take care of caching the branch of the new best offer after calling dislodgeOffer.

The new updated local is returned, along with whether the branch in local ended up being updated.

71
72
73
74
75
76
77
78
79
80
81
82
  function dislodgeOffer(
    OfferList storage offerList,
    uint tickSpacing,
    Offer offer,
    Local local,
    Bin bestBin,
    bool shouldUpdateBranch
  ) internal returns (Local, bool) {
    unchecked {
      Leaf leaf;
      Bin offerBin = offer.bin(tickSpacing);
      {

save stack space

84
85
        uint prevId = offer.prev();
        uint nextId = offer.next();

Only load offer’s leaf if the offer leaf must be updated. If offer is in the middle of a bin’s linked list, the bin’s first&last offers will not change, so the leaf does not have to be loaded.

87
88
89
90
        if (prevId == 0 || nextId == 0) {
          leaf = offerList.leafs[offerBin.leafIndex()].clean();
        }

Update the forward pointer to offer (either in a leaf or in an offer’s next pointer)

92
        if (prevId == 0) {

If offer was its bin’s first offer, the new first offer is nextId (may be 0).

94
95
          leaf = leaf.setBinFirst(offerBin, nextId);
        } else {

Otherwise, the next pointer of offer’s prev becomes nextId.

97
98
99
100
          OfferData storage prevOfferData = offerList.offerData[prevId];
          prevOfferData.offer = prevOfferData.offer.next(nextId);
        }

Update the backward pointer to offer (either in a leaf or in an offer’s prev pointer)

102
        if (nextId == 0) {

If offer was its bin’s last offer, the new last offer is prevId (may be 0).

104
105
          leaf = leaf.setBinLast(offerBin, prevId);
        } else {

Otherwise, the prev pointer of offer’s next becomes prevId.

107
108
109
110
          OfferData storage nextOfferData = offerList.offerData[nextId];
          nextOfferData.offer = nextOfferData.offer.prev(prevId);
        }

If previous pointer updates only updated offer pointers, offer’s leaf has not changed and we can return early

112
113
114
115
        if (prevId != 0 && nextId != 0) {
          return (local, false);
        }

Only plan on updating the branch if the caller requested it and if offer is the best.

117
118
119
        shouldUpdateBranch = shouldUpdateBranch && prevId == 0 && !bestBin.strictlyBetter(offerBin);
      }

Since offer was the first or last of its bin, its leaf must be updated

121
      offerList.leafs[offerBin.leafIndex()] = leaf.dirty();

If the leaf is now empty, flip off its bit in offer’s level3

123
      if (leaf.isEmpty()) {

We reuse the same index variable for all 3 level indices and for the leaf index.

125
        int index = offerBin.level3Index();

We reuse the same field variable for all 3 level indices.

127
        Field field;

Local cache management conditional

129
        if (index == bestBin.level3Index()) {

If offer’s level3 is cached, update it in local.

131
132
          field = local.level3().flipBitAtLevel3(offerBin);
          local = local.level3(field);

If shouldUpdateBranch=true and the level3 is now empty, another level3 may take its place. We immediately evict the empty value of level3 to storage. (If the level3 is not empty, then the new best offer will use that level3, so no eviction necessary).

134
          if (shouldUpdateBranch && field.isEmpty()) {

Clean/dirty management. ifs are nested to avoid a useless SLOAD.

136
137
138
139
140
            if (!offerList.level3s[index].eq(DirtyFieldLib.CLEAN_EMPTY)) {
              offerList.level3s[index] = DirtyFieldLib.DIRTY_EMPTY;
            }
          }
        } else {

If offer’s level3 is not cached, update it in storage.

142
143
144
          field = offerList.level3s[index].clean().flipBitAtLevel3(offerBin);
          offerList.level3s[index] = field.dirty();
        }

If offer’s level3 is now empty, flip off its bit in the removed offer’s level2

146
147
        if (field.isEmpty()) {
          index = offerBin.level2Index();

Local cache management conditional

149
          if (index == bestBin.level2Index()) {

If offer’s level2 is cached, update it in local.

151
152
            field = local.level2().flipBitAtLevel2(offerBin);
            local = local.level2(field);

If shouldUpdateBranch=true and the level2 is now empty, another level2 may take its place. We immediately evict the empty value of level2 to storage. (If the level2 is not empty, then the new best offer will use that level2, so no eviction necessary).

154
            if (shouldUpdateBranch && field.isEmpty()) {

Clean/dirty management. Ifs are nested to avoid a useless SLOAD.

156
157
158
159
160
              if (!offerList.level2s[index].eq(DirtyFieldLib.CLEAN_EMPTY)) {
                offerList.level2s[index] = DirtyFieldLib.DIRTY_EMPTY;
              }
            }
          } else {

If offer’s level2 is not cached, update it in storage.

162
163
164
            field = offerList.level2s[index].clean().flipBitAtLevel2(offerBin);
            offerList.level2s[index] = field.dirty();
          }

If offer’s level2 is now empty, flip off its bit in offer’s level1

166
167
          if (field.isEmpty()) {
            index = offerBin.level1Index();

Local cache management conditional

169
            if (index == bestBin.level1Index()) {

If offer’s level1 is cached, update it in local.

171
172
              field = local.level1().flipBitAtLevel1(offerBin);
              local = local.level1(field);

If shouldUpdateBranch=true and the level1 is now empty, another level1 may take its place. We immediately evict the empty value of level1 to storage. (If the level1 is not empty, then the new best offer will use that level1, so no eviction necessary).

174
              if (shouldUpdateBranch && field.isEmpty()) {

Unlike with level3 and level2, level1 cannot be CLEAN_EMPTY (it gets dirtied in activate)

176
177
178
                offerList.level1s[index] = field.dirty();
              }
            } else {

If offer’s level1 is not cached, update it in storage.

180
181
182
              field = offerList.level1s[index].clean().flipBitAtLevel1(offerBin);
              offerList.level1s[index] = field.dirty();
            }

If offer’s level1 is now empty, flip off its bit in the root field.

184
            if (field.isEmpty()) {

root is always in local

186
187
188
              field = local.root().flipBitAtRoot(offerBin);
              local = local.root(field);

If the root is now empty, return the updated local

190
191
192
              if (field.isEmpty()) {
                return (local, shouldUpdateBranch);
              }

Since offer’s level1 became empty, if we have to update the branch, load the level1 containing the new best offer in local.

194
195
196
197
198
199
              if (shouldUpdateBranch) {
                index = field.firstLevel1Index();
                field = offerList.level1s[index].clean();
                local = local.level1(field);
              }
            }

Since offer’s level2 became empty, if we have to update the branch, load the level2 containing the new best offer in local.

201
202
203
204
205
206
            if (shouldUpdateBranch) {
              index = field.firstLevel2Index(index);
              field = offerList.level2s[index].clean();
              local = local.level2(field);
            }
          }

Since offer’s level3 became empty, if we have to update the branch, load the level3 containing the new best offer in local.

208
209
210
211
212
213
          if (shouldUpdateBranch) {
            index = field.firstLevel3Index(index);
            field = offerList.level3s[index].clean();
            local = local.level3(field);
          }
        }

Since offer’s leaf became empty, if we have to update the branch, load the leaf containing the new best offer in leaf (so that we can find the position of the first non-empty bin in the leaf).

215
216
217
218
        if (shouldUpdateBranch) {
          leaf = offerList.leafs[field.firstLeafIndex(index)].clean();
        }
      }

Since offer’s bin became empty if we have to update the branch, load the position of the first non-empty bin in the current leaf in local.

220
221
222
223
224
225
226
      if (shouldUpdateBranch) {
        local = local.binPosInLeaf(leaf.bestNonEmptyBinPos());
      }
    }
    return (local, shouldUpdateBranch);
  }
}
MgvOfferMaking.sol
2
3
4
5
6
pragma solidity ^0.8.10;

import "@mgv/src/core/MgvLib.sol";
import {MgvHasOffers} from "./MgvHasOffers.sol";

MgvOfferMaking contains market-making-related functions.

8
contract MgvOfferMaking is MgvHasOffers {

Public Maker operations

New Offer


        

        

In Mangrove, makers and takers call separate functions. Market makers call newOffer to fill the book, and takers call functions such as marketOrder to consume it.


        

        

The following structs holds offer creation/update parameters in memory. This frees up stack space for local variables.

17
18
19
20
21
22
23
24
  struct OfferPack {
    OLKey olKey;
    uint gives;
    uint id;
    uint gasreq;
    uint gasprice;
    Global global;
    Local local;

used on update only

26
27
28
    Offer oldOffer;
  }

The function newOffer is for market makers only; no match with the existing offer list is done. The maker specifies how much olKey.outbound_tkn token it gives and at which tick (which induces the price 1.0001^tick). The actual tick of the offer will be the smallest tick offerTick > tick that satisfies offerTick % tickSpacing == 0.

It also specify with gasreq how much gas should be given when executing their offer.

gasprice indicates an upper bound on the gasprice (in Mwei) at which the maker is ready to be penalised if their offer fails. Any value below Mangrove’s internal gasprice configuration value will be ignored.

gasreq, together with gasprice, will contribute to determining the penalty provision set aside by Mangrove from the market maker’s balanceOf balance.

An offer cannot be inserted in a closed market, nor when a reentrancy lock for outbound_tkn,inbound_tkn is on.

No more than 23212^{32}-1 offers can ever be created for one (outbound,inbound, tickSpacing) offer list.

The actual contents of the function is in writeOffer, which is called by both newOffer and updateOffer.

42
43
44
45
46
47
48
  function newOfferByTick(OLKey memory olKey, Tick tick, uint gives, uint gasreq, uint gasprice)
    public
    payable
    returns (uint offerId)
  {
    unchecked {

In preparation for calling writeOffer, we read the outbound_tkn,inbound_tkn, tickSpacing offer list configuration, check for reentrancy and offer list liveness, fill the OfferPack struct and increment the offer list’s last.

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
      OfferPack memory ofp;
      OfferList storage offerList;
      (ofp.global, ofp.local, offerList) = _config(olKey);
      unlockedOfferListOnly(ofp.local);
      activeOfferListOnly(ofp.global, ofp.local);
      if (msg.value > 0) {
        creditWei(msg.sender, msg.value);
      }
      ofp.id = 1 + ofp.local.last();
      require(uint32(ofp.id) == ofp.id, "mgv/offerIdOverflow");

      ofp.local = ofp.local.last(ofp.id);

      ofp.olKey = olKey;
      ofp.gives = gives;
      ofp.gasreq = gasreq;
      ofp.gasprice = gasprice;

The last parameter to writeOffer indicates that we are creating a new offer, not updating an existing one.

69
70
      writeOffer(offerList, ofp, tick, false);

Since we locally modified a field of the local configuration (last), we save the change to storage. Note that writeOffer may have further modified the local configuration by updating the currently cached tick tree branch.

72
73
74
75
76
      offerList.local = ofp.local;
      return ofp.id;
    }
  }

There is a ByVolume variant where the maker specifies how much inbound_tkn it wants and how much outbound_tkn it gives. Volumes should fit on 127 bits.

80
81
82
83
84
85
86
87
88
89
  function newOfferByVolume(OLKey memory olKey, uint wants, uint gives, uint gasreq, uint gasprice)
    external
    payable
    returns (uint offerId)
  {
    unchecked {
      return newOfferByTick(olKey, TickLib.tickFromVolumes(wants, gives), gives, gasreq, gasprice);
    }
  }

Update Offer


        

        

Very similar to newOffer, updateOffer prepares an OfferPack for writeOffer. Makers should use it for updating live offers, but also to save on gas by reusing old, already consumed offers.

Gas use is minimal when:

  1. The offer does not move in the offer list
  2. The offer does not change its gasreq
  3. The (outbound_tkn,inbound_tkn)'s offer_gasbase has not changed since the offer was last written
  4. gasprice has not changed since the offer was last written
  5. gasprice is greater than Mangrove’s gasprice estimation
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
  function updateOfferByTick(OLKey memory olKey, Tick tick, uint gives, uint gasreq, uint gasprice, uint offerId)
    public
    payable
  {
    unchecked {
      OfferPack memory ofp;
      OfferList storage offerList;
      (ofp.global, ofp.local, offerList) = _config(olKey);
      unlockedOfferListOnly(ofp.local);
      activeOfferListOnly(ofp.global, ofp.local);
      if (msg.value > 0) {
        creditWei(msg.sender, msg.value);
      }
      ofp.olKey = olKey;
      ofp.gives = gives;
      ofp.id = offerId;
      ofp.gasreq = gasreq;
      ofp.gasprice = gasprice;
      ofp.oldOffer = offerList.offerData[offerId].offer;

Save local config

121
      Local oldLocal = ofp.local;

The second argument indicates that we are updating an existing offer, not creating a new one.

123
      writeOffer(offerList, ofp, tick, true);

We saved the current offer list’s local configuration before calling writeOffer, since that function may update it. We now check for any change to the configuration and update it if needed.

125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
      if (!oldLocal.eq(ofp.local)) {
        offerList.local = ofp.local;
      }
    }
  }

  function updateOfferByVolume(OLKey memory olKey, uint wants, uint gives, uint gasreq, uint gasprice, uint offerId)
    external
    payable
  {
    unchecked {
      updateOfferByTick(olKey, TickLib.tickFromVolumes(wants, gives), gives, gasreq, gasprice, offerId);
    }
  }

Retract Offer


        

        

retractOffer takes the offer offerId out of the book. However, deprovision == true also refunds the provision associated with the offer.

143
144
145
146
147
148
149
150
151
  function retractOffer(OLKey memory olKey, uint offerId, bool deprovision) external returns (uint provision) {
    unchecked {
      (, Local local, OfferList storage offerList) = _config(olKey);
      unlockedOfferListOnly(local);
      OfferData storage offerData = offerList.offerData[offerId];
      Offer offer = offerData.offer;
      OfferDetail offerDetail = offerData.detail;
      require(msg.sender == offerDetail.maker(), "mgv/retractOffer/unauthorized");

Here, we are about to un-live an offer, so we start by taking it out of the tick tree. Note that unconditionally calling dislodgeOffer even if the offer is not live would break the offer list since it would connect offers that may have since moved.

153
154
155
      if (offer.isLive()) {
        Local oldLocal = local;
        (local,) = dislodgeOffer(offerList, olKey.tickSpacing, offer, local, local.bestBin(), true);

If calling dislodgeOffer has changed the current best offer, we update local.

157
158
159
160
        if (!oldLocal.eq(local)) {
          offerList.local = local;
        }
      }

Set offer.gives to 0 (which is encodes the fact that the offer is dead). The deprovision argument indicates whether the maker wishes to get their provision back (if true, offer.gasprice will be set to 0 as well).

162
163
      dirtyDeleteOffer(offerData, offer, offerDetail, deprovision);

If the user wants to get their provision back, we compute it from the offer’s gasprice, offer_gasbase and gasreq.

165
166
167
      if (deprovision) {
        provision = 1e6 * offerDetail.gasprice() //gasprice is 0 if offer was deprovisioned
          * (offerDetail.gasreq() + offerDetail.offer_gasbase());

credit balanceOf and log transfer

169
170
171
172
173
174
175
        creditWei(msg.sender, provision);
      }

      emit OfferRetract(olKey.hash(), offerDetail.maker(), offerId, deprovision);
    }
  }

Provisioning

Market makers must have enough provisions for possible penalties. These provisions are in native tokens. Every time a new offer is created or an offer is updated, balanceOf is adjusted to provision the offer’s maximum possible penalty (gasprice * (gasreq + offer_gasbase)).

For instance, if the current balanceOf of a maker is 1 ether and they create an offer that requires a provision of 0.01 ethers, their balanceOf will be reduced to 0.99 ethers. No ethers will move; this is just an internal accounting movement to make sure the maker cannot withdraw the provisioned amounts.


        

        

Fund should be called with a nonzero value (hence the payable modifier). The provision will be given to maker, not msg.sender.

185
186
187
188
189
190
191
192
193
194
195
196
197
198
  function fund(address maker) public payable {
    unchecked {
      (Global _global,,) = _config(OLKey(address(0), address(0), 0));
      liveMgvOnly(_global);
      creditWei(maker, msg.value);
    }
  }

  function fund() external payable {
    unchecked {
      fund(msg.sender);
    }
  }

A transfer with enough gas to Mangrove will increase the caller’s available balanceOf balance. You should send enough gas to execute this function when sending money to Mangrove.

200
201
202
203
204
205
  receive() external payable {
    unchecked {
      fund(msg.sender);
    }
  }

Any provision not currently held to secure an offer’s possible penalty is available for withdrawal.

207
208
  function withdraw(uint amount) external returns (bool noRevert) {
    unchecked {

Since we only ever send money to the caller, we do not need to provide any particular amount of gas, the caller should manage this herself.

210
211
212
213
214
215
      debitWei(msg.sender, amount);
      (noRevert,) = msg.sender.call{value: amount}("");
      require(noRevert, "mgv/withdrawCallRevert");
    }
  }

Low-level Maker functions


        

Write Offer


        

Used by updateOfferBy* and newOfferBy*, this function optionally removes an offer then (re)inserts it in the tick tree. The update argument indicates whether the call comes from updateOfferBy* or newOfferBy*.

221
222
  function writeOffer(OfferList storage offerList, OfferPack memory ofp, Tick insertionTick, bool update) internal {
    unchecked {

gasprice’s floor is Mangrove’s own gasprice estimate, ofp.global.gasprice. We first check that gasprice fits in 26 bits. Otherwise it could be that uint26(gasprice) < global_gasprice < gasprice and the actual value we store is uint26(gasprice) (using pseudocode here since the type uint26 does not exist).

224
225
226
227
228
229
      require(GlobalLib.gasprice_check(ofp.gasprice), "mgv/writeOffer/gasprice/tooBig");

      if (ofp.gasprice < ofp.global.gasprice()) {
        ofp.gasprice = ofp.global.gasprice();
      }
  • Check gasreq below limit. Implies gasreq at most 24 bits wide, which ensures no overflow in computation of provision (see below).
231
      require(ofp.gasreq <= ofp.global.gasmax(), "mgv/writeOffer/gasreq/tooHigh");
  • Make sure gives > 0 – division by 0 would throw in several places otherwise, and isLive relies on it.
233
      require(ofp.gives > 0, "mgv/writeOffer/gives/tooLow");
  • Make sure that the maker is posting a ‘dense enough’ offer: the ratio of outbound_tkn offered per gas consumed must be high enough. The actual gas cost paid by the taker is overapproximated by adding offer_gasbase to gasreq.
235
236
237
238
239
      require(
        ofp.gives >= ofp.local.density().multiply(ofp.gasreq + ofp.local.offer_gasbase()),
        "mgv/writeOffer/density/tooLow"
      );

The following checks are for the maker’s convenience only.

241
242
243
      require(OfferLib.gives_check(ofp.gives), "mgv/writeOffer/gives/tooBig");

      uint tickSpacing = ofp.olKey.tickSpacing;

Derive bin from given tick, then normalize the tick: available ticks in an offer list are those who are equal to 0 modulo tickSpacing.

245
246
247
248
      Bin insertionBin = insertionTick.nearestBin(tickSpacing);
      insertionTick = insertionBin.tick(tickSpacing);
      require(insertionTick.inRange(), "mgv/writeOffer/tick/outOfRange");

Log the write offer event.

250
251
252
253
254
      uint ofrId = ofp.id;
      emit OfferWrite(
        ofp.olKey.hash(), msg.sender, Tick.unwrap(insertionTick), ofp.gives, ofp.gasprice, ofp.gasreq, ofrId
      );

We now write the new offerDetails and remember the previous provision (0 by default, for new offers) to balance out maker’s balanceOf.

256
257
258
259
260
261
262
263
264
      {
        uint oldProvision;
        OfferData storage offerData = offerList.offerData[ofrId];
        OfferDetail offerDetail = offerData.detail;
        if (update) {
          require(msg.sender == offerDetail.maker(), "mgv/updateOffer/unauthorized");
          oldProvision = 1e6 * offerDetail.gasprice() * (offerDetail.gasreq() + offerDetail.offer_gasbase());
        }

If the offer is new, has a new gasprice, gasreq, or if Mangrove’s offer_gasbase configuration parameter has changed, we also update offerDetails.

266
267
268
269
270
271
272
273
274
275
276
277
278
        if (
          !update || offerDetail.gasreq() != ofp.gasreq || offerDetail.gasprice() != ofp.gasprice
            || offerDetail.offer_gasbase() != ofp.local.offer_gasbase()
        ) {
          uint offer_gasbase = ofp.local.offer_gasbase();
          offerData.detail = OfferDetailLib.pack({
            __maker: msg.sender,
            __gasreq: ofp.gasreq,
            __kilo_offer_gasbase: offer_gasbase / 1e3,
            __gasprice: ofp.gasprice
          });
        }

With every change to an offer, a maker may deduct provisions from its balanceOf balance. It may also get provisions back if the updated offer requires fewer provisions than before.

280
281
282
283
284
285
286
287
        uint provision = (ofp.gasreq + ofp.local.offer_gasbase()) * ofp.gasprice * 1e6;
        if (provision > oldProvision) {
          debitWei(msg.sender, provision - oldProvision);
        } else if (provision < oldProvision) {
          creditWei(msg.sender, oldProvision - provision);
        }
      }

We now cache the current best bin in a stack variable. Since the current best bin is derived from ofp.local, and since ofp.local may change as a new branch is cached in local, not caching that value means potentially losing access to it.

289
      Bin cachedBestBin;

Check if tick tree is currently empty. As an invariant, local.level3 if empty, iff local.level2 is empty, iff local.level1 is empty, iff local.root is empty.

291
      if (ofp.local.level3().isEmpty()) {

If the tick tree is empty, we consider the current best bin to be the bin of the written offer. This makes later comparisons between them pick the right conditional branch every time.

293
294
        cachedBestBin = insertionBin;
      } else {

If the tick tree is currently not empty, we cache the current best bin.

296
        cachedBestBin = ofp.local.bestBin();

If the written offer is currently stored in the tick tree, it must be removed.

298
        if (ofp.oldOffer.isLive()) {

If the insertion bin of the offer is better than the current best bin, the later call to dislodgeOffer does not need to update the cached branch in local since we (writeOffer) will take care of updating the branch as part of insertion the offer in its new bin. Otherwise, we may need dislodgeOffer to take care of updating the cached branch in local. However, that is only th the case if the written offer is currently the best offer of the tick tree. dislodgeOffer will make that determination.

300
301
          bool shouldUpdateBranch = !insertionBin.strictlyBetter(cachedBestBin);

local is updated, and shouldUpdateBranch now means “did update branch”.

303
304
          (ofp.local, shouldUpdateBranch) =
            dislodgeOffer(offerList, tickSpacing, ofp.oldOffer, ofp.local, cachedBestBin, shouldUpdateBranch);

If dislodgeOffer did update the information in local, it means the cached best bin may be stale – the best offer may now be in a different bin. So we update it.

306
307
          if (shouldUpdateBranch) {
            if (ofp.local.level3().isEmpty()) {

A call to bestBin() is invalid when the branch cached in local is for an empty tick tree. In that case, as we did earlier if the tick tree was already empty, we set the current best bin to the bin of the written offer.

309
310
311
312
313
314
315
316
              cachedBestBin = insertionBin;
            } else {
              cachedBestBin = ofp.local.bestBin();
            }
          }
        }
      }

We will now insert the offer to its new position in the tick tree. If the offer is now the best offer, we update the cached “bin position in leaf” information in local.

318
319
320
321
      if (!cachedBestBin.strictlyBetter(insertionBin)) {
        ofp.local = ofp.local.binPosInLeaf(insertionBin.posInLeaf());
      }

Next, we load the leaf of the offer to check whether it needs updating.

323
324
      Leaf leaf = offerList.leafs[insertionBin.leafIndex()].clean();

If the written offer’s leaf was empty, the level3 above it needs updating.

326
      if (leaf.isEmpty()) {

We reuse the same field variable for all 3 level indices.

328
        Field field;

We reuse the same insertionIndex and currentIndex variables for all 3 level indices and for the leaf index.

330
331
332
        int insertionIndex = insertionBin.level3Index();
        int currentIndex = cachedBestBin.level3Index();
        if (insertionIndex == currentIndex) {

If the written offer’s level3 is the cached level3, we load the written offer’s level3 from local.

334
335
          field = ofp.local.level3();
        } else {

Otherwise we load the written offer’s level3 from storage.

337
          field = offerList.level3s[insertionIndex].clean();

If the written offer’s level3 is strictly better than the cached level3, we evict the cached level3.

339
340
341
          if (insertionIndex < currentIndex) {
            Field localLevel3 = ofp.local.level3();
            bool shouldSaveLevel3 = !localLevel3.isEmpty();

Clean/dirty management. ifs are sequenced to avoid a useless SLOAD.

343
344
345
346
347
348
349
350
351
352
            if (!shouldSaveLevel3) {
              shouldSaveLevel3 = !offerList.level3s[currentIndex].eq(DirtyFieldLib.CLEAN_EMPTY);
            }
            if (shouldSaveLevel3) {
              offerList.level3s[currentIndex] = localLevel3.dirty();
            }
          }
        }

        if (insertionIndex <= currentIndex) {

If the written offer’s level3 is as good as or better than the cached level3, we cache the written offer’s level3 in local.

354
355
          ofp.local = ofp.local.level3(field.flipBitAtLevel3(insertionBin));
        } else {

Otherwise, we put it in storage

357
358
359
          offerList.level3s[insertionIndex] = field.flipBitAtLevel3(insertionBin).dirty();
        }

If the written offer’s level3 was empty, the level2 above it needs updating.

361
362
363
364
365
        if (field.isEmpty()) {
          insertionIndex = insertionBin.level2Index();
          currentIndex = cachedBestBin.level2Index();

          if (insertionIndex == currentIndex) {

If the written offer’s level2 is the cached level2, we load the written offer’s level2 from local.

367
368
            field = ofp.local.level2();
          } else {

Otherwise we load the written offer’s level2 from storage.

370
371
            field = offerList.level2s[insertionIndex].clean();

If the written offer’s level2 is strictly better than the cached level2, we evict the cached level2.

373
374
375
376
            if (insertionIndex < currentIndex) {
              Field localLevel2 = ofp.local.level2();
              bool shouldSaveLevel2 = !localLevel2.isEmpty();

Clean/dirty management. ifs are sequenced to avoid a useless SLOAD.

378
379
380
381
382
383
384
385
386
387
              if (!shouldSaveLevel2) {
                shouldSaveLevel2 = !offerList.level2s[currentIndex].eq(DirtyFieldLib.CLEAN_EMPTY);
              }
              if (shouldSaveLevel2) {
                offerList.level2s[currentIndex] = localLevel2.dirty();
              }
            }
          }

          if (insertionIndex <= currentIndex) {

If the written offer’s level3 is as good as or better than the cached level3, we cache the written offer’s level3 in local.

389
390
            ofp.local = ofp.local.level2(field.flipBitAtLevel2(insertionBin));
          } else {

Otherwise, we put it in storage

392
393
            offerList.level2s[insertionIndex] = field.flipBitAtLevel2(insertionBin).dirty();
          }

If the written offer’s level2 was empty, the level1 above it needs updating.

395
396
397
398
399
          if (field.isEmpty()) {
            insertionIndex = insertionBin.level1Index();
            currentIndex = cachedBestBin.level1Index();

            if (insertionIndex == currentIndex) {

If the written offer’s level1 is the cached level1, we load the written offer’s level1 from local.

401
402
              field = ofp.local.level1();
            } else {

Otherwise we load the written offer’s level1 from storage.

404
              field = offerList.level1s[insertionIndex].clean();

If the written offer’s level1 is strictly better than the cached level1, we evict the cached level1.

406
              if (insertionIndex < currentIndex) {

Unlike with level2 and level3, level1 cannot be CLEAN_EMPTY (it gets dirtied in activate)

408
409
410
411
412
                offerList.level1s[currentIndex] = ofp.local.level1().dirty();
              }
            }

            if (insertionIndex <= currentIndex) {

If the written offer’s level1 is as good as or better than the cached level1, we cache the written offer’s level1 in local.

414
415
              ofp.local = ofp.local.level1(field.flipBitAtLevel1(insertionBin));
            } else {

Otherwise, we put it in storage

417
418
              offerList.level1s[insertionIndex] = field.flipBitAtLevel1(insertionBin).dirty();
            }

If the written offer’s level1 was empty, the root needs updating.

420
421
422
423
424
425
426
            if (field.isEmpty()) {
              ofp.local = ofp.local.root(ofp.local.root().flipBitAtRoot(insertionBin));
            }
          }
        }
      }

Now that we are done checking the current state of the leaf, we can update it. By reading the last id of the written offer’s bin in the offer’s leaf, we can check if the bin is currently empty or not (as an invariant, an empty bin has both firstId and lastId equal to 0, and a nonempty bin has both ids different from 0).

Note that offers are always inserted at the end of their bin, so that earlier offer are taken first during market orders.

431
432
433
      uint lastId = leaf.lastOfBin(insertionBin);
      if (lastId == 0) {

If the bin was empty, we update the bin’s first id (a bin with a single offer has that offer id as firstId and as lastId).

435
436
        leaf = leaf.setBinFirst(insertionBin, ofrId);
      } else {

Otherwise, the written offer will become the new last offer of the bin, and the current last offer will have the written offer as next offer.

438
439
440
441
        OfferData storage offerData = offerList.offerData[lastId];
        offerData.offer = offerData.offer.next(ofrId);
      }

We now store the written offer id as the last offer of the bin.

443
444
445
      leaf = leaf.setBinLast(insertionBin, ofrId);
      offerList.leafs[insertionBin.leafIndex()] = leaf.dirty();

Finally, we store the offer information, including a pointer to the previous last offer of the bin (it may be 0).

447
448
449
450
451
      Offer ofr = OfferLib.pack({__prev: lastId, __next: 0, __tick: insertionTick, __gives: ofp.gives});
      offerList.offerData[ofrId].offer = ofr;
    }
  }
}
MgvOfferTaking.sol
2
3
4
5
6
7
pragma solidity ^0.8.10;

import "./MgvLib.sol";
import {MgvHasOffers} from "./MgvHasOffers.sol";
import {TickTreeLib} from "@mgv/lib/core/TickTreeLib.sol";

There are 2 ways to take offers in Mangrove:

  • Market order. A market order walks the offer list from the best offer and up, can specify a limit price, as well as a buy/sell behaviour (i.e. whether to limit the order buy the amount bought or by the amount sold).
  • Clean. Since offers can fail, bots can ‘clean’ specific offers and walk away with the bounty. If an offer does not fail, cleaning it reverts and leaves it in place.
12
abstract contract MgvOfferTaking is MgvHasOffers {

MultiOrder struct


        

The MultiOrder struct is used by market orders and cleans. Some of its fields are only used by market orders. We need a common data structure for both since low-level calls are shared between market orders and cleans. The struct is helpful in decreasing stack use.

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  struct MultiOrder {
    uint totalGot; // used globally by market order, per-offer by cleans
    uint totalGave; // used globally by market order, per-offer by cleans
    uint totalPenalty; // used globally
    address taker; // used globally
    bool fillWants; // used globally
    uint fillVolume; // used globally
    uint feePaid; // used globally
    Leaf leaf; // used by market order
    Tick maxTick; // used globally
    uint maxGasreqForFailingOffers; // used by market order
    uint gasreqForFailingOffers; // used by market order
    uint maxRecursionDepth; // used by market order
  }

Market Orders


        

Market Order


        

        

A market order specifies a (outbound, inbound,tickSpacing) offer list, a limit price it is ready to pay (in the form of maxTick, the log base 1.0001 of the price), and a volume fillVolume. If fillWants is true, that volume is the amount of olKey.outbound_tkn the taker wants to buy. If fillWants is false, that volume is the amount of olKey.inbound_tkn the taker wants to sell.

It returns four uints: the total amount of olKey.outbound_tkn received, the total amount of olKey.inbound_tkn spent, the penalty received by msg.sender (in wei), and the fee paid by the taker (in wei of olKey.outbound_tkn).

The market order stops when the price exceeds (an approximation of) 1.0001^maxTick, or when the end of the book has been reached, or:

  • If fillWants is true, the market order stops when fillVolume units of olKey.outbound_tkn have been obtained. To buy a specific volume of olKey.outbound_tkn at any price, set fillWants to true, set fillVolume to volume you want to buy, and set maxTick to the MAX_TICK constant.
  • If fillWants is false, the market order stops when fillVolume units of olKey.inbound_tkn have been paid. To sell a specific volume of olKey.inbound_tkn at any price, set fillWants to false, set fillVolume to the volume you want to sell, and set maxTick to the MAX_TICK constant.

For a maximum fillVolume and a maximum (when fillWants=true) or minimum (when fillWants=false) price, the taker can end up receiving a volume of about 2**255 tokens.

45
46
47
48
49
50
51
52
53
54
  function marketOrderByTick(OLKey memory olKey, Tick maxTick, uint fillVolume, bool fillWants)
    public
    returns (uint takerGot, uint takerGave, uint bounty, uint feePaid)
  {
    unchecked {
      return generalMarketOrder(olKey, maxTick, fillVolume, fillWants, msg.sender, 0);
    }
  }

There is a ByVolume variant where the taker specifies a desired total amount of olKey.outbound_tkn tokens (takerWants) and an available total amount of olKey.inbound_tkn (takerGives). Volumes should fit on 127 bits.

56
57
58
59
60
61
62
63
64
  function marketOrderByVolume(OLKey memory olKey, uint takerWants, uint takerGives, bool fillWants)
    public
    returns (uint takerGot, uint takerGave, uint bounty, uint feePaid)
  {
    uint fillVolume = fillWants ? takerWants : takerGives;
    Tick maxTick = TickLib.tickFromVolumes(takerGives, takerWants);
    return generalMarketOrder(olKey, maxTick, fillVolume, fillWants, msg.sender, 0);
  }

If the offer list is filled with failing offers such that the default maxGasreqForFailingOffers is inadequate, this version of the market order lets the taker specify an upper bound on the gas they are ready to spend on failing offers.

66
67
68
69
70
71
72
73
74
75
76
77
  function marketOrderByTickCustom(
    OLKey memory olKey,
    Tick maxTick,
    uint fillVolume,
    bool fillWants,
    uint maxGasreqForFailingOffers
  ) public returns (uint takerGot, uint takerGave, uint bounty, uint feePaid) {
    unchecked {
      return generalMarketOrder(olKey, maxTick, fillVolume, fillWants, msg.sender, maxGasreqForFailingOffers);
    }
  }

Get offer after current offer. Will also remove the current offer and return the corresponding updated local.

During a market order, once an offer has been executed, the next one must be fetched. Since offers are structured in a tick tree, the next offer might be:

  • In the same bin, referred to by the currentOffer.next pointer.
  • In the same leaf, but in another bin.
  • In a bin that descend from the same level3 but not the same leaf.
  • In a bin that descend from the same level2 but not the same level3.
  • In a bin that descend from the same level1 but not the same level1.
  • In a bin that descend from a different level1. Or there might not be a next offer.

In any case, the ‘current branch’ is now the branch of the next offer, so local must be updated.

getNextBest returns the id of the next offer if there is one (and id 0 otherwise), as well as the updated local.

However, this function is very unsafe taken in isolation:

  • it does not update offer pointers. Since a market order repeatedly calls it and does not inspect the .prev of an offer, the optimization is correct as long as the market order updates the prev pointer of the last offer it sees to 0. After a call, it is your responsibility to do:
OfferData storage = offerList.offerData[offerId];
offerData.offer = offer.prev(0);
  • it does not flush an updated leaf to storage unless the current leaf has become empty and it needs to load a new one. After a call, it is your responsibility to write the new leaf to storage, if necessary.
101
102
103
104
  function getNextBest(OfferList storage offerList, MultiOrder memory mor, Offer offer, Local local, uint tickSpacing)
    internal
    returns (uint offerId, Local)
  {

Get tick from current offer tick and tickSpacing

106
107
108
    Bin offerBin = offer.bin(tickSpacing);
    uint nextId = offer.next();

Update the bin’s first offer. If nextId is 0, then the bin’s last offer will be updated immediately after.

110
111
112
    Leaf leaf = mor.leaf;
    leaf = leaf.setBinFirst(offerBin, nextId);

If the current bin is now empty, we go up the tick tree to find the next offer.

114
    if (nextId == 0) {

Mark the bin as empty.

116
      leaf = leaf.setBinLast(offerBin, 0);

If the current leaf is now empty, we keep going up the tick tree.

118
      if (leaf.isEmpty()) {

Flush the current empty leaf since we will load a new one. Note that the slot cannot be empty since we just emptied the leaf (and unlike fields, leaves don’t stay cached in another slot).

120
121
        offerList.leafs[offerBin.leafIndex()] = leaf.dirty();

We reuse the same field variable for all 3 level indices.

123
        Field field = local.level3().flipBitAtLevel3(offerBin);

We reuse the same index variable for all 3 level indices and for the leaf index.

125
        int index = offerBin.level3Index();

If the current level3 is now empty, we keep going up the tick tree.

127
        if (field.isEmpty()) {

Flush the current empty level3 since we will load a new one. We avoid the write if the slot is already cleanly empty.

129
130
131
132
133
          if (!offerList.level3s[index].eq(DirtyFieldLib.CLEAN_EMPTY)) {
            offerList.level3s[index] = DirtyFieldLib.DIRTY_EMPTY;
          }
          index = offerBin.level2Index();
          field = local.level2().flipBitAtLevel2(offerBin);

If the current level2 is now empty, we keep going up the tick tree.

135
          if (field.isEmpty()) {

Flush the current empty level2 since we will load a new one. We avoid the write if the slot is already cleanly empty.

137
138
139
140
141
            if (!offerList.level2s[index].eq(DirtyFieldLib.CLEAN_EMPTY)) {
              offerList.level2s[index] = DirtyFieldLib.DIRTY_EMPTY;
            }
            index = offerBin.level1Index();
            field = local.level1().flipBitAtLevel1(offerBin);

If the current level1 is now empty, we keep going up the tick tree.

143
            if (field.isEmpty()) {

Flush the current empty level1 since we will load a new one. Unlike with level2 and level3, level1 cannot be CLEAN_EMPTY (it gets dirtied in activate)

145
146
147
              offerList.level1s[index] = DirtyFieldLib.DIRTY_EMPTY;
              field = local.root().flipBitAtRoot(offerBin);
              local = local.root(field);

If the root is now empty, we mark all fields in local and the current leaf as empty and return the 0 offer id.

149
150
151
152
153
154
155
              if (field.isEmpty()) {
                local = local.level1(field);
                local = local.level2(field);
                local = local.level3(field);
                mor.leaf = LeafLib.EMPTY;
                return (0, local);
              }

Last level1 was empty, load the new level1

157
              index = field.firstLevel1Index();

Low-level optim: avoid dirty/clean cycle, dirty bit will be erased anyway.

159
160
              field = Field.wrap(DirtyField.unwrap(offerList.level1s[index]));
            }

Store current level1 in local.

162
            local = local.level1(field);

Last level2 was empty, load the new level2

164
            index = field.firstLevel2Index(index);

Low-level optim: avoid dirty/clean cycle, dirty bit will be erased anyway.

166
167
            field = Field.wrap(DirtyField.unwrap(offerList.level2s[index]));
          }

Store current level2 in local.

169
          local = local.level2(field);

Last level3 was empty, load the new level3

171
          index = field.firstLevel3Index(index);

Low-level optim: avoid dirty/clean cycle, dirty bit will be erased anyway.

173
174
          field = Field.wrap(DirtyField.unwrap(offerList.level3s[index]));
        }

Store current level3 in local.

176
        local = local.level3(field);

Last leaf was empty, load the new leaf.

178
179
        leaf = offerList.leafs[field.firstLeafIndex(index)].clean();
      }

Find the position of the best non-empty bin in the current leaf, save it to local, and read the first offer id of that bin.

181
182
183
184
185
186
187
      uint bestNonEmptyBinPos = leaf.bestNonEmptyBinPos();
      local = local.binPosInLeaf(bestNonEmptyBinPos);
      nextId = leaf.firstOfPos(bestNonEmptyBinPos);
    }
    mor.leaf = leaf;
    return (nextId, local);
  }

General Market Order


        

        

General market orders set up the market order with a given taker (msg.sender in the most common case). Returns (totalGot, totalGave, penaltyReceived, feePaid). Note that the taker can be anyone. This is safe when taker == msg.sender, but generalMarketOrder must not be called with taker != msg.sender unless a security check is done after (see MgvOfferTakingWithPermit`.

192
193
194
195
196
197
198
199
200
201
  function generalMarketOrder(
    OLKey memory olKey,
    Tick maxTick,
    uint fillVolume,
    bool fillWants,
    address taker,
    uint maxGasreqForFailingOffers
  ) internal returns (uint takerGot, uint takerGave, uint bounty, uint feePaid) {
    unchecked {

Checking that fillVolume fits in 127 ensures no overflow during the market order recursion.

203
204
205
      require(fillVolume <= MAX_SAFE_VOLUME, "mgv/mOrder/fillVolume/tooBig");
      require(maxTick.inRange(), "mgv/mOrder/tick/outOfRange");

MultiOrder (defined above) maintains information related to the entire market order.

207
208
209
210
211
      MultiOrder memory mor;
      mor.maxTick = maxTick;
      mor.taker = taker;
      mor.fillWants = fillWants;

SingleOrder is defined in MgvLib.sol and holds information related to the execution of one offer. It also contains olKey, which concerns the entire market order, because it will be sent to the maker, who needs that information.

213
214
215
216
217
      MgvLib.SingleOrder memory sor;
      sor.olKey = olKey;
      OfferList storage offerList;
      (sor.global, sor.local, offerList) = _config(olKey);
      mor.maxRecursionDepth = sor.global.maxRecursionDepth();

We have an upper limit on total gasreq for failing offers to avoid failing offers delivering nothing and exhausting gaslimit for the transaction.

219
220
221
      mor.maxGasreqForFailingOffers =
        maxGasreqForFailingOffers > 0 ? maxGasreqForFailingOffers : sor.global.maxGasreqForFailingOffers();

Throughout the execution of the market order, the sor’s offer id and other parameters will change. We start with the current best offer id (0 if the book is empty).

223
224
225
226
227
      mor.leaf = offerList.leafs[sor.local.bestBin().leafIndex()].clean();
      sor.offerId = mor.leaf.bestOfferId();
      sor.offer = offerList.offerData[sor.offerId].offer;

Throughout the market order, fillVolume represents the amount left to buy (if fillWants) or sell (if !fillWants).

229
230
      mor.fillVolume = fillVolume;

For the market order to start, the offer list needs to be both active and not currently protected from reentrancy.

232
233
234
      activeOfferListOnly(sor.global, sor.local);
      unlockedOfferListOnly(sor.local);

Initialization


        

The market order will operate as follows : it will go through offers from best to worse, starting from offerId, and:


        

keep an up-to-date fillVolume.

  • not set prev/next pointers to their correct locations at each offer taken (this is an optimization enabled by forbidding reentrancy).
  • after consuming a segment of offers, will update the current best offer to be the best remaining offer on the book.

        

We start by enabling the reentrancy lock for this (olKey.outbound_tkn,olKey.inbound_tkn, olKey.tickSpacing) offer list.

242
243
244
245
246
      sor.local = sor.local.lock(true);
      offerList.local = sor.local;

      emit OrderStart(sor.olKey.hash(), taker, maxTick, fillVolume, fillWants);

Call recursive internalMarketOrder function.

248
249
      internalMarketOrder(offerList, mor, sor);

Over the course of the market order, a penalty reserved for msg.sender has accumulated in mor.totalPenalty. No actual transfers have occurred yet – all the ethers given by the makers as provision are owned by Mangrove. sendPenalty finally gives the accumulated penalty to msg.sender.

251
252
253
254
      sendPenalty(mor.totalPenalty);

      emit OrderComplete(sor.olKey.hash(), taker, mor.feePaid);
256
257
258
259
      return (mor.totalGot, mor.totalGave, mor.totalPenalty, mor.feePaid);
    }
  }

Internal market order


        

        

internalMarketOrder works recursively. Going downward, each successive offer is executed until the market order stops (due to: volume exhausted, bad price, or empty offer list). Then the reentrancy lock is lifted. As the recursion unrolls, each offer’s maker contract is called again with its remaining gas and given the chance to update its offers on the book.

263
264
265
266
  function internalMarketOrder(OfferList storage offerList, MultiOrder memory mor, MgvLib.SingleOrder memory sor)
    internal
  {
    unchecked {

The market order proceeds only if the following conditions are all met:

  • there is some volume left to buy/sell
  • the current best offer is not too expensive
  • there is a current best offer
  • we are not too deep in the recursive calls (stack overflow risk)
  • we are not at risk of consuming too much gas because of failing offers
274
275
276
277
      if (
        mor.fillVolume > 0 && Tick.unwrap(sor.offer.tick()) <= Tick.unwrap(mor.maxTick) && sor.offerId > 0
          && mor.maxRecursionDepth > 0 && mor.gasreqForFailingOffers <= mor.maxGasreqForFailingOffers
      ) {

Market order execution

279
280
281
282
283
        mor.maxRecursionDepth--;

        uint gasused; // gas used by `makerExecute`
        bytes32 makerData; // data returned by maker

mgvData is a Mangrove status code. It appears in an OrderResult. Its possible values are:

  • "mgv/tradeSuccess": offer execution succeeded.
  • "mgv/makerRevert": execution of makerExecute reverted.
  • "mgv/makerTransferFail": maker could not send olKey.outbound_tkn.
  • "mgv/makerReceiveFail": maker could not receive olKey.inbound_tkn.

mgvData should not be exploitable by the maker!

291
292
        bytes32 mgvData;

Load additional information about the offer.

294
295
        sor.offerDetail = offerList.offerData[sor.offerId].detail;

execute attempts to execute the offer by calling its maker. execute may modify mor and sor. It is crucial that an error due to taker triggers a revert. That way, if mgvData is not "mgv/tradeSuccess", then the maker is at fault.


        

Post-execution, sor.takerWants/sor.takerGives reflect how much was sent/taken by the offer. We will need it after the recursive call, so we save it in local variables. Same goes for sor.offerId, sor.offer and sor.offerDetail.

298
299
        (gasused, makerData, mgvData) = execute(offerList, mor, sor);

Keep cached copy of current sor values to restore them later to send to posthook.

301
302
303
304
305
306
        uint takerWants = sor.takerWants;
        uint takerGives = sor.takerGives;
        uint offerId = sor.offerId;
        Offer offer = sor.offer;
        OfferDetail offerDetail = sor.offerDetail;

If execution was successful, we decrease fillVolume. This cannot underflow, see the execute function for details.

308
309
310
311
        if (mgvData == "mgv/tradeSuccess") {
          mor.fillVolume -= mor.fillWants ? takerWants : takerGives;
        }

We move sor to the next offer. Note that the current state is inconsistent, since we have not yet updated sor.offerDetails.

313
314
315
316
        (sor.offerId, sor.local) = getNextBest(offerList, mor, offer, sor.local, sor.olKey.tickSpacing);

        sor.offer = offerList.offerData[sor.offerId].offer;

Recursive call with the next offer.

318
319
        internalMarketOrder(offerList, mor, sor);

Restore sor values from before recursive call

321
322
323
324
325
326
        sor.takerWants = takerWants;
        sor.takerGives = takerGives;
        sor.offerId = offerId;
        sor.offer = offer;
        sor.offerDetail = offerDetail;

After an offer execution, we may run callbacks and increase the total penalty. As that part is common to market orders and cleaning, it lives in its own postExecute function.

328
329
        postExecute(mor, sor, gasused, makerData, mgvData);
      } else {

Market order end

331
332
333
        Offer offer = sor.offer;
        Bin bin = offer.bin(sor.olKey.tickSpacing);

If the offer list is not empty, the best offer may need a pointer update and the current leaf must be stored:

335
        if (sor.offerId != 0) {

If offer.prev is 0, we ended the market order right at the beginning of a bin, so no write is necessary.

This also explains why the update below is safe when a market takes 0 offers: necessarily at this point offer.prev() is 0, since we are looking at the best offer of the offer list.

Warning: do not locally update offer.prev() before this point, or the test wil spuriously fail and the updated offer will never be written to storage.

342
343
344
345
346
          if (offer.prev() != 0) {
            offerList.offerData[sor.offerId].offer = sor.offer.prev(0);
          }

          int index = bin.leafIndex();

This write may not be necessary if the mor.leaf was just loaded in the last getNextBest, but it will be a hot storage write anyway (so not expensive).

348
349
350
          offerList.leafs[index] = mor.leaf.dirty();
        }

Now that the market order is over, we can lift the lock on the book. In the same operation we

  • lift the reentrancy lock and
  • update the storage

so we are free from out of order storage writes.

358
359
360
        sor.local = sor.local.lock(false);
        offerList.local = sor.local;

payTakerMinusFees keeps the fee in Mangrove, proportional to the amount purchased, and gives the rest to the taker

362
363
364
365
366
        payTakerMinusFees(mor, sor);
      }
    }
  }

Cleaning


        

Cleans multiple offers, i.e. executes them and remove them from the book if they fail, transferring the failure penalty as bounty to the caller. If an offer succeeds, the execution of that offer is reverted, it stays in the book, and no bounty is paid; The cleanByImpersonation function itself will not revert.

Its second argument is a CleanTarget[] with each CleanTarget identifying an offer to clean and the execution parameters that will make it fail. The return values are the number of successfully cleaned offers and the total bounty received.

Note that Mangrove won’t attempt to execute an offer if the values in a CleanTarget don’t match its offer. To distinguish between a non-executed clean and a fail clean (due to the offer itself not failing), you must inspect the log (see MgvLib.sol) or check the received bounty.

Any taker can be impersonated when cleaning because:

  • The function reverts if the offer succeeds, reverting any token transfers.
  • After a clean where the offer has failed, all ERC20 token transfers have also been reverted – but the sender will still have received the bounty of the failing offers.
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
  function cleanByImpersonation(OLKey memory olKey, MgvLib.CleanTarget[] calldata targets, address taker)
    external
    returns (uint successes, uint bounty)
  {
    unchecked {
      emit CleanStart(olKey.hash(), taker, targets.length);

      for (uint i = 0; i < targets.length; ++i) {
        bytes memory encodedCall;
        {
          MgvLib.CleanTarget calldata target = targets[i];
          encodedCall = abi.encodeCall(
            this.internalCleanByImpersonation,
            (olKey, target.offerId, target.tick, target.gasreq, target.takerWants, taker)
          );
        }
        bytes memory retdata;
        {
          bool success;
          (success, retdata) = address(this).call(encodedCall);
          if (!success) {
            continue;
          }
        }

        successes++;

        {
          (uint offerBounty) = abi.decode(retdata, (uint));
          bounty += offerBounty;
        }
      }
      sendPenalty(bounty);

      emit CleanComplete();
    }
  }

  function internalCleanByImpersonation(
    OLKey memory olKey,
    uint offerId,
    Tick tick,
    uint gasreq,
    uint takerWants,
    address taker
  ) external returns (uint bounty) {
    unchecked {

internalClean must be used with a call (hence the external modifier) so its effect can be reverted. But a call from the outside would mean the bounty would get stuck in Mangrove.

425
426
427
428
429
430
431
432
433
434
435
436
437
438
      require(msg.sender == address(this), "mgv/clean/protected");

      MultiOrder memory mor;
      {
        require(tick.inRange(), "mgv/clean/tick/outOfRange");
        mor.maxTick = tick;
      }
      {
        require(takerWants <= MAX_SAFE_VOLUME, "mgv/clean/takerWants/tooBig");
        mor.fillVolume = takerWants;
      }
      mor.taker = taker;
      mor.fillWants = true;

Initialize single order struct.

440
441
442
443
444
445
446
447
448
      MgvLib.SingleOrder memory sor;
      sor.olKey = olKey;
      OfferList storage offerList;
      (sor.global, sor.local, offerList) = _config(olKey);
      sor.offerId = offerId;
      OfferData storage offerData = offerList.offerData[sor.offerId];
      sor.offer = offerData.offer;
      sor.offerDetail = offerData.detail;

For the cleaning to start, the offer list needs to be both active and not currently protected from reentrancy.

450
451
452
453
      activeOfferListOnly(sor.global, sor.local);
      unlockedOfferListOnly(sor.local);

      require(sor.offer.isLive(), "mgv/clean/offerNotLive");

We also check that gasreq is not worse than specified. A taker who does not care about gasreq can specify any amount larger than the maximum gasreq.

455
456
457
      require(sor.offerDetail.gasreq() <= gasreq, "mgv/clean/gasreqTooLow");
      require(sor.offer.tick().eq(tick), "mgv/clean/tickMismatch");

We start be enabling the reentrancy lock for this offer list.

459
460
461
462
      sor.local = sor.local.lock(true);
      offerList.local = sor.local;

      {

execute attempts to execute the offer by calling its maker. execute may modify mor and sor. It is crucial that an error due to taker triggers a revert. That way, if mgvData is not "mgv/tradeSuccess", then the maker is at fault.


        

Post-execution, sor.takerWants/sor.takerGives reflect how much was sent/taken by the offer.

465
466
467
468
        (uint gasused, bytes32 makerData, bytes32 mgvData) = execute(offerList, mor, sor);

        require(mgvData != "mgv/tradeSuccess", "mgv/clean/offerDidNotFail");

In the market order, we were able to avoid stitching back offers after every execute since we knew a segment starting from the best offer would be consumed. Here, we cannot do this optimisation since the offer may be anywhere in the book. So we stitch together offers immediately after execute.

470
471
        (sor.local,) = dislodgeOffer(offerList, olKey.tickSpacing, sor.offer, sor.local, sor.local.bestBin(), true);

Now that the current clean is over, we can lift the lock on the book. In the same operation we

  • lift the reentrancy lock and
  • update the storage

so we are free from out of order storage writes.

479
480
481
        sor.local = sor.local.lock(false);
        offerList.local = sor.local;

No fees are paid since offer execution failed.


        

After an offer execution, we may run callbacks and increase the total penalty. As that part is common to market orders and cleans, it lives in its own postExecute function.

485
486
487
488
489
490
491
        postExecute(mor, sor, gasused, makerData, mgvData);
      }

      bounty = mor.totalPenalty;
    }
  }

General execution


        

During a market order or a clean, offers get executed. The following code takes care of executing a single offer with parameters given by a SingleOrder within a larger context given by a MultiOrder.


        

Execute


        

Execution of the offer will be attempted with volume limited by the offer’s advertised volume. Warning: The caller must ensure that the price of the offer is low enough; This is not checked here.

Summary of the meaning of the return values:

  • gasused is the gas consumed by the execution
  • makerData is the data returned after executing the offer
  • internalMgvData is a status code internal to execute. It can hold any value that mgvData can hold. Within execute, it can additionally hold the following values:
  • "mgv/notEnoughGasForMakerTrade": cannot give maker close enough to gasreq. Triggers a revert of the entire order.
  • "mgv/takerTransferFail": taker could not send olKey.inbound_tkn. Triggers a revert of the entire order.
506
507
508
509
510
511
512
513
514
  function execute(OfferList storage offerList, MultiOrder memory mor, MgvLib.SingleOrder memory sor)
    internal
    returns (uint gasused, bytes32 makerData, bytes32 internalMgvData)
  {
    unchecked {
      {
        uint fillVolume = mor.fillVolume;
        uint offerGives = sor.offer.gives();
        uint offerWants = sor.offer.wants();

Volume requested depends on total gives (or wants) by taker. Let volume = mor.fillWants ? sor.takerWants : sor.takerGives. One can check that volume <= fillVolume in all cases.

Example with fillWants=true: if offerGives < fillVolume the first branch of the outer if sets volume = offerGives and we are done; otherwise the 1st branch of the inner if is taken and sets volume = fillVolume and we are done.

518
519
520
521
522
        if ((mor.fillWants && offerGives <= fillVolume) || (!mor.fillWants && offerWants <= fillVolume)) {
          sor.takerWants = offerGives;
          sor.takerGives = offerWants;
        } else {
          if (mor.fillWants) {

While a possible offer.wants()=0 is the maker’s responsibility, a small enough partial fill may round to 0, so we round up. It is immaterial but more fair to the maker.

524
525
526
527
528
529
530
531
            sor.takerGives = sor.offer.tick().inboundFromOutboundUp(fillVolume);
            sor.takerWants = fillVolume;
          } else {
            sor.takerWants = sor.offer.tick().outboundFromInbound(fillVolume);
            sor.takerGives = fillVolume;
          }
        }
      }

The flashloan is executed by call to flashloan. If the call reverts, it means the maker failed to send back sor.takerWants units of olKey.outbound_tkn to the taker. Notes :

  • msg.sender is Mangrove itself in those calls – all operations related to the actual caller should be done outside of this call.
  • any spurious exception due to an error in Mangrove code will be falsely blamed on the Maker and its provision for the offer will be unfairly taken away.
536
537
538
      bool success;
      bytes memory retdata;
      {

Clear fields that maker must not see.

NB: It should be more efficient to do this in makerExecute instead as we would not have to restore the fields afterwards. However, for unknown reasons that solution consumes significantly more gas, so we do it here instead.

543
544
545
546
547
548
549
        Offer offer = sor.offer;
        sor.offer = offer.clearFieldsForMaker();
        Local local = sor.local;
        sor.local = local.clearFieldsForMaker();

        (success, retdata) = address(this).call(abi.encodeCall(this.flashloan, (sor, mor.taker)));

Restore cleared fields

551
552
553
554
        sor.offer = offer;
        sor.local = local;
      }

success is true: trade is complete

556
      if (success) {

In case of success, retdata encodes the gas used by the offer and an arbitrary 256 bits word sent by the maker.

558
        (gasused, makerData) = abi.decode(retdata, (uint, bytes32));

internalMgvData indicates trade success

560
561
        internalMgvData = bytes32("mgv/tradeSuccess");

If configured to do so, Mangrove notifies an external contract that a successful trade has taken place.

563
564
565
566
        if (sor.global.notify()) {
          IMgvMonitor(sor.global.monitor()).notifySuccess(sor, mor.taker);
        }

We update the totals in the multi order based on the adjusted sor.takerWants/sor.takerGives.

568
569
570
571
572
        mor.totalGot += sor.takerWants;
        require(mor.totalGot >= sor.takerWants, "mgv/totalGot/overflow");
        mor.totalGave += sor.takerGives;
        require(mor.totalGave >= sor.takerGives, "mgv/totalGave/overflow");
      } else {

In case of failure, retdata encodes an internal status code, the gas used by the offer, and an arbitrary 256 bits word sent by the maker.

574
        (internalMgvData, gasused, makerData) = innerDecode(retdata);

Note that in the literals returned are bytes32 (stack values), while the revert arguments are strings (memory pointers).

576
577
578
579
        if (
          internalMgvData == "mgv/makerRevert" || internalMgvData == "mgv/makerTransferFail"
            || internalMgvData == "mgv/makerReceiveFail"
        ) {

Update (an upper bound) on gasreq required for failing offers

581
          mor.gasreqForFailingOffers += sor.offerDetail.gasreq();

If configured to do so, Mangrove notifies an external contract that a failed trade has taken place.

583
584
585
          if (sor.global.notify()) {
            IMgvMonitor(sor.global.monitor()).notifyFail(sor, mor.taker);
          }

It is crucial that any error code which indicates an error caused by the taker triggers a revert, because functions that call execute consider that when internalMgvData is not "mgv/tradeSuccess", then the maker should be blamed.

587
588
589
590
591
        } else if (internalMgvData == "mgv/notEnoughGasForMakerTrade") {
          revert("mgv/notEnoughGasForMakerTrade");
        } else if (internalMgvData == "mgv/takerTransferFail") {
          revert("mgv/takerTransferFail");
        } else {

This code must be unreachable except if the call to flashloan went OOG and there is enough gas to revert here. Danger: if a well-crafted offer/maker offer list can force a revert of flashloan, Mangrove will be stuck.

593
594
595
596
          revert("mgv/swapError");
        }
      }

Delete the offer. The last argument indicates whether the offer should be stripped of its provision (yes if execution failed, no otherwise). We cannot partially strip an offer provision (for instance, remove only the penalty from a failing offer and leave the rest) since the provision associated with an offer is always deduced from the (gasprice,gasbase,gasreq) parameters and not stored independently. We delete offers whether the amount remaining on offer is > density or not for the sake of uniformity (code is much simpler). We also expect prices to move often enough that the maker will want to update their offer anyway. To simulate leaving the remaining volume in the offer, the maker can program their makerPosthook to updateOffer and put the remaining volume back in.

598
599
600
601
602
603
      dirtyDeleteOffer(
        offerList.offerData[sor.offerId], sor.offer, sor.offerDetail, internalMgvData != "mgv/tradeSuccess"
      );
    }
  }

Flashloan


        

Externally called by execute, flashloan lends money to the maker then calls makerExecute to run the maker liquidity fetching code. If makerExecute is unsuccessful, flashloan reverts (but the larger orderbook traversal will continue).

In detail:

  1. Flashloans takerGives units of sor.olKey.inbound_tkn from the taker to the maker and returns false if the loan fails.
  2. Runs offerDetail.maker’s execute function.
  3. Returns the result of the operations, with optional makerData to help the maker debug.

Made virtual so tests can instrument the function.

614
615
616
617
618
619
  function flashloan(MgvLib.SingleOrder calldata sor, address taker)
    external
    virtual
    returns (uint gasused, bytes32 makerData)
  {
    unchecked {

flashloan must be used with a call (hence the external modifier) so its effect can be reverted. But a call from the outside would be fatal.

621
      require(msg.sender == address(this), "mgv/flashloan/protected");

The transfer taker -> maker is in 2 steps. First, taker->mgv. Then mgv->maker. With a direct taker->maker transfer, if one of taker/maker is blacklisted, we can’t tell which one. We need to know which one: if we incorrectly blame the taker, a blacklisted maker can block an offer list forever; if we incorrectly blame the maker, a blacklisted taker can unfairly make makers fail all the time. Of course we assume that Mangrove is not blacklisted. This 2-step transfer is incompatible with tokens that have transfer fees (more accurately, it uselessly incurs fees twice).

626
627
628
629
630
631
632
633
634
635
636
637
      if (transferTokenFrom(sor.olKey.inbound_tkn, taker, address(this), sor.takerGives)) {
        if (transferToken(sor.olKey.inbound_tkn, sor.offerDetail.maker(), sor.takerGives)) {
          (gasused, makerData) = makerExecute(sor);
        } else {
          innerRevert([bytes32("mgv/makerReceiveFail"), bytes32(0), ""]);
        }
      } else {
        innerRevert([bytes32("mgv/takerTransferFail"), "", ""]);
      }
    }
  }

Maker Execute


        

Called by flashloan, makerExecute runs the maker code and checks that it can safely send the desired assets to the taker.

640
641
642
643
644
645
646
647
  function makerExecute(MgvLib.SingleOrder calldata sor) internal returns (uint gasused, bytes32 makerData) {
    unchecked {
      bytes memory cd = abi.encodeCall(IMaker.makerExecute, (sor));

      uint gasreq = sor.offerDetail.gasreq();
      address maker = sor.offerDetail.maker();
      uint oldGas = gasleft();

We let the maker pay for the overhead of checking remaining gas and making the call, as well as handling the return data (constant gas since only the first 32 bytes of return data are read). So the require below is just an approximation: if the overhead of (require + cost of CALL) is hh, the maker will receive at worst gasreq63h64\textrm{gasreq} - \frac{63h}{64} gas.

649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
      if (!(oldGas - oldGas / 64 >= gasreq)) {
        innerRevert([bytes32("mgv/notEnoughGasForMakerTrade"), "", ""]);
      }

      bool callSuccess;
      (callSuccess, makerData) = controlledCall(maker, gasreq, cd);

      gasused = oldGas - gasleft();

      if (!callSuccess) {
        innerRevert([bytes32("mgv/makerRevert"), bytes32(gasused), makerData]);
      }

      bool transferSuccess = transferTokenFrom(sor.olKey.outbound_tkn, maker, address(this), sor.takerWants);

      if (!transferSuccess) {
        innerRevert([bytes32("mgv/makerTransferFail"), bytes32(gasused), makerData]);
      }
    }
  }

Post execute


        

At this point, we know an offer execution was attempted. After executing an offer (whether in a market order or in cleans), we

  1. Call the maker’s posthook and sum the total gas used.
  2. If offer failed: sum total penalty due to msg.sender and give remainder to maker.

Made virtual so tests can instrument it.

677
678
679
680
681
682
683
684
685
686
  function postExecute(
    MultiOrder memory mor,
    MgvLib.SingleOrder memory sor,
    uint gasused,
    bytes32 makerData,
    bytes32 mgvData
  ) internal virtual {
    unchecked {
      uint gasreq = sor.offerDetail.gasreq();

We are about to call back the maker, giving it its unused gas (gasreq - gasused). Since the gas used so far may exceed gasreq, we prevent underflow in the subtraction below by bounding gasused above with gasreq. We could have decided not to call back the maker at all when there is no gas left, but we do it for uniformity.

688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
      if (gasused > gasreq) {
        gasused = gasreq;
      }
      (uint posthookGas, bool callSuccess, bytes32 posthookData) =
        makerPosthook(sor, gasreq - gasused, makerData, mgvData);
      gasused = gasused + posthookGas;

      if (mgvData != "mgv/tradeSuccess") {
        uint penalty = applyPenalty(sor, gasused);
        mor.totalPenalty += penalty;
        if (!callSuccess) {
          emit OfferFailWithPosthookData(
            sor.olKey.hash(), mor.taker, sor.offerId, sor.takerWants, sor.takerGives, penalty, mgvData, posthookData
          );
        } else {
          emit OfferFail(sor.olKey.hash(), mor.taker, sor.offerId, sor.takerWants, sor.takerGives, penalty, mgvData);
        }
      } else {
        if (!callSuccess) {
          emit OfferSuccessWithPosthookData(
            sor.olKey.hash(), mor.taker, sor.offerId, sor.takerWants, sor.takerGives, posthookData
          );
        } else {
          emit OfferSuccess(sor.olKey.hash(), mor.taker, sor.offerId, sor.takerWants, sor.takerGives);
        }
      }
    }
  }

Maker Posthook

718
719
720
721
722
723
724
725
726
727
728
729
  function makerPosthook(MgvLib.SingleOrder memory sor, uint gasLeft, bytes32 makerData, bytes32 mgvData)
    internal
    virtual
    returns (uint gasused, bool callSuccess, bytes32 posthookData)
  {
    unchecked {
      bytes memory cd =
        abi.encodeCall(IMaker.makerPosthook, (sor, MgvLib.OrderResult({makerData: makerData, mgvData: mgvData})));

      address maker = sor.offerDetail.maker();

      uint oldGas = gasleft();

We let the maker pay for the overhead of checking remaining gas and making the call. So the require below is just an approximation: if the overhead of (require + cost of CALL) is hh, the maker will receive at worst gasreq63h64\textrm{gasreq} - \frac{63h}{64} gas.

731
732
733
734
735
736
737
738
739
740
      if (!(oldGas - oldGas / 64 >= gasLeft)) {
        revert("mgv/notEnoughGasForMakerPosthook");
      }

      (callSuccess, posthookData) = controlledCall(maker, gasLeft, cd);

      gasused = oldGas - gasleft();
    }
  }

controlledCall


        

Calls an external function with controlled gas expense. A direct call of the form (,bytes memory retdata) = maker.call{gas}(selector,...args) enables a griefing attack: the maker uses half its gas to write in its memory, then reverts with that memory segment as argument. After a low-level call, solidity automatically copies returndatasize bytes of returndata into memory. So the total gas consumed to execute a failing offer could exceed gasreq + offer_gasbase where n is the number of failing offers. In case of success, we read the first 32 bytes of returndata (the signature of makerExecute is bytes32). Otherwise, for compatibility with most errors that bubble up from contract calls and Solidity’s require, we read 32 bytes of returndata starting from the 69th (4 bytes of method sig + 32 bytes of offset + 32 bytes of string length).

743
744
745
746
  function controlledCall(address callee, uint gasreq, bytes memory cd) internal returns (bool success, bytes32 data) {
    unchecked {
      bytes32[4] memory retdata;

if success, read returned bytes 1…32, otherwise read returned bytes 69…100.

748
749
750
751
752
753
754
      assembly ("memory-safe") {
        success := call(gasreq, callee, 0, add(cd, 32), mload(cd), retdata, 100)
        data := mload(add(mul(iszero(success), 68), retdata))
      }
    }
  }

Penalties


        

Offers are just promises. They can fail. Penalty provisioning discourages from failing too much: we ask makers to provision more ETH than the expected gas cost of executing their offer and penalize them according to wasted gas.

Under normal circumstances, we should expect to see bots with a profit expectation dry-running offers locally and executing cleans on failing offers, collecting the penalty. The result should be a mostly clean book for actual takers (i.e. a book with only successful offers).

Incentive issue: if the gas price increases enough after an offer has been created, there may not be an immediately profitable way to remove the fake offers. In that case, we count on 3 factors to keep the book clean:

  1. Gas price eventually comes down.
  2. Other market makers want to keep Mangrove attractive and maintain their offer flow.
  3. Mangrove governance (who may collect a fee) wants to keep Mangrove attractive and maximize exchange volume.

        

        

After an offer failed, part of its provision is given back to the maker and the rest is stored to be sent to the taker after the entire order completes. In applyPenalty, we only credit the maker with its excess provision. So it looks like the maker is gaining something. In fact they’re just getting back a fraction of what they provisioned earlier.


        

Penalty application summary:

  • applyPenalty is not called if the offer executes successfully, so the offer remains provisioned. The maker can move the provision from the offer to its internal balance by calling retractOffer(olKey,offerId,true).
  • Otherwise, the maker loses the cost of gasused + offer_gasbase gas. The gas price is estimated by gasprice.
  • To create the offer, the maker had to provision for gasreq + offer_gasbase gas at a price of offerDetail.gasprice.
  • We do not consider the tx.gasprice.
  • offerDetail.gasbase and offerDetail.gasprice are the values of Mangrove parameters config.offer_gasbase and config.gasprice when the offer was created. Without caching those values, the provision set aside could end up insufficient to reimburse the maker (or to retribute the taker).
776
777
778
779
780
781
  function applyPenalty(MgvLib.SingleOrder memory sor, uint gasused) internal returns (uint) {
    unchecked {
      uint gasreq = sor.offerDetail.gasreq();

      uint provision = 1e6 * sor.offerDetail.gasprice() * (gasreq + sor.offerDetail.offer_gasbase());

We set gasused = min(gasused,gasreq) since gasreq < gasused is possible e.g. with gasreq = 0 (all calls consume nonzero gas).

783
784
785
786
      if (gasused > gasreq) {
        gasused = gasreq;
      }

As an invariant, applyPenalty is only called when mgvData is in ["mgv/makerRevert","mgv/makerTransferFail","mgv/makerReceiveFail"].

788
789
790
791
792
793
      uint penalty = 1e6 * sor.global.gasprice() * (gasused + sor.local.offer_gasbase());

      if (penalty > provision) {
        penalty = provision;
      }

Here we write to storage the new maker balance. This occurs after possible reentrant calls. How do we know we’re not crediting twice the same amounts? Because the offer’s provision was set to 0 in storage (through dirtyDeleteOffer) before the reentrant calls. In this function, we are working with cached copies of the offer as it was before it was consumed.

795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
      creditWei(sor.offerDetail.maker(), provision - penalty);

      return penalty;
    }
  }

  function sendPenalty(uint amount) internal {
    unchecked {
      if (amount > 0) {
        (bool noRevert,) = msg.sender.call{value: amount}("");
        require(noRevert, "mgv/sendPenaltyReverted");
      }
    }
  }

Post-trade, payTakerMinusFees sends what’s due to the taker and keeps the rest (the fees). Routing through the Mangrove like that also deals with blacklisting issues (separates the maker-blacklisted and the taker-blacklisted cases).

811
812
813
814
815
816
817
818
  function payTakerMinusFees(MultiOrder memory mor, MgvLib.SingleOrder memory sor) internal {
    unchecked {
      uint concreteFee = (mor.totalGot * sor.local.fee()) / 10_000;
      if (concreteFee > 0) {
        mor.totalGot -= concreteFee;
        mor.feePaid = concreteFee;
      }
      if (mor.totalGot > 0) {

It should be statically provable that this transfer cannot return false under well-behaved ERC20s and a non-blacklisted, non-0 target, if governance does not call withdrawERC20 during order execution, unless the caller set a gas limit which precisely makes transferToken go OOG but retains enough gas to revert here.

820
821
822
823
824
        require(transferToken(sor.olKey.outbound_tkn, mor.taker, mor.totalGot), "mgv/MgvFailToPayTaker");
      }
    }
  }

Misc. functions


        

Regular solidity reverts prepend the string argument with a function signature. Since we wish to transfer data through a revert, the innerRevert function does a low-level revert with only the required data. innerCode decodes this data.

828
829
  function innerDecode(bytes memory data) internal pure returns (bytes32 mgvData, uint gasused, bytes32 makerData) {
    unchecked {

The data pointer is of the form [mgvData,gasused,makerData] where each array element is contiguous and has size 256 bits.

831
832
833
834
835
836
837
838
      assembly ("memory-safe") {
        mgvData := mload(add(data, 32))
        gasused := mload(add(data, 64))
        makerData := mload(add(data, 96))
      }
    }
  }

innerRevert reverts a raw triple of values to be interpreted by innerDecode.

840
841
842
843
844
845
846
847
  function innerRevert(bytes32[3] memory data) internal pure {
    unchecked {
      assembly ("memory-safe") {
        revert(data, 96)
      }
    }
  }
}
MgvOfferTakingWithPermit.sol
2
3
4
5
6
7
8
pragma solidity ^0.8.10;

import "@mgv/src/core/MgvLib.sol";
import {MgvOfferTaking} from "./MgvOfferTaking.sol";
import {TickTreeLib} from "@mgv/lib/core/TickTreeLib.sol";

abstract contract MgvOfferTakingWithPermit is MgvOfferTaking {

Since DOMAIN_SEPARATOR is immutable, it cannot use MgvAppendix to provide an accessor (because the value will come from code, not from storage), so we generate the accessor here.

10
11
12
  bytes32 public immutable DOMAIN_SEPARATOR;

  constructor(string memory contractName) {

Initialize EIP712 DOMAIN_SEPARATOR.

14
15
16
17
18
19
20
21
22
23
24
    DOMAIN_SEPARATOR = keccak256(
      abi.encode(
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
        keccak256(bytes(contractName)),
        keccak256(bytes("1")),
        block.chainid,
        address(this)
      )
    );
  }

Delegation public functions


        

Adapted from Uniswap v2 contract

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
  function permit(
    address outbound_tkn,
    address inbound_tkn,
    address owner,
    address spender,
    uint value,
    uint deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
  ) external {
    unchecked {
      require(deadline >= block.timestamp, "mgv/permit/expired");

      uint nonce = _nonces[owner]++;
      bytes32 digest = keccak256(
        abi.encodePacked(
          "\x19\x01",
          DOMAIN_SEPARATOR,
          keccak256(abi.encode(_PERMIT_TYPEHASH, outbound_tkn, inbound_tkn, owner, spender, value, nonce, deadline))
        )
      );
      address recoveredAddress = ecrecover(digest, v, r, s);
      require(recoveredAddress != address(0) && recoveredAddress == owner, "mgv/permit/invalidSignature");

      _allowance[outbound_tkn][inbound_tkn][owner][spender] = value;
      emit Approval(outbound_tkn, inbound_tkn, owner, spender, value);
    }
  }

  function approve(address outbound_tkn, address inbound_tkn, address spender, uint value) external returns (bool) {
    unchecked {
      _allowance[outbound_tkn][inbound_tkn][msg.sender][spender] = value;
      emit Approval(outbound_tkn, inbound_tkn, msg.sender, spender, value);
      return true;
    }
  }

The delegate version of marketOrder is marketOrderFor, which takes a taker address as additional argument. Penalties incurred by failed offers will still be sent to msg.sender, but exchanged amounts will be transferred from and to the taker. If the msg.sender’s allowance for the given outbound_tkn,inbound_tkn and taker are strictly less than the total amount eventually spent by taker, the call will fail.


        

Note: marketOrderFor and cleanByImpersonation may emit ERC20 Transfer events of value 0 from taker, but that’s already the case with common ERC20 implementations.

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
  function marketOrderForByVolume(OLKey memory olKey, uint takerWants, uint takerGives, bool fillWants, address taker)
    external
    returns (uint takerGot, uint takerGave, uint bounty, uint feePaid)
  {
    unchecked {
      uint fillVolume = fillWants ? takerWants : takerGives;
      Tick tick = TickLib.tickFromVolumes(takerGives, takerWants);
      return marketOrderForByTick(olKey, tick, fillVolume, fillWants, taker);
    }
  }

  function marketOrderForByTick(OLKey memory olKey, Tick maxTick, uint fillVolume, bool fillWants, address taker)
    public
    returns (uint takerGot, uint takerGave, uint bounty, uint feePaid)
  {
    unchecked {
      (takerGot, takerGave, bounty, feePaid) = generalMarketOrder(olKey, maxTick, fillVolume, fillWants, taker, 0);

The sender’s allowance is verified after the order complete so that takerGave rather than takerGives is checked against the allowance. The former may be lower.

87
88
89
90
      deductSenderAllowance(olKey.outbound_tkn, olKey.inbound_tkn, taker, takerGave);
    }
  }

Misc. low-level functions


        

Used by *For functions, it both checks that msg.sender was allowed to use the taker’s funds and decreases the former’s allowance.

94
95
96
97
98
99
100
101
102
103
104
  function deductSenderAllowance(address outbound_tkn, address inbound_tkn, address owner, uint amount) internal {
    unchecked {
      mapping(address => uint) storage curriedAllow = _allowance[outbound_tkn][inbound_tkn][owner];
      uint allowed = curriedAllow[msg.sender];
      require(allowed >= amount, "mgv/lowAllowance");
      curriedAllow[msg.sender] = allowed - amount;

      emit Approval(outbound_tkn, inbound_tkn, owner, msg.sender, allowed - amount);
    }
  }
}
MgvGovernable.sol
2
3
4
5
6
pragma solidity ^0.8.10;

import {MgvLib, IMgvMonitor, IERC20, Leaf, Field, Density, DensityLib, OLKey, DirtyFieldLib} from "./MgvLib.sol";
import "@mgv/src/core/MgvCommon.sol";

Contains governance functions, to reduce Mangrove contract size

8
contract MgvGovernable is MgvCommon {

authOnly check

10
11
12
13
14
15
16
  function authOnly() internal view {
    unchecked {
      require(msg.sender == _governance || msg.sender == address(this) || _governance == address(0), "mgv/unauthorized");
    }
  }

Transfer ERC20 tokens to governance.

If this function is called while an order is executing, the reentrancy may prevent a taker from receiving their tokens. This is fine as the order execution will then fail and the tx will revert. So the most a malicious governance can do is render Mangrove unusable.

21
22
23
24
25
  function withdrawERC20(address tokenAddress, uint value) external {
    authOnly();
    require(transferToken(tokenAddress, _governance, value), "mgv/withdrawERC20Fail");
  }

Set configuration and Mangrove state


        

Locals


        

active

30
31
32
33
  function activate(OLKey memory olKey, uint fee, uint density96X32, uint offer_gasbase) public {
    unchecked {
      authOnly();
      bytes32 olKeyHash = olKey.hash();

save hash->key mapping

35
36
      _olKeys[olKeyHash] = olKey;
      OfferList storage offerList = offerLists[olKeyHash];

activate market

38
39
40
41
42
      offerList.local = offerList.local.active(true);
      emit SetActive(olKey.hash(), olKey.outbound_tkn, olKey.inbound_tkn, olKey.tickSpacing, true);
      setFee(olKey, fee);
      setDensity96X32(olKey, density96X32);
      setGasbase(olKey, offer_gasbase);

warm level1s

44
45
46
47
48
49
50
51
52
53
54
55
      offerList.level1s[-1] = DirtyFieldLib.DIRTY_EMPTY;
      offerList.level1s[0] = DirtyFieldLib.DIRTY_EMPTY;
    }
  }

  function deactivate(OLKey memory olKey) public {
    authOnly();
    OfferList storage offerList = offerLists[olKey.hash()];
    offerList.local = offerList.local.active(false);
    emit SetActive(olKey.hash(), olKey.outbound_tkn, olKey.inbound_tkn, olKey.tickSpacing, false);
  }

fee

57
58
59
  function setFee(OLKey memory olKey, uint fee) public {
    unchecked {
      authOnly();

fee is in basis points, i.e. in percents of a percent.

61
62
63
64
65
66
67
      require(LocalLib.fee_check(fee), LocalLib.fee_size_error);
      OfferList storage offerList = offerLists[olKey.hash()];
      offerList.local = offerList.local.fee(fee);
      emit SetFee(olKey.hash(), fee);
    }
  }

density


        

Useless if global.useOracle != 0 and oracle returns a valid density.


        

Density is given as a 96.32 fixed point number. It will be stored as a 9-bit float and be approximated towards 0. The maximum error is 20%. See DensityLib for more information.

71
72
73
74
  function setDensity96X32(OLKey memory olKey, uint density96X32) public {
    unchecked {
      authOnly();
76
      OfferList storage offerList = offerLists[olKey.hash()];

Checking the size of density is necessary to prevent overflow before storing density as a float.

78
79
80
81
82
83
84
      require(DensityLib.checkDensity96X32(density96X32), "mgv/config/density96X32/wrong");

      offerList.local = offerList.local.densityFrom96X32(density96X32);
      emit SetDensity96X32(olKey.hash(), density96X32);
    }
  }

gasbase

86
87
88
  function setGasbase(OLKey memory olKey, uint offer_gasbase) public {
    unchecked {
      authOnly();

Checking the size of offer_gasbase is necessary to prevent a) data loss when copied to an OfferDetail struct and b) overflow when used in calculations.

90
      require(LocalLib.kilo_offer_gasbase_check(offer_gasbase / 1e3), LocalLib.kilo_offer_gasbase_size_error);

require(uint24(offer_gasbase) == offer_gasbase, “mgv/config/offer_gasbase/24bits”);


        
93
94
95
96
97
98
      OfferList storage offerList = offerLists[olKey.hash()];
      offerList.local = offerList.local.offer_gasbase(offer_gasbase);
      emit SetGasbase(olKey.hash(), offer_gasbase);
    }
  }

Globals


        

kill

101
102
103
104
105
106
107
108
  function kill() public {
    unchecked {
      authOnly();
      internal_global = internal_global.dead(true);
      emit Kill();
    }
  }

gasprice


        

Useless if global.useOracle is != 0

111
112
113
114
115
  function setGasprice(uint gasprice) public {
    unchecked {
      authOnly();
      require(GlobalLib.gasprice_check(gasprice), GlobalLib.gasprice_size_error);
117
118
119
120
121
122
      internal_global = internal_global.gasprice(gasprice);
      emit SetGasprice(gasprice);
    }
  }

gasmax

124
125
126
  function setGasmax(uint gasmax) public {
    unchecked {
      authOnly();

Since any new gasreq is bounded above by config.gasmax, this check implies that all offers’ gasreq is 24 bits wide at most.

128
      require(GlobalLib.gasmax_check(gasmax), GlobalLib.gasmax_size_error);
130
131
132
133
134
      internal_global = internal_global.gasmax(gasmax);
      emit SetGasmax(gasmax);
    }
  }

maxRecursionDepth

136
137
138
139
140
141
142
143
144
  function setMaxRecursionDepth(uint maxRecursionDepth) public {
    unchecked {
      authOnly();
      require(GlobalLib.maxRecursionDepth_check(maxRecursionDepth), GlobalLib.maxRecursionDepth_size_error);
      internal_global = internal_global.maxRecursionDepth(maxRecursionDepth);
      emit SetMaxRecursionDepth(maxRecursionDepth);
    }
  }

maxGasreqForFailingOffers

146
147
148
149
150
151
152
153
154
155
156
157
  function setMaxGasreqForFailingOffers(uint maxGasreqForFailingOffers) public {
    unchecked {
      authOnly();
      require(
        GlobalLib.maxGasreqForFailingOffers_check(maxGasreqForFailingOffers),
        GlobalLib.maxGasreqForFailingOffers_size_error
      );
      internal_global = internal_global.maxGasreqForFailingOffers(maxGasreqForFailingOffers);
      emit SetMaxGasreqForFailingOffers(maxGasreqForFailingOffers);
    }
  }

governance

159
160
161
162
163
164
165
166
167
  function setGovernance(address governanceAddress) public {
    unchecked {
      authOnly();
      require(governanceAddress != address(0), "mgv/config/gov/not0");
      _governance = governanceAddress;
      emit SetGovernance(governanceAddress);
    }
  }

monitor

169
170
171
172
173
174
175
176
  function setMonitor(address monitor) public {
    unchecked {
      authOnly();
      internal_global = internal_global.monitor(monitor);
      emit SetMonitor(monitor);
    }
  }

useOracle

178
179
180
181
182
183
184
185
  function setUseOracle(bool useOracle) public {
    unchecked {
      authOnly();
      internal_global = internal_global.useOracle(useOracle);
      emit SetUseOracle(useOracle);
    }
  }

notify

187
188
189
190
191
192
193
194
  function setNotify(bool notify) public {
    unchecked {
      authOnly();
      internal_global = internal_global.notify(notify);
      emit SetNotify(notify);
    }
  }
}
MgvView.sol
2
3
4
5
6
pragma solidity ^0.8.10;

import "@mgv/src/core/MgvLib.sol";
import "@mgv/src/core/MgvCommon.sol";

Contains view functions, to reduce Mangrove contract size

8
contract MgvView is MgvCommon {

Configuration Reads


        

Get the address of Mangrove’s governance. Only governance can successfully call functions of MgvGovernable.

11
12
13
14
  function governance() external view returns (address) {
    return _governance;
  }

Reading the configuration for an offer list involves reading the config global to all offer lists and the local one. In addition, a global parameter (gasprice) and a local one (density) may be read from the oracle.

16
17
18
19
20
21
22
  function config(OLKey memory olKey) external view returns (Global _global, Local _local) {
    unchecked {
      (_global, _local,) = _config(olKey);
      unlockedOfferListOnly(_local);
    }
  }

Sugar for getting only local config

24
25
26
27
28
29
30
  function local(OLKey memory olKey) external view returns (Local _local) {
    unchecked {
      (, _local,) = _config(olKey);
      unlockedOfferListOnly(_local);
    }
  }

Reading the global configuration. In addition, a parameter (gasprice) may be read from the oracle.

32
33
34
35
36
37
38
39
40
41
42
43
  function global() external view returns (Global _global) {
    unchecked {
      (_global,,) = _config(OLKey(address(0), address(0), 0));
    }
  }

  function balanceOf(address maker) external view returns (uint balance) {
    unchecked {
      balance = _balanceOf[maker];
    }
  }

Tick tree view functions

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
  function leafs(OLKey memory olKey, int index) external view returns (Leaf) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      unlockedOfferListOnly(offerList.local);
      return offerList.leafs[index].clean();
    }
  }

  function level3s(OLKey memory olKey, int index) external view returns (Field) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      Local _local = offerList.local;
      unlockedOfferListOnly(_local);

      if (_local.bestBin().level3Index() == index) {
        return _local.level3();
      } else {
        return offerList.level3s[index].clean();
      }
    }
  }

  function level2s(OLKey memory olKey, int index) external view returns (Field) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      Local _local = offerList.local;
      unlockedOfferListOnly(_local);

      if (_local.bestBin().level2Index() == index) {
        return _local.level2();
      } else {
        return offerList.level2s[index].clean();
      }
    }
  }

  function level1s(OLKey memory olKey, int index) external view returns (Field) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      Local _local = offerList.local;
      unlockedOfferListOnly(_local);

      if (_local.bestBin().level1Index() == index) {
        return _local.level1();
      } else {
        return offerList.level1s[index].clean();
      }
    }
  }

  function root(OLKey memory olKey) external view returns (Field) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      Local _local = offerList.local;
      unlockedOfferListOnly(_local);
      return _local.root();
    }
  }

Offer list view functions


        

Function to check whether given an offer list is locked. Contrary to other offer list view functions, this does not revert if the offer list is locked.

108
109
110
111
112
113
  function locked(OLKey memory olKey) external view returns (bool) {
    unchecked {
      return offerLists[olKey.hash()].local.lock();
    }
  }

Convenience function to get best offer of the given offerList

115
116
117
118
119
120
121
122
123
  function best(OLKey memory olKey) external view returns (uint offerId) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      Local _local = offerList.local;
      unlockedOfferListOnly(_local);
      return offerList.leafs[_local.bestBin().leafIndex()].clean().bestOfferId();
    }
  }

Get the olKey that corresponds to a hash, only works for offer lists that have been activated > 0 times

125
126
127
128
129
130
  function olKeys(bytes32 olKeyHash) external view returns (OLKey memory olKey) {
    unchecked {
      olKey = _olKeys[olKeyHash];
    }
  }

Offer view functions


        

Get an offer in packed format

134
135
136
137
138
139
140
141
  function offers(OLKey memory olKey, uint offerId) external view returns (Offer offer) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      unlockedOfferListOnly(offerList.local);
      return offerList.offerData[offerId].offer;
    }
  }

Get an offer detail in packed format

143
144
145
146
147
148
149
150
  function offerDetails(OLKey memory olKey, uint offerId) external view returns (OfferDetail offerDetail) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      unlockedOfferListOnly(offerList.local);
      return offerList.offerData[offerId].detail;
    }
  }

Get both offer and offer detail in packed format

152
153
154
155
156
157
158
159
160
  function offerData(OLKey memory olKey, uint offerId) external view returns (Offer offer, OfferDetail offerDetail) {
    unchecked {
      OfferList storage offerList = offerLists[olKey.hash()];
      unlockedOfferListOnly(offerList.local);
      OfferData storage _offerData = offerList.offerData[offerId];
      return (_offerData.offer, _offerData.detail);
    }
  }

Permit-related view functions

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
  function allowance(address outbound_tkn, address inbound_tkn, address owner, address spender)
    external
    view
    returns (uint amount)
  {
    unchecked {
      amount = _allowance[outbound_tkn][inbound_tkn][owner][spender];
    }
  }

  function nonces(address owner) external view returns (uint nonce) {
    unchecked {
      nonce = _nonces[owner];
    }
  }

Note: the accessor for DOMAIN_SEPARATOR is defined in MgvOfferTakingWithPermit

180
181
182
183
184
185
  function PERMIT_TYPEHASH() external pure returns (bytes32) {
    unchecked {
      return _PERMIT_TYPEHASH;
    }
  }
}
MgvAppendix.sol
2
3
4
5
6
7
pragma solidity ^0.8.10;

import "@mgv/src/core/MgvLib.sol";
import {MgvView} from "@mgv/src/core/MgvView.sol";
import {MgvGovernable} from "@mgv/src/core/MgvGovernable.sol";

The MgvAppendix contract contains Mangrove functions related to:

  • Getters (view functions)
  • Governance functions

Due to bytecode size limits, not all Mangrove code can reside at the address of Mangrove. So when constructed, Mangrove creates a MgvAppendix instance and sets up a fallback to that instance when receiving an unknown function selector.

The functions moved to MgvAppendix have been selected because they are less gas-sensitive than core Mangrove functionality.

15
contract MgvAppendix is MgvView, MgvGovernable {}
Mangrove.sol
2
3
4
5
6
7
8
9
10
pragma solidity ^0.8.10;

import "@mgv/src/core/MgvLib.sol";

import {MgvOfferMaking} from "./MgvOfferMaking.sol";
import {MgvOfferTakingWithPermit} from "./MgvOfferTakingWithPermit.sol";
import {MgvAppendix} from "@mgv/src/core/MgvAppendix.sol";
import {MgvGovernable} from "@mgv/src/core/MgvGovernable.sol";

The Mangrove contract inherits both the maker and taker functionality. It also deploys MgvAppendix when constructed.

12
13
14
15
16
17
18
19
20
contract Mangrove is MgvOfferTakingWithPermit, MgvOfferMaking {
  address internal immutable APPENDIX;

  constructor(address governance, uint gasprice, uint gasmax) MgvOfferTakingWithPermit("Mangrove") {
    unchecked {
      emit NewMgv();

      APPENDIX = address(new MgvAppendix());

Set initial gasprice, gasmax, recursion depth and max gasreq for failing offers. See MgvAppendix for why this happens through a delegatecall.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
      bool success;
      (success,) = APPENDIX.delegatecall(abi.encodeCall(MgvGovernable.setGasprice, (gasprice)));
      require(success, "mgv/ctor/gasprice");
      (success,) = APPENDIX.delegatecall(abi.encodeCall(MgvGovernable.setGasmax, (gasmax)));
      require(success, "mgv/ctor/gasmax");
      (success,) =
        APPENDIX.delegatecall(abi.encodeCall(MgvGovernable.setMaxRecursionDepth, (INITIAL_MAX_RECURSION_DEPTH)));
      require(success, "mgv/ctor/maxRecursionDepth");
      (success,) = APPENDIX.delegatecall(
        abi.encodeCall(
          MgvGovernable.setMaxGasreqForFailingOffers, (INITIAL_MAX_GASREQ_FOR_FAILING_OFFERS_MULTIPLIER * gasmax)
        )
      );
      require(success, "mgv/ctor/maxGasreqForFailingOffers");

Initially, governance is open to anyone so that Mangrove can set its own default parameters. After that, governance is set to the governance constructor argument.

37
38
39
40
41
      (success,) = APPENDIX.delegatecall(abi.encodeCall(MgvGovernable.setGovernance, (governance)));
      require(success, "mgv/ctor/governance");
    }
  }

Fallback to APPENDIX if function selector is unknown.

43
44
45
46
47
48
49
50
51
52
53
  fallback(bytes calldata callData) external returns (bytes memory) {
    (bool success, bytes memory res) = APPENDIX.delegatecall(callData);
    if (success) {
      return res;
    } else {
      assembly ("memory-safe") {
        revert(add(res, 32), mload(res))
      }
    }
  }
}
DensityLib.sol
2
3
4
5
6
7
pragma solidity ^0.8.17;

import {Field} from "@mgv/lib/core/TickTreeLib.sol";
import {ONES} from "@mgv/lib/core/Constants.sol";
import {BitLib} from "@mgv/lib/core/BitLib.sol";

The density of a semibook is the number of outbound tokens per gas required. An offer must a respect a semibook’s density.

Density can be < 1.

The density of a semibook is stored as a 9 bits float. For convenience, governance functions read density as a 96.32 fixed point number. The functions below give conversion utilities between the two formats

As a guideline, fixed-point densities should be uints and should use hungarian notation (for instance uint density96X32). Floating-point densities should use the Density user-defined type.

The float <-> fixed conversion is format agnostic but the expectation is that fixed points are 96x32, and floats are 2-bit mantissa, 7bit exponent with bias 32.

The encoding is nonstandard so the code can be simpler.

There are no subnormal floats in this encoding, [exp][mantissa] means:

if exp is 0 or 1:   0bmantissa   * 2^-32
otherwise:          0b1.mantissa * 2^(exp-32)

so the small values have some holes:

coeff   exponent  available    |   coeff   exponent  available
--------------------------------------------------------------
0b0.00                         |  0b1.10     -31
0b1.00     -32                 |  0b1.11     -31        no
0b1.01     -32        no       |  0b1.00     -30
0b1.10     -32        no       |  0b1.01     -30
0b1.11     -32        no       |  0b1.10     -30
0b1.00     -31                 |  0b1.11     -30
0b1.01     -31        no       |  0b1.00     -29
43
44
45
46
47
type Density is uint;
using DensityLib for Density global;

library DensityLib {

Numbers in this file assume that density is 9 bits in structs.ts

49
50
51
52
53
54
55
56
57
58
59
60
  uint constant BITS = 9; // must match structs.ts
  uint constant MANTISSA_BITS = 2;
  uint constant SUBNORMAL_LIMIT = ~(ONES << (MANTISSA_BITS+1));
  uint constant MANTISSA_MASK = ~(ONES << MANTISSA_BITS);
  uint constant MASK = ~(ONES << BITS);
  uint constant MANTISSA_INTEGER = 1 << MANTISSA_BITS;
  uint constant EXPONENT_BITS = BITS - MANTISSA_BITS;

  function eq(Density a, Density b) internal pure returns (bool) { unchecked {
    return Density.unwrap(a) == Density.unwrap(b);
  }}

Check the size of a fixed-point formatted density

62
63
64
65
  function checkDensity96X32(uint density96X32) internal pure returns (bool) { unchecked {
    return density96X32 < (1<<(96+32));
  }}

fixed-point -> float conversion


        

Warning: no bit cleaning (expected to be done by Local’s code), no checking that the input is on 128 bits.


        

floats with [exp=1] are not in the image of fromFixed. They are considered noncanonical.

69
70
71
72
  function from96X32(uint density96X32) internal pure returns (Density) { unchecked {
    if (density96X32 <= MANTISSA_MASK) {
      return Density.wrap(density96X32);
    }

invariant: exp >= 2 (so not 0)

74
75
76
77
    uint exp = BitLib.fls(density96X32);
    return make(density96X32 >> (exp-MANTISSA_BITS),exp);
  }}

float -> fixed-point conversion

79
  function to96X32(Density density) internal pure returns (uint) { unchecked {

also accepts floats not generated by fixedToFloat, i.e. with exp=1

81
82
83
    if (Density.unwrap(density) <= SUBNORMAL_LIMIT) {
      return Density.unwrap(density) & MANTISSA_MASK;
    }

assumes exp is on the right number of bits


        

invariant: exp >= 2

86
87
88
89
90
91
92
93
94
95
96
97
    uint shift = (Density.unwrap(density) >> MANTISSA_BITS) - MANTISSA_BITS;
    return ((Density.unwrap(density) & MANTISSA_MASK) | MANTISSA_INTEGER) << shift;
  }}

  function mantissa(Density density) internal pure returns (uint) { unchecked {
    return Density.unwrap(density) & MANTISSA_MASK;
  }}

  function exponent(Density density) internal pure returns (uint) { unchecked {
    return Density.unwrap(density) >> MANTISSA_BITS;
  }}

Make a float from a mantissa and an exponent. May make a noncanonical float.


        

Warning: no checks

100
101
102
103
  function make(uint _mantissa, uint _exponent) internal pure returns (Density) { unchecked {
    return Density.wrap((_exponent << MANTISSA_BITS) | (_mantissa & MANTISSA_MASK));
  }}

None of the functions below will overflow if m is 96bit wide. Density being a 96.32 number is useful because:

  • Most of its range is representable with the 9-bits float format chosen
  • It does not overflow when multiplied with a 96bit number, which is the size chosen to represent token amounts in Mangrove.
  • Densities below 2^-32 need > 4e9 gasreq to force gives > 0, which is not realistic

        

Multiply the density with m, rounded towards zero.


        

May overflow if |m|>9

112
113
114
  function multiply(Density density, uint m) internal pure returns (uint) { unchecked {
    return (m * density.to96X32())>>32;
  }}

Multiply the density with m, rounded towards +infinity.


        

May overflow if |m|>96

117
118
119
120
121
  function multiplyUp(Density density, uint m) internal pure returns (uint) { unchecked {
    uint part = m * density.to96X32();
    return (part >> 32) + (part%(2<<32) == 0 ? 0 : 1);
  }}

Convenience function: get a fixed-point density from the given parameters. Computes the price of gas in outbound tokens (base units), then multiplies by cover_factor.


        

Warning: you must multiply input usd prices by 100


        

not supposed to be gas optimized

125
126
127
128
129
130
131
  function paramsTo96X32(
    uint outbound_decimals, 
    uint gasprice_in_Mwei, 
    uint eth_in_centiusd, 
    uint outbound_display_in_centiusd, 
    uint cover_factor
  ) internal pure returns (uint) {

Do not use unchecked here

133
134
    require(uint8(outbound_decimals) == outbound_decimals,"DensityLib/fixedFromParams1/decimals/wrong");
    uint num = cover_factor * gasprice_in_Mwei * (10**outbound_decimals) * eth_in_centiusd;

use * instead of << to trigger overflow check

136
137
138
    return (num * (1 << 32)) / (outbound_display_in_centiusd * 1e12);
  }

Version with token in Mwei instead of usd

140
141
142
143
144
145
  function paramsTo96X32(
    uint outbound_decimals, 
    uint gasprice_in_Mwei, 
    uint outbound_display_in_Mwei, 
    uint cover_factor
  ) internal pure returns (uint) {

Do not use unchecked here.

147
148
    require(uint8(outbound_decimals) == outbound_decimals,"DensityLib/fixedFromParams2/decimals/wrong");
    uint num = cover_factor * gasprice_in_Mwei * (10**outbound_decimals);

use * instead of << to trigger overflow check

150
151
152
    return (num * (1 << 32)) / outbound_display_in_Mwei;
  }
}
TickTreeLib.sol
2
3
4
5
6
7
8
9
10
pragma solidity ^0.8.17;

import "@mgv/lib/core/Constants.sol";
import {Tick} from "@mgv/lib/core/TickLib.sol";
import {BitLib} from "@mgv/lib/core/BitLib.sol";
import {console2 as csf} from "@mgv/forge-std/console2.sol";
import {Local} from "@mgv/src/preprocessed/Local.post.sol";

Libraries for tick tree manipulation

Offers in Mangrove are structured in a tree so that offer insertion, removal, and update can happen in constant time.

The tree has the following structure: nodes at height 0, 1, 2 and 3 are bit fields (type Field) and nodes at height 4 (leaves) are arrays of pairs of offers (type Leaf).

  • The root node is a 2-bit Field and has 2 child nodes.
  • The nodes below are called level1 nodes and are 64-bit Fields, with 64 child nodes each.
  • The nodes below are called level2 nodes and are 64-bit Fields with 64 child nodes each.
  • The nodes below are called level3 nodes and are 64-bit Fields with 64 child nodes each.
  • The nodes below are called leaf nodes and are arrays of pairs of offers. Each pair of offers represents the first and last offer of a bin.
  • Bins are linked lists of offers.

For each field, the i-th bit (starting from least significant) is set if there is at least one bin holding offers below the i-th child of the field.


        

Globally enable leaf.method(...)

28
29
30
type Leaf is uint;
using LeafLib for Leaf global;

Each Leaf holds information about 4 bins as follows: [firstId,lastId] [firstId,lastId] [firstId,lastId] [firstId,lastId]. For each bin firstId is used by marketOrder to start consuming offers in the bin (each offer contains a pointer to the next offer in the bin, until lastId is reacher). lastId is used when inserting offers in the bin: the newly inserted offer replaces the last offer.

Each leaf has an index: it is the number of leaves before it.

35
36
37
library LeafLib {
  Leaf constant EMPTY = Leaf.wrap(uint(0));

Checks if a leaf is dirty or not (see below for more on dirty leaves).

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  function dirty(Leaf leaf) internal pure returns (DirtyLeaf) {
    unchecked {
      assembly ("memory-safe") {
        leaf := or(iszero(leaf),leaf)
      }
      return DirtyLeaf.wrap(Leaf.unwrap(leaf));
    }
  }

  function eq(Leaf leaf1, Leaf leaf2) internal pure returns (bool) {
    unchecked {
      return Leaf.unwrap(leaf1) == Leaf.unwrap(leaf2);
    }
  }

  function isEmpty(Leaf leaf) internal pure returns (bool) {
    unchecked {
      return Leaf.unwrap(leaf) == Leaf.unwrap(EMPTY);
    }
  }

bool -> int cast

61
62
63
64
65
66
67
68
  function uint_of_bool(bool b) internal pure returns (uint u) {
    unchecked {
      assembly ("memory-safe") {
        u := b
      }
    }
  }

Set either tick’s first (at possize) or last (at possize + 1)

70
71
72
73
74
75
76
77
78
79
80
81
  function setPosFirstOrLast(Leaf leaf, uint pos, uint id, bool last) internal pure returns (Leaf) {
    unchecked {
      uint before = OFFER_BITS * ((pos * 2) + uint_of_bool(last));

      uint cleanupMask = ~(OFFER_MASK << (256 - OFFER_BITS - before));

      uint shiftedId = id << (256 - OFFER_BITS - before);
      uint newLeaf = Leaf.unwrap(leaf) & cleanupMask | shiftedId;
      return Leaf.wrap(newLeaf);
    }
  }

Assume leaf is the leaf of bin. Set the first offer of bin in leaf to id.

83
84
85
86
87
88
89
  function setBinFirst(Leaf leaf, Bin bin, uint id) internal pure returns (Leaf) {
    unchecked {
      uint posInLeaf = bin.posInLeaf();
      return setPosFirstOrLast(leaf, posInLeaf, id, false);
    }
  }

Assume leaf is the leaf of bin. Set the last offer of bin in leaf to id.

91
92
93
94
95
96
97
  function setBinLast(Leaf leaf, Bin bin, uint id) internal pure returns (Leaf) {
    unchecked {
      uint posInLeaf = bin.posInLeaf();
      return setPosFirstOrLast(leaf, posInLeaf, id, true);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes leaf is the leaf of bin. Erase contents of bins in leaf to (not including) bin.

100
101
102
103
104
105
106
  function eraseBelow(Leaf leaf, Bin bin) internal pure returns (Leaf) {
    unchecked {
      uint mask = ONES >> ((bin.posInLeaf() + 1) * OFFER_BITS * 2);
      return Leaf.wrap(Leaf.unwrap(leaf) & mask);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes leaf is the leaf of bin. Erase contents of bins in leaf from (not including) bin.

109
110
111
112
113
114
115
116
  function eraseAbove(Leaf leaf, Bin bin) internal pure returns (Leaf) {
    unchecked {
      uint mask = ~(ONES >> (bin.posInLeaf() * OFFER_BITS * 2));
      return Leaf.wrap(Leaf.unwrap(leaf) & mask);
    }
  }

This function is not used by Mangrove but is part of the library.


        

Return the id of the first offer in bin, assuming bin’s leaf is leaf.

119
120
121
122
123
124
  function firstOfBin(Leaf leaf, Bin bin) internal pure returns (uint) {
    unchecked {
      return firstOfPos(leaf, bin.posInLeaf());
    }
  }

Return the id of the last offer in bin, assuming bin’s leaf is leaf.

126
127
128
129
130
131
  function lastOfBin(Leaf leaf, Bin bin) internal pure returns (uint) {
    unchecked {
      return lastOfPos(leaf, bin.posInLeaf());
    }
  }

Return first offer of pair in position pos

133
134
135
136
137
138
139
  function firstOfPos(Leaf leaf, uint pos) internal pure returns (uint) {
    unchecked {
      uint raw = Leaf.unwrap(leaf);
      return uint(raw << (pos * OFFER_BITS * 2) >> (256 - OFFER_BITS));
    }
  }

Return second offer of pair in position pos

141
142
143
144
145
146
147
  function lastOfPos(Leaf leaf, uint pos) internal pure returns (uint) {
    unchecked {
      uint raw = Leaf.unwrap(leaf);
      return uint(raw << (OFFER_BITS * ((pos * 2) + 1)) >> (256 - OFFER_BITS));
    }
  }

Return the position of the first pair of leaf (0, 1, 2 or 3) that has a nonzero offer.

Explanation: Note that unlike in fields, have their low bin on the most significant bits. pos is initially 1 if leaf has some nonzero bit in its MSB half, 0 otherwise. Then pos is A | B, where A is iszero(pos)<<1, so A is 0 if leaf has some nonzero bit in its MSB half, 2 otherwise. And B is 0 if leaf >> (pos << 7) has some nonzero bit in its most significant 192 bits, 1 otherwise.

154
155
156
157
158
159
160
  function bestNonEmptyBinPos(Leaf leaf) internal pure returns (uint pos) {
    assembly("memory-safe") {
      pos := gt(leaf,0xffffffffffffffffffffffffffffffff)
      pos := or(shl(1,iszero(pos)),iszero(gt(shr(shl(7,pos),leaf),0xffffffffffffffff)))
    }
  }

Return the offer id of the first offer of the first non-empty pair in leaf.

162
163
164
165
166
167
168
169
  function bestOfferId(Leaf leaf) internal pure returns (uint offerId) {
    unchecked {
      return firstOfPos(leaf,bestNonEmptyBinPos(leaf));
    }
  }
}

Bins are numbered from MIN_BIN to MAX_BIN (inclusive). Each bin contains the offers at a given price. For a given tickSpacing, bins represent the following prices (centered on the central bin):

...
1.0001^-(tickSpacing*2)
1.0001^-(tickSpacing*1)
1.0001
1.0001^(tickSpacing*1)
1.0001^(tickSpacing*2)
...

There are 4 bins per leaf, 4 * 64 bins per level3, etc. The leaf of a bin is the leaf that holds its first/last offer id. The level3 of a bin is the level3 field above its leaf; the level2 of a bin is the level2 field above its level3, etc.


        

Globally enable bin.method(...)

184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
type Bin is int;
using TickTreeLib for Bin global;

library TickTreeLib {

  function eq(Bin bin1, Bin bin2) internal pure returns (bool) {
    unchecked {
      return Bin.unwrap(bin1) == Bin.unwrap(bin2);
    }
  }

  function inRange(Bin bin) internal pure returns (bool) {
    unchecked {
      return Bin.unwrap(bin) >= MIN_BIN && Bin.unwrap(bin) <= MAX_BIN;
    }
  }

This function is not used by Mangrove but is part of the library for convenience.


        

Not optimized for gas. Returns the bin induced by the branch formed by the arguments. Returned value will make no sense if any of the Field arguments are empty.

204
205
206
207
208
209
210
211
  function bestBinFromBranch(uint binPosInLeaf,Field level3, Field level2, Field level1, Field root) internal pure returns (Bin) {
    unchecked {
      Local local;
      local = local.binPosInLeaf(binPosInLeaf).level3(level3).level2(level2).level1(level1).root(root);
      return bestBinFromLocal(local);
    }
  }

Returns tick held by the bin, given a tickSpacing.

213
214
215
216
  function tick(Bin bin, uint tickSpacing) internal pure returns (Tick) {
    return Tick.wrap(Bin.unwrap(bin) * int(tickSpacing));
  }

Returns the bin induced by the branch held in local. Returned value will make no sense if any of the fields of local are empty.

218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
  function bestBinFromLocal(Local local) internal pure returns (Bin) {
    unchecked {
      uint ubin = local.binPosInLeaf() |
        ((BitLib.ctz64(Field.unwrap(local.level3())) |
          (BitLib.ctz64(Field.unwrap(local.level2())) |
            (BitLib.ctz64(Field.unwrap(local.level1())) |
              uint(
                (int(BitLib.ctz64(Field.unwrap(local.root())))-ROOT_SIZE/2) << LEVEL_SIZE_BITS)) 
              << LEVEL_SIZE_BITS)
            << LEVEL_SIZE_BITS)
          << LEAF_SIZE_BITS);
      return Bin.wrap(int(ubin));
    }
  }

Returns the index of the leaf that holds bin

234
235
236
237
238
239
  function leafIndex(Bin bin) internal pure returns (int) {
    unchecked {
      return Bin.unwrap(bin) >> LEAF_SIZE_BITS;
    }
  }

Returns the position of bin in its leaf.

241
242
243
244
245
246
  function posInLeaf(Bin bin) internal pure returns (uint) {
    unchecked {
      return uint(Bin.unwrap(bin)) & LEAF_SIZE_MASK;
    }
  }

Returns the index of bin’s level3

248
249
250
251
252
253
  function level3Index(Bin bin) internal pure returns (int) {
    unchecked {
      return Bin.unwrap(bin) >> (LEAF_SIZE_BITS + LEVEL_SIZE_BITS);
    }
  }

Returns the position of bin’s leaf in bin’s level3

255
256
257
258
259
260
  function posInLevel3(Bin bin) internal pure returns (uint) {
    unchecked {
      return uint(bin.leafIndex()) & LEVEL_SIZE_MASK;
    }
  }

Returns the index of bin’s level2

262
263
264
265
266
267
  function level2Index(Bin bin) internal pure returns (int) {
    unchecked {
      return Bin.unwrap(bin) >> (LEAF_SIZE_BITS + 2* LEVEL_SIZE_BITS);
    }
  }

Returns the position of bin’s level3 in bin’s level2

269
270
271
272
273
274
  function posInLevel2(Bin bin) internal pure returns (uint) {
    unchecked {
      return uint(bin.level3Index()) & LEVEL_SIZE_MASK;
    }
  }

Returns the index of bin’s level1

276
277
278
279
280
281
  function level1Index(Bin bin) internal pure returns (int) {
    unchecked {
      return Bin.unwrap(bin) >> (LEAF_SIZE_BITS + 3 * LEVEL_SIZE_BITS);
    }
  }

Returns the position of bin’s level2 in bin’s level1

283
284
285
286
287
288
  function posInLevel1(Bin bin) internal pure returns (uint) {
    unchecked {
      return uint(bin.level2Index()) & LEVEL_SIZE_MASK;
    }
  }

Returns the position of bin’s level1 in root

290
291
292
293
294
295
  function posInRoot(Bin bin) internal pure returns (uint) {
    unchecked {
      return uint(bin.level1Index() + ROOT_SIZE / 2);
    }
  }

<, typed for bin

297
298
299
300
301
302
  function strictlyBetter(Bin bin1, Bin bin2) internal pure returns (bool) {
    unchecked {
      return Bin.unwrap(bin1) < Bin.unwrap(bin2);
    }
  }

<=, typed for bin

304
305
306
307
308
309
310
311
  function better(Bin bin1, Bin bin2) internal pure returns (bool) {
    unchecked {
      return Bin.unwrap(bin1) <= Bin.unwrap(bin2);
    }
  }  

}

Globally enable field.method(...)

313
314
315
type Field is uint;
using FieldLib for Field global;

Fields are bit fields. Each bit position of a field corresponds to a child of the node in the tick tree. The i-th bit of a field is set iff its child is the parent of at least one non-emtpy field.

Using bit fields market orders can locate the next bin containing an offer in constant time: once a leaf has been emptied, one must at most walk up along the branch of the current bin, up to the first 1 in a field, then go down to the of the tree, then go down the corresponding child to the nonempty bin found.


        

In fields, positions are counted from the least significant bits

322
323
324
library FieldLib {
  Field constant EMPTY = Field.wrap(uint(0));

Checks if a field is dirty or not (see below for more on dirty fields).

326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
  function dirty(Field field) internal pure returns (DirtyField) {
    unchecked {
      assembly ("memory-safe") {
        field := or(TOPBIT,field)
      }
      return DirtyField.wrap(Field.unwrap(field));
    }
  }

  function eq(Field field1, Field field2) internal pure returns (bool) {
    unchecked {
      return Field.unwrap(field1) == Field.unwrap(field2);
    }
  }

  function isEmpty(Field field) internal pure returns (bool) {
    unchecked {
      return Field.unwrap(field) == Field.unwrap(EMPTY);
    }
  }

Flip the bit at the position of bin’s leaf

348
349
350
351
352
353
354
355
  function flipBitAtLevel3(Field level3, Bin bin) internal pure returns (Field) {
    unchecked {
      uint pos = bin.posInLevel3();
      level3 = Field.wrap(Field.unwrap(level3) ^ (1 << pos));
      return level3;
    }
  }

Flip the bit at the position of bin’s level3

357
358
359
360
361
362
363
364
  function flipBitAtLevel2(Field level2, Bin bin) internal pure returns (Field) {
    unchecked {
      uint pos = bin.posInLevel2();
      level2 = Field.wrap(Field.unwrap(level2) ^ (1 << pos));
      return level2;
    }
  }

Flip the bit at the position of bin’s level2

366
367
368
369
370
371
372
373
  function flipBitAtLevel1(Field level1, Bin bin) internal pure returns (Field) {
    unchecked {
      uint pos = bin.posInLevel1();
      level1 = Field.wrap(Field.unwrap(level1) ^ (1 << pos));
      return level1;
    }
  }

Flip the bit at the position of bin’s level1

375
376
377
378
379
380
381
382
  function flipBitAtRoot(Field root, Bin bin) internal pure returns (Field) {
    unchecked {
      uint pos = bin.posInRoot();
      root = Field.wrap(Field.unwrap(root) ^ (1 << pos));
      return root;
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes field is the level3 of bin. Erase contents of field up to (not including) bin.

385
386
387
388
389
390
391
  function eraseBelowInLevel3(Field field, Bin bin) internal pure returns (Field) {
    unchecked {
      uint mask = ONES << (bin.posInLevel3() + 1);
      return Field.wrap(Field.unwrap(field) & mask);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes field is the level3 of bin. Erase contents of field from (not including) bin.

394
395
396
397
398
399
400
  function eraseAboveInLevel3(Field field, Bin bin) internal pure returns (Field) {
    unchecked {
      uint mask = ~(ONES << bin.posInLevel3());
      return Field.wrap(Field.unwrap(field) & mask);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes field is the level2 of bin. Erase contents of field up to (not including) bin.

403
404
405
406
407
408
409
  function eraseBelowInLevel2(Field field, Bin bin) internal pure returns (Field) {
    unchecked {
      uint mask = ONES << (bin.posInLevel2() + 1);
      return Field.wrap(Field.unwrap(field) & mask);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes field is the level2 of bin. Erase contents of field from (not including) bin.

412
413
414
415
416
417
418
  function eraseAboveInLevel2(Field field, Bin bin) internal pure returns (Field) {
    unchecked {
      uint mask = ~(ONES << bin.posInLevel2());
      return Field.wrap(Field.unwrap(field) & mask);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes field is the level1 of bin. Erase contents of field up to (not including) bin.

421
422
423
424
425
426
427
  function eraseBelowInLevel1(Field field, Bin bin) internal pure returns (Field) {
    unchecked {
      uint mask = ONES << (bin.posInLevel1() + 1);
      return Field.wrap(Field.unwrap(field) & mask);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes field is the level1 of bin. Erase contents of field from (not including) bin.

430
431
432
433
434
435
436
  function eraseAboveInLevel1(Field field, Bin bin) internal pure returns (Field) {
    unchecked {
      uint mask = ~(ONES << bin.posInLevel1());
      return Field.wrap(Field.unwrap(field) & mask);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes field is the root of bin. Erase contents of field up to (not including) bin.

439
440
441
442
443
444
445
  function eraseBelowInRoot(Field field, Bin bin) internal pure returns (Field) {
    unchecked {
      uint mask = ONES << (bin.posInRoot() + 1);
      return Field.wrap(Field.unwrap(field) & mask);
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Assumes field is the root of bin. Erase contents of field from (not including) bin.

448
449
450
451
452
453
454
  function eraseAboveInRoot(Field field, Bin bin) internal pure returns (Field) {
    unchecked {
      uint mask = ~(ONES << bin.posInRoot());
      return Field.wrap(Field.unwrap(field) & mask);
    }
  }

Returns first nonzero position of field. Will return 64 if field is empty

456
457
458
459
460
461
  function firstOnePosition(Field field) internal pure returns (uint) {
    unchecked {
      return BitLib.ctz64(Field.unwrap(field));
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Returns last nonzero position of field. Will return 256 if field is empty

464
465
466
467
468
469
  function lastOnePosition(Field field) internal pure returns (uint) {
    unchecked {
      return BitLib.fls(Field.unwrap(field));
    }
  }

Return index of the first nonempty level1 below root (root should not be empty)

471
472
473
474
475
476
  function firstLevel1Index(Field root) internal pure returns (int) {
    unchecked {
      return int(root.firstOnePosition()) - ROOT_SIZE / 2;
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Return index of the last nonempty level1 below root (root should not be empty)

479
480
481
482
483
484
  function lastLevel1Index(Field root) internal pure returns (int) {
    unchecked {
      return int(root.lastOnePosition()) - ROOT_SIZE / 2;
    }
  }

Return index of the first nonempty level2 below level1 assuming its index is level1Index (level1 should not be empty).

486
487
488
489
490
  function firstLevel2Index(Field level1, int level1Index) internal pure returns (int) {
    unchecked {
      return level1Index * LEVEL_SIZE + int(level1.firstOnePosition());
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Return index of the last nonempty level2 below level1 assuming its index is level1Index (level1 should not be empty).

493
494
495
496
497
498
  function lastLevel2Index(Field level1, int level1Index) internal pure returns (int) {
    unchecked {
      return level1Index * LEVEL_SIZE + int(level1.lastOnePosition());
    }
  }

Return index of the first nonempty level3 below level2 assuming its index is level2Index (level2 should not be empty).

500
501
502
503
504
505
  function firstLevel3Index(Field level2, int level2Index) internal pure returns (int) {
    unchecked {
      return level2Index * LEVEL_SIZE + int(level2.firstOnePosition());
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Return index of the last nonempty level3 below level2 assuming its index is level2Index (level2 should not be empty).

508
509
510
511
512
513
  function lastLevel3Index(Field level2, int level2Index) internal pure returns (int) {
    unchecked {
      return level2Index * LEVEL_SIZE + int(level2.lastOnePosition());
    }
  }

Return index of the first nonempty leaf below level3 assuming its index is level3Index (level3 should not be empty).

515
516
517
518
519
520
  function firstLeafIndex(Field level3, int level3Index) internal pure returns (int) {
    unchecked {
      return level3Index * LEVEL_SIZE + int(level3.firstOnePosition());
    }
  }

This function is not used by Mangrove but is useful for MgvReader.


        

Return index of the last nonempty leaf below level3 assuming its index is level3Index (level3 should not be empty).

523
524
525
526
527
528
529
530
  function lastLeafIndex(Field level3, int level3Index) internal pure returns (int) {
    unchecked {
      return level3Index * LEVEL_SIZE + int(level3.lastOnePosition());
    }
  }

 }

Clean/Dirty Fields and Leaves

To save gas, leaves and fields at never zeroed out but written with a dirty bit. This is especially helpful when the price oscillates quickly between two nodes.

Leaves don’t have ‘available bits’ so an empty leaf is encoded as 1. This is not a valid leaf because for every offer pair in a leaf, either both are 0 (the bin is empty) or both are nonzero (they are the same if the bin has a single offer).


        

Globally enable dirtyLeaf.method(...)

539
540
541
542
543
type DirtyLeaf is uint;
using DirtyLeafLib for DirtyLeaf global;

library DirtyLeafLib {
  

Return 0 if leaf is 1, leaf otherwise

545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
  function clean(DirtyLeaf leaf) internal pure returns (Leaf) {
    unchecked {
      assembly ("memory-safe") {
        leaf := xor(eq(leaf,ONE),leaf)
      }
      return Leaf.wrap(DirtyLeaf.unwrap(leaf));
    }
  }
  function isDirty(DirtyLeaf leaf) internal pure returns (bool) {
    unchecked {
      return DirtyLeaf.unwrap(leaf) == ONE;
    }
  }
  function eq(DirtyLeaf leaf1, DirtyLeaf leaf2) internal pure returns (bool) {
    unchecked {
      return DirtyLeaf.unwrap(leaf1) == DirtyLeaf.unwrap(leaf2);
    }
  }
}

We use TOPBIT as the optimized in-storage value since 1 is a valid Field value


        

For fields, it’s simpler, since they do not use 64 bits we store the word with top bit at 1 and everything else at 0 to mark emptiness.


        

Globally enable dirtyField.method(...)

570
571
572
573
574
575
576
type DirtyField is uint;
using DirtyFieldLib for DirtyField global;

library DirtyFieldLib {
  DirtyField constant DIRTY_EMPTY = DirtyField.wrap(TOPBIT);
  DirtyField constant CLEAN_EMPTY = DirtyField.wrap(0);

Return clean field with topbit set to 0

578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
  function clean(DirtyField field) internal pure returns (Field) {
    unchecked {
      assembly ("memory-safe") {
        field := and(NOT_TOPBIT,field)
      }
      return Field.wrap(DirtyField.unwrap(field));
    }
  }
  function isDirty(DirtyField field) internal pure returns (bool) {
    unchecked {
      return DirtyField.unwrap(field) & TOPBIT == TOPBIT;
    }
  }

  function eq(DirtyField leaf1, DirtyField leaf2) internal pure returns (bool) {
    unchecked {
      return DirtyField.unwrap(leaf1) == DirtyField.unwrap(leaf2);
    }
  }
}
TickLib.sol
2
3
4
5
6
7
pragma solidity ^0.8.17;

import {Bin} from "@mgv/lib/core/TickTreeLib.sol";
import "@mgv/lib/core/BitLib.sol";
import "@mgv/lib/core/Constants.sol";

This file is inspired by Uniswap’s approach to ticks, with the following notable changes:

  • directly compute ticks base 1.0001 (not base sqrt(1.0001))
  • directly compute ratios (not sqrt(ratio)) (simpler code elsewhere when dealing with actual ratios and logs of ratios)
  • ratios are floating-point numbers, not fixed-point numbers (increases precision when computing amounts)

        

TickLib

The TickLib file contains tick math-related code and utilities for manipulating ticks. It also holds functions related to ratios, which are represented as (mantissa,exponent) pairs.


        

Globally enable tick.method(...)

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type Tick is int;
using TickLib for Tick global;

library TickLib {

  function inRange(Tick tick) internal pure returns (bool) {
    return Tick.unwrap(tick) >= MIN_TICK && Tick.unwrap(tick) <= MAX_TICK;
  }

  function eq(Tick tick1, Tick tick2) internal pure returns (bool) {
    unchecked {
      return Tick.unwrap(tick1) == Tick.unwrap(tick2);
    }
  }

Returns the nearest, higher bin to the given tick at the given tickSpacing

We do not force ticks to fit the tickSpacing (aka tick%tickSpacing==0). Ratios are rounded up that the maker is always paid at least what they asked for

39
40
  function nearestBin(Tick tick, uint tickSpacing) internal pure returns (Bin bin) {
    unchecked {

By default division rounds towards 0. Since smod is signed we get the sign of tick and tick%tickSpacing in a single instruction.

42
43
44
45
46
47
48
      assembly("memory-safe") {
        bin := sdiv(tick,tickSpacing)
        bin := add(bin,sgt(smod(tick,tickSpacing),0))
      }
    }
  }

Conversion functions


        

(inbound,tick) → outbound

inboundFromOutbound[Up] converts an outbound amount (i.e. an offer.gives or a takerWants), to an inbound amount, following the price induced by tick. There’s a rounding-up and a rounding-down variant.

outboundAmt should not exceed 127 bits.

56
57
58
59
60
61
62
63
64
65
66
67
  function inboundFromOutbound(Tick tick, uint outboundAmt) internal pure returns (uint) {
    (uint sig, uint exp) = ratioFromTick(tick);
    return (sig * outboundAmt) >> exp;
  }

  function inboundFromOutboundUp(Tick tick, uint outboundAmt) internal pure returns (uint) {
    unchecked {
      (uint sig, uint exp) = ratioFromTick(tick);
      return divExpUp(sig*outboundAmt,exp);
    }
  }

(outbound,tick) → inbound


        

outboundFromInbound[Up] converts an inbound amount (i.e. an offer.wants or a takerGives), to an outbound amount, following the price induced by tick. There’s a rounding-up and a rounding-down variant.

inboundAmt should not exceed 127 bits.

73
74
75
76
77
78
79
80
81
82
83
84
  function outboundFromInbound(Tick tick, uint inboundAmt) internal pure returns (uint) {
    (uint sig, uint exp) = ratioFromTick(Tick.wrap(-Tick.unwrap(tick)));
    return (sig * inboundAmt) >> exp;
  }

  function outboundFromInboundUp(Tick tick, uint inboundAmt) internal pure returns (uint) {
    unchecked {
      (uint sig, uint exp) = ratioFromTick(Tick.wrap(-Tick.unwrap(tick)));
      return divExpUp(sig*inboundAmt,exp);
    }
  }

Ratio representation

Ratios are represented as a (mantissa,exponent) pair which represents the number mantissa * 2**-exponent.

The exponent is negated so that, for ratios in the accepted range, the exponent is >= 0. This simplifies the code.

Floats are normalized so that the mantissa uses exactly 128 bits. It enables easy comparison between floats and ensures they can be multiplied by amounts without overflow.

The accepted ratio range is between ratioFromTick(MIN_TICK) and ratioFromTick(MAX_TICK) (inclusive).


        

(inbound,outbound) → ratio


        

ratioFromVolumes converts a pair of (inbound,outbound) volumes to a floating-point, normalized ratio. It rounds down.

  • outboundAmt = 0 has a special meaning and the highest possible price will be returned.
  • inboundAmt = 0 has a special meaning if outboundAmt != 0 and the lowest possible price will be returned.
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
  function ratioFromVolumes(uint inboundAmt, uint outboundAmt) internal pure returns (uint mantissa, uint exp) {
    unchecked {
      require(inboundAmt <= MAX_SAFE_VOLUME, "mgv/ratioFromVol/inbound/tooBig");
      require(outboundAmt <= MAX_SAFE_VOLUME, "mgv/ratioFromVol/outbound/tooBig");
      if (outboundAmt == 0) {
        return (MAX_RATIO_MANTISSA,uint(MAX_RATIO_EXP));
      } else if (inboundAmt == 0) {
        return (MIN_RATIO_MANTISSA,uint(MIN_RATIO_EXP));
      }
      uint ratio = (inboundAmt << MANTISSA_BITS) / outboundAmt; 
      uint log2 = BitLib.fls(ratio);
      require(ratio != 0,"mgv/ratioFromVolumes/zeroRatio");
      if (log2 > MANTISSA_BITS_MINUS_ONE) {
        uint diff = log2 - MANTISSA_BITS_MINUS_ONE;
        return (ratio >> diff, MANTISSA_BITS - diff);
      } else {
        uint diff = MANTISSA_BITS_MINUS_ONE - log2;
        return (ratio << diff, MANTISSA_BITS + diff);
      }
    }
  }

(inbound,outbound) → tick

126
127
128
129
130
  function tickFromVolumes(uint inboundAmt, uint outboundAmt) internal pure returns (Tick tick) {
    (uint man, uint exp) = ratioFromVolumes(inboundAmt, outboundAmt);
    return tickFromNormalizedRatio(man,exp);
  }

ratio → tick


        

Does not require a normalized ratio.

133
134
135
136
137
138
  function tickFromRatio(uint mantissa, int exp) internal pure returns (Tick) {
    uint normalized_exp;
    (mantissa, normalized_exp) = normalizeRatio(mantissa, exp);
    return tickFromNormalizedRatio(mantissa,normalized_exp);
  }

low-level ratio → tick


        

Given ratio, return greatest tick t such that ratioFromTick(t) <= ratio.

  • Input ratio must be within the maximum and minimum ratios returned by the available ticks.
  • Does not expected a normalized float.

The function works as follows:

  • Approximate log2(ratio) to the 13th fractional digit.
  • Following https://hackmd.io/@mangrovedao/HJvl21zla, obtain tickLow and tickHigh such that log1.0001(ratio)\log_{1.0001}(ratio) is between them
  • Return the highest one that yields a ratio below the input ratio.
149
150
151
152
153
154
155
156
157
158
  function tickFromNormalizedRatio(uint mantissa, uint exp) internal pure returns (Tick tick) {
    if (floatLt(mantissa, exp, MIN_RATIO_MANTISSA, uint(MIN_RATIO_EXP))) {
      revert("mgv/tickFromRatio/tooLow");
    }
    if (floatLt(MAX_RATIO_MANTISSA, uint(MAX_RATIO_EXP), mantissa, exp)) {
      revert("mgv/tickFromRatio/tooHigh");
    }
    int log2ratio = int(MANTISSA_BITS_MINUS_ONE) - int(exp) << 64;
    uint mpow = mantissa >> MANTISSA_BITS_MINUS_ONE - 127; // give 129 bits of room left

How the fractional digits of the log are computed:

  • for a given n compute n2n^2.
  • If log2(n2)=2log2(n)\lfloor\log_2(n^2)\rfloor = 2\lfloor\log_2(n)\rfloor then the fractional part of log2(n2)\log_2(n^2) was <0.5< 0.5 (first digit is 0).
  • If log2(n2)=1+2log2(n)\lfloor\log_2(n^2)\rfloor = 1 + 2\lfloor\log_2(n)\rfloor then the fractional part of log2(n2)\log_2(n^2) was 0.5\geq 0.5 (first digit is 1).
  • Apply starting with n=mpow repeatedly by keeping n on 127 bits through right-shifts (has no impact on high fractional bits).
165
166
    assembly ("memory-safe") {

13 bits of precision

168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
      mpow := shr(127, mul(mpow, mpow))
      let highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(63, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(62, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(61, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(60, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(59, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(58, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(57, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(56, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(55, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(54, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(53, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(52, highbit))
      mpow := shr(highbit, mpow)

      mpow := shr(127, mul(mpow, mpow))
      highbit := shr(128, mpow)
      log2ratio := or(log2ratio, shl(51, highbit))
    }

Convert log base 2 to log base 1.0001 (multiply by log2(1.0001)^-1 << 64), since log2ratio is x64 this yields a x128 number.

234
235
    int log_bp_ratio = log2ratio * 127869479499801913173571;

tickLow is approx - maximum error

237
    int tickLow = int((log_bp_ratio - 1701496478404567508395759362389778998) >> 128);

tickHigh is approx + minimum error

239
240
241
242
243
244
245
246
247
248
249
250
    int tickHigh = int((log_bp_ratio + 289637967442836606107396900709005211253) >> 128);

    (uint mantissaHigh, uint expHigh) = ratioFromTick(Tick.wrap(tickHigh));

    bool ratioHighGt = floatLt(mantissa, exp, mantissaHigh, expHigh);
    if (tickLow == tickHigh || ratioHighGt) {
      tick = Tick.wrap(tickLow);
    } else { 
      tick = Tick.wrap(tickHigh);
    }
  }

tick → ratio conversion function


        

Returns a normalized (man,exp) ratio floating-point number. The mantissa is on 128 bits to avoid overflow when mulitplying with token amounts. The exponent has no bias. for easy comparison.

253
254
255
256
257
  function ratioFromTick(Tick tick) internal pure returns (uint man, uint exp) {
    unchecked {
      (man, exp) = nonNormalizedRatioFromTick(tick);
      int shiftedTick = Tick.unwrap(tick) << LOG_BP_SHIFT;
      int log2ratio;

floor log2 of ratio towards negative infinity

259
260
261
262
263
264
      assembly ("memory-safe") {
        log2ratio := sdiv(shiftedTick,LOG_BP_2X235)
        log2ratio := sub(log2ratio,slt(smod(shiftedTick,LOG_BP_2X235),0))
      }
      int diff = log2ratio+int(exp)-int(MANTISSA_BITS_MINUS_ONE);
      if (diff > 0) {

For |tick| <= 887272, this drops at most 5 bits of precision

266
267
268
269
        man = man >> uint(diff);
      } else {
        man = man << uint(-diff);
      }

For |tick| << 887272, log2ratio <= 127

271
272
273
274
      exp = uint(int(MANTISSA_BITS_MINUS_ONE)-log2ratio);
    }
  }

low-level tick → ratio conversion


        

Compute 1.0001^tick and returns it as a (mantissa,exponent) pair. Works by checking each set bit of |tick| multiplying by 1.0001^(-2**i)<<128 if the ith bit of tick is set. Since we inspect the absolute value of tick, -1048576 is not a valid tick. If the tick is positive this computes 1.0001^-tick, and we take the inverse at the end. For maximum precision some powers of 1.0001 are shifted until they occupy 128 bits. The extra_shift is recorded and added to the exponent.

Since the resulting mantissa is left-shifted by 128 bits, if tick was positive, we divide 2**256 by the mantissa to get the 128-bit left-shifted inverse of the mantissa.

The error (relative to 1.0001^tick) may be negative or positive.

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
  function nonNormalizedRatioFromTick(Tick tick) internal pure returns (uint man, uint exp) {
    uint absTick = Tick.unwrap(tick) < 0 ? uint(-Tick.unwrap(tick)) : uint(Tick.unwrap(tick));
    require(absTick <= uint(MAX_TICK), "mgv/absTick/outOfBounds");

    int extra_shift;
    if (absTick & 0x1 != 0) {
      man = 0xfff97272373d413259a46990580e2139;
    } else {
      man = 0x100000000000000000000000000000000;
    }
    if (absTick & 0x2 != 0) {
      man = (man * 0xfff2e50f5f656932ef12357cf3c7fdcb) >> 128;
    }
    if (absTick & 0x4 != 0) {
      man = (man * 0xffe5caca7e10e4e61c3624eaa0941ccf) >> 128;
    }
    if (absTick & 0x8 != 0) {
      man = (man * 0xffcb9843d60f6159c9db58835c926643) >> 128;
    }
    if (absTick & 0x10 != 0) {
      man = (man * 0xff973b41fa98c081472e6896dfb254bf) >> 128;
    }
    if (absTick & 0x20 != 0) {
      man = (man * 0xff2ea16466c96a3843ec78b326b52860) >> 128;
    }
    if (absTick & 0x40 != 0) {
      man = (man * 0xfe5dee046a99a2a811c461f1969c3052) >> 128;
    }
    if (absTick & 0x80 != 0) {
      man = (man * 0xfcbe86c7900a88aedcffc83b479aa3a3) >> 128;
    }
    if (absTick & 0x100 != 0) {
      man = (man * 0xf987a7253ac413176f2b074cf7815e53) >> 128;
    }
    if (absTick & 0x200 != 0) {
      man = (man * 0xf3392b0822b70005940c7a398e4b70f2) >> 128;
    }
    if (absTick & 0x400 != 0) {
      man = (man * 0xe7159475a2c29b7443b29c7fa6e889d8) >> 128;
    }
    if (absTick & 0x800 != 0) {
      man = (man * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
    }
    if (absTick & 0x1000 != 0) {
      man = (man * 0xa9f746462d870fdf8a65dc1f90e061e4) >> 128;
    }
    if (absTick & 0x2000 != 0) {
      man = (man * 0xe1b0d342ada5437121767bec575e65ed) >> 128;
      extra_shift += 1;
    }
    if (absTick & 0x4000 != 0) {
      man = (man * 0xc6f84d7e5f423f66048c541550bf3e96) >> 128;
      extra_shift += 2;
    }
    if (absTick & 0x8000 != 0) {
      man = (man * 0x9aa508b5b7a84e1c677de54f3e99bc8f) >> 128;
      extra_shift += 4;
    }
    if (absTick & 0x10000 != 0) {
      man = (man * 0xbad5f1bdb70232cd33865244bdcc089c) >> 128;
      extra_shift += 9;
    }
    if (absTick & 0x20000 != 0) {
      man = (man * 0x885b9613d7e87aa498106fb7fa5edd37) >> 128;
      extra_shift += 18;
    }
    if (absTick & 0x40000 != 0) {
      man = (man * 0x9142e0723efb884889d1f447715afacd) >> 128;
      extra_shift += 37;
    }
    if (absTick & 0x80000 != 0) {
      man = (man * 0xa4d9a773d61316918f140bd96e8e6814) >> 128;
      extra_shift += 75;
    }
    if (Tick.unwrap(tick) > 0) {

We use Remco Bloemen’s trick to divide 2**256 by man:

358
359
360
361
362
363
364
365
366
      assembly("memory-safe") {
        man := add(div(sub(0, man), man), 1)
      }
      extra_shift = -extra_shift;
    }
    exp = uint(128 + extra_shift);

  }

Shift mantissa so it occupies exactly MANTISSA_BITS and adjust exp in consequence.

A float is normalized when its mantissa occupies exactly 128 bits. All in-range normalized floats have exp >= 0, so we can use a uint for exponents everywhere we expect a normalized float.

When a non-normalized float is expected/used, exp can be negative since there is no constraint on the size of the mantissa.

374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
  function normalizeRatio(uint mantissa, int exp) internal pure returns (uint, uint) {
    require(mantissa != 0,"mgv/normalizeRatio/mantissaIs0");
    uint log2ratio = BitLib.fls(mantissa);
    int shift = int(MANTISSA_BITS_MINUS_ONE) - int(log2ratio);
    if (shift < 0) {
      mantissa = mantissa >> uint(-shift);
    } else {
      mantissa = mantissa << uint(shift);
    }
    exp = exp + shift;
    if (exp < 0) {
      revert("mgv/normalizeRatio/lowExp");
    }
    return (mantissa,uint(exp));
  }

Return a/(2**e) rounded up

391
392
393
  function divExpUp(uint a, uint e) internal pure returns (uint) {
    unchecked {
      uint rem;

Let mask be (1<<e)-1, rem is 1 if a & mask > 0, and 0 otherwise. Explanation:

  • if a is 0 then rem must be 0. 0 & mask is 0.
  • else if e > 255 then 0 < a < 2^e, so rem must be 1. (1<<e)-1 is type(uint).max, so a & mask is a > 0.
  • else a & mask is a % 2**e
401
402
403
404
405
406
407
      assembly("memory-safe") {
        rem := gt(and(a,sub(shl(e,1),1)),0)
      }
      return (a>>e) + rem;
    }
  }

Floats are normalized to 128 bits to ensure no overflow when multiplying with amounts, and for easier comparisons. Normalized in-range floats have exp>=0.

409
  function floatLt(uint mantissa_a, uint exp_a, uint mantissa_b, uint exp_b) internal pure returns (bool) {

Exponents are negated (so that exponents of ratios within the accepted range as >= 0, which simplifies the code), which explains the direction of the exp_a > exp_b comparison.

411
412
413
414
    return (exp_a > exp_b || (exp_a == exp_b && mantissa_a < mantissa_b));
  }

}
BitLib.sol
2
3
4
pragma solidity ^0.8.17;

library BitLib {
  • if x is a nonzero uint64:

        

return number of zeroes in x that do not have a 1 to their right


        
  • otherwise:

        

return 64

9
10
11
  function ctz64(uint x) internal pure returns (uint256 c) {
    unchecked {
      assembly ("memory-safe") {

clean

13
14
        x:= and(x,0xffffffffffffffff)

7th bit

16
17
        c:= shl(6,iszero(x))

isolate lsb

19
20
        x := and(x, add(not(x), 1))

6th bit

22
23
        c := or(c,shl(5, lt(0xffffffff, x)))

debruijn lookup

25
26
27
28
29
30
        c := or(c, byte(shr(251, mul(shr(c, x), shl(224, 0x077cb531))), 
            0x00011c021d0e18031e16140f191104081f1b0d17151310071a0c12060b050a09))
      }
    }
  }

Function fls is MIT License. Copyright © 2022 Solady.


        

/ @dev find last set.


        

/ Returns the index of the most significant bit of x,


        

/ counting from the least significant bit position.


        

/ If x is zero, returns 256.


        

/ Equivalent to log2(x), but without reverting for the zero case.

37
38
39
40
41
42
43
44
    function fls(uint256 x) internal pure returns (uint256 r) {
        assembly ("memory-safe") {
            r := shl(8, iszero(x))

            r := or(r, shl(7, lt(0xffffffffffffffffffffffffffffffff, x)))
            r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x))))
            r := or(r, shl(5, lt(0xffffffff, shr(r, x))))

For the remaining 32 bits, use a De Bruijn lookup.

46
47
48
49
50
51
52
            x := shr(r, x)
            x := or(x, shr(1, x))
            x := or(x, shr(2, x))
            x := or(x, shr(4, x))
            x := or(x, shr(8, x))
            x := or(x, shr(16, x))

forgefmt: disable-next-item

54
55
56
57
58
            r := or(r, byte(shr(251, mul(x, shl(224, 0x07c4acdd))),
                0x0009010a0d15021d0b0e10121619031e080c141c0f111807131b17061a05041f))
        }
    }
}
OfferExtra.sol
2
3
4
5
6
7
8
pragma solidity ^0.8.17;

import "@mgv/lib/core/TickTreeLib.sol";
import "@mgv/lib/core/TickLib.sol";
import {Offer,OfferUnpacked,OfferLib} from "@mgv/src/preprocessed/Offer.post.sol";

cleanup-mask: 0s at location of fields to hide from maker, 1s elsewhere

10
11
uint constant HIDE_FIELDS_FROM_MAKER_MASK = ~(OfferLib.prev_mask_inv | OfferLib.next_mask_inv);

Extra functions for the offer

13
library OfferExtra {

Compute wants from tick and gives

15
16
17
18
  function wants(Offer offer) internal pure returns (uint) {
    return offer.tick().inboundFromOutboundUp(offer.gives());
  }

Sugar to test offer liveness

20
21
22
23
24
25
26
  function isLive(Offer offer) internal pure returns (bool resp) {
    uint gives = offer.gives();
    assembly ("memory-safe") {
      resp := iszero(iszero(gives))
    }
  }

Get the bin where the offer is stored given an offer list that has tickSpacing

28
29
30
31
  function bin(Offer offer, uint tickSpacing) internal pure returns (Bin) {
    return offer.tick().nearestBin(tickSpacing);
  }

Removes prev/next pointers from an offer before sending it to the maker. Ensures that the maker has no information about the state of the book when it gets called.

33
34
35
36
37
38
39
40
41
  function clearFieldsForMaker(Offer offer) internal pure returns (Offer) {
    unchecked {
      return Offer.wrap(
        Offer.unwrap(offer)
        & HIDE_FIELDS_FROM_MAKER_MASK);
    }
  }
}

Extra functions for the struct version of the offer

43
library OfferUnpackedExtra {

Compute wants from tick and gives

45
46
47
48
  function wants(OfferUnpacked memory offer) internal pure returns (uint) {
    return offer.tick.inboundFromOutboundUp(offer.gives);
  }

Sugar to test offer liveness

50
51
52
53
54
55
56
  function isLive(OfferUnpacked memory offer) internal pure returns (bool resp) {
    uint gives = offer.gives;
    assembly ("memory-safe") {
      resp := iszero(iszero(gives))
    }
  }

Removes prev/next pointers from an offer before sending it to the maker; Ensures that the maker has no information about the state of the book when it gets called.

58
59
60
61
62
  function bin(OfferUnpacked memory offer, uint tickSpacing) internal pure returns (Bin) {
    return offer.tick.nearestBin(tickSpacing);
  }

}
OfferDetailExtra.sol
2
3
4
5
6
pragma solidity ^0.8.17;

import {OfferDetail,OfferDetailUnpacked} from "@mgv/src/preprocessed/OfferDetail.post.sol";

Extra functions for the offer details

8
9
10
11
12
13
14
15
16
library OfferDetailExtra {
  function offer_gasbase(OfferDetail offerDetail) internal pure returns (uint) { unchecked {
    return offerDetail.kilo_offer_gasbase() * 1e3;
  }}
  function offer_gasbase(OfferDetail offerDetail,uint val) internal pure returns (OfferDetail) { unchecked {
    return offerDetail.kilo_offer_gasbase(val/1e3);
  }}
}

Extra functions for the struct version of the offer details

18
19
20
21
22
23
24
25
library OfferDetailUnpackedExtra {
  function offer_gasbase(OfferDetailUnpacked memory offerDetail) internal pure returns (uint) { unchecked {
    return offerDetail.kilo_offer_gasbase * 1e3;
  }}
  function offer_gasbase(OfferDetailUnpacked memory offerDetail,uint val) internal pure { unchecked {
    offerDetail.kilo_offer_gasbase = val/1e3;
  }}
}
LocalExtra.sol
2
3
4
5
6
7
8
9
pragma solidity ^0.8.17;

import {Bin,TickTreeLib,Field} from "@mgv/lib/core/TickTreeLib.sol";
import {Density, DensityLib} from "@mgv/lib/core/DensityLib.sol";
import {Local,LocalUnpacked,LocalLib} from "@mgv/src/preprocessed/Local.post.sol";


cleanup-mask: 0s at location of fields to hide from maker, 1s elsewhere

11
12
uint constant HIDE_FIELDS_FROM_MAKER_MASK = ~(LocalLib.binPosInLeaf_mask_inv | LocalLib.level3_mask_inv | LocalLib.level2_mask_inv | LocalLib.level1_mask_inv | LocalLib.root_mask_inv | LocalLib.last_mask_inv);

Extra functions for local

14
15
library LocalExtra {

Sets the density in fixed-point 96X32 format (it is stored in floating-point, see DensityLib for more information).

17
18
19
20
  function densityFrom96X32(Local local, uint density96X32) internal pure returns (Local) { unchecked {
    return local.density(DensityLib.from96X32(density96X32));
  }}

Returns the gasbase in gas (it is stored in kilogas)

22
23
24
25
  function offer_gasbase(Local local) internal pure returns (uint) { unchecked {
    return local.kilo_offer_gasbase() * 1e3;
  }}

Sets the gasbase in gas (it is stored in kilogas)

27
28
29
30
  function offer_gasbase(Local local,uint val) internal pure returns (Local) { unchecked {
    return local.kilo_offer_gasbase(val/1e3);
  }}

Returns the bin that contains the best offer in `local`'s offer list

32
33
34
35
  function bestBin(Local local) internal pure returns (Bin) { unchecked {
    return TickTreeLib.bestBinFromLocal(local);
  }}

Erases field that give information about the current structure of the offer list.

37
38
39
40
41
42
43
  function clearFieldsForMaker(Local local) internal pure returns (Local) { unchecked {
    return Local.wrap(
      Local.unwrap(local)
      & HIDE_FIELDS_FROM_MAKER_MASK);
  }}
}

Extra functions for the struct version of local

45
library LocalUnpackedExtra {

Sets the density in fixed-point 96X32 format (it is stored in floating-point, see DensityLib for more information).

47
48
49
50
  function densityFrom96X32(LocalUnpacked memory local, uint density96X32) internal pure { unchecked {
    local.density = DensityLib.from96X32(density96X32);
  }}

Returns the gasbase in gas (it is stored in kilogas)

52
53
54
55
  function offer_gasbase(LocalUnpacked memory local) internal pure returns (uint) { unchecked {
    return local.kilo_offer_gasbase * 1e3;
  }}

Sets the gasbase in gas (it is stored in kilogas)

57
58
59
60
  function offer_gasbase(LocalUnpacked memory local,uint val) internal pure { unchecked {
    local.kilo_offer_gasbase = val/1e3;
  }}

Returns the bin that contains the best offer in `local`'s offer list

62
63
64
65
  function bestBin(LocalUnpacked memory local) internal pure returns (Bin) { unchecked {
    return TickTreeLib.bestBinFromBranch(local.binPosInLeaf,local.level3,local.level2,local.level1,local.root);
  }}
}