Fusion swap resolving: onchain component
The third article in our series explaining Fusion mode is focused on the onchain component of swap resolving.
In the series’ two previous articles we discussed, respectively, the concept of a resolver and the swap resolving process’ offchain component.
Let’s pick up where we left off. We are at a stage in the swap resolution process when the resolver’s backend has “decided” to fill a Fusion order that it received from the 1inch relayer service, in a certain block, with a certain amount of the swapped asset to be returned to the user. Now, we’ll go through the onchain part of the swap resolving process. But first, we’ll describe the participants in this process.
The onchain part of the Fusion swap execution involves quite complex interactions between the following blockchain entities:
A very important notice: in some cases (not always), a resolver makes several fills in one batch, up to 32. Normally, there are multiple token balances in the resolver’s worker contract and the backend can take several orders from the “stream” that the relayer provides, and make a sequence of fills.
We’ll go over the following scenario.
A resolver selected 3 potentially lucrative orders from the relayer to fill:
- 100 DAI for at least 0.6 WETH
- 0.6 WETH for at least 100 DAI
- 0.01 WBTC for at least 36 UNI
The business goal of the resolver is to execute all 3 swaps in a way that the users get at least the amount they expected. We’ll leave out resolvers’ possible earning strategies and simplify the calculations so you can understand the general idea.
Now, the backend provides information about the selected orders to the worker contract. What happens next?
NB: this chart is a continuation of one from the article dedicated to the offchain component of swap resolving. But, for simplicity of understanding, we start from step 1.
The worker contract (or wallet) calls the settleOrders() method of the 1inch settlement contract, delivering the order information in a lightweight compressed format of bytes; calldata and tokensAndAmounts arguments are used to store this information.
Here, you might notice a few interesting details:
- rateBump is coming from the quoter and effectively determines the return. It is the percentage difference between the current auction amount and minimal auction amount. For example, if the rateBump value is 4_000_000 and auctionEndAmount (min. return) is 500, the current auction taking amount is 700.
- totalFee is a fee that all resolvers have to pay to the 1inch Foundation (Important: currently this feature is not used - resolvers don’t pay any fees to the 1inch Foundation).
- limitOrderProtocol is an instance of the protocol that is declared in the settlement contract via a respective Solidity interface. It’s used to call this contract from the settlement contract.
The settlement contract calls the fillOrderTo() method of the limit order contract, delivering the data of one of 3 orders from the list - say, 100 DAI for at least 0.6 WETH:
- Interaction - this contains information that there are 2 more orders in the bulk that will have to be executed afterwards.
- Making or taking amounts - literally how much to pay or how much to get. Only one of these amounts can be non-zero. If you set makingAmount, you specify how much you would like to receive as a resolver. Alternatively, by setting takingAmount, you determine how much you would like to sell as a resolver. One will be calculated based on another.
- Target - the address of the worker contract that will receive the source tokens.
The limit order contract calls the source token contract to transfer the swap amount from the user to the resolver’s worker contract, using the ERC20 Solidity interface.
An assembly part that’s not very human-readable, forms calldata and calls the token contract. The contract’s assembly parts are created for gas efficiency and delivery of complex data.
The limit order contract calls back to the settlement contract with a method called fillOrderInteraction() and sends interactiveData containing information about further orders in the batch (the information previously received in interactions). On the settlement side, the code decodes interactiveData converting it back to interactions.
If the batch contains no more orders (scenario 1 below), it finalizes the interaction and calls the resolver’s worker contract “telling” it to resolve the orders. This will be possible because the worker contract will import our interface called IResolver. In Solidity, cross-contract interactions are usually done in this way. We’ll cover what the worker does in steps 16-17 below.
If any other orders remain in the batch, the settlement contract calls the limit order contract again. The two contracts will continue exchanging data until all the orders have been settled and all the funds have been collected from the users’ accounts (100 DAI, 0.6 WETH and 0.01 WBTC in our example).
We’re almost done. The worker-resolver contract has received all the funds from the users via the 1inch settlement and limit order contracts. Now, it’s time to actually resolve the orders!
Here are the orders’ properties:
- Order 1: 100 DAI for at least 0.6 WETH
- Order 2: 0.6 WETH for at least 100 DAI
- Order 3: 0.01 WBTC for at least 36 UNI
It looks like we can literally match order 1 and order 2 without any additional actions. So, we’ll just safely send 0.6 WETH collected from the user who submitted order 2 to the user who submitted order 1, and vice versa. Thus, orders 2 of 3 have been resolved using the users’ own funds.
The last remaining order is a swap of 0.01 WBTC for 36 UNI. The worker contract doesn’t have any UNI on its balance. So, the worker contract calls the 1inch aggregation router contract the same way any user does it in a legacy swap. The resolver’s backend can call our aggregation API to get an optimal route and pass it to the worker for execution. The router executes this swap and delivers 36 UNI to the resolver.
In this simplified example, the resolver didn’t earn anything but spent funds on gas to transfer data between contracts. In real life, as mentioned above, the resolver’s backend would take all the expenses into account and make sure that trades are profitable for them and stay within the framework provided by the quoter service. The resolver would also try to make the swap maximally profitable for the user.
Now, after the worker-resolver has the destination tokens, they have to respond to the callback and transfer them to the settlement contract. But wait… Why can’t they send the tokens directly to the user?
No, they can’t because of the way the architecture is implemented. Remember, step 5 of this flow is a callback to the fillOrderTo() method called by the settlement contract to the limit order contract. So, the function is still being executed!
Since the caller was the settlement contract, in this configuration, it’s considered to be the taker of the order. Hence, this instance is supposed to receive a response from the resolver and the transferred tokens. Then, it provides approval to the limit order contract, which, in turn, calls transferFrom() to the destination token contract and the swap amount finally lands in the user’s wallet. We’re done!
A real life Etherscan example
As the final exercise, let's review a real life case of a Fusion swap: 1INCH to DAI, resolved by the Seawise resolver, and its ERC20 token transfers. For the best experience, you need to cross-refer the image below with the diagram above, matching the steps.
At step 4, the limit order contract calls transferFrom() on the 1INCH token contract to transfer the source token from the maker address (0x90…9044) to the resolver address.
Steps 5 and 6 are not reflected on this list because they don’t entail any ERC20 token transfers.
At step 7, Seawise as a resolver swaps 1INCH into DAI in the Uniswap pool as liquidity source.
At step 8, Seawise transfers DAI to the 1inch settlement contract.
Steps 9 and 10 are also skipped here because these are internal callback responses between the resolver’s and the 1inch contracts.
Finally, at step 11, the 1inch settlement contract transfers the destination tokens to the maker.
Feel free to explore this transaction on Etherscan: