How to Become a Filler?

category
Application
date
Oct 9, 2024
slug
how-to-become-a-filler
type
Post
lang
en
status
Published
author: banri
Many thanks to Uniswap Labs team for reviews.
Titania Research has started participating in the market as a Filler, and this post explains what is necessary to become a Filler on UniswapX.
A Filler is an entity that executes transactions on behalf of users. Intent-based architectures like UniswapX are continuously gaining attention, and their development is expected to continue in the future. However, currently, there is limited information available on implementing Fillers, and even using the official SDK, it is challenging to fill orders on Arbitrum.
This time, we modified the Uniswap SDK and implemented sample code using it. By using this template, anyone can become a Filler. Even if an official SDK is released in the future, the implementation method will not change.
uniswapx-filler-template
titania-research-labsUpdated Oct 29, 2024
Additionally, we have created a simple dashboard for use when filling orders. Existing tools like Solverscan are not compatible with UniswapX on Arbitrum's Intent, so the dashboard we introduce here will be helpful in understanding Intent in this case.
Important Notes
  • UniswapX on Arbitrum is an experimental program that began in June 2024. It's expected that there may be changes to its specifications in the future. Please keep in mind that this is still in its early stages.
  • In reality, it is challenging to generate significant profits, but it may be worth trying to gain an understanding of the market structure.
  • This information does not include specific trading strategies or arbitrage methods that could provide an edge in the market.

0. Overall Flow to Fill

This post explains what is necessary to become a Filler on UniswapX. A Filler is an entity that executes transactions on behalf of users. Currently, the only permissionless way to become a Filler is through UniswapX on Arbitrum's Dutch Order V2, and unless otherwise specified, this will be the basis for the explanation.
The overall flow when acting as a Filler is as follows:
  1. A user (Swapper) signs and submits an Intent through the Uniswap Web app frontend.
  1. The Filler uses the UniswapX API to retrieve open Intents (retrieve ongoing auctions).
  1. The Filler decodes the Intent and determines whether it should be filled (determines whether to participate in the auction).
  1. At the appropriate time, the Filler calls the execute function of the UniswapX contract (bids in the auction).
  1. If the fill is successful (if the auction is won), the Filler uses Uniswap v3 to swap the tokens converted during the fill back to the original tokens.
  1. (Repeat the process.)
The 5th flow may not be mandatory depending on the strategy, but we will proceed with this assumption in this article. In the template implementation, we divided the Filler's tasks into two:
  1. Identification: Identifying the Intent (steps 2 and 3 in the above diagram)
  1. Fill: Executing the Fill (steps 4 and 5 in the above diagram)
Identification can be considered an off-chain process, while Fill is an on-chain process. In the actual implementation, we execute the following functions at 0.2-second intervals, adjusted to avoid hitting the API rate limit (increasing the limit parameter would cause it to hit the limit).
  • identificationService: A service that handles Intent identification
  • fillService: A service that handles Fill execution
We specify the target token for Fill in config.ts. In this case, we set it up as follows:
For example, this setting targets Intent to convert WETH to USDT. The input tokens we specified are representative samples of frequently traded tokens on UniswapX on Arbitrum at the time of writing.
Additionally, the technology stack used in this implementation is as follows.
  • Node.js
  • TypeScript
  • UniswapX SDK (with some modifications on my end)
  • Ethers.js v5
  • TypeChain
  • Axios
  • ESLint
  • Prettier
  • Winston
  • pnpm

1. Identifying Intent: IdentificationService

The first step is to identify the intent.
The identifyIntent method in the IdentificationService class is responsible for this task. There are two main processes involved:
  1. Fetching open intents from the UniswapX API.
  1. Decoding the intents and determining if they should be filled.

Fetching Open Intents from the API

First, we fetch intents from the off-chain UniswapX API. The UniswapX API offers both REST API and Websocket interfaces. While the Websocket API is technically more ideal, it requires additional procedures such as submitting forms to Uniswap, so we implemented it using the REST API this time. The implementation for fetching the API is as follows:
The parameters are set to fetch one intent that is open and in the Dutch V2 auction format. We use Axios to send a GET request to the UniswapX API endpoint.
Note that if you specify the status as filled, you can view the history of past filled intents.
The raw intent fetched looks like this JSON:
In this implementation, we only use type, orderStatus, signature, encodedOrder, and orderHash.

Decoding Intent

By decoding the encodedOrder value of the raw Intent using the parse method, we can obtain the CosignedV2DutchOrder. Here's how it's done:
The order information of the Intent is stored in the info field of CosignedV2DutchOrder. Let's take a closer look at the specific values for better understanding.
The key information includes:
  • reactor: The contract address of the Reactor, which is fixed at 0x1bd1aAdc9E230626C44a139d7E70d842749351eb.
  • swapper: The wallet address of the Swapper.
  • deadline: The deadline for the Intent.
  • input:
    • token: The contract address of the input token.
    • startAmount: The payment amount of the input token (same as endAmount).
  • outputs:
    • token: The contract address of the output token.
    • startAmount: The starting amount of the output token in the Dutch auction.
    • endAmount: The ending amount of the output token in the Dutch auction.
    • recipient: The recipient of the output token.
  • cosigner: The co-signer, which is fixed at 0x4449Cd34d1eb1FEDCF02A1Be3834FfDe8E6A6180.
  • cosignerData:
    • decayStartTime: The start time of the Dutch auction.
    • decayEndTime: The end time of the Dutch auction (16 seconds after the start time).
    • exclusiveFiller: Fixed at 0x0000000000000000000000000000000000000000 for UniswapX on Arbitrum, indicating it is permissionless.
The reason outputs is an array is that there can be cases where the tokens from a fill are received by entities other than just the swapper. In the example above, the first element (index 0) contains information about the tokens received by the swapper, while the second element (index 1) contains information about the tokens received by Uniswap as fees. In fact, in the specific values shown above, outputs[0].recipient is 0x4b462aFa98169c7B46adA58Fd6339D7e69CfFa92, which matches the swapper's address, and outputs[1].recipient is 0x89F30783108E2F9191Db4A44aE2A516327C99575, which is Uniswap's fee-receiving address.
There are concepts that are not explicitly mentioned in the official documentation, such as cosigner, cosignerData, and cosignature. Looking at the Solidity code in the V2DutchOrderReactor.sol contract, we can see the following comment:
In essence, V2 Dutch Order always includes a role of a cosigner in addition to the main signer (the swapper). This co-signer has the ability to modify the auction time and token amounts, and can be considered the entity that ultimately signs the full order. Of course, the cosigner can't change values arbitrarily; they can only make changes that are favorable to the user (as pre-specified by the user's signature). Specifically, the cosigner address is uniformly set to 0x4449Cd34d1eb1FEDCF02A1Be3834FfDe8E6A6180 across all orders.
If you would like to see more specific values, please refer to the simple dashboard I created.

Determining Whether to Fill an Intent

Next, it is necessary to identify whether an Intent should be filled. Since this article aims to provide the minimum implementation required to execute Fill, the logic here will be kept concise. The implemented checks are as follows:
  • Whether orderStatus is open and type is OrderType.Dutch_V2.
    • Whether inputToken is supported.
      • Whether outputToken is supported.
        • Whether the auction is within its scheduled time.
          • Whether the balance is greater than the bid price.

            Full Implementation

            The full implementation of IdentificationService is as follows.

            2. Executing Fill: FillService

            Next, we have the flow for actually executing the Fill and settling the Intent. This role is handled by the FillService class. The implementation of the fillIntent method, which is called in FillService, is as follows:
            The executeFill function calls the on-chain execute function, and the swapInputTokenBackToOriginalToken method restores the consumed token to its original state.
            Calling this execute function means bidding in UniswapX's Dutch auction. Therefore, we will discuss the auction structure of UniswapX.

            UniswapX as a Dutch Auction

            In UniswapX, Intents are auctioned off to determine who will Fill them. This auction is in the form of a Dutch auction, where the price of the target item decreases over a certain period, and the first bidder wins. Currently, the auction period for Intents sent from Swappers on UniswapX on Arbitrum is 16 seconds *1. In other words, Fillers participate in a "game of chicken" where they watch the price decrease and try to be the first to raise their hand within a total of 16 seconds.
            Dutch auctions are characterized by prices that decrease over time. However, since blockchain is a discrete system where the state changes with each block time, the price in a Dutch auction on blockchain decreases in a step-like manner. Arbitrum has a block time of 0.25 seconds, and the auction period for UniswapX on Arbitrum is currently set at 16 seconds for all auctions. In theory, this should result in 64 price decreases (16 / 0.25 = 64) during the auction period.
            notion image
            In reality, though, the price only decreases 16 times (16 / 1 = 16). This is because blockchain timestamps inherently cannot express milliseconds. Currently, Uniswap is experimenting with a new system to address this limitation.
            notion image

            Executing the Fill Intent using the Reactor Contract's execute Function

            In UniswapX, calling the execute function of the Reactor contract corresponds to bidding in an auction. The part of the FillService that actually calls the execute function is quite simple.
            The execute function is called with an object that consists of two parts: the encoded intent obtained by calling the serialize method of the intent object, and the signature that was included in the raw intent.
            An important point to note is that the arguments passed to the execute function are the intent itself and the signature, not the intent's ID and price. It might be mistakenly assumed that the Uniswap side already has the intent information and only needs to know which intent to select and at what price to bid. However, (referring to the overall diagram again) it becomes clear that all interactions between the swapper and filler's intents until the fill is completed on-chain are off-chain processes through APIs, and the on-chain Reactor contract does not recognize any intent information. Therefore, the filler needs to provide all the intent information to the on-chain. Furthermore, the bidding price is automatically calculated based on the execution time of the execute function, so there is no need to explicitly pass it as an argument.
            As a result, the arguments passed to the execute function are the intent information itself and the swapper's signature, which are defined as the SignedOrderStruct type in the SDK.
            Once the transaction is confirmed, the receipt is returned.

            Swapping the Consumed Input Token Back to the Original Token

            The swapInputTokenBackToOriginalToken method of the FillService class uses the receipt information from executeFill to swap the consumed token back to the original token. Since this is not directly related to the main topic, the details are omitted, but it uses Uniswap v3 to perform the swap.

            Full Implementation

            The full implementation of IdentificationService is as follows.

            3. Execution Procedure

            Setting Up the Development Environment

            To set up the development environment, follow these steps:
            1. Clone the Repository: Use git clone to download the repository.
            1. Install Dependencies:
              1. Create an Environment Variable File:
                1. Enter Required Environment Variables in the .env File:
                    • PRIVATE_KEY: The private key of the EOA wallet used to fill intents.
                    • ALCHEMY_API_KEY: The Alchemy API key used to interact with the Arbitrum network.
                    Example:
                1. Start the Development Server:
                  Once these steps are completed, the development environment will be set up, and you can start working on the project locally.

                  Setting Up the Production Environment

                  For deploying the project, we recommend using Railway, a service that allows for easy deployment and provides clear logs. You can use it for free up to $5 during the initial account registration.
                  By linking your GitHub repository and following the steps, you can deploy smoothly. Note that you need to set environment variables on Railway as well.
                  For reference, here's what the logs looked like when I actually deployed on Railway:
                  notion image
                  notion image

                  Points for Improvement in Implementation

                  There are two main points that can be improved:
                  1. Selection of Target Tokens and Liquidity:
                      • In this implementation, we prioritized simplicity by using the following strategy:
                        • Only hold USDT tokens.
                        • Target intents with USDT as the output token for filling.
                        • If filling is successful, swap back to USDT using Uniswap v3.
                      • We used Uniswap v3's LP for liquidity, but you can also use CEX. In fact, major market makers like Wintermute use CEX liquidity in UniswapX on Ethereum.
                  1. Determining Bidding Timing:
                      • This implementation does not include specific logic for determining bidding timing. It simply fills intents immediately if the input and output tokens match the target. While this approach can win auctions quickly, it may result in overpaying. Essentially, refining this implementation is key to competitive advantage.
                      • Furthermore, UniswapX on Arbitrum has a block time of 0.25 seconds and an auction time of 16 seconds, compared to UniswapX on Ethereum, which has a block time of 12 seconds and an auction time of 1 minute. This means that more performance refinement is required for competitive advantage on Arbitrum.

                  4. Summary

                  In this post, we explained the template implementation of a Filler in UniswapX. We detailed the processes a Filler performs through the following steps:
                  1. Identifying Intent: IdentificationService
                      • Use the UniswapX API to retrieve open Intents.
                      • Decode the Intent and determine if it should be filled.
                  1. Executing Fill: FillService
                      • Call the execute function of the Reactor contract to fill the Intent and obtain a transaction receipt.
                      • Use Uniswap v3 to swap the consumed tokens back to their original form.
                  By using this template, you can easily become a Filler. Although there are still points that need improvement, we hope this helps in understanding Fill as a first step.

                  Note

                  • *1: The 16-second value is purely experimental at this pilot stage and is highly likely to change in the future.

                  © Titania Research 2024