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-labs • Updated 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:
- A user (Swapper) signs and submits an Intent through the Uniswap Web app frontend.
- The Filler uses the UniswapX API to retrieve open Intents (retrieve ongoing auctions).
- The Filler decodes the Intent and determines whether it should be filled (determines whether to participate in the auction).
- At the appropriate time, the Filler calls the
execute
function of the UniswapX contract (bids in the auction).
- 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.
- (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:
- Identification: Identifying the Intent (steps 2 and 3 in the above diagram)
- 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:- Fetching open intents from the UniswapX API.
- 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 at0x1bd1aAdc9E230626C44a139d7E70d842749351eb
.
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 asendAmount
).
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 at0x4449Cd34d1eb1FEDCF02A1Be3834FfDe8E6A6180
.
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 at0x0000000000000000000000000000000000000000
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
isopen
andtype
isOrderType.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.
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.
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:
- Clone the Repository: Use
git clone
to download the repository.
- Install Dependencies:
- Create an Environment Variable File:
- 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:
- 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:
Points for Improvement in Implementation
There are two main points that can be improved:
- 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.
- 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:
- Identifying Intent:
IdentificationService
- Use the UniswapX API to retrieve open Intents.
- Decode the Intent and determine if it should be filled.
- 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.