Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Offers & Lists

At each price point (tick) in the order book, offers are organized in double linked lists. Understanding offer structure and list mechanics is crucial for efficient order book operations.

Offer Structure

Each offer in Vif contains:

struct Offer {
  uint48 gives;        // Amount offered (in units)
  uint48 received;     // Amount received from fills (in units)
  uint40 next;         // Next offer ID in list
  uint40 prev;         // Previous offer ID in list
  uint32 expiry;       // Expiration timestamp (0 = never)
  uint24 provision;    // Provision in native token (in provision units)
  address maker;       // Offer owner
  bool isActive;       // Whether offer is in the book
  int24 tick;          // Price level (redundantly stored)
}

Field Details

gives (uint48)

Amount the maker is offering, in units:

actualAmount = gives * outboundUnits;
 
// Example: WETH offer with outboundUnits = 1e13
gives = 100_000;  // 100,000 units
actualAmount = 100_000 * 1e13 = 1e18 = 1 WETH

Max value: 2^48 - 1 = 281,474,976,710,655 units

received (uint48)

Amount received from partial fills, in inbound units:

// Offer sells 1 WETH for USDC at tick -2,072,336 (~1221 USDC/WETH)
// Taker buys 0.5 WETH
 
gives = 100_000 units (1 WETH initially)
// After fill:
gives = 50_000 units (0.5 WETH remaining)
received = 61_050_000 units (~610.5 USDC in inboundUnits=1e4)

Makers can claim received at any time via vif.claim().

next and prev (uint40 each)

Pointers forming the double linked list:

Head → [Offer 1] ⇄ [Offer 2] ⇄ [Offer 3] ← Tail
       prev=0       prev=1       prev=2
       next=2       next=3       next=0
  • prev = 0: First offer in list
  • next = 0: Last offer in list
  • Both 0: Only offer at this price

Max offer ID: 2^40 - 1 = 1,099,511,627,775

expiry (uint32)

Unix timestamp when offer expires:

expiry = 0;                    // Never expires
expiry = 1735689600;           // Expires Jan 1, 2025
expiry = block.timestamp + 1 days;  // Expires in 1 day

After expiry:

  • Offer becomes inactive
  • Can be cleaned by anyone for bounty
  • Maker gets provision back when claiming

Max timestamp: 2^32 - 1 = 4,294,967,295 (year 2106)

provision (uint24)

Amount of native token (ETH/MATIC/etc.) locked with the offer, in provision units:

provisionUnits = globalProvision;  // Set at contract deployment
actualProvision = provision * provisionUnits;
 
// Example: globalProvision = 1e12
provision = 1000;
actualProvision = 1000 * 1e12 = 1e15 wei = 0.001 ETH

Provision is:

  • Required for offers with expiry
  • Returned when offer is cancelled or claimed after expiry
  • Awarded as bounty to cleaners of expired offers

Max provision: 2^24 - 1 = 16,777,215 units

maker (address)

Immutable owner of the offer:

// Creating offer
vif.make(maker, market, 0, gives, tick, expiry, provision);
// → Creates offer owned by `maker`
 
// Editing offer (must use same maker)
vif.make(maker, market, offerId, newGives, newTick, expiry, provision);
// → Reverts if caller isn't authorized by `maker`

Only the maker (or their authorized operators) can:

  • Edit the offer
  • Cancel the offer
  • Claim filled amounts

isActive (bool)

Whether the offer is currently in the order book:

isActive = true;   // Offer is in book, can be matched
isActive = false;  // Offer removed (fully filled, cancelled, or expired)

When an offer becomes inactive:

  • It's removed from the linked list
  • The tick's total volume is updated
  • If it was the last offer at that tick, the tick is removed from tree

Offer Lists

At each active tick, offers form a double linked list with head and tail pointers.

List Structure

struct OfferList {
  uint40 head;           // First offer ID
  uint40 tail;           // Last offer ID
  uint48 totalVolume;    // Sum of all gives (in units)
  uint40 offerCount;     // Number of active offers
  int24 tick;            // Price level (redundant)
}
Visual representation:
Tick -2,072,336 (Price: ~1221 USDC/WETH)
┌──────────────────────────────────────────┐
│ head: 42                                 │
│ tail: 105                                │
│ totalVolume: 500,000 units (5 WETH)     │
│ offerCount: 3                            │
└──────────────────────────────────────────┘

    [Offer 42] ⇄ [Offer 73] ⇄ [Offer 105]
    gives: 200K   gives: 150K   gives: 150K

Insertion Logic

New offers are inserted at the tail (end) of the list:

// List before: Head → [42] ⇄ [73] ← Tail
// Insert offer 105
 
1. Create offer 105 with:
   - prev = 73 (current tail)
   - next = 0 (will be new tail)
 
2. Update offer 73:
   - next = 105
 
3. Update list:
   - tail = 105
   - totalVolume += 150,000
   - offerCount += 1
 
// List after: Head → [42] ⇄ [73] ⇄ [105] ← Tail

Removal Logic

Offers can be removed by:

  • Full fill during market order
  • Cancellation by maker
  • Expiry (requires cleaning)
// List: Head → [42] ⇄ [73] ⇄ [105] ← Tail
// Remove offer 73
 
1. Update offer 42:
   - next = 105 (skip 73)
 
2. Update offer 105:
   - prev = 42 (skip 73)
 
3. Update offer 73:
   - isActive = false
   - (links remain for historical data)
 
4. Update list:
   - totalVolume -= 150,000
   - offerCount -= 1
 
// List: Head → [42] ⇄ [105] ← Tail

If the last offer is removed:

  • List head and tail become 0
  • Tick is removed from tree
  • List structure is deleted

Offer Lifecycle

1. Creation

vif.make(maker, market, 0, gives, tick, expiry, provision);
State changes:
  • New offer struct created with unique ID
  • Inserted at tail of offer list for tick
  • If new tick: activate in tick tree
  • Debt created in flash accounting (for gives + provision)

2. Partial Fill

vif.consume(taker, market, maxTick, fillVolume, fillWants, maxOffers);
State changes:
  • gives decreased by filled amount
  • received increased by paid amount
  • totalVolume decreased at tick
  • Offer remains active and in list
  • If gives reaches 0: proceed to step 3

3. Full Fill

State changes:
  • isActive set to false
  • Removed from linked list
  • totalVolume and offerCount updated
  • If last offer at tick: tick removed from tree

4. Claiming

vif.claim(market, offerId);
State changes:
  • received reset to 0
  • Credit created in flash accounting
  • If offer is inactive or expired: also returns gives and provision

5. Cancellation

vif.cancel(market, offerId);
State changes:
  • isActive set to false
  • Removed from linked list
  • Credits created for gives, received, and provision
  • Tick removed from tree if last offer

6. Expiry & Cleaning

After block.timestamp > expiry:

vif.clean(market, offerId);
State changes:
  • isActive set to false
  • Removed from linked list
  • Bounty awarded to cleaner (from provision)
  • Remaining provision reserved for maker to claim

Matching Algorithm

During a market order, offers are consumed sequentially:

function consume(taker, market, maxTick, fillVolume, fillWants, maxOffers) {
  cursor = tree.first();  // Get best tick
  remaining = fillVolume;
 
  while (remaining > 0 && offersConsumed < maxOffers) {
    if (cursor.tick > maxTick) break;  // Price limit reached
 
    offer = offerList[cursor.tick].head;  // Get first offer
 
    while (offer != null && remaining > 0) {
      // Match offer
      filled = min(offer.gives, remaining);
      offer.gives -= filled;
      offer.received += calculateInbound(filled, cursor.tick);
      remaining -= filled;
      offersConsumed++;
 
      if (offer.gives == 0) {
        // Fully filled, remove
        offer.isActive = false;
        offer = offer.next;
      } else {
        // Partially filled, stay in list
        break;
      }
    }
 
    if (offerList[cursor.tick].head == null) {
      // All offers consumed at this tick, move to next
      cursor = tree.next(cursor);
    }
  }
}
Gas optimization:
  • Uses maxOffers to cap number of offers matched
  • Stops at maxTick price limit
  • Removes fully-filled offers immediately

Offer Editing

Editing updates an existing offer:

vif.make(maker, market, existingOfferId, newGives, newTick, newExpiry, newProvision);
Behavior:
  1. Verifies maker matches original maker
  2. Claims any received amount (credits to maker)
  3. Removes offer from old tick's list
  4. Updates offer fields
  5. Inserts at tail of new tick's list (even if tick is same)
  6. Creates debts for additional gives or provision

Gas Costs

Offer operations have predictable gas costs:

OperationTypical GasNotes
Create offer~120kIncludes tree update if new tick
Edit offer~100kIncludes list reorganization
Cancel offer~80kIncludes tree cleanup if last
Claim (active)~50kSimple balance update
Claim (expired)~70kAlso removes from list
Fill offer (partial)~60kPer offer matched
Fill offer (full)~80kIncludes removal from list
Optimization tips:
  • Batch claims with other operations using flash accounting
  • Use higher tick spacing to reduce fills per order
  • Set reasonable maxOffers to cap gas costs

Related Concepts