# Universal Intent Centric Router ## Introduction and Motivations Currently we have a couple different implementations of asset routers. They have been working very well for the initial use cases in early days, which are mostly single-purpose actions. However, the complexity has drastically increased since we are starting to have more complex use cases, which are usually sequences of many steps. For each of the new use case, we will need to hardcode all the logics from groud up, despite the fact that they might share some similar steps. We also need to deploy a new factory contract for each of them, and add a new relayer endpoint that is tightly coupled with this specific sequence of logics. ### universal intent centric router In the future, we can upgrade all the routers to **one single router**, which embraces modulized actions and has infinite composability. This will greatly increase the asset router extensibilty, that it can perform any composite actions by assembling low level modulized actions. It will also largely reduce the development effort, since everything will be very DRY, and we only need to add the missing pieces, if any. ![intent-centric-router](https://hackmd.io/_uploads/SkD2oAdKA.png) ## Potential Use Cases some examples of general purpose intent centric routing: - stake from any token to any pool on euphrates - swap USDC to DOT => stake to euphrates - arbitrary euphrates position switch - unstake LDOT from euphrates => swap to jitoSOL => stake to euphrates - cross chain any token to any token swap - xcm DOT from polkadot to Acala (router address) => swap dot to USDC => xcm USDC to hydra - anything we can imaging! ## Architecture One of the possible implementations can be somethinig like this: one factory contract with modulized logic implementations. ![architecture](https://hackmd.io/_uploads/ryOeo0dFC.png) For each of the new composite router, we only need to deploy the missing logic, if any. Then we can call the general `deployRouter` method with the `steps` as the argument, where each of the step consist of the logic to execute, as well as the params for this logic. In this arthictecture, we don't need to deploy a new factory contract for each type of routers, and in fact, all possible routers can be constrcuted by `steps` param at runtime. ### long term benefits After we have implemented more and more low level logics, the marginal effort to add a new router will gardually decrease to **ZERO**. <div style="display: flex;"> <img src="https://hackmd.io/_uploads/HywiiAdKC.png" alt="output" style="width: 45%; margin-right: 10px;"> <img src="https://hackmd.io/_uploads/H1qoi0_F0.png" alt="output (1)" style="width: 45%;"> </div> If we have all the logic pieces ready, the actual composite router, should be ready for execute already! So basically each time we add a new logic, we implicitly implement ALL possible routers derived from `{ new-logic } X { all-existing-logics }`. ### some features - The entrypoint: `routerFactory` contract is immutable, it acts like a scheduler that distribute params to logics - If any params in steps change, the router address will change. Same params will produce same router address. - There is no built-in fee charge for router anymore. This piece of funtionalities is decoupled from router instance, and now it is just another logic module called `FeeLogic`, which becomes more flexible. ## POC I have built a very basic POC of the intent-centric router, which can be found [here](https://github.com/shunjizhan/ic-router/tree/main). ### Logic and Router Factory The key interfaces are the Logic and the Router. Each of the logic instance will implement a unified `execute` method, and should be a small piece of atomic logic. ```solidity abstract contract IRouterLogic { string public identifier; constructor(string memory _identifier) { identifier = _identifier; } function execute(bytes memory params) external virtual; } ``` The router instance will take all the steps (logic + params) as constrcutor, then it will execute each of the logics in sequence. ```solidity contract Router { struct RouterStep { address logic; bytes params; } RouterStep[] public steps; address public immutable collector; constructor( RouterStep[] memory _steps, address _collector ) { for (uint i = 0; i < _steps.length; i++) { steps.push(_steps[i]); } collector = _collector; } function execute() external { for (uint i = 0; i < steps.length; i++) { /* --------------- use delegatecall to execute logics in router's context since all tokens will exist in router, but not logic --------------- */ (bool success, bytes memory result) = steps[i].logic.delegatecall( abi.encodeWithSelector(IRouterLogic.execute.selector, steps[i].params) ); require(success, "Router: delegated execution failed"); // TODO: better err msg } selfdestruct(payable(collector)); } // reap tokens to collector function rescue(address[] calldata tokens) external { ... } } ``` The router factory is very similar to our exisiting `xxxFactory`, but it's a more generalized one that can produce all possible routers. It will compute the router address based on all the steps, deploy the router to that address, and execute it. ```solidity! struct RouterTopUp { IERC20 token; uint256 amount; } contract RouterFactory { function deployRouter( Router.RouterStep[] memory steps, address recipient ) public returns (Router) { bytes32 salt = keccak256(abi.encode(steps, recipient)); Router router = new Router{salt: salt}(steps, recipient); return router; } function topUpRouter( RouterTopUp[] memory topUps, Router router ) public { for (uint i = 0; i < topUps.length; i++) { require( topUps[i].token.transferFrom(msg.sender, address(router), topUps[i].amount), "RouterFactory: Token transfer to router failed" ); } } function deployRouterAndExecute( RouterTopUp[] memory topUps, Router.RouterStep[] memory steps, address recipient ) public { Router router = deployRouter(steps, recipient); topUpRouter(topUps, router); router.execute(); } } ``` Note that the whole routing process is abstracted to two phases: 1) `topup` (prepare): transfer token from relayer or user to the factory 2) `route` (execute): execute a sequence of actions topup is required for actions like token drop for current use cases. And in the future, it can serve as a more generalized entry point for intent centric routing, such as for a user, swap usdc to dot and xcm to hydra (in this case user will interact with the factory directly, sign 1 tx and execute a sequence of actions). ### Interacting with the Routers We can replicate any of the existing asset router contract by wrapping `factory` and call it with pre-defined params (sort of like HOC in React). But as we mentioned above, **we don't necessarily need an actual factory for each of the business logic**. So I think a better and cleaner way is to build the steps, and interact with the general factory directly. By doing so, we implicitly construct the specific router, and can bypass all hardcoded router factories. #### Example 1: manually build the raw steps ```ts const routeHoma = async (recipient: string) => { const routeHomaSteps = [ // step 0: charge fee { logic: FEE_LOGIC_ADDR, params: abiCoder.encode( ['address', 'address'], // should be able to build a `FeeStruct` => ['address', 'address'] via typegen [DOT_ADDR, FEE_REGISTRY_ADDR] ) }, // step 1: liquid staking { logic: HOMA_LOGIC_ADDR, params: abiCoder.encode( ['address', 'address'], [DOT_ADDR, LDOT_ADDR] ) }, // step 2: trasnfer ldot to user { logic: TRANSFER_LOGIC_ADDR, params: abiCoder.encode( ['address', 'address'], [LDOT_ADDR, recipient] ) } ]; const topUps = []; await factory.deployRouterAndExecute(topUps, routeHomaSteps, recipient); } ``` #### Example 2: build steps with sdk The above example is just for showing what's going on under the hood, and in practice we can simplify the step construction by introducing a helper or sdk ```ts const sdk = new AssetRouterSdk(...); const routeHoma = async (recipient: string) => { const routeHomaSteps = [ // step 0: charge fee sdk.step.fee({ feeToken: DOT_ADDR, amount: sdk.defaultFee[DOT_ADDR], }), // step 1: liquid staking sdk.step.homa({ stakingToken: DOT_ADDR, }), // step 2: trasnfer ldot to user sdk.step.transfer({ token: LDOT_ADDR recipient, }), ]; const topUps = []; await factory.deployRouterAndExecute(topUps, routeHomaSteps, recipient); } ``` #### Example 3: route with sdk directly What's more, we can pre-define all the steps for our essential use cases in the SDK, so the interaction will be a one-liner in production ```ts const sdk = new AssetRouterSdk(...); const topUps = []; const routeHoma = async (recipient: string) => { const homaSteps = sdk.getHomaSteps(recipient); await factory.deployRouterAndExecute(topUps, homaSteps, recipient); } const routeEuphrates = async (poolId: number, recipient: string) => { const euphratesSteps = sdk.getEuphratesSteps(poolId, recipient); await factory.deployRouterAndExecute(topUps, euphratesSteps, recipient); } ``` #### Example 4: infinite composability let's say we need a swap and stake router, which swap a token to another, and stake to euphrates. We can easily compose it by the following: ```ts! // or comopose any general purpose interaction const swapAndStake = async ({ tokenIn: string, tokenOut: string, amountIn: BigNumberish, minAmountOut: BigNumberish, path: string[], poolId: number, recipient: string, }) => { // can do some params validation first assert(poolId < 7, 'invalid euphrates poolId') assert(EUPHRATES_TOKENS.includes(tokenOut), 'token not supported by euphrates') const swapAndStakeSteps = [ // step 0: charge fee sdk.step.fee({ feeToken: tokenIn, amount: sdk.defaultFee[tokenIn], }), // step 1: swap sdk.step.swap({ tokenIn, tokenOut, amountIn, minAmountOut, path, }), // step 2: stake to euphrates sdk.step.euphrates({ token: tokenOut, poolId, recipient, }), // step 3: sweep any tokens left sdk.step.sweep({ tokens: [tokenIn], recipient, }), // step 4: drop ACA to user sdk.step.tokenDrop({ token: ACA_ADDR, amount: 5, recipient, }), ]; // top up ACA to factory before dropping to user const topUps = [{ token: ACA_ADDR, amount: 5, }]; await factory.deployRouterAndExecute(topUps, swapAndStakeSteps, recipient); } ``` ### Advantages We mentioned the effectiveness of this architecture in [long term benefits](#long-term-benefits), and the examples should make things more clear. Suppose we already have all the modular logic ready, we can "add" a new router by simply implement `sdk.getXXXSteps(): Step[]`, and that' all -- a 10-minute work load. If there is any missing functionalities for a new router, we only need to deploy that small logic piece, which should be quick and unit-testable. We will never need to copy & paste codes from old routers to new routers, and we can reduce the 20-branch e2e testing for the whole composite router, to a couple small unit tests for the new logic piece. ### Relayer Relayer can keep the current design that has an endpoint for each of the business logic, or use a general endpoint `/shouldRoute` and `/route`. (TBD) For general endpoint, relayer can decide if it wants to relay this request by examining the params, and see if there is fee charge step, and is the token amount enough, etc. But it shouldn't be too hard to add a general validation method `shouldRelay(steps: Step[])`. For specific endpoint, the relayer itself will assemble the params internally, so it can make sure there is a reasonable fee charge step in it, and make sure all steps are intended. ## Into the Tuture... We have seen design for Intent Centric Router 1.0, and there might be something interesting for IC Router 2.0. ### More Advanced Router Feature - **Branching**: if swap failed, transfer token back to recipient. Or more advanced `if .. else ...` execution - **Intent Centric Proxy**: we can extend the router factory to be have some sort of whitelist features, so it can act as a proxy for users. Now each user can deploy a proxy for himself, and approve infinite (or enough) token for it. Now this router can serve as a general purpose proxy that does any sequence of tx for the user, which can achive 1-click intent centric actions. ### Intent Centric Dashboard UI After all the infra is up and running, we should already be able to build an intent centric UI that let users select: - an initial token, such as ACA or USDC - an end goal, such as staking on euprhates pool 3, or get DOT on hydra then the UI will construct the params for the user, and the router will complete everything ### New Narrative We can even upgrade our narrative to intent-centric LSD layer, intent-centric xxx, whatever. `intent-centric` is like a shinning badge, that can be added to anything. ## Questions - How to record router address? Since now we require very precise params for each of the router address, we want to make sure we can reproduce the router address after the token has arrived at it. - How to better read and understand the transactions data? Maybe add more logs and let the indexer interpret it. All mordern and complex txs are hard to understand by inspecting the tx anyways, such as aggregators, v3 liquidities ... so this won't be a big deal