Introducing AggregationRouter

Introducing AggregationRouter

Project
DEX
Lending Protocol
Liquid Staking Protocol
4m

The AggregationRouter is a swap router that facilitates seamless trades across multiple DeFi protocols within a single transaction. By aggregating liquidity from decentralized exchanges (DEXs), lending protocols, and liquid staking protocols, it enables efficient execution of trades. This allows users to optimize their trading strategies and access the best liquidity available across various platforms.

Inspired by the architecture of Synthetix: CoreRouter, AggregationRouter aims to improve the efficiency and accessibility of DeFi transactions. Whether you’re executing trades or leveraging liquidity from multiple sources, the AggregationRouter provides a unified and streamlined approach to managing DeFi interactions.

Technical Overview

The AggregationRouter is a decentralized aggregation protocol that allows the execution of swaps across multiple DeFi protocols in a single transaction. It interfaces with a variety of decentralized exchanges (DEXs), lending protocols, and liquidity staking protocols to facilitate efficient and cost-effective trades.

Encoding Calls and Path Structure

The AggregationRouter performs swaps by delegating calls to various adapters, each corresponding to different DeFi protocols. These calls are encoded into a bytes32 path that contains all the necessary information for the swap.

Path Encoding

The path is a compact encoding of the following parameters:

Path is the encoding of 5 parameters and optional flags (up to 8).
nametypedescription
pooladdressThe address of the liquidity pool
iuint8The index of the currency to swap from
juint8The index of the currency to swap for
wrapInuint8Command for wrapping the input currency
wrapOutuint8Command for wrapping the output currency

Wrapping Commands

The wrapIn and wrapOut parameters are crucial for handling native and wrapped native assets. These commands ensure that tokens are appropriately wrapped or unwrapped during the swap process to maintain compatibility with the liquidity pools involved.

valuedescription
0No action
1Wrap native currency
2Unwrap wrapped native currency

The adapters and wrappers utilize this encoded path to execute their respective swaps, with different adapters requiring different paths based on their unique implementations.

Contract Overview

AggregationRouter

The core of the AggregationRouter contract is a function called aggregate. This function allows for the aggregation of multiple swap actions across various protocols in a single transaction.

IAggregationRouter.sol
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4interface IAggregationRouter {
5	function aggregate(
6		bytes[] calldata calls,
7		address recipient,
8		Currency currencyIn,
9		Currency currencyOut,
10		uint256 amountIn,
11		uint256 amountOutMin
12	) external payable returns (uint256 amountOut);
13}
14

The aggregate function delegates the encoded calls to different adapters based on their function selectors. Each swap is executed by calling the corresponding adapter.

AggregationRouter.sol
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4contract AggregationRouter {
5
6	fallback() external payable {
7		bytes4 selector = msg.sig;
8		address adapter;
9
10		assembly ("memory-safe") {
11			function map(sig) -> imp {
12				if lt(sig, 0x355672eb) {
13					if lt(sig, 0x1f99f749) {
14						switch sig
15						case 0x00befae3 {
16							imp := ATOKEN_WRAPPER // wrapAToken(bytes32)
17						}
18						case 0x04addc5c {
19							imp := FRXETH_WRAPPER // unwrapSFRXETH(bytes32)
20						}
21						case 0x07aa7eb5 {
22							imp := UNISWAP_V2_ADAPTER // uniswapV2Swap(bytes32)
23						}
24						case 0x07af82a0 {
25							imp := PANCAKE_V2_ADAPTER // pancakeV2Swap(bytes32)
26						}
27						case 0x0c3896b8 {
28							imp := STETH_WRAPPER // unwrapWSTETH(bytes32)
29						}
30						leave
31					}
32					switch sig
33					case 0x1f99f749 {
34						imp := FRAXSWAP_V2_ADAPTER // fraxV2Swap(bytes32)
35					}
36					case 0x23a69e75 {
37						imp := PANCAKE_V3_ADAPTER // pancakeV3SwapCallback(int256,int256,bytes)
38					}
39					case 0x2707a06c {
40						imp := STETH_WRAPPER // wrapWSTETH(bytes32)
41					}
42					case 0x2d66d91f {
43						imp := FRXETH_WRAPPER // wrapSFRXETH(bytes32)
44					}
45					case 0x3395962c {
46						imp := SUSHI_V3_ADAPTER // sushiV3Swap(bytes32)
47					}
48					leave
49				}
50
51				if lt(sig, 0xf1553d53) {
52					if lt(sig, 0x85abd8bb) {
53						switch sig
54						case 0x355672eb {
55							imp := FRXETH_WRAPPER // wrapFRXETH(bytes32)
56						}
57						case 0x3d36c8c0 {
58							imp := SUSHI_V2_ADAPTER // sushiV2Swap(bytes32)
59						}
60						case 0x617c7f56 {
61							imp := CTOKEN_WRAPPER // unwrapCToken(bytes32)
62						}
63						case 0x648d8619 {
64							imp := ATOKEN_WRAPPER // unwrapAToken(bytes32)
65						}
66						case 0x75c033fa {
67							imp := UNISWAP_V3_ADAPTER // uniswapV3Swap(bytes32)
68						}
69						leave
70					}
71
72					switch sig
73					case 0x85abd8bb {
74						imp := DODO_V1_ADAPTER // dodoV1Swap(bytes32)
75					}
76					case 0x89d0b46b {
77						imp := STETH_WRAPPER // wrapSTETH(bytes32)
78					}
79					case 0x9d6a221b {
80						imp := BALANCER_V2_ADAPTER // balancerV2Swap(bytes32)
81					}
82					case 0xbc6b4e64 {
83						imp := CTOKEN_WRAPPER // wrapCToken(bytes32)
84					}
85					case 0xe9fd0ca0 {
86						imp := DODO_V2_ADAPTER // dodoV2Swap(bytes32)
87					}
88					leave
89				}
90
91				switch sig
92				case 0xf1553d53 {
93					imp := CURVE_ADAPTER // curveSwap(bytes32)
94				}
95				case 0xf78144f4 {
96					imp := PANCAKE_V3_ADAPTER // pancakeV3Swap(bytes32)
97				}
98				case 0xfa461e33 {
99					imp := UNISWAP_V3_ADAPTER // uniswapV3SwapCallback(int256,int256,bytes)
100				}
101				leave
102			}
103
104			adapter := map(shr(224, selector))
105		}
106
107		if (adapter == address(0)) revert InvalidSelector(selector);
108
109		assembly ("memory-safe") {
110			let ptr := mload(0x40)
111
112			calldatacopy(ptr, 0x00, calldatasize())
113
114			let success := delegatecall(gas(), adapter, ptr, calldatasize(), 0x00, 0x00)
115
116			returndatacopy(ptr, 0x00, returndatasize())
117
118			switch success
119			case 0x00 {
120				revert(ptr, returndatasize())
121			}
122			default {
123				return(ptr, returndatasize())
124			}
125		}
126	}
127}

Fallback Function and Adapter Mapping

The fallback function in the AggregationRouter contract handles incoming calls and dispatches them to the correct adapter based on the function selector. The function utilizes assembly to perform this task efficiently and securely.

By dynamically mapping the function selectors to the corresponding adapters, the fallback function ensures that the AggregationRouter can handle a wide variety of protocol interactions in a single, unified system.

Adapters & Wrappers

To support a wide range of protocols, the AggregationRouter utilizes adapters and wrappers.

IAdapter.sol
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4interface IAdapter {
5	function query(
6		Currency currencyIn,
7		Currency currencyOut,
8		uint256 amountIn
9	) external view returns (bytes32 path, uint256 amountOut);
10
11	function quote(bytes32 path, uint256 amountIn) external view returns (uint256 amountOut);
12}
13
  • Adapters are responsible for interacting with the liquidity pools of different DEX protocols, like Uniswap, Curve, and Balancer. Each adapter defines the unique logic needed to perform a swap on that protocol.
IWrapper.sol
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4interface IWrapper {
5	function query(
6		Currency wrapped,
7		Currency underlying,
8		uint256 amountIn,
9		bool direction
10	) external view returns (bytes32 path, uint256 amountOut);
11
12	function quote(bytes32 path, uint256 amountIn) external view returns (uint256 amountOut);
13}
  • Wrappers manage the wrapping and unwrapping of assets, enabling the AggregationRouter to handle the conversion of tokens to wrapped tokens such as aTokens, cTokens, frxETH, and stETH, vice versa.

  • query: returns the encoded path and expected amount of currency-out to be received in exchange of currency-in.
  • quote: returns expected amount of currency-out to be received in exchange of currency-in.

By using these adapters and wrappers, the AggregationRouter ensures that it can aggregate liquidity from a wide variety of DeFi protocols, whether they are DEXs, lending platforms, or liquid staking providers.

Integrated Protocols

The AggregationRouter can interface with an extensive list of DeFi protocols, providing access to a wide range of liquidity sources. Here’s a list of supported protocols:

This broad support enables users to access the best pricing and liquidity across different platforms, optimizing trades in a way that would be difficult to achieve manually. The ability to aggregate liquidity from multiple sources ensures that users can always access the most efficient trades, regardless of which protocol offers the best pricing at any given time.

Deployment Notes

The constant addresses for adapters and wrappers used in the AggregationRouter are defined for testing purposes using the CREATE3 pattern. These adapters and wrappers must be deployed, and the corresponding addresses must be updated in the AggregationRouter contract before production deployment.

You can explore the full source code and integration tests here.


Explore more posts