# Vif protocol > Vif protocol is an ultra‑efficient, fully onchain order book protocol. ## Getting Started ### Setting up your environment First, you will need to install foundry to be able to compile the contracts. ```bash curl -L https://foundry.paradigm.xyz | bash ``` Then clone the repository and install the dependencies. ```bash git clone https://github.com/mangrovedao/vif-core.git cd vif-core forge install ``` ### Running the tests The project contains a set of tests (unit, fuzz, and invariant tests) that you can run with the following command: ```bash forge test -vvv ``` ### Deploying the contracts Contracts can be used with an upgradeable proxy, but here we'll only see how to deploy the contracts immutably. There is a script called `DeployImmutable`. We just need to define the arguments and run the script. ```bash WETH= OWNER= PROVISION= forge script DeployImmutable ``` Review the [script options](https://getfoundry.sh/forge/reference/script) in the Foundry documentation for more configuration options. ## Vif Protocol: Overview **Vif** is a new, fully on-chain order book protocol designed from the ground up for maximum efficiency. In the landscape of decentralized finance, different blockchains have different strengths. Vif is specifically engineered for environments where computation is relatively cheap, but writing to storage and emitting logsβ€”actions common in many DeFi protocolsβ€”are significantly more expensive. The primary motivation behind Vif is to drastically reduce transaction costs (gas fees) for traders by minimizing these expensive operations. It achieves this by combining and refining concepts from existing order book designs with novel architectural choices. ### The Core Problem: Expensive On-Chain Actions On many EVM-compatible blockchains, every transaction's cost is a sum of its parts. Computational steps are often cheap, but actions that permanently change the blockchain's state, like saving data (`SSTORE`) or creating event logs (`LOG`), can make up the bulk of the fee. Traditional on-chain order books are often designed with chains in mind where storage operations are expensive, without considering that on a growing set of EVM chains, log operations can be even more costly. Additionally, these order books typically don't optimize their data packing efficiently - what could be stored in a single storage slot often gets spread across 4 to 5 slots, making on-chain trading prohibitively expensive compared to centralized alternatives and more traditional AMMs. ### The Vif Approach: Smart On-Chain Optimization The core philosophy of Vif is to achieve radical efficiency through smart on-chain design. **All computations happen on the blockchain**, but the protocol is meticulously engineered to minimize its use of the most expensive operations: storage writes (`SSTORE`) and event logs (`LOG`). This is accomplished in three main ways: 1. **Aggressive Data Packing:** Vif minimizes storage costs by **aggressively packing data**. Where other protocols might use several storage slots to define a single order, Vif is designed to fit as much information as possible into a single slot. This dramatically reduces the gas fees associated with placing and maintaining orders on the book. 2. **Minimalist Event Logs:** It takes a minimalist approach to event logs, a conscious design choice that lowers transaction costs for traders. By shifting the workload of interpreting historical data to off-chain indexers, the on-chain execution remains lean and cheap. 3. **Efficient Data Structures:** The protocol is built on data structures with proven gas efficiency. These allow for near-instant (**constant complexity**) order placement and updates, while ensuring that filling orders scales predictably (**linear complexity**) with the number of price levels crossed. The result is a system that can process complex actions in a single, atomic transaction. Think of it like settling a tab at a restaurant with friends. Instead of each person paying for every item they order (many small, expensive on-chain actions), Vif keeps a running tally *within a single transaction*. At the end, it calculates the net result and settles everything at once. This "running tab" logic is applied to all trading operations, from placing orders to claiming funds. ### A Glimpse of the Technology πŸ’‘ While the deep technical details are for later, here are some of the key concepts that make Vif's efficiency possible: #### A Smarter Order Book Structure Vif uses a highly optimized data structure, the **Tick Tree**, to keep track of all the different price levels where orders have been placed. * **Illustration:** Imagine a massive, multi-volume encyclopedia of all possible prices. Instead of flipping through every page to find the one you want, the Tick Tree acts as a hyper-efficient index. It instantly tells the protocol exactly which "pages" (prices) have active orders, allowing it to jump directly to the best available price without wasting time or gas searching. This makes matching trades incredibly fast and cheap. #### Flash Accounting This is Vif's "running tab" system. It allows users to batch many actionsβ€”creating limit orders, executing market orders, claiming filled orders, canceling old onesβ€”into a single, atomic transaction. * **How it works:** The protocol keeps an internal, temporary balance sheet of credits and debts for your transaction. Did you just sell Token A to buy Token B, and now want to use that Token B to place a new limit order? Instead of executing two separate token transfers, Flash Accounting just updates your internal tab. The actual, expensive token transfers only happen once at the very end of your transaction, after all debts and credits have been netted out. * **A powerful side effect:** This system naturally enables **free flash loans** within the scope of a single transaction, opening the door for sophisticated arbitrage and trading strategies. #### Optimized Data Storage To save every possible bit of space (and therefore gas), Vif employs a couple of clever tricks: * **Token Units:** Instead of tracking token amounts to the 18th decimal place, which requires a lot of storage space, Vif operates on "units." A market for USDC might define a unit as 0.01 USDC. This means the numbers the protocol has to store are much smaller, making them cheaper to save. It's a trade-off: a tiny amount of precision for a large gain in efficiency. * **Operators:** Vif allows users to delegate certain actions to an "operator" contract. This is designed for routers and other third-party services to execute trades on a user's behalf in a highly gas-efficient manner, further enhancing the protocol's composability. By rethinking how an order book interacts with the blockchain, Vif aims to provide a trading experience that feels faster, cheaper, and more powerful for users and developers alike. ## Project Structure ### Dependencies The project depends on [solady's](https://github.com/Vectorized/solady) ultra-efficient libraries. It also draws inspiration from its optimization tricks throughout the codebase. Another honorable mention goes to [Mangrove Core](https://github.com/mangrovedao/mangrove-core), which is the inspiration for the tick tree as well as the binary exponentiations for the conversion from tick to price. The project uses [Foundry](https://github.com/foundry-rs/foundry) for development, testing, and scripting as well as native dependency management with git submodules. ### Project structure The project is organized into the following directories: * `src`: contains the core contracts of the protocol * `test`: contains the tests for the protocol * `script`: contains the scripts for the protocol * `lib`: contains the external dependencies through git submodules * `script`: contains the deployment scripts #### Interfaces Under the `src/interfaces` folder, you will find the interfaces for the protocol. ``` ./src/interfaces β”œβ”€β”€ base β”‚Β Β  β”œβ”€β”€ IVifAuthorizer.sol β”‚Β Β  β”œβ”€β”€ IVifCore.sol β”‚Β Β  β”œβ”€β”€ IVifMaking.sol β”‚Β Β  β”œβ”€β”€ IVifManager.sol β”‚Β Β  └── IVifTaking.sol β”œβ”€β”€ eip2330 β”‚Β Β  └── IExtLoad.sol β”œβ”€β”€ ILockCallback.sol β”œβ”€β”€ IVif.sol └── periphery └── IVifRouter.sol ``` The base subfolder contains the specific components that compose the core contracts. `IVif` combines all the base interfaces. `IExtLoad` is also a parent interface for the `IVif` interface, which helps us load the contract state. #### Libraries Under the `src/libraries` folder, you will find the libraries for the protocol. ``` src/libraries β”œβ”€β”€ external β”‚Β Β  β”œβ”€β”€ LibAuthorizationExt.sol β”‚Β Β  β”œβ”€β”€ LibBlackListExt.sol β”‚Β Β  β”œβ”€β”€ LibDeltasExt.sol β”‚Β Β  β”œβ”€β”€ LibExtLoader.sol β”‚Β Β  β”œβ”€β”€ LibFeesExt.sol β”‚Β Β  β”œβ”€β”€ LibLockExt.sol β”‚Β Β  β”œβ”€β”€ LibMarketExt.sol β”‚Β Β  β”œβ”€β”€ LibOfferExt.sol β”‚Β Β  β”œβ”€β”€ LibOfferListExt.sol β”‚Β Β  β”œβ”€β”€ LibPausableExt.sol β”‚Β Β  β”œβ”€β”€ LibProvisionExt.sol β”‚Β Β  └── LibTreeExt.sol β”œβ”€β”€ LibAuthorization.sol β”œβ”€β”€ LibBit.sol β”œβ”€β”€ LibBlackList.sol β”œβ”€β”€ LibDeltas.sol β”œβ”€β”€ LibFees.sol β”œβ”€β”€ LibLock.sol β”œβ”€β”€ LibMarket.sol β”œβ”€β”€ LibOffer.sol β”œβ”€β”€ LibOfferList.sol β”œβ”€β”€ LibPausable.sol β”œβ”€β”€ LibProvision.sol β”œβ”€β”€ LibTick.sol β”œβ”€β”€ periphery β”‚Β Β  β”œβ”€β”€ Commands.sol β”‚Β Β  └── Types.sol └── tree β”œβ”€β”€ LibFlags.sol └── LibTree.sol ``` The libraries are subdivided into two main categories: * The external libraries, which are used by periphery contracts to get the state of the core contract through the `IExtLoad` interface and `LibExtLoader` library. * The core libraries, which are used by the core contract to mutate and check state internally. #### Periphery Contracts Under the `src/periphery` folder, you will find the periphery contracts for the protocol. ``` src/periphery β”œβ”€β”€ dispatcher β”‚Β Β  β”œβ”€β”€ base β”‚Β Β  β”‚Β Β  β”œβ”€β”€ GeneralDispatcher.sol β”‚Β Β  β”‚Β Β  β”œβ”€β”€ OrdersDispatcher.sol β”‚Β Β  β”‚Β Β  └── SettlementDispatcher.sol β”‚Β Β  └── Dispatcher.sol β”œβ”€β”€ VifReader.sol └── VifRouter.sol ``` The reader is a contract that allows reading the state of the core contract off-chain. It has functions to get the list of markets, order books, offers, etc. The router contract inherits the `Dispatcher` contract and implements the `IVifRouter` interface. It batches operations through the commands defined in the `Commands` library, with input and output types defined in the `Types` library. ### Core contract Finally, the core contract, which is the sum of the base contracts, is defined in the `src/Vif.sol` file. ## Current Book and History The book is fully on-chain and thus it can be obtained directly through an `eth_call` to a periphery reader contract. However, if needed, it can also be constructed off-chain from either an initial state or genesis. For this purpose, we need to build a synthetic clone of the book off-chain. Only by creating this off-chain clone would we be able to construct the full history of the book. ### Offers We already saw how to link offer IDs and makers in the [Offer Owners](/protocol/indexing/offer-owners) section. This link is immutable. An offer consists of the following fields: | name | type | description | mutable | | -------------- | ------- | ------------------------------------------------------------------------------------------------------------- | :-----: | | offer ID (pk) | uint40 | The offer ID, defined below. | ❌ | | market ID (pk) | bytes32 | The market ID on which the offer is consumed. | ❌ | | maker | address | The owner of the offer. | ❌ | | gives | uint256 | The amount of outbound token that the maker is selling to the market. | βœ… | | received | uint256 | The amount of inbound token that the maker is receiving from the market. | βœ… | | tick | int24 | The tick at which the offer is placed. | βœ… | | expiry | uint32 | The expiry of the offer (0 if undefined). | βœ… | | provision | uint24 | The provision locked by the maker that will be used to pay the taker in case of expiration (defined in gwei). | βœ… | | active | bool | The active status of the offer. | βœ… | :::warning All operations below will only focus on mutating single entities. However, several entities must be created on each event to offer a complete history of an offer. ::: ### Snapshot Entities :::note Timed metadata will from now refer to the block number, timestamps, transaction hash, and/or the event index of the transaction. ::: Snapshot entities will refer to value snapshots that could be edition snapshots, matching snapshots, or claiming/deletion snapshots. These should have a composite foreign key of the offer `market` and `offerId` pair, and have a composite primary key of the block and event index. These should contain the full timed metadata of the snapshot. An index can be created on the timestamp which will likely be used frequently while querying the history of an offer. Some snapshot suggestions would be: #### Edition Snapshots This snapshot would be created upon offer creation or edition. This snapshot assumes that `received` is set to 0 in all cases. | name | type | description | | ------------------- | ------- | --------------------------------------------------------------- | | offer ID (fk) | uint40 | The offer ID. | | market ID (fk) | bytes32 | The market ID on which the offer is consumed. | | gives | uint256 | The new amount to sell | | tick | int24 | The new tick at which the offer is placed. | | expiry | uint32 | The new expiry of the offer (0 if undefined). | | provision | uint24 | The new provision (defined in gwei). | | timed metadata (pk) | | The timed metadata of the snapshot (contains the composite pk). | #### Deletion/Claiming Snapshots This snapshot would be created upon offer deletion/claiming. | name | type | description | | ------------------- | ------- | --------------------------------------------------------------- | | offer ID (fk) | uint40 | The offer ID. | | market ID (fk) | bytes32 | The market ID on which the offer is consumed. | | type | enum | either `deletion` or `claiming` | | outbound | uint256 | The amount of outbound token claimed | | inbound | uint256 | The amount of inbound token claimed | | provision | uint24 | The provision claimed (defined in wei). | | timed metadata (pk) | | The timed metadata of the snapshot (contains the composite pk). | #### Cleaning Snapshots This snapshot would be created upon offer cleaning. Since an offer can also be cleaned during the matching process, as well as many other offers on a single event, the primary key must be extended to include the offer ID and the market ID. | name | type | description | | ------------------- | ------- | --------------------------------------------------------------- | | offer ID (fk/pk) | uint40 | The offer ID. | | market ID (fk/pk) | bytes32 | The market ID on which the offer is consumed. | | bounty | uint256 | The amount of provision claimed. | | timed metadata (pk) | | The timed metadata of the snapshot (contains the composite pk). | #### Matching Snapshots This snapshot would be created upon offer matching. Since a single event will be linked to several offers being matched, but an offer can only be matched once per event, we need to extend the composite primary key to include the offer ID and the market ID. | name | type | description | | ------------------- | ------- | --------------------------------------------------------------- | | offer ID (fk/pk) | uint40 | The offer ID. | | market ID (fk/pk) | bytes32 | The market ID on which the offer is consumed. | | sent | uint256 | The amount of outbound token that the maker sent | | received | uint256 | The amount of inbound token that the maker received | | timed metadata (pk) | | The timed metadata of the snapshot (contains the composite pk). | ### Explicit Offer Indexing Events An offer has a few explicit events that can be indexed like those reviewed before. All explicit events are linked to the offer being created, edited, claimed, or cancelled. ```solidity // ====== IVifMaking ====== /// @notice Emitted when a new offer is created. event NewOffer( bytes32 indexed market, uint40 indexed offerId, address indexed maker, uint256 gives, int24 tick, uint32 expiry, uint24 provision ); /// @notice Emitted when an existing offer is updated. event OfferUpdated( bytes32 indexed market, uint40 indexed offerId, uint256 gives, uint256 claimedReceived, int24 tick, uint32 expiry, uint24 provision ); /// @notice Emitted when an offer is cancelled. event OfferCancelled( bytes32 indexed market, uint40 indexed offerId, uint256 outbound, uint256 inbound, uint256 provision ); /// @notice Emitted when an offer's claim is processed. event OfferClaimed( bytes32 indexed market, uint40 indexed offerId, uint256 outbound, uint256 inbound, uint256 provision ); // ====== IVifTaking ====== /// @notice Emitted when an offer is cleaned. event OfferCleaned(bytes32 indexed market, uint40 indexed offerId, uint256 bounty); ``` #### Offer creation: NewOffer The `NewOffer` event is fired when a new offer is created. This event will only be fired once per `market` and `offerId` pair, and will immutably link an offer to its maker. The remaining fields will populate the rest of the offer metadata. When this event is fired, the `active` field is set to `true`. We can create a new edition snapshot entity linked to our offer. #### Offer update: OfferUpdated The `OfferUpdated` event is fired when an offer is updated. All amounts in this event will override the previous values. When this event is fired, the `active` field is set to `true`. We can create a new edition snapshot entity linked to our offer. When this event is fired, all received amounts are withdrawn to avoid overflows. The withdrawn amount is `claimedReceived`, denominated in inbound tokens. #### Offer cleaning: OfferCleaned The `OfferCleaned` event is fired when an offer is cleaned. The emitted amount is the provision taken for the cleaner. The `active` field is set to `false`. The `provision` field must be subtracted by the emitted amount scaled down to `gwei` units (amounts will always be a multiple of `gwei` even though they are expressed in `wei`). We can create a new deletion/claiming snapshot entity linked to our offer. #### Offer cancellation: OfferCancelled When an offer is cancelled, the `active` field is set to `false`. All mutable fields will be reset to 0. The emitted amounts are the amounts to be claimed. The amounts can be strictly checked if the matching is identical to the on-chain matching engine, or they can be loosely checked. We can create a new deletion/claiming snapshot entity linked to our offer. #### Offer claiming: OfferClaimed The `OfferClaimed` event is fired when an offer is claimed. There are two scenarios: 1. The offer is inactive and/or expired The claiming will act as a cancellation, withdrawing all funds from the offer. All mutable fields can be reset to 0. 2. The offer is active and not expired Claiming will only claim the `received` amount. So we must only reset the `received` field to 0. The rest will remain the same. The amounts emitted are the amounts withdrawn from the offer. `inbound` is always equal to the `received` field (adjusted by the units). In scenario one, `outbound` is always equal to the `gives` field (adjusted by the units) and provision is always equal to the `provision` field (adjusted to gwei). In scenario two, `outbound` and `provision` are always equal to 0. ### Implicit offer indexing Offer matching indexing is implicit, meaning we only index all offers matched from a single event: ```solidity /// @notice Emitted when a market order is executed. event MarketOrder( bytes32 indexed market, address indexed taker, uint256 got, uint256 gave, uint256 fee, uint256 bounty, uint256 fillVolume, bool fillWants ); ``` ::::steps #### Rebuilding the data structure First of all, we need to rebuild the full data representation of the book to rebuild the matching engine. The offers are matched via price priority and time priority. If an offer is edited, it goes to the end of the queue. :::note On-chain, the price priority is represented as a tick tree structure, and each tick has a double-linked list of offers representing the time priority. ::: * When an offer is created, it is pushed to the queue at the given tick. * When an offer is edited, it is removed from the queue of its previous tick and pushed to the queue of its new tick (even if the tick remains the same). * When an offer is cancelled/cleaned, it is removed from the queue. * When an offer `gives` falls below the minimum outbound amount (defined in the [market parameters](/protocol/indexing/market-parameters)), it is removed from the queue. #### Matching an offer Now that we know in which order we need to consume offers, we can start matching them. When matching an offer, the first thing to do is to check if it is expired. If so, do the same as above with the [cleaning section](#offer-cleaning-offercleaned). If not, then we will exchange with this offer. This part of the algorithm assumes that you have a remaining amount of token to receive/send. Depending on whether `fillWants` is true or false, we will match an offer differently. We first need to define a few functions before writing the code. The first one is the function to get the virtual `wants` amount. This amount is the amount an offer is implicitly requesting. It is derived from `give` and `tick`. In order to define this function, refer to the [Binary Exponentiation](/protocol/glossary/binary-exponentiation) section for the computation of the price and the [Tick](/protocol/glossary/tick) section for the computation of the virtual `wants` amount. Let's call this function `getWants`. Here is a TypeScript implementation example: :::code-group ```ts [match.ts] /** * Match an offer * @param fillVolume - The amount of token to fill (expressed in units of the correct token) * @param fillWants - Whether the fillVolume is the amount asked or the amount sent * @param gives - The amount of token the offer is giving (in units) * @param tick - The tick of the offer * @param inboundUnits - The units of the inbound token * @param outboundUnits - The units of the outbound token * @returns The amount of token received and the amount of token sent (in units) */ function matchOffer( fillVolume: bigint, fillWants: boolean, gives: bigint, tick: bigint, inboundUnits: bigint, outboundUnits: bigint ): { received: bigint; send: bigint } { const wants = getWants(gives, tick, inboundUnits, outboundUnits); if (fillWants) { if (fillVolume > gives) { return { received: gives, send: wants }; } return { received: fillVolume, send: divUp(wants * fillVolume, gives) }; } // fillWants is false if (fillVolume > wants) { return { received: wants, send: gives }; } return { received: divDown(gives * fillVolume, wants), send: fillVolume }; } ``` ```ts [wants.ts] import { divUp } from "./utils"; import { tickToPrice } from "./tick"; function getWants(gives: bigint, tick: bigint, inboundUnits: bigint, outboundUnits: bigint): bigint { return divUp(gives * outboundUnits * tickToPrice(tick), inboundUnits); } ``` ```ts [tick.ts] /** * Compute Q128.128 price from a tick. * @param tick - The log-price in 0.1 bps ticks. * @return price - The price in Q128.128. */ function tickToPrice(tick: bigint): bigint { const absTick = tick < 0n ? -tick : tick; let price = 0n; if ((absTick & 0x1n) != 0n) price = 0xffff583ac1ac1c114b9160ddeb4791b7n; else price = 0x100000000000000000000000000000000n; if ((absTick & 0x2n) != 0n) price = (price * 0xfffeb075f14b276d06cdbc6b138e4c4bn) >> 128n; if ((absTick & 0x4n) != 0n) price = (price * 0xfffd60ed9a60ebcb383de6edb7557ef0n) >> 128n; if ((absTick & 0x8n) != 0n) price = (price * 0xfffac1e213e349a0cf1e3d3ec62bf25bn) >> 128n; if ((absTick & 0x10n) != 0n) price = (price * 0xfff583dfa4044e3dfe90c4057e3e4c27n) >> 128n; if ((absTick & 0x20n) != 0n) price = (price * 0xffeb082d36bf2958d476ee75c4da258an) >> 128n; if ((absTick & 0x40n) != 0n) price = (price * 0xffd61212165632bd1dda4c1abdf5f9f1n) >> 128n; if ((absTick & 0x80n) != 0n) price = (price * 0xffac2b0240039d9cdadb751e0acc14c4n) >> 128n; if ((absTick & 0x100n) != 0n) price = (price * 0xff5871784dc6fa608dca410bdecb9ff4n) >> 128n; if ((absTick & 0x200n) != 0n) price = (price * 0xfeb1509bdff34ccb280fad9a309403cfn) >> 128n; if ((absTick & 0x400n) != 0n) price = (price * 0xfd6456c5e15445b458f4403d279c1a89n) >> 128n; if ((absTick & 0x800n) != 0n) price = (price * 0xfacf7ad7076227f61d95f764e8d7e35an) >> 128n; if ((absTick & 0x1000n) != 0n) price = (price * 0xf5b9e413dd1b4e7046f8f721e1f1b295n) >> 128n; if ((absTick & 0x2000n) != 0n) price = (price * 0xebdd5589751f38fd7adce84988dba856n) >> 128n; if ((absTick & 0x4000n) != 0n) price = (price * 0xd9501a6728f01c1f121094aacf4c9475n) >> 128n; if ((absTick & 0x8000n) != 0n) price = (price * 0xb878e5d36699c3a0fd844110d8b9945fn) >> 128n; if ((absTick & 0x10000n) != 0n) price = (price * 0x84ee037828011d8035f12eb571b46c2an) >> 128n; if ((absTick & 0x20000n) != 0n) price = (price * 0x450650de5cb791d4a002074d7f179cb3n) >> 128n; if ((absTick & 0x40000n) != 0n) price = (price * 0x129c67bfc1f3084f1f52dd418a4a8f6dn) >> 128n; if ((absTick & 0x80000n) != 0n) price = (price * 0x15a5e2593066b11cd1c3ea05eb95f74n) >> 128n; if ((absTick & 0x100000n) != 0n) price = (price * 0x1d4a2a0310ad5f70ad53ef4d3dcf3n) >> 128n; if ((absTick & 0x200000n) != 0n) price = (price * 0x359e3010271ed5cfce08f99aan) >> 128n; if ((absTick & 0x400000n) != 0n) price = (price * 0xb3ae1a60d291e4871n) >> 128n; if (tick > 0n) return 2n ** 256n / price; return price; } ``` ```ts [utils.ts] /** * Divide a number by another number and round down to the nearest integer * @param a - The number to divide * @param b - The number to divide by * @returns The rounded down result */ function divDown(a: bigint, b: bigint): bigint { return a / b; } /** * Divide a number by another number and round up to the nearest integer * @param a - The number to divide * @param b - The number to divide by * @returns The rounded up result */ function divUp(a: bigint, b: bigint): bigint { const mod = a % b; return mod === 0n ? a / b : a / b + 1n; } ``` ::: #### Final Algorithm Now let's assume that we have the data structure ready. When we receive a market order, we can execute the algorithm below. We need to save other fields, but for this part we'll only use these specific fields. The following is pseudocode, so you might need to adapt it to your needs or make your own implementation. ```ts function executeMarketOrder( fillWants: boolean, got: bigint, gave: bigint ) { let offer = nextOffer(); let fillVolume = fillWants ? got : gave; while (offer) { if (offer.expired()) { cleanOffer(offer); offer = nextOffer(); continue; } const { received, send } = matchOffer( fillVolume, fillWants, offer.gives, offer.tick, inboundUnits, outboundUnits ); saveOffer({ ...offer, gives: offer.gives - received, received: offer.received + send, }); saveOfferMatchSnapshot(offer, received, send); handleRemoveOfferIfBelowMin(offer); offer = nextOffer(); } } ``` :::: ## Global Parameters Vif is an ownable, pausable contract. In addition, it has one global parameter, which is the provision required to create a limit order with an expiry (defined in gwei). | name | type | description | mutable | | --------- | ------- | -------------------------------------------------------------------------------- | :-----: | | provision | uint24 | The provision required to create a limit order with an expiry (defined in gwei). | βœ… | | paused | bool | The paused state of the contract. | βœ… | | owner | address | The owner of the contract. | βœ… | | paused | bool | The paused state of the contract. | βœ… | #### Ownable This contract respects [EIP-173](https://eips.ethereum.org/EIPS/eip-173), so this is easily indexable with the following ABI: ```solidity /// @dev The ownership is transferred from `oldOwner` to `newOwner`. event OwnershipTransferred(address indexed oldOwner, address indexed newOwner); /// @dev An ownership handover to `pendingOwner` has been requested. event OwnershipHandoverRequested(address indexed pendingOwner); /// @dev The ownership handover to `pendingOwner` has been canceled. event OwnershipHandoverCanceled(address indexed pendingOwner); ``` #### Pausable The pausable contract has the following event, and is easily indexable with the following ABI: ```solidity /// @notice Emitted when the paused state of the contract is set. /// @param paused The new paused state. event SetPaused(bool paused); ``` Pausing the core contract will prevent calling the lock function (and subsequently all the state modifying functions, except the admin functions). #### Provision The provision is a global parameter that is set when the core contract is deployed, and can be updated by the owner. It is defined in gwei. ```solidity /// @notice Emitted when the global minimum provision is updated. /// @param provision The new minimum provision value. event SetProvision(uint24 provision); ``` ## Indexing Vif Protocol The Vif protocol has very minimal logs due to the nature of the EVM chains it targets. For this reason, indexing will be more challenging than with traditional AMMs or order book DEXes. It only logs the following: * Order creation/edition/deletion/claiming * Market orders * Market creation/edition :::warning While I recommend using ponder.sh in general for simple and versatile indexing solutions, it will likely not be the most suitable for some of the indexing needs of the Vif protocol. ::: ## Market Orders Market orders are defined within a transaction and can be uniquely identified by block and event index. | name | type | description | | ---------- | ------- | ------------------------------------------------------------------------------ | | market id | bytes32 | The market ID on which the offer is consumed. | | taker | address | The consumer address. | | got | uint256 | The amount of outbound token that the taker received from the market order. | | gave | uint256 | The amount of inbound token used for the market order. | | fee | uint256 | The amount of inbound token used to pay the governance fee. | | bounty | uint256 | The amount of native provisions sent by the expired offers to the taker. | | fillVolume | uint256 | The original amount asked if fillWants is true, or sent if fillWants is false. | | fillWants | bool | Whether the fillVolume is the amount asked or the amount sent. | While indexing, the primary key for a market order should be a composite of the block number and the event index. The following additional metadata should be saved for convenience while querying: | name | type | description | | ---------------- | ------- | ------------------------------------------ | | block | uint256 | The block number of the transaction. | | index | uint256 | The index of the event in the transaction. | | timestamp | uint256 | The timestamp of the transaction. | | transaction hash | bytes32 | The hash of the transaction. | The following event is emitted by the market order: ```solidity /// @notice Emitted when a market order is executed. event MarketOrder( bytes32 indexed market, address indexed taker, uint256 got, uint256 gave, uint256 fee, uint256 bounty, uint256 fillVolume, bool fillWants ); ``` ## Market Creation A market is defined by the following parameters: | name | type | description | mutable | | ---------------- | ------- | ---------------------------------------------------------------- | :-----: | | market id | bytes32 | The market ID, defined below. | ❌ | | outboundToken | address | The token that is sold by the makers, bought by the takers. | ❌ | | inboundToken | address | The token that is bought by the makers, sold by the takers. | ❌ | | outboundUnits | uint64 | The [units](/protocol/glossary/units) of the outbound token. | ❌ | | inboundUnits | uint64 | The [units](/protocol/glossary/units) of the inbound token. | ❌ | | tickSpacing | uint16 | The number of ticks (1 tick = 0.1 bps) between each price level. | ❌ | | fees | uint16 | The fees for the market where 100% is 1e6. | βœ… | | minOutboundUnits | uint32 | The minimum outbound units. | βœ… | :::warning A full market is defined by 2 semi-markets. When we talk about a market here, we are talking about a semi-market. ::: Market creation occurs when a **semi**-market is defined for the first time. Subsequent activations or deactivations are handled via the `SetActive` event. A market ID is defined as the hash of the tokens, the units, and the tick spacing. ```solidity bytes32 marketId = keccak256(abi.encode( outboundToken, outboundUnits, inboundToken, inboundUnits, tickSpacing )); ``` * **Outbound tokens** are the tokens that are sold by the makers, bought by the takers. * **Inbound tokens** are the tokens that are bought by the makers, sold by the takers. * **Tick spacing** is the number of ticks (1 tick = 0.1 bps) between each price level. * **Units** are the multiplier to go from raw amount to the stored amount. Prices does not take units into account but rather the raw amounts. The market creation is defined by the following event: ```solidity /// @notice Emitted when a new market is created. /// @param market The market id. /// @param outboundToken The outbound token address. /// @param inboundToken The inbound token address. /// @param outboundUnits The outbound units. /// @param minOutboundUnits The minimum outbound units. /// @param inboundUnits The inbound units. /// @param fees The fees. /// @dev when this event is emitted, it implies that the market is active by default. event NewMarket( bytes32 indexed market, address indexed outboundToken, address indexed inboundToken, uint64 outboundUnits, uint32 minOutboundUnits, uint64 inboundUnits, uint16 tickSpacing, uint16 fees ); ``` :::note When this event is thrown, the market is set active by default. ::: ### Market parameters changes The market has a handful of mutable parameters that can be changed after market creation: * **Fees** are the fees for the market where the 100% is 1e6. * **Minimum outbound units** is the minimum amount an offer shuold give to be active (defined in the outbound units). * **Active** is the active status of the market. ```solidity /// @notice Emitted when the fees are set for a market. /// @param market The market id. /// @param fees The fees. event SetFees(bytes32 indexed market, uint16 fees); /// @notice Emitted when the minimum outbound units are set for a market. /// @param market The market id. /// @param minOutboundUnits The minimum outbound units. event SetMinOutboundUnits(bytes32 indexed market, uint32 minOutboundUnits); /// @notice Emitted when the active status is set for a market. /// @param market The market id. /// @param active The active status. event SetActive(bytes32 indexed market, bool active); ``` ## Offer Owners :::note If you want to have a simple resourceless indexing solution, this section will be the last one you should index. But you won't have full offer history. If you want full indexing, you can skip this section and go directly to the [Current Book And History](/protocol/indexing/current-book-and-history) section. ::: Offer owners is the last field that is really easy to index. There is an immutable mapping between an offer ID and its owner. An offer is unique by ID and by market. | name | type | description | mutable | | --------- | ------- | --------------------------------------------- | :-----: | | offer ID | uint40 | The offer ID, defined below. | ❌ | | market ID | bytes32 | The market ID on which the offer is consumed. | ❌ | | maker | address | The owner of the offer. | ❌ | The following events are emitted for offer ownership: ```solidity /// @notice Emitted when a new offer is created. event NewOffer( bytes32 indexed market, uint40 indexed offerId, address indexed maker, uint256 gives, int24 tick, uint32 expiry, uint24 provision ); ``` ## Flash Accounting Flash accounting is the innovative mechanism that allows Vif to batch multiple operations without transferring tokens until final settlement. This system uses **transient storage** (EIP-1153) to track per-token debts and credits globally. ### How It Works Instead of transferring tokens on every operation, Vif maintains running balances in transient storage: ```solidity // Transient storage (cleared after transaction) mapping(address token => int256 delta) deltas; ``` * **Positive delta** = credit (protocol owes tokens to user) * **Negative delta** = debt (user owes tokens to protocol) * **Zero delta** = balanced :::info[Global Accounting] Deltas are tracked **per token globally**, not per user. The protocol tracks the net flow of each token across all operations in a lock. ::: ### The Lock Mechanism All operations happen within a `lock` call: ```solidity contract MyRouter is ILockCallback { function myAction() external { // 1. Encode action data bytes memory data = abi.encode(...); // 2. Call lock - Vif will callback lockCallback() bytes memory result = vif.lock(data); // 3. Decode result return abi.decode(result, ...); } function lockCallback(bytes calldata data) external returns (bytes memory) { require(msg.sender == address(vif), "Only Vif can callback"); // 4. Perform operations (they update deltas) vif.consume(...); // Market order vif.make(...); // Limit order // 5. Settle all debts and take all credits _settle(token1); _settle(token2); _take(token3); // 6. Return result data return abi.encode(...); } } ``` **Execution flow:** 1. User calls `lock(data)` 2. Vif calls back `lockCallback(data)` on caller 3. Caller performs operations (updating transient deltas) 4. Caller settles debts and takes credits 5. Vif verifies all deltas are zero 6. If balanced: success; if not: revert entire transaction ### Operations That Update Deltas #### Operations That Only Modify Deltas These operations **don't transfer tokens** - they only update transient storage: ```solidity // Market orders vif.consume(taker, market, maxTick, fillVolume, fillWants, maxOffers); // β†’ Increases debt in inbound token (user gives) // β†’ Increases credit in outbound token (user receives) // β†’ May increase credit in native token (bounties) // Limit orders vif.make(maker, market, offerId, gives, tick, expiry, provision); // β†’ Increases debt in outbound token (maker provides) // β†’ May increase debt in native token (provision) // β†’ May increase credit in inbound token (claiming previous fills) // Cancel orders vif.cancel(market, offerId); // β†’ Increases credit in outbound token (remaining offer amount) // β†’ Increases credit in inbound token (filled amount) // β†’ Increases credit in native token (provision returned) // Claim orders vif.claim(market, offerId); // β†’ Increases credit in inbound token (filled amount) // β†’ If expired: returns outbound + provision as credits // Clean expired offers vif.clean(market, offerId); // β†’ Increases credit in native token (bounty) ``` #### Operations That Transfer Tokens These operations **transfer tokens AND modify deltas**: ```solidity // Settle debt (pull tokens from user) vif.settle(token, amount, from); // β†’ Pulls `amount` tokens from `from` address // β†’ Decreases debt (increases delta by +amount) // For native token: vif.settle{value: amount}(address(0), amount, from); // β†’ Uses msg.value // β†’ Decreases debt by +amount // Take credit (push tokens to user) vif.take(token, amount, receiver); // β†’ Sends `amount` tokens to `receiver` // β†’ Decreases credit (decreases delta by -amount) // Clear dust credit vif.clear(token, amount); // β†’ Donates `amount` to protocol fees // β†’ Decreases credit (decreases delta by -amount) ``` ### Example: Simple Swap Here's how a swap works with flash accounting: ```solidity // User wants: 1 WETH for USDC (market order) // Initial state: delta[WETH] = 0, delta[USDC] = 0 // 1. Execute market order vif.consume(user, market, maxTick, 1 ether, true, 100); // After: delta[WETH] = +1e18 (credit) // delta[USDC] = -2000e6 (debt, user owes 2000 USDC) // 2. Settle USDC debt USDC.transferFrom(user, address(vif), 2000e6); // Actually done by vif.settle() vif.settle(USDC, 2000e6, user); // After: delta[WETH] = +1e18 (credit) // delta[USDC] = 0 (balanced) // 3. Take WETH credit vif.take(WETH, 1 ether, user); WETH.transfer(user, 1 ether); // Actually done by vif.take() // After: delta[WETH] = 0 (balanced) // delta[USDC] = 0 (balanced) // Lock ends successfully - all deltas are zero βœ“ ``` ### Example: Multi-Hop Swap Flash accounting shines with multi-hop trades: ```solidity // User wants: WETH β†’ USDC β†’ DAI // Initial state: all deltas = 0 // 1. Swap WETH for USDC vif.consume(user, wethUsdcMarket, maxTick, 1 ether, true, 100); // After: delta[WETH] = -1e18 (debt) // delta[USDC] = +2000e6 (credit) // 2. Swap USDC for DAI vif.consume(user, usdcDaiMarket, maxTick, 2000e6, false, 100); // After: delta[WETH] = -1e18 (debt) // delta[USDC] = 0 (balanced! Credit netted with debt) // delta[DAI] = +2000e18 (credit) // 3. Settle and take vif.settle(WETH, 1 ether, user); // Pay WETH debt vif.take(DAI, 2000e18, user); // Receive DAI credit // Notice: USDC was never transferred! βœ“ // The protocol netted the intermediate token automatically ``` ### Example: Flash Loan Free flash loans are a side effect of the accounting system: ```solidity function flashLoan() external { bytes memory data = ...; vif.lock(data); } function lockCallback(bytes calldata data) external returns (bytes memory) { // 1. Take a "loan" (create credit) vif.take(WETH, 100 ether, address(this)); // After: delta[WETH] = -100e18 (debt) // 2. Use the tokens _doArbitrage(100 ether); // Make profit // 3. Pay back (settle debt) vif.settle(WETH, 100 ether, address(this)); // After: delta[WETH] = 0 (balanced) return ""; } // Lock ends successfully - no cost flash loan! βœ“ ``` :::tip[Zero-Cost Flash Loans] As long as you settle all debts before the lock ends, you can borrow any amount of any token with **no fees**. This is useful for: * Arbitrage strategies * Liquidations * Collateral swaps * Capital-efficient operations ::: ### Example: Limit Order with Claim Combining operations efficiently: ```solidity // Maker has an active offer that has been partially filled // They want to claim the filled amount and update the offer function updateOffer() external { vif.lock(abi.encode(...)); } function lockCallback(bytes calldata data) external returns (bytes memory) { // 1. Create new offer (also claims old one) (uint40 newOfferId, uint256 claimed) = vif.make( maker, market, oldOfferId, // Edit existing offer 2 ether, // New amount tick, expiry, provision ); // After: delta[WETH] = -2e18 (debt, new offer amount) // delta[USDC] = +claimed (credit, claimed from old fills) // 2. Settle outbound debt with new tokens vif.settle(WETH, 2 ether, maker); // After: delta[WETH] = 0 // delta[USDC] = +claimed // 3. Take inbound credit vif.take(USDC, claimed, maker); // After: delta[WETH] = 0 // delta[USDC] = 0 return abi.encode(newOfferId, claimed); } // In one transaction: claimed proceeds + updated offer βœ“ ``` ### Balance Verification At the end of every `lock`, Vif verifies: ```solidity for each token { require(delta[token] == 0, "Unbalanced"); } ``` If **any** token has a non-zero delta: * The entire transaction reverts * No state changes persist * No tokens are transferred This guarantees: * **Atomic operations** - all or nothing * **No stuck funds** - users can't accidentally leave credits * **Debt protection** - users can't leave without settling ### Gas Savings Flash accounting provides substantial gas savings: | Scenario | Without Flash Accounting | With Flash Accounting | Savings | | ---------------------- | ------------------------ | --------------------- | ------- | | Simple swap | 2 transfers (in + out) | 2 transfers | 0% | | Multi-hop (3 hops) | 6 transfers | 2 transfers | \~67% | | Limit order + claim | 4 transfers | 2 transfers | \~50% | | Flash loan + arbitrage | Impossible or expensive | 2 transfers | ∞ | Each avoided transfer saves **\~21,000 gas** (for ERC20) or **\~30,000 gas** (for native token). ### Security Considerations #### Reentrancy Protection The lock mechanism provides implicit reentrancy protection: * Only one lock can be active at a time * Nested locks revert * All state is verified before unlock #### Native Token Handling Native token (ETH/MATIC/etc.) uses `address(0)` as identifier: ```solidity // Settle native debt vif.settle{value: 1 ether}(address(0), 1 ether, user); // Take native credit vif.take(address(0), 0.5 ether, user); ``` #### Dust Credits For very small credits that cost more gas to claim than they're worth: ```solidity // Clear dust (donates to protocol) vif.clear(USDC, 1); // Clear 1 wei of USDC ``` This prevents griefing attacks where someone creates tiny credits that are expensive to clear. ### Related Concepts * [Overview](/protocol/how-it-works) - Protocol introduction * [Creating Swaps](/developer/interactions/custom-router-swaps) - Practical examples * [Placing Limit Orders](/developer/interactions/custom-router-limits) - Using flash accounting for making ## Overview Vif is an ultra-efficient, fully on-chain order book protocol that combines innovative design patterns to deliver optimal performance and user experience. ### Core Design Principles The protocol is built on three fundamental pillars: #### 1. Efficient Price Discovery with Tick Trees Vif uses a **3-level 256-bit bitmap tree** to efficiently locate active price points in the order book. This structure allows for: * **O(1) best price lookup** - Finding the best available price requires checking at most 3 bitmaps * **Efficient price traversal** - Moving to the next price point is extremely gas-efficient * **Compact storage** - The entire tree fits in a fixed-size array of `1 + 256 + 256*256` elements :::info[Price Representation] Prices are represented as ticks using the formula: `price = 1.00001^tick` Where tick is a signed 24-bit integer representing log base 1.00001 (0.1 basis points) of the price ratio `inboundAmount/outboundAmount`. ::: #### 2. Flash Accounting System Instead of transferring tokens on every operation, Vif uses **transient storage** to track debts and credits: ```solidity // All these operations only update transient storage: - Market orders - Limit orders - Claiming orders - Cancelling orders - Cleaning expired offers // Only these operations transfer tokens: - Settling debts (settle) - Taking credits (take) - Clearing dust (clear) ``` **Key Benefits:** * **Batch multiple operations** without any token transfers until final settlement * **Free flash loans** - Borrow any amount as long as you settle before the lock ends * **Multi-hop swaps** without intermediate transfers * **Gas savings** - Net all operations before transferring tokens #### 3. Double Linked Offer Lists At each price point (tick), offers are organized in a **double linked list** with: * **Head and tail pointers** for efficient insertion/removal * **Total volume tracking** at each price level * **Offer count** for analytics * **Metadata storage** without impacting matching performance ### Market Structure Each market in Vif is uniquely identified by a hash of five parameters: | Parameter | Description | | --------------- | ------------------------------------------------------- | | `outboundToken` | Token being sold by makers | | `inboundToken` | Token being bought by makers | | `outboundUnits` | Unit size for outbound amounts (for storage efficiency) | | `inboundUnits` | Unit size for inbound amounts (for storage efficiency) | | `tickSpacing` | Minimum tick distance between price levels | ```solidity marketId = keccak256( outboundToken, outboundUnits, inboundToken, inboundUnits, tickSpacing ) ``` :::warning[Immutable Parameters] Once a market is created, its `units` and `tickSpacing` are **immutable**. Choose them carefully based on your token's characteristics. ::: ### Order Types #### Limit Orders (Making) Makers place limit orders at specific price points: * **Provide liquidity** at a chosen tick (price level) * **Earn from the spread** when takers consume your offer * **Optional expiry** with provision requirements * **Claim anytime** to withdraw filled amounts #### Market Orders (Taking) Takers consume existing limit orders: * **Instant execution** at best available prices * **Exact input or output** - specify what you give or want to receive * **Price limits** via `maxTick` parameter * **Gas control** via `maxOffers` parameter ### Token Units To achieve optimal storage efficiency, Vif represents amounts in **units**: ```solidity effectiveAmount = floor(amount / units) * units ``` :::tip[Choosing Units] For tokens with decimals: * **USDC (6 decimals)**: `units = 1e4` = 0.0001 USDC precision * Max per offer: \~28B USDC * Max market order: \~1.8e15 USDC * **WETH (18 decimals)**: `units = 1e13` = 0.00001 WETH precision * Max per offer: \~2.8M WETH * Max market order: \~184T WETH ::: Units allow packing amounts into 48 bits (\~281 trillion units), significantly reducing storage costs. ### The Lock Pattern All interactions with Vif go through the **lock pattern**: ```solidity function lock(bytes calldata data) external returns (bytes memory result) ``` 1. User calls `lock()` with encoded action data 2. Vif calls back `lockCallback()` on the caller 3. Caller performs operations (modifying transient balances) 4. Caller settles debts and takes credits 5. Vif verifies all balances are zero 6. Transaction succeeds or reverts if imbalanced This pattern enables **atomic batching** of complex operations while maintaining security. ### Authorization & Operators Users can authorize **operators** to act on their behalf: ```solidity vif.authorize(operatorAddress, true); ``` Operators can: * Create market orders for the user * Create limit orders for the user * Claim and cancel orders * Settle debts using user allowances :::danger[Operator Permissions] Operators have **full access** to: * All user token allowances to the Vif contract * All user funds held in the Vif contract Only authorize trusted contracts (like routers) as operators. ::: ### Governance Features The protocol owner can: * **Pause** the contract (prevents all lock operations) * **Blacklist** malicious router contracts * **Create markets** with custom parameters * **Set fees** (protocol and maker/taker fees) * **Configure provisions** for expired orders * **Withdraw governance fees** ### Next Steps Explore the detailed mechanics: * [Tick Tree Structure](/protocol/how-it-works/tick-tree) - Deep dive into price discovery * [Flash Accounting](/protocol/how-it-works/flash-accounting) - Understanding transient balances * [Markets & Units](/protocol/how-it-works/markets) - Token configuration details * [Offers & Lists](/protocol/how-it-works/offers) - Order book data structures Or jump to practical examples: * [Creating Swaps](/developer/interactions/custom-router-swaps) - Execute market orders * [Placing Limit Orders](/developer/interactions/custom-router-limits) - Provide liquidity ## Markets & Units Markets in Vif are the fundamental trading pairs, uniquely identified by their configuration parameters. Understanding how markets work and how to configure token units is essential for optimal protocol usage. ### Market Identification Each market is uniquely identified by a **keccak256 hash** of five parameters: ```solidity struct MarketParams { address outboundToken; // Token makers are selling uint64 outboundUnits; // Unit size for outbound amounts address inboundToken; // Token makers are buying uint64 inboundUnits; // Unit size for inbound amounts uint16 tickSpacing; // Minimum tick distance } bytes32 marketId = keccak256(abi.encodePacked( outboundToken, outboundUnits, inboundToken, inboundUnits, tickSpacing )); ``` :::warning[Immutability] Once a market is created, all five parameters are **immutable**. You cannot change units or tick spacing later, so choose carefully during market creation. ::: ### Market Directionality Markets are **directional** - a WETH/USDC market is different from a USDC/WETH market: ```solidity // WETH/USDC Market (asks) // Makers sell WETH, buy USDC marketId1 = keccak256(WETH, units1, USDC, units2, spacing); // USDC/WETH Market (bids) // Makers sell USDC, buy WETH marketId2 = keccak256(USDC, units2, WETH, units1, spacing); // These are DIFFERENT markets marketId1 != marketId2 ``` **Terminology:** * **Asks market**: Makers sell base asset (WETH), buy quote asset (USDC) * **Bids market**: Makers sell quote asset (USDC), buy base asset (WETH) ### Creating Markets Only the **protocol owner** can create new markets: ```solidity function openMarket( address outboundToken, address inboundToken, uint64 outboundUnits, uint64 inboundUnits, uint16 tickSpacing, uint24 fee, uint24 minOutboundUnits ) external onlyOwner returns (bytes32 marketId); ``` **Parameters:** * `outboundToken` / `inboundToken`: Token addresses * `outboundUnits` / `inboundUnits`: Unit sizes (must be powers of 10) * `tickSpacing`: Minimum tick distance between offers * `fee`: Initial fee in basis points * `minOutboundUnits`: Minimum offer size in units #### Example: WETH/USDC Pair ```solidity // Create asks market (sell WETH for USDC) bytes32 asksMarket = vif.openMarket( WETH, // outboundToken USDC, // inboundToken 1e13, // outboundUnits (0.00001 WETH) 1e4, // inboundUnits (0.0001 USDC) 1, // tickSpacing 0, // fee (0 bps) 1000 // minOutboundUnits (0.01 WETH minimum) ); // Create bids market (sell USDC for WETH) bytes32 bidsMarket = vif.openMarket( USDC, // outboundToken WETH, // inboundToken 1e4, // outboundUnits 1e13, // inboundUnits 1, // tickSpacing 0, // fee 1000 // minOutboundUnits (0.1 USDC minimum) ); ``` ### Token Units Units are the fundamental precision level for token amounts in Vif. They allow **packing amounts into 48 bits** for storage efficiency. #### Why Units? Without units, storing a full `uint256` amount requires: * **32 bytes per amount** in storage * Extremely expensive for high-volume order books With units, amounts fit in **6 bytes (48 bits)**: * `uint48` can represent up to **281,474,976,710,655 units** * Storage cost reduced by \~81% #### How Units Work All amounts are **floored to the nearest unit**: ```solidity actualAmount = floor(requestedAmount / units) * units; ``` **Example with USDC** (`units = 1e4`, decimals = 6): ```solidity units = 10_000; // 0.0001 USDC precision // Maker wants to offer 1.000011 USDC requestedAmount = 1_000_011; // 1.000011 USDC in 6 decimals // Actual amount stored actualAmount = (1_000_011 / 10_000) * 10_000 = 100_001 * 10_000 = 1_000_010; // 1.00001 USDC // Lost precision: 1 wei (0.000001 USDC) ``` :::info[Precision Loss] Users may lose up to `(units - 1)` wei when creating offers. This is usually negligible but important for UX design. ::: #### Choosing Units Units should be **powers of 10** for readability. Choose based on: 1. **Token decimals** 2. **Expected price ranges** 3. **Minimum meaningful amounts** **General formula:** ```solidity units = 10^(decimals - precisionDigits) ``` Where `precisionDigits` is how many significant digits you want to preserve. #### Examples by Token Type ##### Stablecoins (USDC, USDT, DAI) ```solidity // USDC (6 decimals) units = 1e4; // 10,000 wei = 0.0001 USDC // Max offer: 281T units * 1e4 / 1e6 = 2.8 billion USDC // Min tick move: ~0.0001 USDC on $1000 offer // DAI (18 decimals) units = 1e16; // 0.01 DAI // Max offer: 281T units * 1e16 / 1e18 = 2.8 billion DAI ``` ##### Major Assets (WETH, WBTC) ```solidity // WETH (18 decimals) units = 1e13; // 0.00001 WETH // Max offer: 281T units * 1e13 / 1e18 = 2.8 million WETH // At $3000/WETH = $8.4 billion max // Min tick move: ~$0.03 on 1 WETH offer // WBTC (8 decimals) units = 1e3; // 0.00001 WBTC // Max offer: 281T units * 1e3 / 1e8 = 2.8 million WBTC ``` ##### Low-Value Tokens (PEPE, SHIB) ```solidity // PEPE (18 decimals, price ~$0.000001) units = 1e18; // 1 whole PEPE // Max offer: 281 trillion PEPE // At $0.000001 = $281 million max ``` ##### High-Value Tokens (Rare NFT fractions) ```solidity // Fractionalized NFT (18 decimals, price ~$10,000/unit) units = 1e10; // 0.00000001 units // Max offer: 281T units * 1e10 / 1e18 = 2,814 units // At $10,000 = $28 million max ``` #### Units and Tick Precision The combination of units and tick spacing determines effective price precision: ```solidity // WETH/USDC market outboundUnits = 1e13; // 0.00001 WETH inboundUnits = 1e4; // 0.0001 USDC tickSpacing = 1; // Price changes by 0.001% per tick // On a 1 WETH offer: // 1 tick = 1 WETH * 0.00001 = 0.00001 WETH = $0.03 (at $3000 WETH) // Effective price grid: // ..., 2999.97, 3000.00, 3000.03, 3000.06, ... ``` ### Minimum Offer Sizes Markets enforce a **minimum outbound amount** to prevent dust: ```solidity minOutboundUnits = 1000; // Set during market creation outboundUnits = 1e13; // WETH units // Minimum WETH offer: minOffer = minOutboundUnits * outboundUnits = 1000 * 1e13 = 1e16 wei = 0.01 WETH ``` :::warning[Offer Size Validation] Attempting to create an offer below `minOutboundUnits * outboundUnits` will **revert**. ::: ### Market Metadata Each market stores additional metadata: ```solidity struct Market { bool isActive; // Can be toggled by owner uint24 fee; // Protocol fee in basis points uint24 minOutboundUnits; // Minimum offer size uint256 totalFees; // Accumulated protocol fees // ... other fields } ``` **Owner can modify:** * `isActive` - Pause/unpause trading * `fee` - Adjust protocol fees * `minOutboundUnits` - Change minimum order size **Cannot modify:** * Token addresses * Units * Tick spacing ### Multi-Market Strategies Since markets are directional, you often need both directions: #### Order Book Display ```solidity // Get asks (sell WETH for USDC) Market asks = { outboundToken: WETH, inboundToken: USDC, outboundUnits: 1e13, inboundUnits: 1e4, tickSpacing: 1 }; // Get bids (sell USDC for WETH) Market bids = { outboundToken: USDC, inboundToken: WETH, outboundUnits: 1e4, inboundUnits: 1e13, tickSpacing: 1 }; // Display spread: // Best ask: lowest tick in asks market // Best bid: lowest tick in bids market (inverted for display) ``` #### Market Making ```solidity // Place offers on both sides function provideLiquidity() external { bytes memory data = abi.encode(...); vif.lock(data); } function lockCallback(bytes calldata) external returns (bytes memory) { // Sell WETH at higher price (ask) vif.make(maker, asksMarket, 0, 1 ether, askTick, 0, 0); // Sell USDC at lower price (bid) vif.make(maker, bidsMarket, 0, 3000e6, bidTick, 0, 0); // Settle both _settle(WETH, 1 ether); _settle(USDC, 3000e6); return ""; } // Now earning spread on both sides βœ“ ``` ### Gas Considerations Market parameters affect gas costs: | Parameter | Impact on Gas | | --------------------- | --------------------------------------- | | `tickSpacing = 1` | More price levels, higher matching cost | | `tickSpacing = 10` | Fewer price levels, lower matching cost | | `outboundUnits` small | More precision, but same gas | | `outboundUnits` large | Less precision, same gas | **Recommendation**: Use larger tick spacing (10-100) for volatile or low-volume pairs to reduce gas costs. ### Related Concepts * [Tick Tree Structure](/protocol/how-it-works/tick-tree) - Price level organization * [Units](/protocol/glossary/units) - Detailed unit explanation * [Creating Swaps](/developer/interactions/custom-router-swaps) - Using markets for trading ## 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: ```solidity 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**: ```solidity 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**: ```solidity // 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: ```solidity 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) :::info[Provision Required] If `expiry > 0`, you **must** provide a provision in native token. This incentivizes cleaners to remove expired offers. ::: ##### `provision` (uint24) Amount of native token (ETH/MATIC/etc.) locked with the offer, in **provision units**: ```solidity 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: ```solidity // 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: ```solidity 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 ```solidity 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: ```solidity // 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 ``` :::info[FIFO Matching] Offers at the same price are matched in **insertion order** (first in, first out). Earlier offers get filled first. ::: #### Removal Logic Offers can be removed by: * **Full fill** during market order * **Cancellation** by maker * **Expiry** (requires cleaning) ```solidity // 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 ```solidity 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 ```solidity 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 ```solidity 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 ```solidity 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`: ```solidity 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**: ```solidity 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: ```solidity 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` :::warning[Tick Change Penalty] Editing an offer's tick moves it to the **back of the queue** at the new tick, even if the tick didn't change. This prevents queue-jumping. ::: ### 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 | **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 * [Flash Accounting](/protocol/how-it-works/flash-accounting) - How debts/credits work * [Tick Tree Structure](/protocol/how-it-works/tick-tree) - Price level organization * [Placing Limit Orders](/developer/interactions/custom-router-limits) - Creating offers * [Creating Swaps](/developer/interactions/custom-router-swaps) - Consuming offers ## Tick Tree Structure The tick tree is the core data structure that enables efficient price discovery in Vif. It's a **3-level bitmap tree** where each level uses 256-bit integers as bitmaps. ### Tree Architecture ``` Root Layer (256 bits) ↓ Level 1 Layer (256 Γ— 256 bits) ↓ Level 2 Layer (256 Γ— 256 Γ— 256 bits) ``` Each set bit in a bitmap indicates that price level is active (has offers). ### Finding the Best Price To find the best price (lowest tick for asks, highest tick for bids): ```solidity 1. Find least significant set bit in root bitmap β†’ gives rootIndex (0-255) 2. Go to level1[rootIndex], find LSB β†’ gives level1Index (0-255) 3. Go to level2[rootIndex][level1Index], find LSB β†’ gives level2Index (0-255) 4. Calculate final tick index: index = (rootIndex << 16) | (level1Index << 8) | level2Index or equivalently: index = rootIndex * 65536 + level1Index * 256 + level2Index ``` :::tip[Visual Example] If the tree looks like this: * Root bitmap: `...0001000...` (bit 3 set) * Level1\[3]: `...0000100...` (bit 2 set) * Level2\[3]\[2]: `...0100000...` (bit 5 set) Then: `index = (3 << 16) | (2 << 8) | 5 = 196,613` ::: ### Finding the Next Price After consuming all offers at the current price, we need to find the next best price: ```solidity 1. Unset the current bit in level2 bitmap 2. Find next LSB in level2 - If found: done - If not found: move to step 3 3. Unset the current bit in level1 bitmap 4. Find next LSB in level1 - If found: go down to corresponding level2, find its LSB - If not found: move to step 5 5. Unset the current bit in root bitmap 6. Find next LSB in root - If found: go down through level1 and level2 - If not found: no more prices available ``` This traversal algorithm ensures **O(1) complexity** for moving to the next price point. ### Storage Layout The entire tree is stored as a **fixed-size array**: ```solidity // Total elements: 1 + 256 + 256*256 = 65,793 uint256 slots tree[0] // Root bitmap (1 slot) tree[1..256] // Level 1 bitmaps (256 slots) tree[257..65792] // Level 2 bitmaps (65,536 slots) ``` This fixed allocation means: * **Predictable gas costs** for tree operations * **No dynamic allocation** overhead * **Maximum of 16,777,216 possible price points** (256Β³) ### Index ↔ Tick ↔ Price Conversion #### Index to Tick Ticks are signed 24-bit integers, while indices are unsigned: ```solidity const MAX_TICK = 8_388_607; // type(int24).max tick = index - MAX_TICK; index = tick + MAX_TICK; ``` :::warning[Invalid Tick] The tick value `-8_388_608` (type(int24).min) is **invalid** and cannot be used. ::: #### Tick to Price The price is calculated as: $$ price = 1.00001^{tick} $$ Where price represents the ratio `inboundAmount / outboundAmount`. **Key properties:** * Higher tick = higher price = worse for taker * Lower tick = lower price = better for taker * 1 tick = 0.1 basis point (0.001%) price movement #### In-Contract Calculation Prices are represented as **Q128.128 fixed-point numbers** using binary exponentiation: ```solidity // For negative ticks: price = binaryExp(-|tick|) // For positive ticks: price = 1 / binaryExp(|tick|) ``` See [Binary Exponentiation](/protocol/glossary/binary-exponentiation) for implementation details. ### Tick Spacing Markets can specify a **tick spacing** to: * Reduce gas costs on volatile pairs * Prevent penny-jumping (makers placing orders 1 tick better) * Align with traditional market tick sizes ```solidity // Example: tickSpacing = 10 // Only ticks ..., -20, -10, 0, 10, 20, ... are valid actualTick = round(requestedTick / tickSpacing) * tickSpacing ``` When placing an order: * Your requested tick is **rounded towards zero** to the nearest valid tick * The tree only activates indices corresponding to valid ticks :::info[Effect on Tree] With tick spacing of 10: * Only every 10th index in the tree can be active * This doesn't reduce tree size, but reduces active price points * Gas savings come from fewer price levels to traverse during matching ::: ### Example: WETH/USDC Market Let's trace finding the best ask price in a WETH/USDC market: ```solidity // Setup outboundToken = WETH inboundToken = USDC tickSpacing = 1 // Current state: offers at ticks -2,072,336 and -2,000,000 // These correspond to prices ~1000 USDC and ~1200 USDC per WETH // Finding best price (lowest tick): tick₁ = -2,072,336 index₁ = -2,072,336 + 8,388,607 = 6,316,271 // Convert to tree coordinates: rootIndex = 6,316,271 >> 16 = 96 level1Index = (6,316,271 >> 8) & 0xFF = 101 level2Index = 6,316,271 & 0xFF = 143 // Tree lookup: root[96] has bit set β†’ check level1[96][101] level1[96][101] has bit set β†’ check level2[96][101][143] level2[96][101][143] has bit set at position 143 // Result: Best offer is at tick -2,072,336 price = 1.00001^(-2,072,336) β‰ˆ 0.8187 WETH/USDC or equivalently: ~1221.7 USDC/WETH ``` ### Gas Optimization The bitmap structure provides exceptional gas efficiency: | Operation | Gas Cost | Notes | | ------------------ | ----------- | ------------------------ | | Find best price | \~3 SLOAD | One per level | | Move to next price | \~3-9 SLOAD | Depends on tree descent | | Activate price | \~3 SSTORE | Set bits in each level | | Deactivate price | \~3 SSTORE | Unset bits in each level | Compare this to: * **Linked list**: O(n) traversal, unpredictable gas * **Heap**: O(log n) operations, expensive updates * **Sorted array**: O(n) insertion, O(1) lookup The bitmap tree achieves **near-constant time operations** regardless of the number of active price points. ### Related Concepts * [Tick](/protocol/glossary/tick) - Understanding tick representation * [Binary Exponentiation](/protocol/glossary/binary-exponentiation) - Price calculation * [Markets & Units](/protocol/how-it-works/markets) - Market configuration ## Binary exponentiation To compute the price from a tick value, we use [binary exponentiation](https://cp-algorithms.com/algebra/binary-exp.html). ### Implementation We don't actually compute the price from the initial formula, but rather first compute the following: $$price = pow(1.00001, -|tick|)$$ Finally, if the tick is positive, we return the inverse of the result. Here are the binary exponents for the initial computation: | bit[^1] | exponent | | ------- | ---------------------------------- | | 0 | 0xffff583ac1ac1c114b9160ddeb4791b7 | | 1 | 0xfffeb075f14b276d06cdbc6b138e4c4b | | 2 | 0xfffd60ed9a60ebcb383de6edb7557ef0 | | 3 | 0xfffac1e213e349a0cf1e3d3ec62bf25b | | 4 | 0xfff583dfa4044e3dfe90c4057e3e4c27 | | 5 | 0xffeb082d36bf2958d476ee75c4da258a | | 6 | 0xffd61212165632bd1dda4c1abdf5f9f1 | | 7 | 0xffac2b0240039d9cdadb751e0acc14c4 | | 8 | 0xff5871784dc6fa608dca410bdecb9ff4 | | 9 | 0xfeb1509bdff34ccb280fad9a309403cf | | 10 | 0xfd6456c5e15445b458f4403d279c1a89 | | 11 | 0xfacf7ad7076227f61d95f764e8d7e35a | | 12 | 0xf5b9e413dd1b4e7046f8f721e1f1b295 | | 13 | 0xebdd5589751f38fd7adce84988dba856 | | 14 | 0xd9501a6728f01c1f121094aacf4c9475 | | 15 | 0xb878e5d36699c3a0fd844110d8b9945f | | 16 | 0x84ee037828011d8035f12eb571b46c2a | | 17 | 0x450650de5cb791d4a002074d7f179cb3 | | 18 | 0x129c67bfc1f3084f1f52dd418a4a8f6d | | 19 | 0x15a5e2593066b11cd1c3ea05eb95f74 | | 20 | 0x1d4a2a0310ad5f70ad53ef4d3dcf3 | | 21 | 0x359e3010271ed5cfce08f99aa | | 22 | 0xb3ae1a60d291e4871 | #### Solidity implementation This implementation uses many optimizations to save gas. ```solidity uint256 private constant _MAX_TICK = 8_388_607; /// @notice Compute Q128.128 price from a tick. /// @param tick_ The log-price in 0.1 bps ticks. /// @return price The price in Q128.128. function tickToPrice(int24 tick_) internal pure returns (uint256 price) { unchecked { // absolute value of tick uint256 absTick = (uint256(int256(tick_)) + uint256(int256(tick_) >> 255)) ^ uint256(int256(tick_) >> 255); /// @solidity memory-safe-assembly assembly { if gt(absTick, _MAX_TICK) { mstore(0x00, 0x4ce861de) // `TickOverflow()`. revert(0x1c, 0x04) } } if (absTick & 0x1 != 0) price = 0xffff583ac1ac1c114b9160ddeb4791b7; else price = 0x100000000000000000000000000000000; if (absTick & 0x2 != 0) price = (price * 0xfffeb075f14b276d06cdbc6b138e4c4b) >> 128; if (absTick & 0x4 != 0) price = (price * 0xfffd60ed9a60ebcb383de6edb7557ef0) >> 128; if (absTick & 0x8 != 0) price = (price * 0xfffac1e213e349a0cf1e3d3ec62bf25b) >> 128; if (absTick & 0x10 != 0) price = (price * 0xfff583dfa4044e3dfe90c4057e3e4c27) >> 128; if (absTick & 0x20 != 0) price = (price * 0xffeb082d36bf2958d476ee75c4da258a) >> 128; if (absTick & 0x40 != 0) price = (price * 0xffd61212165632bd1dda4c1abdf5f9f1) >> 128; if (absTick & 0x80 != 0) price = (price * 0xffac2b0240039d9cdadb751e0acc14c4) >> 128; if (absTick & 0x100 != 0) price = (price * 0xff5871784dc6fa608dca410bdecb9ff4) >> 128; if (absTick & 0x200 != 0) price = (price * 0xfeb1509bdff34ccb280fad9a309403cf) >> 128; if (absTick & 0x400 != 0) price = (price * 0xfd6456c5e15445b458f4403d279c1a89) >> 128; if (absTick & 0x800 != 0) price = (price * 0xfacf7ad7076227f61d95f764e8d7e35a) >> 128; if (absTick & 0x1000 != 0) price = (price * 0xf5b9e413dd1b4e7046f8f721e1f1b295) >> 128; if (absTick & 0x2000 != 0) price = (price * 0xebdd5589751f38fd7adce84988dba856) >> 128; if (absTick & 0x4000 != 0) price = (price * 0xd9501a6728f01c1f121094aacf4c9475) >> 128; if (absTick & 0x8000 != 0) price = (price * 0xb878e5d36699c3a0fd844110d8b9945f) >> 128; if (absTick & 0x10000 != 0) price = (price * 0x84ee037828011d8035f12eb571b46c2a) >> 128; if (absTick & 0x20000 != 0) price = (price * 0x450650de5cb791d4a002074d7f179cb3) >> 128; if (absTick & 0x40000 != 0) price = (price * 0x129c67bfc1f3084f1f52dd418a4a8f6d) >> 128; if (absTick & 0x80000 != 0) price = (price * 0x15a5e2593066b11cd1c3ea05eb95f74) >> 128; if (absTick & 0x100000 != 0) price = (price * 0x1d4a2a0310ad5f70ad53ef4d3dcf3) >> 128; if (absTick & 0x200000 != 0) price = (price * 0x359e3010271ed5cfce08f99aa) >> 128; if (absTick & 0x400000 != 0) price = (price * 0xb3ae1a60d291e4871) >> 128; if (tick_ > 0) { /// @solidity memory-safe-assembly assembly { price := add(div(sub(0, price), price), 1) } } } } ``` #### JavaScript Implementation This algorithm can easily be implemented with JavaScript `BigInt`s, since the `price` will never overflow 256 bits in the original algorithm. Apart from this, `BigInt` operations are roughly the same as Solidity ones. #### Other Language Implementations For the initial part of the computation, price is bounded to 129 bits. Then while inverting the result, the `price` will be bounded to 256 bits. `price` is an unsigned integer. `absTick` is a 24-bit unsigned integer and `tick` is a 24-bit signed integer. ### Computing the Binary Exponents In order to compute the binary exponents, we used the following Python code defined below. :::warning Please do not recompute the binary exponents yourself but rather use the ones defined in the core contract to ensure consistency. The current section is only here for reference. ::: ```python import math from decimal import Decimal, localcontext, ROUND_DOWN, ROUND_CEILING, ROUND_FLOOR def _to_decimal(base: float | str | Decimal) -> Decimal: if isinstance(base, Decimal): return base if isinstance(base, str): return Decimal(base) # Fallback for numeric types: go through str to avoid binary float artifacts return Decimal(str(base)) def _inverse_power_by_squaring( base_dec: Decimal, i: int, precision_digits: int, rounding: str ) -> Decimal: """ Compute base^(-2^i) as a Decimal using i squarings of base^{-1} under the given rounding mode and precision. Rounding is applied at each operation per Decimal context. """ if i < 0: raise ValueError("i must be non-negative") if base_dec <= 0: raise ValueError("base must be positive") with localcontext() as ctx: ctx.prec = precision_digits ctx.rounding = rounding inv = Decimal(1) / base_dec # rounded per context # Each squaring doubles the exponent: inv^(2^k) for _ in range(i): inv = inv * inv # rounded per context return inv def _required_decimal_digits(shift_bits: int, i: int) -> int: """ Heuristic for the number of decimal digits needed to safely determine floor(2^shift * value). We start with enough digits to represent shift_bits plus margin and grow with i to account for error accumulation across i squarings. """ # Bits to decimal digits conversion: digits = bits * log10(2) base_digits = math.ceil(shift_bits * math.log10(2)) # Add generous safety margin and some per-squaring buffer return base_digits + 40 + max(0, i) * 3 def exponent_for( base: float | str | Decimal, i: int, shift: int = 128, max_iter: int = 10 ) -> int: """ Compute floor(2^shift * base^(-2^i)) as an integer with guaranteed correctness. Parameters: - base: numeric or string convertible to Decimal. Example: "1.00001" - i: non-negative integer, exponent index (computes 2^i) - shift: number of bits for the scaling factor (2^shift). Typical: 128 or 256 - max_iter: maximum precision refinement iterations """ base_dec = _to_decimal(base) if base_dec <= 0: raise ValueError("base must be positive") if i < 0: raise ValueError("i must be non-negative") if shift < 1: raise ValueError("shift must be >= 1") scale = 1 << shift # Iteratively refine precision until the floored values from lower and upper bounds converge precision_digits = _required_decimal_digits(shift, i) for _ in range(max_iter): # Lower bound using downward rounding at every step lb = _inverse_power_by_squaring(base_dec, i, precision_digits, ROUND_DOWN) # Upper bound using upward rounding at every step ub = _inverse_power_by_squaring(base_dec, i, precision_digits, ROUND_CEILING) with localcontext() as ctx_lb: ctx_lb.prec = precision_digits ctx_lb.rounding = ROUND_FLOOR a = int((lb * Decimal(scale)).to_integral_value(rounding=ROUND_FLOOR)) with localcontext() as ctx_ub: ctx_ub.prec = precision_digits ctx_ub.rounding = ROUND_FLOOR c = int((ub * Decimal(scale)).to_integral_value(rounding=ROUND_FLOOR)) if c == a: return a if c == a + 1: # Decide boundary precisely using the lower bound # If lb >= (a+1)/scale then the true value >= threshold, so floor is a+1 with localcontext() as ctx_chk: ctx_chk.prec = precision_digits ctx_chk.rounding = ROUND_DOWN threshold = Decimal(a + 1) / Decimal(scale) if lb >= threshold: return a + 1 return a # Ambiguity larger than 1 ULP: increase precision and try again precision_digits = int(precision_digits * 1.6) + 10 # Fallback: compute with high precision and return the floor using downward rounding precision_digits = precision_digits + 80 lb = _inverse_power_by_squaring(base_dec, i, precision_digits, ROUND_DOWN) with localcontext() as ctx: ctx.prec = precision_digits ctx.rounding = ROUND_FLOOR return int((lb * Decimal(scale)).to_integral_value(rounding=ROUND_FLOOR)) ``` We ran the `exponent_for` function with the following parameters: * `base`: `1.00001` * `i`: the bit index * `shift`: 128 * `max_iter`: 10 [^1]: The bits indexes are counted from the least significant bit (LSB) to the most significant bit (MSB). ## Tick The tick is a price representation of a price level. It is a 24-bit integer that is the log base `1.00001` of the price. This means that the price difference between 2 ticks is 0.1 basis points. The mathematical formula to go from tick to price is: $$p = 1.00001^{tick}$$ And the inverse formula to go from price to tick is: $$tick = \log\_{1.00001}(p) = \frac{ln(p)}{ln(1.00001)}$$ ### Computation The price represents the ratio of the inbound token to the outbound token. This ratio is completely agnostic of the units/decimals of the tokens. For this reason, we need a wide range of prices. The price representation is a [fixed point 128.128 bits integer](https://en.wikipedia.org/wiki/Fixed-point_arithmetic). On-chain, the only computation we need is to convert the tick to price with binary exponentiation. :::note For the full details on the implementation, see the [Binary Exponentiation](/protocol/glossary/binary-exponentiation) section. ::: ### Getting the virtual `wants` amount The price represents the ratio $p = \frac{inbound}{outbound}$. The only conversion that has to be done on-chain is the conversion from outbound amount `gives` to inbound amount. This is used to compute the implicit `wants` amount, which is the total amount of inbound token an offer is requesting. Given a `price`, the `outbound` amount and units, as well as the `inbound` units, the wants amount scaled down to the `inbound` units is computed as follows: $u\_o$: units of the outbound token $u\_i$: units of the inbound token $$inbound = \left\lceil \frac{outbound}{u_i} \times price \times u\_o \right\rceil $$ In essence, we round up the result of the division of the raw outbound amount times the price by the inbound units to the nearest integer (the goal is to favor the maker to avoid round downs causing 0 wants). ## Units In order to save gas and space while storing offer details, we decided not to use the usual 128 bits to store an amount, but rather 48 bits to store received and sent amounts, and 64 bits for market orders. In order to do this, we had to define units. **Units are the multiplier to go from raw amount to the stored amount**. ### Example Units Most likely, we'll use a units amount equivalent to `$0.0001`, but only the closest one to a power of 10. This way it is still human readable - we believe it does not make sense to try to sell less than $0.0001 and we can sell up to ($2^{48} \times 0.00001 = 28e9$) $28B in value. | Asset | USD Price | Unit | Maximum Value | | ----- | --------- | ------------- | ------------- | | ETH | $4,000 | 1e-8 ETH | 2.8M ETH | | USDC | $1 | 0.0001 USDC | 28B USDC | | BTC | $100,000 | 1e-8 BTC [^1] | 2.8M BTC | [^1]: BTC unit size will actually be the smallest possible unit as BTC only has 8 decimals precision. ### Example conversion When going from raw amount to stored amount, we will divide the raw amount and floor the result. ```solidity uint256 rawAmount = 1.000000001 ether; uint256 units = 1e10; // because ETH has 18 decimals precision uint48 storedAmount = uint48(rawAmount / units); // ^1e8 ``` When going from stored amount to raw amount, we will multiply the stored amount and ceil the result. ```solidity uint64 rawAmount = storedAmount * units; ``` ## Developer Documentation Welcome to the Vif Protocol developer documentation. This section covers everything you need to build applications on top of Vif. ### What You'll Find Here #### TypeScript SDK (vifdk) The official TypeScript SDK for building applications on Vif: * **[Getting Started](/developer/sdk)** - Installation, setup, and quick start guide * **[Core Classes](/developer/sdk/core-classes)** - Token, Market, and fundamental types * **[VifRouter](/developer/sdk/vif-router)** - Building and executing transactions * **[Router Actions](/developer/sdk/router-actions)** - Available actions and parameters #### Contract Interactions Learn how to interact directly with the Vif smart contracts: * **[Using VifRouter](/developer/interactions/using-vif-router)** - The built-in command-based router for executing trades, placing limit orders, and managing positions * **[Reading State](/developer/reading-state)** - Query order book data, market information, and user positions using VifReader * **[Building Custom Routers](/developer/interactions/custom-router-swaps)** - Create your own specialized routers for advanced use cases #### Indexer Documentation The Vif indexer provides efficient access to historical and current protocol data through GraphQL queries. ### Getting Started If you're new to Vif, we recommend: 1. **Understand the protocol** - Start with [How It Works](/protocol/how-it-works) to learn about tick trees, flash accounting, and the order book structure 2. **Use the TypeScript SDK** - Most applications should use [vifdk](/developer/sdk) for a type-safe, high-level interface 3. **Or use VifRouter directly** - For direct contract interaction, use the [built-in VifRouter](/developer/interactions/using-vif-router) 4. **Read state efficiently** - Use [VifReader](/developer/reading-state) to query order book data 5. **Build advanced features** - Create [custom routers](/developer/interactions/custom-router-swaps) only when needed ### Contract Addresses :::info[Deployment Information] Contract addresses and deployment information will be available here once the protocol is deployed to mainnet and testnets. ::: ### Support & Resources * **GitHub**: [github.com/mangrovedao/vif-core](https://github.com/mangrovedao/vif-core) * **Discord**: Join the Mangrove DAO community for developer support * **Twitter**: [@MangroveDAO](https://x.com/MangroveDAO) ### What's Next? Choose your path: * **Building a TypeScript/JavaScript app?** β†’ Start with the [TypeScript SDK](/developer/sdk) * **Displaying order book data?** β†’ Check out [Reading State](/developer/reading-state) * **Direct contract interaction?** β†’ Use [VifRouter](/developer/interactions/using-vif-router) * **Creating a custom integration?** β†’ Learn about [Building Custom Routers](/developer/interactions/custom-router-swaps) * **Need protocol background?** β†’ Read [How the Protocol Works](/protocol/how-it-works) ## Reading Protocol State The `VifReader` contract provides gas-efficient methods for reading the Vif protocol state off-chain. Use it to query order book data, market information, and user positions. ### Overview VifReader is designed for **off-chain reads** via Etherscan, block explorers, or RPC calls. Most methods are gas-unbounded and should not be used in transactions. **Use VifReader to:** * Query order book depth and liquidity * Read specific offers and their owners * Get market configurations * List all active markets * Check authorizations and fees :::info[Contract Interaction] You can interact with VifReader directly through: * Etherscan's "Read Contract" interface * Block explorer contract pages * RPC calls via `eth_call` * Web3 libraries (ethers.js, viem, web3.js) ::: ### Setup Before reading market data, you must register the market pair: #### `updateMarkets` ``` updateMarkets( address token0, address token1, uint64 units0, uint64 units1, uint16 tickSpacing ) ``` Registers both directions of a market pair (e.g., WETH/USDC and USDC/WETH). The tokens are automatically ordered, so you can pass them in any order. **Behavior:** * Adds the market pair if either direction is active * Removes the pair if both directions become inactive * Must be called before using book reading functions ### Reading the Order Book #### `packedOfferList` ``` packedOfferList( bytes32 marketId, uint40 fromId, uint256 maxOffers ) β†’ ( uint40 nextOfferId, uint40[] offerIds, uint256[] offers, address[] owners ) ``` Returns a paginated list of offers from the order book. **Parameters:** * `marketId`: Market identifier * `fromId`: Offer ID to start from (0 for beginning) * `maxOffers`: Maximum number of offers to return **Returns:** * `nextOfferId`: Next offer ID for pagination (0 if end reached) * `offerIds`: Array of offer IDs * `offers`: Array of packed offer data (use LibOffer.unpack to decode) * `owners`: Array of offer owner addresses **Use for:** * Building order book displays * Paginating through all offers * Finding user's active offers #### `packedBook` ``` packedBook( bytes32 marketId, uint24 fromPricePoint, uint24 maxPricePoints ) β†’ ( uint24 nextPricePoint, uint256[] offerListsPacked ) ``` Returns aggregated offer list data at each price level. **Parameters:** * `marketId`: Market identifier * `fromPricePoint`: Index to start from (0 for best price) * `maxPricePoints`: Maximum price levels to return **Returns:** * `nextPricePoint`: Next price index for pagination * `offerListsPacked`: Array of packed offer list data (contains head, tail, total volume, offer count, tick) **Use for:** * Getting market depth overview * Displaying price levels with aggregated volume * Analyzing liquidity distribution #### `offerListEndPoints` ``` offerListEndPoints( bytes32 marketId, uint40 fromId, uint256 maxOffers ) β†’ ( uint40 startId, uint256 length ) ``` Calculates pagination endpoints for offer lists. **Parameters:** * `marketId`: Market identifier * `fromId`: Starting offer ID * `maxOffers`: Maximum offers to count **Returns:** * `startId`: Actual starting offer ID * `length`: Number of offers available (capped at maxOffers) **Use for:** * Determining pagination boundaries * Calculating total offer counts ### Reading Individual Offers #### `offer` ``` offer( bytes32 marketId, uint40 offerId ) β†’ ( OfferUnpacked memory offer, address owner ) ``` Returns unpacked offer details with owner address. **Returns an OfferUnpacked struct containing:** * `gives`: Amount offered (in units) * `received`: Amount received from fills (in units) * `next`: Next offer ID in list * `prev`: Previous offer ID in list * `expiry`: Expiration timestamp (0 = never) * `provision`: Provision amount (in provision units) * `maker`: Offer owner address * `isActive`: Whether offer is in the book * `tick`: Price level **Use for:** * Checking specific offer details * Monitoring offer status * Calculating claimable amounts #### `offerPacked` ``` offerPacked( bytes32 marketId, uint40 offerId ) β†’ ( uint256 offer, address owner ) ``` Returns packed offer data as a single uint256 (more gas efficient). **Use for:** * Gas-efficient offer queries * Batch reading offers * Off-chain processing (unpack the uint256 using LibOffer.unpack) ### Reading Markets #### `market` ``` market( bytes32 marketId ) β†’ Market memory ``` Returns complete market configuration. **Returns a Market struct containing:** * `outboundToken`: Token makers are selling * `inboundToken`: Token makers are buying * `outboundUnits`: Unit size for outbound amounts * `inboundUnits`: Unit size for inbound amounts * `tickSpacing`: Minimum tick distance * `fee`: Protocol fee in basis points * `active`: Whether market is active * `minOutboundUnits`: Minimum offer size **Use for:** * Getting market parameters * Calculating unit conversions * Checking market status #### `openMarkets` ``` openMarkets() β†’ OpenMarketsResult[] ``` Returns all registered active market pairs. **Returns array of OpenMarketsResult containing:** * `market01`: First direction market data * `market10`: Second direction market data :::danger[Gas Unbounded] This method iterates over all markets and should **only** be called off-chain. Use the paginated version for on-chain calls. ::: **Use for:** * Discovering all available markets * Building market selection UIs * Market analytics #### `openMarkets` (paginated) ``` openMarkets( uint256 from, uint256 maxLength ) β†’ OpenMarketsResult[] ``` Returns a page of active markets. **Parameters:** * `from`: Index to start from * `maxLength`: Maximum markets to return **Use for:** * On-chain market queries * Paginated market loading #### `openMarketsLength` ``` openMarketsLength() β†’ uint256 ``` Returns the total number of registered market pairs. **Use for:** * Calculating pagination * Counting active markets ### Authorization & Fees #### `isAuthorized` ``` isAuthorized( address authorizer, address authorized ) β†’ bool ``` Checks if an address is authorized to act on behalf of another. **Parameters:** * `authorizer`: The user granting permission * `authorized`: The address that may have permission (typically a router) **Returns:** * `true` if authorized address can act for authorizer * `false` otherwise **Use for:** * Verifying router permissions * Checking operator status * Authorization debugging #### `authorizerNonce` ``` authorizerNonce( address authorizer ) β†’ uint256 ``` Returns the current nonce for EIP-712 signature-based authorization. **Use for:** * Building authorization signatures * Nonce management * Replay attack prevention #### `fees` ``` fees( address token ) β†’ uint256 ``` Returns accumulated protocol fees for a token. **Use for:** * Checking fee reserves * Protocol analytics * Fee monitoring #### `minProvision` ``` minProvision() β†’ uint24 ``` Returns the global minimum provision requirement (in provision units). **Use for:** * Calculating provision amounts for expiring offers * Validating offer parameters * Cost estimation ### Protocol Status #### `isPaused` ``` isPaused() β†’ bool ``` Returns whether the Vif contract is currently paused. **Use for:** * Checking if trading is available * UI state management * Error handling #### `isBlacklisted` ``` isBlacklisted( address user ) β†’ bool ``` Returns whether a user is blacklisted from using the protocol. **Parameters:** * `user`: Address to check **Use for:** * Access control validation * User status verification * Security checks ### Reading from the Vif Contract Directly The VifReader contract wraps many Vif contract functions for convenience, but you can also read directly from the Vif contract: #### Tree Reading **`treeFor(bytes32 marketId)`** - Returns the tick tree structure for a market Use to: * Find best prices * Traverse price levels * Check active ticks #### Offer List Reading **`offerList(bytes32 marketId, uint24 index)`** - Returns offer list at a specific price index **`offerListPacked(bytes32 marketId, uint24 index)`** - Returns packed offer list data Use to: * Get offers at specific price * Check liquidity at price level * Read head/tail pointers #### Delta Reading **`deltaOf(address token)`** (via LibDeltasExt) - Returns current flash accounting delta for a token Use to: * Monitor transient balances * Debug settlement issues * Track debt/credit state ### Best Practices #### For Off-Chain Applications 1. **Use VifReader for all queries** - It provides convenient aggregation and pagination 2. **Register markets first** - Always call `updateMarkets` before reading book data 3. **Paginate large queries** - Use `fromId` and `maxOffers` parameters to avoid timeouts 4. **Cache results** - Order book data can be cached and invalidated on events 5. **Listen to events** - Subscribe to Vif events to know when to refresh data #### For On-Chain Applications 1. **Avoid unbounded methods** - Never call `openMarkets()` or large `packedOfferList` queries in transactions 2. **Use specific queries** - Read individual offers or markets rather than lists 3. **Consider gas costs** - Even "view" functions cost gas in transactions 4. **Validate data** - Always check `isActive` status on offers and markets #### For UI Development 1. **Progressive loading** - Load best prices first, then paginate deeper levels 2. **Event-driven updates** - Refresh only affected price levels when offers change 3. **Error handling** - Handle cases where markets aren't registered or offers don't exist 4. **Format units** - Remember to multiply packed units by market unit sizes ### Common Use Cases #### Building an Order Book Display 1. Call `updateMarkets` to register the market pair 2. Use `packedBook` to get aggregated depth at each price level 3. Use `packedOfferList` to get individual offers if needed 4. Listen to `NewOffer`, `MarketOrder`, `OfferCancelled` events 5. Refresh affected price levels on events #### Showing User Portfolio 1. Get all markets with `openMarkets()` 2. For each market, call `packedOfferList` with reasonable pagination 3. Filter offers by user address from the `owners` array 4. Display offer details using returned packed data 5. Monitor `OfferClaimed` and `OfferUpdated` events for the user #### Market Analytics 1. Use `openMarkets()` to discover all markets 2. For each market, read `market()` to get configuration 3. Use `packedBook` to calculate total liquidity 4. Check `fees()` for each token to see protocol revenue 5. Track `MarketOrder` events to measure trading volume #### Price Monitoring 1. Register target markets with `updateMarkets` 2. Read best price using Vif's `treeFor` and finding first cursor 3. Subscribe to `NewOffer` and `MarketOrder` events 4. Re-read best price when events indicate price may have changed 5. Use `packedBook` to get spread between best bid and ask ### Related Concepts * [Using VifRouter](/developer/interactions/using-vif-router) - Executing trades and orders * [Markets & Units](/protocol/how-it-works/markets) - Understanding market structure * [Offers & Lists](/protocol/how-it-works/offers) - Offer data structures * [Tick Tree Structure](/protocol/how-it-works/tick-tree) - Price level organization ## Core Classes The vifdk provides several core classes for working with tokens, markets, offers, and order books. ### Token The `Token` class represents an ERC20 token with Vif-specific unit configuration. #### Creating Tokens ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:token-create] ``` #### Units Units define the precision level for amounts stored in Vif. Amounts are floored to the nearest unit: ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:token-units] ``` #### Native Token ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:token-native] ``` Override if needed: ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:token-native-override] ``` #### Provision Token ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:token-provision] ``` ### TokenAmount Represents an amount of a specific token with automatic unit flooring. #### Creating Amounts ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:amount-create] ``` #### Properties and Methods ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:amount-properties] ``` ### Market The `Market` class represents a trading pair with both directions (asks and bids). #### Creating Markets ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:market-create] ``` #### Market Directions ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:market-directions] ``` #### Market Keys Each direction has a unique identifier: ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:market-keys] ``` #### Price Conversion Convert between price and tick: ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:market-price] ``` #### Fee Calculations ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:market-fees] ``` #### From Open Markets Create markets from VifReader results: ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:client-setup] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:market-open] ``` ### SemiMarket Represents one direction of a market (either asks or bids). ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:semimarket] ``` ### Tick Represents a price level in the order book. ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:tick] ``` ### Offer Represents a limit order in the book. ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:client-setup] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:offer] ``` ### OfferList Represents a list of offers from the order book. ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:client-setup] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:offerlist] ``` ### Book Represents aggregated price levels from the order book. ```ts twoslash // [!include ~/snippets/sdk/core-classes.ts:config] // [!include ~/snippets/sdk/core-classes.ts:token-create] // [!include ~/snippets/sdk/core-classes.ts:client-setup] // [!include ~/snippets/sdk/core-classes.ts:market-create] // ---cut--- // [!include ~/snippets/sdk/core-classes.ts:book] ``` ### Best Practices 1. **Define tokens once** - Create token instances at app startup and reuse them 2. **Use strings for amounts** - Avoid floating point precision issues 3. **Choose appropriate units** - Balance precision with gas costs 4. **Cache markets** - Create Market instances once and share them ### Related Concepts * [VifRouter](/developer/sdk/vif-router) - Building transactions * [Router Actions](/developer/sdk/router-actions) - Available actions * [Viewing Offers](/developer/sdk/guides/view-offers) - Querying order books * [Markets & Units](/protocol/how-it-works/markets) - Protocol documentation ## TypeScript SDK (vifdk) The vifdk is the official TypeScript SDK for the Vif protocol. It provides a high-level, type-safe interface for interacting with Vif smart contracts using [viem](https://viem.sh). ### Installation :::code-group ```bash [npm] npm install vifdk viem ``` ```bash [yarn] yarn add vifdk viem ``` ```bash [bun] bun add vifdk viem ``` ```bash [pnpm] pnpm add vifdk viem ``` ::: :::info[Peer Dependency] vifdk requires `viem >=2.38.0` as a peer dependency. Make sure you have viem installed in your project. ::: ### Quick Start #### 1. Set Up Viem Client ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:client-setup] ``` #### 2. Define Tokens ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:tokens] ``` #### 3. Create a Market ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // [!include ~/snippets/sdk/getting-started.ts:tokens] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:market] ``` #### 4. Initialize VifRouter ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:router] ``` #### 5. Execute a Market Order (Swap) ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // [!include ~/snippets/sdk/getting-started.ts:client-setup] // [!include ~/snippets/sdk/getting-started.ts:tokens] // [!include ~/snippets/sdk/getting-started.ts:market] // [!include ~/snippets/sdk/getting-started.ts:router] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:market-order] ``` #### 6. Place a Limit Order ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // [!include ~/snippets/sdk/getting-started.ts:client-setup] // [!include ~/snippets/sdk/getting-started.ts:tokens] // [!include ~/snippets/sdk/getting-started.ts:market] // [!include ~/snippets/sdk/getting-started.ts:router] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:limit-order] ``` ### Core Concepts #### Type Safety vifdk is fully typed and provides excellent TypeScript support: ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // [!include ~/snippets/sdk/getting-started.ts:client-setup] // [!include ~/snippets/sdk/getting-started.ts:tokens] // [!include ~/snippets/sdk/getting-started.ts:market] // [!include ~/snippets/sdk/getting-started.ts:router] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:type-safety] ``` #### Builder Pattern All operations use a fluent builder pattern: ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // [!include ~/snippets/sdk/getting-started.ts:client-setup] // [!include ~/snippets/sdk/getting-started.ts:tokens] // [!include ~/snippets/sdk/getting-started.ts:market] // [!include ~/snippets/sdk/getting-started.ts:router] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:builder] ``` Actions are automatically ordered and optimized when calling `.build()`. #### Token Amounts Token amounts are represented with proper precision: ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // [!include ~/snippets/sdk/getting-started.ts:tokens] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:amounts] ``` #### Market Directions Markets have two directions (asks and bids): ```ts twoslash // [!include ~/snippets/sdk/getting-started.ts:config] // [!include ~/snippets/sdk/getting-started.ts:tokens] // [!include ~/snippets/sdk/getting-started.ts:market] // ---cut--- // [!include ~/snippets/sdk/getting-started.ts:directions] ``` ### Key Features * **Type-safe** - Full TypeScript support with strict typing * **Builder pattern** - Fluent API for building complex transactions * **Viem integration** - Works seamlessly with viem clients * **Automatic optimization** - Actions are automatically ordered for efficiency * **Result parsing** - Parse simulation results and transaction logs * **Market helpers** - Price conversions, fee calculations, and more ### Next Steps Explore the SDK in detail: * **[Core Classes](/developer/sdk/core-classes)** - Token, Market, and other fundamental classes * **[VifRouter](/developer/sdk/vif-router)** - Building and executing transactions * **[Router Actions](/developer/sdk/router-actions)** - Available actions and their parameters * **[Viewing Offers](/developer/sdk/guides/view-offers)** - Query order book data ### Common Use Cases Check out guides for common patterns: * **[Market Orders](/developer/sdk/guides/market-order)** - Execute swaps with slippage protection * **[Create Limit Orders](/developer/sdk/guides/create-limit-order)** - Place orders on the book * **[Edit Limit Orders](/developer/sdk/guides/edit-limit-order)** - Update existing orders * **[Cancel Limit Orders](/developer/sdk/guides/cancel-limit-order)** - Remove orders from book * **[Claim Limit Orders](/developer/sdk/guides/claim-limit-order)** - Collect filled amounts ### Support * **GitHub**: [github.com/mangrovedao/vifdk](https://github.com/mangrovedao/vifdk) * **Issues**: [Report bugs](https://github.com/mangrovedao/vifdk/issues) * **Protocol Docs**: [Understanding Vif](/protocol/how-it-works) ## Router Actions Actions are the building blocks of Vif transactions. The vifdk provides type-safe methods for all available router actions. ### Market Orders #### `orderSingle()` Execute a single market order (swap). ```typescript .orderSingle({ market: SemiMarket, fillVolume: TokenAmount, maxTick?: Tick, maxOffers?: bigint, fillWants?: boolean }) ``` **Parameters:** * `market`: The semi-market to trade on (asks or bids) * `fillVolume`: Amount to trade * `maxTick`: Maximum acceptable tick (price limit). Defaults to no limit * `maxOffers`: Maximum offers to consume. Defaults to 100 * `fillWants`: If true, `fillVolume` is output; if false, it's input. Defaults to false **Example:** ```typescript // Buy WETH with 1000 USDC router.createTypedActions() .orderSingle({ market: market.asks, fillVolume: USDC.amount('1000'), // Spend 1000 USDC (input) maxTick: market.asks.price(4000), // Don't pay more than $4000/WETH maxOffers: 50n }) ``` **Returns:** `OrderResult` with `gave`, `got`, `fee`, `bounty` #### `orderMulti()` Execute a multi-hop market order (exact input only). ```typescript .orderMulti({ markets: SemiMarket[], fillVolume: TokenAmount, maxTicks?: Tick[], maxOffers?: bigint[] }) ``` **Parameters:** * `markets`: Array of semi-markets to hop through * `fillVolume`: Input amount (first market's inbound token) * `maxTicks`: Price limits for each market * `maxOffers`: Offer limits for each market **Example:** ```typescript // USDC β†’ WETH β†’ DAI router.createTypedActions() .orderMulti({ markets: [market1.asks, market2.asks], fillVolume: USDC.amount('1000'), maxTicks: [ market1.asks.price(4000), market2.asks.price(4100) ], maxOffers: [50n, 50n] }) ``` **Returns:** `MultiOrderResult[]` - One result per market hop ### Limit Orders #### `limitSingle()` Create or edit a limit order. ```typescript .limitSingle({ market: SemiMarket, gives: TokenAmount, tick: Tick, initialOfferId?: number, expiry?: Date, provision?: TokenAmount }) ``` **Parameters:** * `market`: The semi-market to place the offer on * `gives`: Amount to offer * `tick`: Price level * `initialOfferId`: 0 or undefined for new, existing ID to edit * `expiry`: Optional expiration date * `provision`: Native token provision (required if expiry is set) **Example:** ```typescript // Sell 1 WETH at 3500 USDC/WETH router.createTypedActions() .limitSingle({ market: market.asks, gives: WETH.amount('1'), tick: market.asks.price(3500), expiry: new Date(Date.now() + 86400000), // 24 hours provision: Token.PROVISION_TOKEN.amount('0.001') }) ``` **Returns:** `LimitOrderResult` with `offerId`, `claimedReceived` #### `claim()` Claim filled amounts from an offer. ```typescript .claim({ market: SemiMarket, offerId: number }) ``` **Example:** ```typescript router.createTypedActions() .claim({ market: market.asks, offerId: 42 }) ``` **Returns:** `ClaimCancelResult` with `inbound`, `outbound`, `provision` #### `cancel()` Cancel an offer and claim all remaining balances. ```typescript .cancel({ market: SemiMarket, offerId: number }) ``` **Example:** ```typescript router.createTypedActions() .cancel({ market: market.asks, offerId: 42 }) ``` **Returns:** `ClaimCancelResult` with `inbound`, `outbound`, `provision` ### Settlement Actions #### `settleAll()` Settle all debt for a token. ```typescript .settleAll(token: Token) ``` **Example:** ```typescript router.createTypedActions() .orderSingle({ /* ... */ }) .settleAll(USDC) // Pay USDC debt ``` #### `settle()` Settle a specific amount of debt. ```typescript .settle({ token: Token, amount: TokenAmount }) ``` **Example:** ```typescript .settle({ token: USDC, amount: USDC.amount('1000') }) ``` #### `takeAll()` Take all credit for a token. ```typescript .takeAll({ token: Token, receiver: Address }) ``` **Example:** ```typescript router.createTypedActions() .orderSingle({ /* ... */ }) .settleAll(USDC) .takeAll({ token: WETH, receiver: walletAddress }) ``` #### `take()` Take a specific amount of credit. ```typescript .take({ token: Token, amount: TokenAmount, receiver: Address }) ``` ### Utility Actions #### `wrapNative()` Wrap native token (ETH β†’ WETH). ```typescript .wrapNative(amount: TokenAmount) ``` **Example:** ```typescript router.createTypedActions() .wrapNative(Token.NATIVE_TOKEN.amount('1')) .limitSingle({ /* use wrapped ETH */ }) ``` #### `unwrapNative()` Unwrap native token (WETH β†’ ETH). ```typescript .unwrapNative({ amount: TokenAmount, receiver: Address }) ``` #### `sweep()` Send all tokens from router to receiver. ```typescript .sweep({ token: Token, receiver: Address }) ``` **Example:** ```typescript router.createTypedActions() .limitSingle({ /* ... */ }) .settleAll(WETH) .sweep({ token: Token.NATIVE_TOKEN, receiver: walletAddress }) ``` #### `authorize()` Authorize an operator via the router. ```typescript .authorize({ authorized: Address, isAuthorized: boolean }) ``` #### `clearAllUpto()` Clear dust credit up to a maximum amount. ```typescript .clearAllUpto({ token: Token, maxAmount: TokenAmount }) ``` ### Action Ordering Actions are automatically ordered when calling `.build()`: 1. **Operations** (orders, limits, claims, cancels) 2. **Settlements** (settle, settleAll) 3. **Takes** (take, takeAll) 4. **Utilities** (sweep, wrap, unwrap) This ensures proper execution order for flash accounting. ### Failable Actions Actions can be marked as failable to prevent transaction reversion: ```typescript import { toFailableAction, Action } from 'vifdk' router.createActions() .add(Action.ORDER_SINGLE, params) // Reverts if fails .add(toFailableAction(Action.ORDER_SINGLE), params) // Continues if fails .build() ``` Check if an action is failable: ```typescript import { isFailableAction } from 'vifdk' if (isFailableAction(action)) { // Handle failable action } ``` ### Action Results #### OrderResult ```typescript { gave: TokenAmount // Amount paid got: TokenAmount // Amount received fee: TokenAmount // Fee paid bounty: TokenAmount // Bounty earned } ``` #### LimitOrderResult ```typescript { offerId: number // Created/updated offer ID claimedReceived: TokenAmount // Claimed from previous fills } ``` #### ClaimCancelResult ```typescript { inbound: TokenAmount // Filled amount outbound: TokenAmount // Remaining/returned amount provision: TokenAmount // Provision returned } ``` ### Common Patterns #### Simple Swap ```typescript router.createTypedActions() .orderSingle({ market: market.asks, fillVolume: USDC.amount('1000') }) .settleAll(USDC) .takeAll({ token: WETH, receiver }) .build() ``` #### Swap with Price Limit ```typescript router.createTypedActions() .orderSingle({ market: market.asks, fillVolume: USDC.amount('1000'), maxTick: market.asks.price(4000) // Max $4000/WETH }) .settleAll(USDC) .takeAll({ token: WETH, receiver }) .build() ``` #### Place Limit Order with Provision ```typescript router.createTypedActions() .wrapNative(Token.NATIVE_TOKEN.amount('0.1')) // For provision .limitSingle({ market: market.asks, gives: WETH.amount('1'), tick: market.asks.price(3500), expiry: new Date(Date.now() + 86400000), provision: Token.PROVISION_TOKEN.amount('0.001') }) .settleAll(Token.NATIVE_TOKEN) // Pay provision .settleAll(WETH) // Pay WETH .sweep({ token: Token.NATIVE_TOKEN, receiver }) // Return extra .build() ``` #### Update and Claim Limit Order ```typescript router.createTypedActions() .limitSingle({ market: market.asks, initialOfferId: 42, // Edit existing gives: WETH.amount('2'), // Update amount tick: market.asks.price(3600) // Update price }) .settleAll(WETH) // Pay additional WETH .takeAll({ token: USDC, receiver }) // Claim filled USDC .build() ``` #### Multi-Hop Swap ```typescript router.createTypedActions() .orderMulti({ markets: [ wethUsdcMarket.asks, // WETH β†’ USDC usdcDaiMarket.asks // USDC β†’ DAI ], fillVolume: WETH.amount('1') }) .settleAll(WETH) .takeAll({ token: DAI, receiver }) .build() // USDC is never transferred! Netted by flash accounting ``` #### Batch Multiple Limit Orders ```typescript router.createTypedActions() .limitSingle({ market: market.asks, gives: WETH.amount('1'), tick: market.asks.price(3500) }) .limitSingle({ market: market.asks, gives: WETH.amount('1'), tick: market.asks.price(3600) }) .limitSingle({ market: market.asks, gives: WETH.amount('1'), tick: market.asks.price(3700) }) .settleAll(WETH) // Settle once for all three .build() ``` ### Related Documentation * [VifRouter](/developer/sdk/vif-router) - Router class documentation * [Core Classes](/developer/sdk/core-classes) - Token, Market, etc. * [Using VifRouter](/developer/interactions/using-vif-router) - Contract-level actions * [Flash Accounting](/protocol/how-it-works/flash-accounting) - Understanding debts/credits ## VifRouter The `VifRouter` class is the main entry point for building and executing transactions with the Vif protocol. ### Initialization ```typescript import { VifRouter } from 'vifdk' const router = new VifRouter( routerAddress, // VifRouter contract address coreAddress, // Vif core contract address chainId // Network chain ID ) ``` ### Creating Actions VifRouter provides two methods for creating action builders: #### `createTypedActions()` **Use when actions are known in advance** - provides better TypeScript support and autocomplete: ```typescript const actions = router.createTypedActions() .orderSingle({ /* typed params */ }) .settleAll(/* typed token */) .build() ``` #### `createActions()` **Use when actions are unknown in advance** - for dynamic flows (loops, conditionals, user-driven): ```typescript const actions = router.createActions() // Build actions dynamically .build() ``` ### Building Transactions Actions use a fluent builder pattern: ```typescript const actions = router .createTypedActions() .action1(params) .action2(params) .action3(params) .build(options) ``` #### Build Options ```typescript .build({ addRecommendedActions?: boolean // Auto-add settlements receiver?: Address // Default receiver for takes }) ``` **`addRecommendedActions`**: Automatically adds necessary `settleAll` and `takeAll` actions based on the operations performed. ```typescript const actions = router .createTypedActions() .orderSingle({ market: market.asks, fillVolume: USDC.amount('1000') }) .build({ addRecommendedActions: true, receiver: walletAddress }) // Automatically adds settleAll(USDC) and takeAll(WETH) ``` ### Getting Transaction Data #### `txData()` Returns the encoded commands and arguments for the VifRouter contract: ```typescript const actions = router.createTypedActions() .orderSingle({ /* ... */ }) .build() const { commands, args } = actions.txData() ``` Use with viem to execute the transaction: ```typescript import { execute } from 'vifdk' const hash = await walletClient.writeContract({ address: routerAddress, ...execute(commands, args), value: actions.expectedValue({ globalProvision }) }) ``` #### `expectedValue()` Calculates the native token value needed for the transaction: ```typescript const value = actions.expectedValue({ globalProvision: Token.PROVISION_TOKEN.amount('0.001') }) ``` This includes: * Provisions for limit orders with expiry * Native token wrapping amounts * Any other native token settlements #### `expectedAllowances()` Returns required token approvals: ```typescript const approvals = actions.expectedAllowances() for (const approval of approvals) { console.log(`Approve ${approval.amountString} ${approval.token.symbol}`) // Check and approve if needed const currentAllowance = await client.readContract({ address: approval.token.address, abi: erc20Abi, functionName: 'allowance', args: [walletAddress, coreAddress] }) if (currentAllowance < approval.amount) { await walletClient.writeContract({ address: approval.token.address, abi: erc20Abi, functionName: 'approve', args: [coreAddress, approval.amount] }) } } ``` ### Parsing Results #### `parseSimulationResult()` Parse the result from a simulation: ```typescript const { result } = await client.simulateContract({ address: routerAddress, ...execute(commands, args) }) const parsed = actions.parseSimulationResult(result) for (const actionResult of parsed) { console.log(`Action: ${actionResult.type}`) console.log(`Success: ${actionResult.success}`) if (actionResult.data) { // Data is typed based on action type console.log('Data:', actionResult.data) } if (actionResult.error) { console.log('Error:', actionResult.error) } } ``` #### `parseLogs()` Parse transaction receipt logs: ```typescript const receipt = await client.waitForTransactionReceipt({ hash }) const results = actions.parseLogs(receipt.logs) // results[0] contains the first action's result if (results[0].type === Action.ORDER_SINGLE && results[0].data) { console.log('Gave:', results[0].data.gave.amountString) console.log('Got:', results[0].data.got.amountString) } ``` ### Complete Example ```typescript import { VifRouter, Token, Market, Action } from 'vifdk' import { createWalletClient } from 'viem' // Initialize const router = new VifRouter(routerAddress, coreAddress, 1) const market = Market.from({ base: WETH, quote: USDC, tickSpacing: 1n }) // Build actions const actions = router .createTypedActions() .orderSingle({ market: market.asks, fillVolume: USDC.amount('1000'), maxTick: market.asks.price(4000) }) .build({ addRecommendedActions: true, receiver: walletAddress }) // Check approvals const approvals = actions.expectedAllowances() for (const approval of approvals) { // Approve tokens... } // Simulate const { result, request } = await client.simulateContract({ address: routerAddress, ...execute(...actions.txData()) }) // Parse simulation const simResult = actions.parseSimulationResult(result) console.log('Expected output:', simResult[0].data.got.amountString) // Execute const hash = await walletClient.writeContract(request) // Parse receipt const receipt = await client.waitForTransactionReceipt({ hash }) const results = actions.parseLogs(receipt.logs) console.log('Actual output:', results[0].data.got.amountString) ``` ### Authorization For signature-based authorization (EIP-712): #### `authorizationData()` Create authorization data for signing: ```typescript const auth = router.authorizationData( userAddress, nonce, deadline ) ``` #### `singatureDataForAuthorization()` Get typed data for wallet signing: ```typescript const typedData = router.singatureDataForAuthorization(auth) const signature = await walletClient.signTypedData(typedData) ``` Then use the signature with the `authorize` action. ### Error Handling Actions can be marked as failable: ```typescript import { Action, toFailableAction } from 'vifdk' const actions = router .createActions() .add(Action.ORDER_SINGLE, params) // Will revert if fails .add(toFailableAction(Action.ORDER_SINGLE), params) // Won't revert .build() ``` Results indicate success/failure: ```typescript const results = actions.parseSimulationResult(result) for (const res of results) { if (!res.success) { console.error(`Action ${res.type} failed:`, res.error) } } ``` ### Best Practices #### Always Simulate First ```typescript // βœ“ Good: Simulate before executing const { result, request } = await client.simulateContract({ /* ... */ }) const simResult = actions.parseSimulationResult(result) // Check simResult await walletClient.writeContract(request) // βœ— Bad: Execute without simulation await walletClient.writeContract({ /* ... */ }) ``` #### Use Typed Actions ```typescript // βœ“ Good: Type-safe const actions = router.createTypedActions() .orderSingle({ /* autocomplete works */ }) // βœ— Less ideal: Manual action adding const actions = router.createActions() .add(Action.ORDER_SINGLE, params) ``` #### Add Recommended Actions ```typescript // βœ“ Good: Automatic settlements .build({ addRecommendedActions: true, receiver }) // βœ— Manual: Forgetting settlements causes reverts .build() ``` #### Check Expected Values ```typescript // βœ“ Good: Calculate exact value needed const value = actions.expectedValue({ globalProvision }) await walletClient.writeContract({ value, /* ... */ }) // βœ— Bad: Guessing value await walletClient.writeContract({ value: parseEther('1'), /* ... */ }) ``` ### Related Documentation * [Router Actions](/developer/sdk/router-actions) - Available actions and parameters * [Core Classes](/developer/sdk/core-classes) - Token, Market, and other classes * [Using VifRouter](/developer/interactions/using-vif-router) - Contract-level documentation * [Flash Accounting](/protocol/how-it-works/flash-accounting) - Protocol mechanics ## Authorization Learn how to authorize the VifRouter to act on your behalf in the Vif protocol. ### Why Authorization? Before the VifRouter can execute trades or manage orders for you, it needs to be authorized by your account in the Vif core contract. This is a security feature that gives you control over which contracts can interact with your positions. ### Two Authorization Methods You can authorize the router in two ways: 1. **Standalone Transaction** - Simple one-time authorization transaction 2. **Batch with Signature** - Include authorization in the same transaction as your first action ### Method 1: Standalone Transaction The simplest way to authorize the router is with a dedicated transaction. :::steps #### Setup ```ts twoslash // [!include ~/snippets/sdk/authorization.ts:config] // ---cut--- // [!include ~/snippets/sdk/authorization.ts:import] // [!include ~/snippets/sdk/authorization.ts:setup] ``` #### Authorize ```ts twoslash // [!include ~/snippets/sdk/authorization.ts:config] // [!include ~/snippets/sdk/authorization.ts:import] // [!include ~/snippets/sdk/authorization.ts:setup] // ---cut--- // [!include ~/snippets/sdk/authorization.ts:standalone] ``` This sends a transaction to the Vif core contract authorizing the router to act on your behalf. ::: :::tip[When to Use] Use standalone authorization when: * Setting up for the first time * You want a simple, explicit authorization step * You don't need to batch with other actions ::: ### Method 2: Batch with Typed Signature For a better user experience, you can include authorization in your first transaction using a typed signature (EIP-712). :::steps #### Get Current Nonce First, retrieve your current authorization nonce from the contract: ```ts twoslash // [!include ~/snippets/sdk/authorization.ts:config] // [!include ~/snippets/sdk/authorization.ts:import] // [!include ~/snippets/sdk/authorization.ts:setup] // ---cut--- // [!include ~/snippets/sdk/authorization.ts:get-nonce] ``` #### Create and Sign Authorization Create authorization data and get the user's signature: ```ts twoslash // [!include ~/snippets/sdk/authorization.ts:config] // [!include ~/snippets/sdk/authorization.ts:import] // [!include ~/snippets/sdk/authorization.ts:setup] const userNonce = 0n; // ---cut--- // [!include ~/snippets/sdk/authorization.ts:batch-signature] ``` #### Include in Transaction Add the authorization action to your transaction, then chain it with your first action: ```ts twoslash // [!include ~/snippets/sdk/authorization.ts:config] // [!include ~/snippets/sdk/authorization.ts:import] // [!include ~/snippets/sdk/authorization.ts:setup] const userNonce = 0n; // [!include ~/snippets/sdk/authorization.ts:batch-signature] // ---cut--- // [!include ~/snippets/sdk/authorization.ts:batch-use] ``` You would then add your trading action (like `orderSingle`, `limitSingle`, etc.) before calling `.build()`. ::: :::tip[When to Use] Use batch authorization when: * You want the best user experience (one transaction instead of two) * The user is making their first trade * You can get their signature before the transaction ::: ### Checking Authorization Status You can check if the router is already authorized: ```ts twoslash // [!include ~/snippets/sdk/authorization.ts:config] // [!include ~/snippets/sdk/authorization.ts:import] // [!include ~/snippets/sdk/authorization.ts:setup] // ---cut--- // [!include ~/snippets/sdk/authorization.ts:check-authorization] ``` :::info[Authorization is Permanent] Once authorized, the router remains authorized until you explicitly revoke it by calling `authorize(router, false)`. ::: ### Revoking Authorization To revoke authorization: ```typescript await client.writeContract({ address: VIF_CORE_ADDRESS, abi: [ { type: "function", name: "authorize", inputs: [ { name: "authorized", type: "address" }, { name: "value", type: "bool" }, ], outputs: [], stateMutability: "nonpayable", }, ], functionName: "authorize", args: [VIF_ROUTER_ADDRESS, false], // false to revoke }); ``` ### Best Practices 1. **Check First** - Always check if authorization is needed before requesting it 2. **Use Batch for UX** - Batch authorization provides better user experience 3. **Nonce Management** - Always use the current nonce from the contract when creating authorization data 4. **Deadline** - Set reasonable deadlines for authorizations (e.g., 30 days) 5. **Revoke When Done** - Consider revoking authorization if you're no longer using the router ### Next Steps * [Market Orders](/developer/sdk/guides/market-order) - Execute swaps * [Create Limit Orders](/developer/sdk/guides/create-limit-order) - Place orders on the book * [VifRouter](/developer/sdk/vif-router) - Learn about the router class ## Cancelling Limit Orders Learn how to cancel (delete) limit orders from the Vif order book. ### What is Cancelling? Cancelling a limit order removes it from the order book and returns: * Any unfilled amount (remaining `gives`) * Any filled amount that hasn't been claimed (accumulated `received`) * The provision (if the order had an expiration) All returned amounts become credits in flash accounting and must be taken in the same transaction. ### Basic Cancel :::steps #### Setup ```ts twoslash // [!include ~/snippets/sdk/cancel-limit-order.ts:config] // ---cut--- // [!include ~/snippets/sdk/cancel-limit-order.ts:import] // [!include ~/snippets/sdk/cancel-limit-order.ts:setup] ``` #### Cancel Order ```ts twoslash // [!include ~/snippets/sdk/cancel-limit-order.ts:config] // [!include ~/snippets/sdk/cancel-limit-order.ts:import] // [!include ~/snippets/sdk/cancel-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/cancel-limit-order.ts:basic] ``` ::: ### Cancel with Provision If your order had an expiration, you'll get the provision back: ```ts twoslash // [!include ~/snippets/sdk/cancel-limit-order.ts:config] // [!include ~/snippets/sdk/cancel-limit-order.ts:import] // [!include ~/snippets/sdk/cancel-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/cancel-limit-order.ts:provision] ``` ### Batch Cancellations Cancel multiple orders in one transaction: ```ts twoslash // [!include ~/snippets/sdk/cancel-limit-order.ts:config] // [!include ~/snippets/sdk/cancel-limit-order.ts:import] // [!include ~/snippets/sdk/cancel-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/cancel-limit-order.ts:batch] ``` ### What Cancel Returns Cancelling returns: * **Unfilled amount** - The remaining `gives` that hasn't been matched * **Filled amount** - Any accumulated `received` from partial fills * **Provision** - If the order had an expiration :::info[Automatic Claiming] Cancel automatically claims filled amounts. You get both unfilled AND filled portions. ::: ### Cancel vs Claim | Operation | Returns | When to Use | | ---------- | ----------------------------- | --------------------------------------------- | | **Cancel** | Unfilled + filled + provision | Exit position, change price significantly | | **Claim** | Only filled amounts | Keep order active, collect fills periodically | ### Next Steps * [Claim Limit Orders](/developer/sdk/guides/claim-limit-order) - Collect fills without cancelling * [Create Limit Orders](/developer/sdk/guides/create-limit-order) - Create new orders * [Edit Limit Orders](/developer/sdk/guides/edit-limit-order) - Update existing orders * [Router Actions](/developer/sdk/router-actions) - All available actions ## Claiming Limit Orders Learn how to claim filled amounts from your limit orders while keeping them active on the order book. ### What is Claiming? Claiming allows you to collect amounts that have been filled (received) from your limit order **without removing the order** from the book. This is useful when: * Your order is at a good price and you want to keep it active * You want to periodically collect profits while maintaining your position * The order is partially filled and you want the rest to remain available :::info[Claim vs Cancel] **Claim**: Collects filled amounts, order stays active **Cancel**: Removes order and returns both filled and unfilled amounts ::: ### Basic Claim :::steps #### Setup ```ts twoslash // [!include ~/snippets/sdk/claim-limit-order.ts:config] // ---cut--- // [!include ~/snippets/sdk/claim-limit-order.ts:import] // [!include ~/snippets/sdk/claim-limit-order.ts:setup] ``` #### Claim Filled Amounts ```ts twoslash // [!include ~/snippets/sdk/claim-limit-order.ts:config] // [!include ~/snippets/sdk/claim-limit-order.ts:import] // [!include ~/snippets/sdk/claim-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/claim-limit-order.ts:basic] ``` ::: ### Check Before Claiming Check if there are filled amounts to claim: ```ts twoslash // [!include ~/snippets/sdk/claim-limit-order.ts:config] // [!include ~/snippets/sdk/claim-limit-order.ts:import] // [!include ~/snippets/sdk/claim-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/claim-limit-order.ts:check] ``` :::tip[Order Stays Active] Claiming collects filled amounts while keeping the order active on the book. ::: ### Batch Claims Claim from multiple orders in one transaction: ```ts twoslash // [!include ~/snippets/sdk/claim-limit-order.ts:config] // [!include ~/snippets/sdk/claim-limit-order.ts:import] // [!include ~/snippets/sdk/claim-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/claim-limit-order.ts:batch] ``` ### Claim vs Cancel | Operation | Order Status | Returns | When to Use | | ---------- | ------------ | ----------------------------- | ----------------------------------------- | | **Claim** | Active | Only filled amounts | Keep providing liquidity at current price | | **Cancel** | Removed | Filled + unfilled + provision | Exit position or change price | ### Next Steps * [Cancel Limit Orders](/developer/sdk/guides/cancel-limit-order) - Remove orders completely * [Edit Limit Orders](/developer/sdk/guides/edit-limit-order) - Edit also claims automatically * [Create Limit Orders](/developer/sdk/guides/create-limit-order) - Create new orders * [Router Actions](/developer/sdk/router-actions) - All available actions ## Creating Limit Orders Learn how to place limit orders (make offers) on the Vif order book using vifdk. ### What is a Limit Order? A limit order in Vif is called a "make" operation. When you create a limit order, you: * Provide liquidity to the order book at a specific price * Lock tokens that will be sold when takers match your order * Optionally set an expiration time with a provision ### Basic Limit Order :::steps #### Setup ```ts twoslash // [!include ~/snippets/sdk/create-limit-order.ts:config] // ---cut--- // [!include ~/snippets/sdk/create-limit-order.ts:import] // [!include ~/snippets/sdk/create-limit-order.ts:setup] ``` #### Approve Tokens ```ts twoslash // [!include ~/snippets/sdk/create-limit-order.ts:config] // [!include ~/snippets/sdk/create-limit-order.ts:import] // [!include ~/snippets/sdk/create-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/create-limit-order.ts:approve] ``` #### Create Limit Order ```ts twoslash // [!include ~/snippets/sdk/create-limit-order.ts:config] // [!include ~/snippets/sdk/create-limit-order.ts:import] // [!include ~/snippets/sdk/create-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/create-limit-order.ts:basic] ``` ::: ### Limit Order with Expiration Adding an expiration requires a provision in native token: ```ts twoslash // [!include ~/snippets/sdk/create-limit-order.ts:config] // [!include ~/snippets/sdk/create-limit-order.ts:import] // [!include ~/snippets/sdk/create-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/create-limit-order.ts:expiry] ``` ### Using Native Token (ETH) To create a limit order selling ETH directly: ```ts twoslash // [!include ~/snippets/sdk/create-limit-order.ts:config] // [!include ~/snippets/sdk/create-limit-order.ts:import] // [!include ~/snippets/sdk/create-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/create-limit-order.ts:native] ``` ### Placing Multiple Orders Create a ladder of limit orders at different price levels: ```ts twoslash // [!include ~/snippets/sdk/create-limit-order.ts:config] // [!include ~/snippets/sdk/create-limit-order.ts:import] // [!include ~/snippets/sdk/create-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/create-limit-order.ts:multiple] ``` ### Common Patterns ```ts twoslash // [!include ~/snippets/sdk/create-limit-order.ts:config] // [!include ~/snippets/sdk/create-limit-order.ts:import] // [!include ~/snippets/sdk/create-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/create-limit-order.ts:patterns] ``` ### Best Practices 1. **Always simulate first** - Catch errors before spending gas 2. **Check approvals** - Use `expectedAllowances()` to verify approvals 3. **Verify balance** - Ensure you have enough tokens 4. **Set appropriate provision** - For expiring orders, provision should cover gas costs 5. **Use appropriate tick spacing** - Respect market's tick spacing ### Next Steps * [Edit Limit Orders](/developer/sdk/guides/edit-limit-order) - Update existing orders * [Cancel Limit Orders](/developer/sdk/guides/cancel-limit-order) - Remove orders from book * [Claim Limit Orders](/developer/sdk/guides/claim-limit-order) - Collect filled amounts * [Router Actions](/developer/sdk/router-actions) - All available actions ## Editing Limit Orders Learn how to update existing limit orders on the Vif order book. ### What is Editing? Editing a limit order allows you to: * Change the price (tick) of an existing order * Increase or decrease the amount offered * Update the expiration time * Automatically claim any filled amounts in the same transaction :::info[Automatic Claiming] When you edit an order, Vif automatically claims any amounts that have been filled. The `claimedReceived` value in the result tells you how much was claimed. ::: ### Basic Edit :::steps #### Setup ```ts twoslash // [!include ~/snippets/sdk/edit-limit-order.ts:config] // ---cut--- // [!include ~/snippets/sdk/edit-limit-order.ts:import] // [!include ~/snippets/sdk/edit-limit-order.ts:setup] ``` #### Edit Order ```ts twoslash // [!include ~/snippets/sdk/edit-limit-order.ts:config] // [!include ~/snippets/sdk/edit-limit-order.ts:import] // [!include ~/snippets/sdk/edit-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/edit-limit-order.ts:basic] ``` ::: ### What You Can Edit **Change Price**: ```ts twoslash // [!include ~/snippets/sdk/edit-limit-order.ts:config] // [!include ~/snippets/sdk/edit-limit-order.ts:import] // [!include ~/snippets/sdk/edit-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/edit-limit-order.ts:change-price] ``` **Increase Size**: ```ts twoslash // [!include ~/snippets/sdk/edit-limit-order.ts:config] // [!include ~/snippets/sdk/edit-limit-order.ts:import] // [!include ~/snippets/sdk/edit-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/edit-limit-order.ts:increase-size] ``` **Add Expiration**: ```ts twoslash // [!include ~/snippets/sdk/edit-limit-order.ts:config] // [!include ~/snippets/sdk/edit-limit-order.ts:import] // [!include ~/snippets/sdk/edit-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/edit-limit-order.ts:add-expiry] ``` ### Batch Edits Edit multiple orders in one transaction: ```ts twoslash // [!include ~/snippets/sdk/edit-limit-order.ts:config] // [!include ~/snippets/sdk/edit-limit-order.ts:import] // [!include ~/snippets/sdk/edit-limit-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/edit-limit-order.ts:batch] ``` ### Best Practices 1. **Always simulate first** - Catch errors and see claimed amounts before executing 2. **Check ownership** - Verify you own the offer before editing 3. **Handle claims** - Claims are automatic when editing 4. **Monitor approvals** - Increasing size requires additional approval ### Important Notes :::warning[Queue Position] Editing an offer moves it to the **back of the queue** at its new price level, even if the price doesn't change. ::: :::info[Automatic Claiming] You cannot edit an offer without claiming filled amounts. The `claimedReceived` is always returned. ::: ### Next Steps * [Cancel Limit Orders](/developer/sdk/guides/cancel-limit-order) - Remove orders completely * [Claim Limit Orders](/developer/sdk/guides/claim-limit-order) - Claim without editing * [Create Limit Orders](/developer/sdk/guides/create-limit-order) - Create new orders * [Router Actions](/developer/sdk/router-actions) - All available actions ## Creating Market Orders Learn how to execute market orders (swaps) on the Vif order book using vifdk. ### What is a Market Order? A market order is a "take" operation that matches against existing limit orders on the book. When you create a market order, you: * Specify the maximum amount you want to give (spend) * Receive tokens by taking from existing offers at the best available prices * Execute immediately as a taker (no waiting for fills) :::info[Market vs Limit] **Market Order**: Takes liquidity immediately from the book (taker) **Limit Order**: Provides liquidity to the book and waits for fills (maker) ::: ### Basic Market Order :::steps #### Step 1: Setup First, ensure you have: * Tokens defined * Market created * VifRouter initialized * User authorized the router ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // [!include ~/snippets/sdk/market-order.ts:wallet] ``` #### Step 2: Authorize Router ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // [!include ~/snippets/sdk/market-order.ts:wallet] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:authorize] ``` #### Step 3: Approve Tokens ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // [!include ~/snippets/sdk/market-order.ts:wallet] // [!include ~/snippets/sdk/market-order.ts:authorize] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:approve] ``` #### Step 4: Create Market Order ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // [!include ~/snippets/sdk/market-order.ts:wallet] // [!include ~/snippets/sdk/market-order.ts:authorize] // [!include ~/snippets/sdk/market-order.ts:approve] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:execute] ``` ::: ### Sell Order (Ask Side) Selling base token (WETH) for quote token (USDC): ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // [!include ~/snippets/sdk/market-order.ts:wallet] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:sell] ``` ### Using Native Token (ETH) To swap ETH directly without wrapping first: ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // [!include ~/snippets/sdk/market-order.ts:wallet] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:wrap] ``` ### Receiving Native Token (ETH) To receive ETH instead of WETH: ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // [!include ~/snippets/sdk/market-order.ts:wallet] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:unwrap] ``` ### Slippage Protection Control the trade outcome with `fillVolume` and `maxTick`: **Exact Input** - Spend exact amount: ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:exact-input] ``` **Exact Output** - Receive exact amount: ```ts twoslash // [!include ~/snippets/sdk/market-order.ts:config] // [!include ~/snippets/sdk/market-order.ts:import] // [!include ~/snippets/sdk/market-order.ts:setup] // ---cut--- // [!include ~/snippets/sdk/market-order.ts:exact-output] ``` ### Error Handling Common errors to handle: * **Insufficient Liquidity** - Not enough offers in the order book * **Slippage Exceeded** - Received less than `minReceives` * **Insufficient Balance** - Not enough input tokens :::tip[Best Practices] - Always simulate first to check output - Set appropriate slippage (0.5-1% for stable pairs, 1-3% for volatile) - Check order book depth before large trades - Multi-hop swaps use flash accounting - you only need approval for input token ::: ### Next Steps * [Create Limit Orders](/developer/sdk/guides/create-limit-order) - Provide liquidity as a maker * [Cancel Limit Orders](/developer/sdk/guides/cancel-limit-order) - Manage your orders * [Router Actions](/developer/sdk/router-actions) - All available actions * [Reading State](/developer/reading-state) - Query order book data ## Viewing Offers Learn how to query and view offers from the Vif order book using vifdk. ### Overview The SDK provides functions to read offer data from the order book: * **`packedOfferList()`** - Get a list of offers from a market * **`packedBook()`** - Get aggregated price levels (book depth) * **`rawOffer()`** - Get a single offer by ID These functions return contract call parameters that you use with viem's `readContract`. ### Reading Offer Lists :::steps #### Setup ```ts twoslash // [!include ~/snippets/sdk/view-offers.ts:config] // ---cut--- // [!include ~/snippets/sdk/view-offers.ts:import] // [!include ~/snippets/sdk/view-offers.ts:setup] ``` #### Query Offers Use `packedOfferList()` to get detailed offer data: ```ts twoslash // [!include ~/snippets/sdk/view-offers.ts:config] // [!include ~/snippets/sdk/view-offers.ts:import] // [!include ~/snippets/sdk/view-offers.ts:setup] // ---cut--- // [!include ~/snippets/sdk/view-offers.ts:offer-list] ``` ::: #### Pagination The `fromId` parameter allows pagination through large offer lists: ```ts twoslash // [!include ~/snippets/sdk/view-offers.ts:config] // [!include ~/snippets/sdk/view-offers.ts:import] // [!include ~/snippets/sdk/view-offers.ts:setup] // ---cut--- // [!include ~/snippets/sdk/view-offers.ts:pagination] ``` ### Reading a Single Offer Use `rawOffer()` to get a specific offer by ID: ```ts twoslash // [!include ~/snippets/sdk/view-offers.ts:config] // [!include ~/snippets/sdk/view-offers.ts:import] // [!include ~/snippets/sdk/view-offers.ts:setup] // ---cut--- // [!include ~/snippets/sdk/view-offers.ts:single-offer] ``` ### Reading Book Depth Use `packedBook()` to get aggregated price levels: ```ts twoslash // [!include ~/snippets/sdk/view-offers.ts:config] // [!include ~/snippets/sdk/view-offers.ts:import] // [!include ~/snippets/sdk/view-offers.ts:setup] // ---cut--- // [!include ~/snippets/sdk/view-offers.ts:book] ``` ### Simulating Orders Once you have an offer list, you can simulate what an order would do: ```ts twoslash // [!include ~/snippets/sdk/view-offers.ts:config] // [!include ~/snippets/sdk/view-offers.ts:import] // [!include ~/snippets/sdk/view-offers.ts:setup] // ---cut--- // [!include ~/snippets/sdk/view-offers.ts:simulate] ``` ### Filtering Offers Filter offers by owner or other criteria: ```ts twoslash // [!include ~/snippets/sdk/view-offers.ts:config] // [!include ~/snippets/sdk/view-offers.ts:import] // [!include ~/snippets/sdk/view-offers.ts:setup] // ---cut--- // [!include ~/snippets/sdk/view-offers.ts:filter] ``` ### Best Practices 1. **Use appropriate page sizes** - 100 offers is usually sufficient 2. **Cache results** - Order book data doesn't change every block 3. **Use Book for price quotes** - Faster than loading all offers 4. **Use OfferList for simulations** - More accurate than Book 5. **Handle pagination** - Large markets may have thousands of offers ### Next Steps * [Create Limit Orders](/developer/sdk/guides/create-limit-order) - Place your own offers * [Market Orders](/developer/sdk/guides/market-order) - Take from the book * [Core Classes](/developer/sdk/core-classes) - Learn about Offer, Book, and OfferList * [Reading State](/developer/reading-state) - Direct contract reading ## Building Custom Routers: Limit Orders This guide shows how to build a custom router for placing limit orders. For most use cases, consider using the [built-in VifRouter](/developer/interactions/using-vif-router) instead. Limit orders in Vif are called "make" operations. They provide liquidity to the order book at specific price levels and earn from the spread when filled. ### Prerequisites Before placing limit orders: 1. **Market exists** - Trading pair must be created 2. **Router is authorized** - User must authorize router as operator 3. **Token approval** - Approve Vif contract for outbound token 4. **Native token** - Have ETH/native token for provisions (if using expiry) ### Basic Limit Order Pattern ```solidity contract MyRouter is ILockCallback { IVif public immutable vif; function placeLimitOrder(...) external payable returns (uint40 offerId, uint256 claimed) { bytes memory data = abi.encode(...); bytes memory result = vif.lock(data); return abi.decode(result, (uint40, uint256)); } function lockCallback(bytes calldata data) external returns (bytes memory) { require(msg.sender == address(vif), "Only Vif"); // 1. Create/edit limit order (uint40 offerId, uint256 claimed) = vif.make(...); // 2. Settle debts (outbound token + provision) _settleDebts(...); // 3. Take credits (claimed inbound token) _takeCredits(...); return abi.encode(offerId, claimed); } } ``` ### The `make` Function ```solidity function make( address maker, // Owner of the offer bytes32 market, // Market identifier uint40 initialOfferId, // 0 for new, existing ID to edit uint256 gives, // Amount to offer (in token decimals) int24 tick, // Price level uint32 expiry, // Expiration timestamp (0 = never) uint24 provision // Native token provision (in provision units) ) external returns ( uint40 offerId, // Created/updated offer ID uint256 claimedReceived // Amount claimed from previous fills ); ``` #### Parameters Explained ##### `maker` The offer owner. Must be: * The caller, OR * An account that authorized the caller When editing, must match the original offer's maker. ##### `market` Market identifier (same as for swaps): ```solidity bytes32 marketId = keccak256(abi.encodePacked( outboundToken, outboundUnits, inboundToken, inboundUnits, tickSpacing )); ``` ##### `initialOfferId` * **`0`**: Create new offer * **`> 0`**: Edit existing offer with this ID ```solidity // Create new offer (uint40 newId,) = vif.make(maker, market, 0, gives, tick, expiry, provision); // Edit existing offer (uint40 sameId, uint256 claimed) = vif.make( maker, market, newId, // Edit the offer we just created newGives, newTick, newExpiry, newProvision ); // sameId == newId βœ“ ``` :::warning[Edit Behavior] Editing an offer: * Claims any filled amounts automatically * Moves offer to tail of queue (even if tick unchanged) * Requires same `maker` as original ::: ##### `gives` Amount of outbound token to offer, in **token decimals**: ```solidity // WETH market with outboundUnits = 1e13 gives = 1.5 ether; // 1.5 WETH // Actual stored amount (floored to units): actualGives = floor(1.5e18 / 1e13) * 1e13 = 150000 * 1e13 = 1.5e18 // Perfect fit βœ“ // Another example with imperfect fit: gives = 1.50000001 ether; actualGives = floor(1.50000001e18 / 1e13) * 1e13 = 150000 * 1e13 = 1.5e18 // Truncated βœ“ ``` :::info[Minimum Size] Must satisfy: `actualGives >= minOutboundUnits * outboundUnits` ```solidity minOutboundUnits = 1000; outboundUnits = 1e13; minGives = 1000 * 1e13 = 1e16 = 0.01 WETH ``` ::: ##### `tick` Price level for the offer: * Lower tick = lower price = better for takers * Higher tick = higher price = better for maker * Must align with `tickSpacing` ```solidity // Calculate tick from price // price = inboundAmount / outboundAmount // tick = log(price) / log(1.00001) // Example: Sell 1 WETH for 3000 USDC price = 3000e6 / 1e18 = 3000 tick = log(3000) / log(1.00001) tick β‰ˆ -2,054,985 // With tickSpacing = 10: actualTick = round(tick / 10) * 10 = -2,054,990 ``` **Common price points:** | Price (USDC/WETH) | Approximate Tick | | ----------------- | ---------------- | | 1000 | -2,072,336 | | 2000 | -2,063,403 | | 3000 | -2,054,985 | | 4000 | -2,047,189 | | 5000 | -2,039,906 | ##### `expiry` Unix timestamp when offer expires: ```solidity expiry = 0; // Never expires expiry = block.timestamp + 1 days; // Expires in 24 hours expiry = 1735689600; // Jan 1, 2025 ``` :::danger[Provision Required] If `expiry > 0`, you **must** provide `provision` in native token. Otherwise the call reverts. ::: ##### `provision` Amount of native token to lock with the offer, in **provision units**: ```solidity // Global provision units (set at deployment) provisionUnits = 1e12; // Lock 0.001 ETH provision = 1000; // units actualProvision = 1000 * 1e12 = 1e15 wei = 0.001 ETH // When calling make: vif.make{value: 0.001 ether}(maker, market, 0, gives, tick, expiry, 1000); ``` Provision is: * **Locked** with the offer * **Returned** when cancelling or claiming after expiry * **Paid as bounty** to cleaners of expired offers ### Example: Simple Limit Order Router ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {IVif} from "vif-core/interfaces/IVif.sol"; import {ILockCallback} from "vif-core/interfaces/ILockCallback.sol"; import {LibDeltasExt} from "vif-core/libraries/external/LibDeltasExt.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; contract SimpleLimitOrderRouter is ILockCallback { using LibDeltasExt for address; using SafeTransferLib for address; IVif public immutable VIF; struct Market { address outboundToken; uint64 outboundUnits; address inboundToken; uint64 inboundUnits; uint16 tickSpacing; } constructor(address _vif) { VIF = IVif(_vif); } function _marketId(Market memory market) internal pure returns (bytes32) { return keccak256(abi.encodePacked( market.outboundToken, market.outboundUnits, market.inboundToken, market.inboundUnits, market.tickSpacing )); } function _settleFullDelta(address token, address user) internal { int256 delta = address(VIF).deltaOf(token); if (delta > 0) { // Credit: take tokens VIF.take(token, uint256(delta), user); } else if (delta < 0) { // Debt: settle tokens uint256 amount = uint256(-delta); if (token == LibDeltasExt.NATIVE) { VIF.settle{value: amount}(token, amount, address(this)); } else { VIF.settle(token, amount, user); } } } function limitOrder( Market memory market, int24 tick, uint40 initialOfferId, uint256 gives, uint32 expiry, uint24 provision ) external payable returns (uint40 offerId, uint256 claimedReceived) { bytes memory data = abi.encode( market, tick, initialOfferId, gives, expiry, provision ); bytes memory result = VIF.lock(data); (offerId, claimedReceived) = abi.decode(result, (uint40, uint256)); // Return leftover native token if (address(this).balance > 0) { msg.sender.safeTransferAllETH(); } } function lockCallback(bytes calldata data) external returns (bytes memory) { require(msg.sender == address(VIF), "Only Vif"); ( Market memory market, int24 tick, uint40 initialOfferId, uint256 gives, uint32 expiry, uint24 provision ) = abi.decode( data, (Market, int24, uint40, uint256, uint32, uint24) ); // Create/edit offer uint256 claimedReceived; uint40 offerId; (offerId, claimedReceived) = VIF.make( msg.sender, _marketId(market), initialOfferId, gives, tick, expiry, provision ); // Settle all balances _settleFullDelta(market.outboundToken, msg.sender); _settleFullDelta(LibDeltasExt.NATIVE, msg.sender); _settleFullDelta(market.inboundToken, msg.sender); return abi.encode(offerId, claimedReceived); } receive() external payable {} } ``` ### Usage Examples #### Create Simple Limit Order ```solidity // 1. Authorize router vif.authorize(address(router), true); // 2. Approve outbound token WETH.approve(address(vif), type(uint256).max); // 3. Define market Market memory market = Market({ outboundToken: address(WETH), inboundToken: address(USDC), outboundUnits: 1e13, inboundUnits: 1e4, tickSpacing: 1 }); // 4. Place order: Sell 1 WETH for 3000 USDC (uint40 offerId, uint256 claimed) = router.limitOrder( market, -2_054_990, // tick for ~3000 USDC/WETH 0, // new offer 1 ether, // give 1 WETH 0, // never expires 0 // no provision needed ); console.log("Offer ID:", offerId); console.log("Claimed:", claimed); // 0 for new offer ``` #### Create Expiring Limit Order ```solidity // Place order that expires in 24 hours uint32 expiryTime = uint32(block.timestamp + 1 days); uint24 provisionAmount = 1000; // 0.001 ETH in provision units (uint40 offerId,) = router.limitOrder{value: 0.001 ether}( market, tick, 0, // new offer 1 ether, expiryTime, provisionAmount ); ``` #### Edit Existing Order ```solidity // Update offer to sell more at a better price (uint40 sameId, uint256 claimed) = router.limitOrder( market, -2_060_000, // better price (lower tick) offerId, // edit existing 2 ether, // increase to 2 WETH 0, 0 ); console.log("Claimed from fills:", claimed / 1e6, "USDC"); // sameId == offerId βœ“ ``` ### Claiming Filled Amounts #### Using `claim` ```solidity function claim(bytes32 market, uint40 offerId) external returns (uint256 inbound, uint256 outbound, uint256 provision); ``` ```solidity // In router callback: (uint256 inbound, uint256 outbound, uint256 prov) = VIF.claim(marketId, offerId); // Settle balances _settleFullDelta(inboundToken, maker); // Claim filled amount _settleFullDelta(outboundToken, maker); // If fully filled/expired _settleFullDelta(NATIVE, maker); // If provision returned return abi.encode(inbound, outbound, prov); ``` **When claim returns outbound + provision:** * Offer was fully filled * Offer expired * Offer was cancelled ### Cancelling Orders #### Using `cancel` ```solidity function cancel(bytes32 market, uint40 offerId) external returns (uint256 inbound, uint256 outbound, uint256 provision); ``` ```solidity // In router callback: (uint256 inbound, uint256 outbound, uint256 prov) = VIF.cancel(marketId, offerId); // Returns: // - inbound: filled amount not yet claimed // - outbound: unfilled amount // - provision: locked native token _settleFullDelta(inboundToken, maker); _settleFullDelta(outboundToken, maker); _settleFullDelta(NATIVE, maker); return abi.encode(inbound, outbound, prov); ``` ### Advanced Patterns #### Market Making Place orders on both sides of the book: ```solidity function provideLiquidity( Market memory asks, Market memory bids, int24 askTick, int24 bidTick, uint256 size ) external { bytes memory data = abi.encode(asks, bids, askTick, bidTick, size); vif.lock(data); } function lockCallback(bytes calldata data) external returns (bytes memory) { (Market memory asks, Market memory bids, int24 askTick, int24 bidTick, uint256 size) = abi.decode(data, (Market, Market, int24, int24, uint256)); address user = tx.origin; // Or decode from data // Place ask (sell WETH for USDC at higher price) vif.make(user, _marketId(asks), 0, size, askTick, 0, 0); // Place bid (sell USDC for WETH at lower price) uint256 bidSize = calculateBidSize(size, askTick, bidTick); vif.make(user, _marketId(bids), 0, bidSize, bidTick, 0, 0); // Settle both sides _settleFullDelta(asks.outboundToken, user); // WETH _settleFullDelta(bids.outboundToken, user); // USDC return ""; } ``` #### Update and Claim in One Transaction ```solidity // Claim filled amount and update offer size (uint40 offerId, uint256 claimed) = vif.make( maker, market, existingOfferId, // Edit existing newGives, tick, expiry, provision ); // `claimed` contains the filled amount // Offer is updated with new parameters // All in one transaction βœ“ ``` #### Ladder Orders Place multiple orders at different price levels: ```solidity function placeLadder( Market memory market, int24[] calldata ticks, uint256[] calldata amounts ) external returns (uint40[] memory offerIds) { require(ticks.length == amounts.length, "Length mismatch"); bytes memory data = abi.encode(market, ticks, amounts); bytes memory result = vif.lock(data); return abi.decode(result, (uint40[])); } function lockCallback(bytes calldata data) external returns (bytes memory) { (Market memory market, int24[] memory ticks, uint256[] memory amounts) = abi.decode(data, (Market, int24[], uint256[])); uint40[] memory offerIds = new uint40[](ticks.length); for (uint256 i = 0; i < ticks.length; i++) { (offerIds[i],) = VIF.make( msg.sender, _marketId(market), 0, amounts[i], ticks[i], 0, 0 ); } // Single settle for all offers _settleFullDelta(market.outboundToken, msg.sender); return abi.encode(offerIds); } // Usage: int24[] memory ticks = new int24[](3); ticks[0] = -2_050_000; // 3100 USDC/WETH ticks[1] = -2_055_000; // 3000 USDC/WETH ticks[2] = -2_060_000; // 2900 USDC/WETH uint256[] memory amounts = new uint256[](3); amounts[0] = 1 ether; amounts[1] = 2 ether; amounts[2] = 3 ether; uint40[] memory ids = router.placeLadder(market, ticks, amounts); // Created 3 offers with one transaction! βœ“ ``` ### Price Calculation Helpers #### Tick from Price ```solidity function tickFromPrice( uint256 inboundAmount, uint256 outboundAmount ) public pure returns (int24) { // price = inboundAmount / outboundAmount // tick = log(price) / log(1.00001) // Using binary search or approximation // This is a simplified version uint256 price = (inboundAmount * 1e18) / outboundAmount; return int24(int256( (log2(price) * 1e18) / log2(100_001e13) )); } // Example: int24 tick = tickFromPrice(3000e6, 1e18); // 3000 USDC per WETH // tick β‰ˆ -2,054,985 ``` #### Price from Tick ```solidity // Use LibTick from vif-core import {LibTick} from "vif-core/libraries/LibTick.sol"; uint256 price = LibTick.price(tick); // Returns Q128.128 fixed-point number // Convert to readable price: uint256 readablePrice = (price * outboundAmount) / (1 << 128) / inboundAmount; ``` ### Gas Optimization 1. **Batch operations**: Use flash accounting to combine multiple actions 2. **Larger tick spacing**: Reduces tree traversal costs 3. **Avoid unnecessary edits**: Each edit costs \~100k gas 4. **Claim with edits**: When editing, claiming is free (included) ### Common Pitfalls #### ❌ Wrong: Forgetting to settle provision ```solidity vif.make{value: 0.001 ether}(maker, market, 0, gives, tick, expiry, 1000); // Sent ETH via msg.value, but also need to settle in callback! ``` #### βœ“ Correct: Settle provision debt ```solidity vif.make(maker, market, 0, gives, tick, expiry, 1000); // In callback: _settleFullDelta(LibDeltasExt.NATIVE, maker); // Settles provision βœ“ ``` #### ❌ Wrong: Editing with wrong maker ```solidity // Offer created by Alice uint40 id = vif.make(alice, market, 0, gives, tick, 0, 0); // Bob tries to edit - REVERTS vif.make(bob, market, id, newGives, tick, 0, 0); // ❌ ``` #### βœ“ Correct: Same maker for edits ```solidity vif.make(alice, market, id, newGives, tick, 0, 0); // βœ“ ``` ### Related Concepts * [Flash Accounting](/protocol/how-it-works/flash-accounting) - Delta management * [Offers & Lists](/protocol/how-it-works/offers) - Offer structure details * [Creating Swaps](/developer/interactions/custom-router-swaps) - Consuming limit orders * [Tick](/protocol/glossary/tick) - Price representation ## Building Custom Routers: Swaps This guide shows how to build a custom router for executing market orders (swaps). For most use cases, consider using the [built-in VifRouter](/developer/interactions/using-vif-router) instead. Market orders in Vif are called "swaps" or "consume" operations. They execute immediately by matching against existing limit orders in the book. ### Prerequisites Before executing swaps, ensure: 1. **Market exists** - The trading pair must be created by the protocol owner 2. **Router is authorized** - User must authorize the router as an operator 3. **Token approval** - User must approve the Vif contract to spend tokens ### Basic Swap Pattern All swaps follow the lock-callback pattern: ```solidity contract MyRouter is ILockCallback { IVif public immutable vif; function swap(...) external returns (...) { bytes memory data = abi.encode(...); bytes memory result = vif.lock(data); return abi.decode(result, ...); } function lockCallback(bytes calldata data) external returns (bytes memory) { require(msg.sender == address(vif), "Only Vif"); // 1. Execute market order (uint256 gave, uint256 got, uint256 fee, uint256 bounty) = vif.consume(...); // 2. Settle debts and take credits _settleAndTake(...); return abi.encode(gave, got, fee, bounty); } } ``` ### The `consume` Function ```solidity function consume( address taker, // Who is taking the order bytes32 market, // Market identifier int24 maxTick, // Maximum acceptable tick (price limit) uint256 fillVolume, // Amount to fill bool fillWants, // If true: fillVolume is output; if false: input uint256 maxOffers // Maximum offers to consume (gas limit) ) external returns ( uint256 gave, // Amount taker paid uint256 got, // Amount taker received uint256 fee, // Fee paid by taker uint256 bounty // Bounty earned from cleaning expired offers ); ``` #### Parameters Explained ##### `taker` The account executing the swap. Must be either: * The caller directly * An account that authorized the caller as operator ##### `market` Market identifier calculated as: ```solidity bytes32 marketId = keccak256(abi.encodePacked( outboundToken, outboundUnits, inboundToken, inboundUnits, tickSpacing )); ``` ##### `maxTick` Price limit for the swap: * Stops consuming offers when offer tick > maxTick * Use `type(int24).max` for no price limit * Lower tick = better price for taker ```solidity // Example: WETH/USDC market // Want to buy WETH but not pay more than 3100 USDC/WETH // Calculate maxTick for 3100 USDC/WETH: // price = 1.00001^tick // 3100 = 1.00001^tick // tick = log(3100) / log(1.00001) // tick β‰ˆ -2,055,000 maxTick = -2_055_000; // Won't buy if price > 3100 ``` :::tip[Price Protection] Always set a reasonable `maxTick` to protect against: * Slippage from front-running * Stale price data * Low liquidity ::: ##### `fillVolume` and `fillWants` These parameters work together to specify the trade size: **Exact Input** (`fillWants = false`): ```solidity fillVolume = 1000e6; // Spend exactly 1000 USDC fillWants = false; // Result: got = ~0.32 WETH (depends on book) // gave = 1000e6 USDC (exactly as specified) ``` **Exact Output** (`fillWants = true`): ```solidity fillVolume = 1e18; // Receive exactly 1 WETH fillWants = true; // Result: got = 1e18 WETH (exactly as specified) // gave = ~3000e6 USDC (depends on book) ``` :::warning[Units Matter] `fillVolume` is in **token decimals**, not units. The protocol will floor it to the nearest unit internally: ```solidity // USDC with inboundUnits = 1e4 fillVolume = 1000.5011e6; // 1000.5011 USDC requested // Actually used: 1000.5010e6 USDC (floored to 0.0001 precision) ``` ::: ##### `maxOffers` Maximum number of offers to match against: * Limits gas consumption * Prevents running out of gas on deep books * Trade completes even if volume target not reached ```solidity maxOffers = 100; // Match at most 100 offers // If 150 offers needed for full fill, only fills first 100 ``` **Choosing maxOffers:** * **Small trades**: 10-50 offers usually sufficient * **Large trades**: 100-500 offers for deep liquidity * **Gas-sensitive**: Lower for guaranteed execution * **Fill-sensitive**: Higher for better fill rates ### Example: Simple WETH/USDC Swap ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {IVif} from "vif-core/interfaces/IVif.sol"; import {ILockCallback} from "vif-core/interfaces/ILockCallback.sol"; import {LibDeltasExt} from "vif-core/libraries/external/LibDeltasExt.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; contract SimpleSwapRouter is ILockCallback { using LibDeltasExt for address; using SafeTransferLib for address; IVif public immutable VIF; struct Market { address outboundToken; uint64 outboundUnits; address inboundToken; uint64 inboundUnits; uint16 tickSpacing; } constructor(address _vif) { VIF = IVif(_vif); } function _marketId(Market memory market) internal pure returns (bytes32) { return keccak256(abi.encodePacked( market.outboundToken, market.outboundUnits, market.inboundToken, market.inboundUnits, market.tickSpacing )); } function _settleFullDelta(address token, address user) internal { int256 delta = address(VIF).deltaOf(token); if (delta > 0) { // Credit: take tokens VIF.take(token, uint256(delta), user); } else if (delta < 0) { // Debt: settle tokens uint256 amount = uint256(-delta); if (token == LibDeltasExt.NATIVE) { // Native token (ETH) VIF.settle{value: amount}(token, amount, address(this)); } else { // ERC20 token VIF.settle(token, amount, user); } } } function swap( Market memory market, int24 maxTick, uint256 fillVolume, bool fillWants, uint256 maxOffers ) external payable returns ( uint256 gave, uint256 got, uint256 fees, uint256 bounty ) { bytes memory data = abi.encode( market, msg.sender, maxTick, fillVolume, fillWants, maxOffers ); bytes memory result = VIF.lock(data); (gave, got, fees, bounty) = abi.decode( result, (uint256, uint256, uint256, uint256) ); // Return any leftover native token if (address(this).balance > 0) { msg.sender.safeTransferAllETH(); } } function lockCallback(bytes calldata data) external returns (bytes memory) { require(msg.sender == address(VIF), "Only Vif"); ( Market memory market, address user, int24 maxTick, uint256 fillVolume, bool fillWants, uint256 maxOffers ) = abi.decode( data, (Market, address, int24, uint256, bool, uint256) ); // Execute swap (uint256 gave, uint256 got, uint256 fees, uint256 bounty) = VIF.consume(user, _marketId(market), maxTick, fillVolume, fillWants, maxOffers); // Settle all balances _settleFullDelta(market.outboundToken, user); _settleFullDelta(market.inboundToken, user); _settleFullDelta(LibDeltasExt.NATIVE, user); return abi.encode(gave, got, fees, bounty); } receive() external payable {} } ``` ### Usage Example ```solidity // 1. Deploy router SimpleSwapRouter router = new SimpleSwapRouter(address(vif)); // 2. Authorize router vif.authorize(address(router), true); // 3. Approve tokens USDC.approve(address(vif), type(uint256).max); // 4. Define market SimpleSwapRouter.Market memory market = SimpleSwapRouter.Market({ outboundToken: address(WETH), inboundToken: address(USDC), outboundUnits: 1e13, // 0.00001 WETH inboundUnits: 1e4, // 0.0001 USDC tickSpacing: 1 }); // 5. Execute swap: Buy 1 WETH with USDC (uint256 gave, uint256 got, uint256 fees, uint256 bounty) = router.swap( market, type(int24).max, // No price limit 1 ether, // Want exactly 1 WETH true, // fillWants = true (exact output) 100 // Max 100 offers ); console.log("Gave:", gave / 1e6, "USDC"); console.log("Got:", got / 1e18, "WETH"); console.log("Fees:", fees / 1e6, "USDC"); console.log("Bounty:", bounty / 1e18, "ETH"); ``` ### Multi-Hop Swaps Flash accounting enables efficient multi-hop swaps: ```solidity function multiHopSwap( Market memory market1, // WETH β†’ USDC Market memory market2, // USDC β†’ DAI uint256 amountIn ) external returns (uint256 amountOut) { bytes memory data = abi.encode(market1, market2, msg.sender, amountIn); bytes memory result = VIF.lock(data); return abi.decode(result, (uint256)); } function lockCallback(bytes calldata data) external returns (bytes memory) { (Market memory m1, Market memory m2, address user, uint256 amountIn) = abi.decode(data, (Market, Market, address, uint256)); // Hop 1: WETH β†’ USDC (uint256 gave1, uint256 got1,,) = VIF.consume( user, _marketId(m1), type(int24).max, amountIn, false, // Exact input 100 ); // Hop 2: USDC β†’ DAI (uint256 gave2, uint256 got2,,) = VIF.consume( user, _marketId(m2), type(int24).max, got1, // Use output from hop 1 false, // Exact input 100 ); // Settle only first and last token _settleFullDelta(m1.inboundToken, user); // WETH _settleFullDelta(m2.outboundToken, user); // DAI // USDC is netted automatically - no transfer! βœ“ return abi.encode(got2); } ``` ### Price Calculation To calculate expected output before swapping: ```solidity // Read offers from the book LibTreeExt.Tree memory tree = address(vif).treeFor(marketId); LibTreeExt.Cursor memory cursor; bool found; (cursor, found) = tree.first(); uint256 remaining = fillVolume; uint256 totalGot = 0; while (found && remaining > 0) { int24 tick = cursor.index().tick(tickSpacing); if (tick > maxTick) break; LibOfferListExt.OfferList memory list = address(vif).offerListFor(marketId, tick); // Price at this tick uint256 price = LibTick.price(tick); // Available liquidity uint256 available = list.totalVolume * outboundUnits; uint256 consumed = min(available, remaining); totalGot += consumed; remaining -= calculateCost(consumed, price); found = cursor.next(tree); } return totalGot; ``` ### Slippage Protection Always protect against slippage: ```solidity // Calculate minimum output (5% slippage tolerance) uint256 expectedOut = quoteSwap(market, amountIn); uint256 minOut = expectedOut * 95 / 100; // Execute swap (, uint256 got,,) = router.swap(market, maxTick, amountIn, false, 100); require(got >= minOut, "Slippage too high"); ``` ### Gas Optimization Tips 1. **Set realistic `maxOffers`**: Don't over-allocate gas 2. **Use exact input**: `fillWants = false` is slightly cheaper 3. **Batch operations**: Use flash accounting to combine swaps 4. **Monitor bounties**: Cleaning expired offers earns native tokens ### Common Patterns #### Limit Price Swap ```solidity // Only buy if price <= 3000 USDC/WETH int24 maxTick = calculateTickFromPrice(3000e6, 1e18); router.swap(market, maxTick, 1 ether, true, 100); ``` #### Fill-or-Kill ```solidity (, uint256 got,,) = router.swap(market, maxTick, amount, fillWants, maxOffers); require(got == expectedAmount, "Partial fill not acceptable"); ``` #### Partial Fill Acceptable ```solidity uint256 minAcceptable = expectedAmount * 90 / 100; (, uint256 got,,) = router.swap(market, maxTick, amount, fillWants, maxOffers); require(got >= minAcceptable, "Fill too small"); ``` ### Related Concepts * [Flash Accounting](/protocol/how-it-works/flash-accounting) - Understanding deltas * [Tick Tree Structure](/protocol/how-it-works/tick-tree) - How prices are organized * [Placing Limit Orders](/developer/interactions/custom-router-limits) - Providing liquidity ## Using VifRouter The built-in `VifRouter` is a command-based router that provides a flexible interface for interacting with the Vif protocol. It uses a dispatcher pattern to execute multiple operations in a single transaction. ### Overview VifRouter allows you to: * Execute market orders (swaps) * Place and manage limit orders * Batch multiple operations * Handle token wrapping/unwrapping * Manage settlements efficiently All operations use a **command-based approach** where you encode commands and their arguments to execute complex workflows. ### Basic Usage Pattern ```solidity // 1. Encode commands and arguments bytes memory commands = hex"0005"; // ORDER_SINGLE, SETTLE_ALL bytes[] memory args = new bytes[](2); args[0] = abi.encode(...); // Order arguments args[1] = abi.encode(...); // Settlement arguments // 2. Execute with optional deadline vifRouter.execute{value: ethAmount}(commands, args, deadline); ``` ### Command Types #### Order Commands ```solidity // From Commands.sol uint8 constant ORDER_SINGLE = 0x00; // Single market order uint8 constant ORDER_MULTI = 0x01; // Multi-hop market order uint8 constant LIMIT_SINGLE = 0x02; // Create limit order uint8 constant CLAIM = 0x03; // Claim filled amounts uint8 constant CANCEL = 0x04; // Cancel limit order ``` #### Settlement Commands ```solidity uint8 constant SETTLE = 0x05; // Settle specific amount uint8 constant TAKE = 0x06; // Take specific amount uint8 constant SETTLE_ALL = 0x07; // Settle all debt uint8 constant TAKE_ALL = 0x08; // Take all credit ``` #### Utility Commands ```solidity uint8 constant SWEEP = 0x09; // Sweep tokens from router uint8 constant WRAP_NATIVE = 0x0a; // Wrap ETH to WETH uint8 constant UNWRAP_NATIVE = 0x0b; // Unwrap WETH to ETH uint8 constant AUTHORIZE = 0x0c; // Authorize operator uint8 constant CLEAR_ALL = 0x0d; // Clear all dust uint8 constant CLEAR_UPTO_OR_CLAIM = 0x0e; // Clear dust or claim ``` ### Command Encoding Commands are encoded as single bytes where: * **Bits 0-3**: Command type (0x00 - 0x0f) * **Bit 7**: Allow failure flag (0x80) ```solidity // Normal command bytes1 cmd = 0x00; // ORDER_SINGLE // Allow failure (won't revert if fails) bytes1 cmdCanFail = 0x80; // ORDER_SINGLE with allow failure // Batch commands bytes memory commands = hex"00070"; // ORDER_SINGLE, SETTLE_ALL, SETTLE ``` ### Example: Simple Swap ```solidity import {Commands} from "vif-core/libraries/periphery/Commands.sol"; contract MyContract { IVifRouter public router; IVif public vif; struct MarketParams { bytes32 marketId; int24 maxTick; uint256 fillVolume; bool fillWants; uint256 maxOffers; } function swap( MarketParams memory params, address tokenIn, uint256 deadline ) external returns (uint256 amountOut) { // Approve router IERC20(tokenIn).approve(address(router), params.fillVolume); // Build commands bytes memory commands = abi.encodePacked( bytes1(Commands.ORDER_SINGLE), bytes1(Commands.SETTLE_ALL), bytes1(Commands.TAKE_ALL) ); // Build arguments bytes[] memory args = new bytes[](3); // ORDER_SINGLE arguments args[0] = abi.encode( params.marketId, params.maxTick, params.fillVolume, params.fillWants, params.maxOffers ); // SETTLE_ALL arguments args[1] = abi.encode( tokenIn, msg.sender ); // TAKE_ALL arguments (token will be determined by order) args[2] = abi.encode( address(0), // Will use the output token from order msg.sender ); // Execute DispatchResult[] memory results = router.execute( commands, args, deadline ); // Decode result (,amountOut,,) = abi.decode( results[0].data, (uint256, uint256, uint256, uint256) ); } } ``` ### ORDER\_SINGLE Command Execute a single market order. #### Arguments ```solidity struct OrderSingleArgs { bytes32 marketId; int24 maxTick; uint256 fillVolume; bool fillWants; uint256 maxOffers; } // Encode bytes memory args = abi.encode( marketId, maxTick, fillVolume, fillWants, maxOffers ); ``` #### Returns ```solidity struct OrderResult { uint256 gave; // Amount paid uint256 got; // Amount received uint256 fee; // Fee paid uint256 bounty; // Bounty earned } // Decode from DispatchResult (uint256 gave, uint256 got, uint256 fee, uint256 bounty) = abi.decode(result.data, (uint256, uint256, uint256, uint256)); ``` ### ORDER\_MULTI Command Execute a multi-hop market order (exact input only). #### Arguments ```solidity struct OrderMultiArgs { bytes32[] marketIds; int24[] maxTicks; uint256 fillVolume; uint256[] maxOffers; } // Encode bytes memory args = abi.encode( marketIds, maxTicks, fillVolume, maxOffers ); ``` **Example: WETH β†’ USDC β†’ DAI** ```solidity bytes32[] memory markets = new bytes32[](2); markets[0] = wethUsdcMarketId; markets[1] = usdcDaiMarketId; int24[] memory maxTicks = new int24[](2); maxTicks[0] = type(int24).max; // No price limit maxTicks[1] = type(int24).max; uint256[] memory maxOffers = new uint256[](2); maxOffers[0] = 100; maxOffers[1] = 100; bytes memory args = abi.encode( markets, maxTicks, 1 ether, // Spend 1 WETH maxOffers ); bytes memory commands = abi.encodePacked( bytes1(Commands.ORDER_MULTI), bytes1(Commands.SETTLE_ALL), bytes1(Commands.TAKE_ALL) ); ``` ### LIMIT\_SINGLE Command Create or edit a limit order. #### Arguments ```solidity struct LimitSingleArgs { bytes32 marketId; uint40 initialOfferId; // 0 for new uint256 gives; int24 tick; uint32 expiry; uint24 provision; } // Encode bytes memory args = abi.encode( marketId, initialOfferId, gives, tick, expiry, provision ); ``` #### Returns ```solidity struct LimitResult { uint40 offerId; uint256 claimedReceived; } // Decode (uint40 offerId, uint256 claimed) = abi.decode(result.data, (uint40, uint256)); ``` **Example: Place Limit Order** ```solidity // Approve tokens WETH.approve(address(router), 1 ether); // Build command bytes memory commands = abi.encodePacked( bytes1(Commands.LIMIT_SINGLE), bytes1(Commands.SETTLE_ALL), bytes1(Commands.TAKE_ALL) ); bytes[] memory args = new bytes[](3); // Create offer selling 1 WETH args[0] = abi.encode( marketId, uint40(0), // New offer 1 ether, // Give 1 WETH -2_054_990, // Price tick uint32(0), // No expiry uint24(0) // No provision ); // Settle WETH debt args[1] = abi.encode(address(WETH), msg.sender); // Take any claimed credits (if editing) args[2] = abi.encode(address(0), msg.sender); DispatchResult[] memory results = router.execute(commands, args); (uint40 offerId,) = abi.decode(results[0].data, (uint40, uint256)); console.log("Created offer ID:", offerId); ``` ### CLAIM and CANCEL Commands #### CLAIM Arguments ```solidity // Claim filled amounts bytes memory args = abi.encode( marketId, offerId ); ``` #### CANCEL Arguments ```solidity // Cancel and claim all bytes memory args = abi.encode( marketId, offerId ); ``` #### Returns (both) ```solidity struct ClaimResult { uint256 inbound; uint256 outbound; uint256 provision; } (uint256 inbound, uint256 outbound, uint256 provision) = abi.decode(result.data, (uint256, uint256, uint256)); ``` **Example: Claim and Settle** ```solidity bytes memory commands = abi.encodePacked( bytes1(Commands.CLAIM), bytes1(Commands.TAKE_ALL) ); bytes[] memory args = new bytes[](2); args[0] = abi.encode(marketId, offerId); args[1] = abi.encode(address(0), msg.sender); // Take all credits router.execute(commands, args); ``` ### Settlement Commands #### SETTLE\_ALL Settle all debt for a token: ```solidity // Arguments bytes memory args = abi.encode( token, from // Address to pull from ); ``` #### TAKE\_ALL Take all credit for a token: ```solidity // Arguments bytes memory args = abi.encode( token, receiver // Address to send to ); ``` #### SETTLE (specific amount) ```solidity bytes memory args = abi.encode( token, amount, from ); ``` #### TAKE (specific amount) ```solidity bytes memory args = abi.encode( token, amount, receiver ); ``` ### Utility Commands #### WRAP\_NATIVE Wrap ETH to WETH: ```solidity // Arguments bytes memory args = abi.encode( amountOrAll // Amount or type(uint256).max for all ); // Must send ETH with call router.execute{value: 1 ether}(commands, args); ``` #### UNWRAP\_NATIVE Unwrap WETH to ETH: ```solidity bytes memory args = abi.encode( amountOrAll, receiver ); ``` #### SWEEP Send all tokens from router to receiver: ```solidity bytes memory args = abi.encode( token, receiver ); ``` #### AUTHORIZE Authorize an operator on behalf of msg.sender: ```solidity bytes memory args = abi.encode( operator, authorized // true/false ); ``` ### Advanced Patterns #### Batch Multiple Limit Orders ```solidity // Place 3 limit orders in one transaction bytes memory commands = abi.encodePacked( bytes1(Commands.LIMIT_SINGLE), bytes1(Commands.LIMIT_SINGLE), bytes1(Commands.LIMIT_SINGLE), bytes1(Commands.SETTLE_ALL), bytes1(Commands.TAKE_ALL) ); bytes[] memory args = new bytes[](5); // Three offers at different prices args[0] = abi.encode(marketId, 0, 1 ether, tick1, 0, 0); args[1] = abi.encode(marketId, 0, 1 ether, tick2, 0, 0); args[2] = abi.encode(marketId, 0, 1 ether, tick3, 0, 0); // Settle total outbound args[3] = abi.encode(WETH, msg.sender); // Take any credits args[4] = abi.encode(address(0), msg.sender); router.execute(commands, args); // Created 3 offers with one transaction! βœ“ ``` #### Swap with Slippage Protection ```solidity // Calculate minimum output uint256 expectedOut = quoteSwap(marketId, amountIn); uint256 minOut = expectedOut * 95 / 100; // 5% slippage // Execute swap bytes memory commands = abi.encodePacked( bytes1(Commands.ORDER_SINGLE), bytes1(Commands.SETTLE_ALL), bytes1(Commands.TAKE_ALL) ); bytes[] memory args = new bytes[](3); args[0] = abi.encode(marketId, maxTick, amountIn, false, 100); args[1] = abi.encode(tokenIn, msg.sender); args[2] = abi.encode(tokenOut, msg.sender); DispatchResult[] memory results = router.execute(commands, args); (, uint256 got,,) = abi.decode( results[0].data, (uint256, uint256, uint256, uint256) ); require(got >= minOut, "Slippage too high"); ``` #### Flash Loan Pattern ```solidity // Borrow, arbitrage, repay in one transaction bytes memory commands = abi.encodePacked( bytes1(Commands.TAKE), // Borrow bytes1(Commands.ORDER_SINGLE), // Arbitrage swap bytes1(Commands.SETTLE), // Repay bytes1(Commands.TAKE_ALL) // Take profit ); bytes[] memory args = new bytes[](4); // Borrow 100 WETH args[0] = abi.encode(WETH, 100 ether, address(this)); // Swap on external DEX (handled separately) args[1] = abi.encode(...); // Market order args // Repay 100 WETH args[2] = abi.encode(WETH, 100 ether, address(this)); // Keep profit args[3] = abi.encode(address(0), msg.sender); router.execute(commands, args); ``` ### Error Handling #### Allow Failure Flag Commands with bit 7 set (0x80) can fail without reverting the entire transaction: ```solidity // This command can fail bytes1 cmdCanFail = bytes1(uint8(Commands.ORDER_SINGLE) | 0x80); bytes memory commands = abi.encodePacked( cmdCanFail, // Try order, continue if fails bytes1(Commands.LIMIT_SINGLE) // Always execute this ); ``` #### Checking Results ```solidity DispatchResult[] memory results = router.execute(commands, args); for (uint256 i = 0; i < results.length; i++) { if (!results[i].success) { // Command i failed // results[i].data contains error data } } ``` ### Gas Optimization Tips 1. **Batch operations**: Combine multiple commands to save on lock overhead 2. **Use SETTLE\_ALL/TAKE\_ALL**: Cheaper than specific amounts when clearing all 3. **Order matters**: Put operations that may fail early if using allow failure 4. **Minimize commands**: Each command has dispatch overhead ### Common Pitfalls #### ❌ Wrong: Forgetting to settle ```solidity bytes memory commands = abi.encodePacked( bytes1(Commands.ORDER_SINGLE) // Missing SETTLE_ALL / TAKE_ALL - will revert! ); ``` #### βœ“ Correct: Always settle and take ```solidity bytes memory commands = abi.encodePacked( bytes1(Commands.ORDER_SINGLE), bytes1(Commands.SETTLE_ALL), bytes1(Commands.TAKE_ALL) ); ``` #### ❌ Wrong: Wrong argument encoding ```solidity // ORDER_SINGLE expects 5 arguments bytes memory args = abi.encode(marketId, maxTick); // Only 2! ❌ ``` #### βœ“ Correct: Match command signature ```solidity bytes memory args = abi.encode( marketId, maxTick, fillVolume, fillWants, maxOffers ); // All 5 arguments βœ“ ``` ### Transaction Deadline Protect against stale transactions: ```solidity uint256 deadline = block.timestamp + 5 minutes; router.execute(commands, args, deadline); // Reverts if block.timestamp > deadline ``` ### Related Concepts * [Creating Swaps](/developer/interactions/custom-router-swaps) - Market order concepts * [Placing Limit Orders](/developer/interactions/custom-router-limits) - Limit order concepts * [Building Custom Routers](/developer/interactions/custom-router-swaps) - Roll your own * [Flash Accounting](/protocol/how-it-works/flash-accounting) - Understanding deltas