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 WETHMax 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=0prev = 0: First offer in listnext = 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 dayAfter 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 ETHProvision 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)
}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: 150KInsertion 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] ← TailRemoval 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] ← TailIf 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);- 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);givesdecreased by filled amountreceivedincreased by paid amounttotalVolumedecreased at tick- Offer remains active and in list
- If
givesreaches 0: proceed to step 3
3. Full Fill
State changes:isActiveset tofalse- Removed from linked list
totalVolumeandofferCountupdated- If last offer at tick: tick removed from tree
4. Claiming
vif.claim(market, offerId);receivedreset to 0- Credit created in flash accounting
- If offer is inactive or expired: also returns
givesandprovision
5. Cancellation
vif.cancel(market, offerId);isActiveset tofalse- Removed from linked list
- Credits created for
gives,received, andprovision - Tick removed from tree if last offer
6. Expiry & Cleaning
After block.timestamp > expiry:
vif.clean(market, offerId);isActiveset tofalse- 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);
}
}
}- Uses
maxOffersto cap number of offers matched - Stops at
maxTickprice limit - Removes fully-filled offers immediately
Offer Editing
Editing updates an existing offer:
vif.make(maker, market, existingOfferId, newGives, newTick, newExpiry, newProvision);- Verifies
makermatches original maker - Claims any
receivedamount (credits to maker) - Removes offer from old tick's list
- Updates offer fields
- Inserts at tail of new tick's list (even if tick is same)
- Creates debts for additional
givesorprovision
Gas Costs
Offer operations have predictable gas costs:
| Operation | Typical Gas | Notes |
|---|---|---|
| Create offer | ~120k | Includes tree update if new tick |
| Edit offer | ~100k | Includes list reorganization |
| Cancel offer | ~80k | Includes tree cleanup if last |
| Claim (active) | ~50k | Simple balance update |
| Claim (expired) | ~70k | Also removes from list |
| Fill offer (partial) | ~60k | Per offer matched |
| Fill offer (full) | ~80k | Includes removal from list |
- Batch claims with other operations using flash accounting
- Use higher tick spacing to reduce fills per order
- Set reasonable
maxOffersto cap gas costs
Related Concepts
- Flash Accounting - How debts/credits work
- Tick Tree Structure - Price level organization
- Placing Limit Orders - Creating offers
- Creating Swaps - Consuming offers