# Problem Radiant Capital requires a significant number of on-chain swaps for the normal functioning of its rewarding mechanism. Namely, two major operations perform swaps: 1. Zapping: when users zap dLP, they provide a certain asset that needs to be swapped to ETH and RNDT, so they can be deposited into the RDNT/ETH liquidity pool on Uniswap/Balancer/Pancakeswap 2. Compounding: users who zapped dLP are getting a portion of the revenue from the Lending Pool and they can opt-in for compounding of the revenue into additional dLP. Since the revenue is paid out in the form of all assets listed on the lending protocol (e.g. 7 assets on Arbitrum), all these assets need to be swapped into ETH and RDNT in order to deposit them into the liquidity pool. All swaps are currently performed using Uniswap V2 Router which ensures the best execution on Uniswap. The best execution means the swap with minimal slippage, where slippage, in the Radiant Capital context, is defined as the difference between the oracle price and the price achieved by performing an on-chain swap. Without going too much into the theory of slippage, the important premise here is that slippage tends to decrease as the liquidity against which the swap is performed increases. The problem with the current approach is that the local best execution on Uniswap can significantly differ from the global best execution on the specific chain. Even though Uniswap has significant TVL on all chains, there is much more liquidity outside of Uniswap, which, combined with Uniswap’s liquidity, could ensure a much better price. Therefore, the problem that’s being tackled here is the high slippage (in comparison to what could be achieved) when the swaps are performed directly on Uniswap. # High-level solution: DEX aggregators The probelem could be tackled by increasing the liquidity size against which swaps are performed. That can be only done by combining the liquidity of multiple DEXs and that’s where DEX aggregators come into play. Swap that’s executed against a DEX aggregator is executed against the liquidity of multiple liquidity sources thus ensuring much lower slippage in comparison to execution on a single DEX, such as Uniswap. For every swap, the DEX aggregator finds the best route which can include: separating the swap into multiple smaller chunks where each chunk can be executed on a different DEX or using multiple different pools on the same or different DEXs (e.g. swapping ETH -> USDC can be done by ETH -> USDT -> WBTC -> USDC). Additionally, most of them implement MEV protection, whereas swaps performed against a single DEX can be susceptible to sandwiching. *A benchmark can be and should be executed to showcase the improvement in the slippage that will be achieved if swaps on Uniswap are replaced with swaps on DEX aggregators.* However, DEX aggregators come with couple a of downsides: 1. **Finding the best route is executed off-chain** Most DEX aggregators provide an API endpoint that needs to be queried to find the best route. Then, this route is converted into a calldata of a specific function on the smart contract provided by the DEX aggregator which needs to be invoked to perform the swap. This means that only the API endpoint can find the best route and consequently, swaps against DEX aggregators cannot be executed in the isolated on-chain ecosystem. This has multiple consequences: - centralized point of failure: if the API goes down or if the development team behind the API starts serving wrong data on purpose, Radiant Capital can end up with dysfunctional compounding and zapping. - users need to submit the swap data retrieved from the API to the chain: it needs to be ensured that malicious users cannot manipulate the swap data retrieved from the API - integrations with other protocols are impaired: other smart contracts, outside of Radiant Capital, cannot integrate with functions that require the data that can be only achieved via API 2. **Swapping against DEX aggregators consumes more gas** Swaps on Uniswap are usually more gas-efficient than swaps against DEX aggregators. By examining a few random examples, swaps against Uniswap V2 router were on average 10x cheaper than swaps executed against Paraswap (Uni V2 router swaps spent 50k - 200k gas, while Paraswap spent 300k - 2M gas). This should be benchmarked further to get more reliable results, but aggregators are surely more expensive than DEXs. Gas costs are especially amplified on chains where gas is pretty expensive, such as Ethereum Mainnet: the difference between 200k gas on DEX and 2M gas on the aggregator can easily mean a $360 difference (1.8M gas x 100 gwei/gas * $2000/ETH). Luckily, these downsides could be significantly mitigated by the following measures: - Integrate multiple DEX aggregators: frontend dApp could query at least 2 DEX aggregators, find the better price, and then pass swap data to the chain. In that way, the impact of failed or tempered API is minimized - Have Uniswap V2 Router as a fallback mechanism in case netiher one DEX aggregator API works. - Allow omitting swap data through dedicated functions or "nullable" parameters, so that external integrations could work (and to support previous case as well). In case of emitted swap data, the swap would be executed against UniSwap V2 Router. - Verify swap data in the smart contract as much as possible to prevent damage by malicious user's tempering with swap data (e.g. whitelist aggregator contracts). - Compare tx cost estimated by the API endpoint against the swap amount and max slippage on the frontend and if: tx cost - arbitrary assumption on UniV2 Router swap cost > arbitrary % x max slippage, then omit swap data and use fallback UniV2 Router. ## Selecting DEX aggregator There are currently dozens of different DEX aggregators and usually those with the highest volumes are the most battle-tested and reliable. Besides volume, it is worth comparing them against the quality and outcome of the smart contract audits, number of liquidity sources, fees, and gas costs. According to this [Dune dashboard](https://dune.com/sixdegree/dex-aggregators-comparision), the leaders in the space are 1inch, 0x API and Paraswap. They are also present on all 3 chains that Radiant Capital supports. While neither one of them charges **fees** on the swaps, 0x API charges usage of the API. Even though they have a free plan, Radiant Capital dApp could easily exceed the free plan and some payments would be needed. Here are 0x API [pricing plans](https://0x.org/pricing). Regarding **smart contract audit**, all of them are audited and here are the audit reports: - Paraswap: audited by [Peckshield](https://3005040223-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MhY5S0piLthPGntvSF6%2Fuploads%2FcBuaYozTezGr8ip8MnES%2FAugustusRFQ_Audit_2021-06-10.pdf?alt=media&token=1a94416b-69b0-4867-ba35-aee0b0d84aa5), [Solidified](https://3005040223-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MhY5S0piLthPGntvSF6%2Fuploads%2FtFD7gN8Zrdn581HHfek8%2FAudit%20Report%20-%20Paraswap%20%5B22.07.2021%5D.pdf?alt=media&token=08ebd50c-ce93-458c-9313-abfd31de0307) and supposedly CertiK, even though the report couldn't be found - 1inch: audited by a number of top-tier auditors such as OpenZeppelin, ABDK and Consensys. The total list includes [10 auditors](https://github.com/1inch/1inch-audits) - 0x API: Exchange V4 was audited by [Consensys](https://consensys.io/diligence/audits/2020/12/0x-exchange-v4/), while the rest of their products was audited by many other equally reputable [auditors](https://0x.org/docs/developer-resources/audits) Regarding **liquidity sources**, they all brag about a high number (around 100) liquidity sources and I have a strong assumption all of them cover most of the liquidity available on supported chains, but *this assumption should be verified*. Finally, **gas costs** should be benchmarked. Based on the quick review of their documentation, *I would suggest starting with Paraswap first, then expanding to 1inch and maybe, if it proves to be needed, to 0x API.* ## Alternatives Besides DEX aggregators, other solutions should be explored. One of them that should probably be looked at is Uniswap's Universal Router which was released last year. It serves as a DEX aggregator whose liquidity sources are limited to Uniswap V2 and V3 pools, but on the positive side, transactions seem to spend much less gas. It requires an API to find the best route across Uniswap V2 and V3 pools as well. Possibly, its slippage should be compared against the slippage of other DEX aggregators and it should be taken into consideration. ## TODOs - benchmark and compare slippage on DEX aggregators against Uniswap V2 - explore Uniswap's Universal Router - read audit reports of DEX aggregators - analyze liquidity sources - benchmark gas consumption # Implementation guide To implement the described solution, smart contracts and the frontend dApp need to be changed. More specifically, `SwappingLib` needs to be implemented and flows of compounding and zapping need to be changed. ## Data structure and library First, we need to introduce the following struct: ``` struct SwapData { ///@dev srcToken address of the source token address srcToken; ///@dev dstToken address of the destination token address dstToken; ///@dev swapAmount amount that is being swapped uint256 swapAmount; ///@dev value amount of gas token that will be paid uint256 value; ///@dev data bytes that will be passed to the aggregator to perform a swap bytes data; ///@dev addressToCall address to call and send bytes data to perform the aggregator swap address addressToCall; ///@dev addressToApprove address to approve tokens that will be swapped address addressToApprove; } ``` Secondly, we need to implement `SwapLib` library that will replace `UniV2Helper.sol` and include its functions. `SwapLib` should have one main `swap` function that will receive `SwapData` struct as an input parameter and first check its `addressToCall`. If it's zero address, it means that the swap needs to be performed using Uniswap V2 router. If it's non-zero address, then it should be checked if it's whitelisted and if not, revert. `swap` function should first approve spending of `swapAmount` of `srcToken` to `addressToApprove` and then, make the call to `addressToCall` with `data` and `value`. It should also check its balance in `dstToken` before and after the call to deduce the `amountReceived` which should be returned by this function. ## Compounding Swapping is done as a part of the compounding process and it uses `UniswapV2Router` and its function `swapExactTokensForTokens`. This is, more specifically, invoked by two internal functions of `Compounder.sol`: `_claimAndSwapToBase` and `_wethToRdnt`. Both of them are only invoked by the public function `claimCompound`. ### Compounder.sol `_claimAndSwapToBase` function should receive an array of `SwapData` structs instead of two arrays of tokens and amounts (in each `SwapData`, `dstToken` would be set to `WETH`, `srcToken` and `swapAmount` to values from arrays of tokens and amounts that were previously passed) and iterate through each of them in the same way it currently iterates and passes `SwapData` struct to `swap` function from `SwapLib`. `_wethToRdnt` should receive `SwapData` struct instead of `_wethIn` and also invoke new `swap` function. `claimCompound` should have an additional input param of the array of `SwapData` structs that will be accordingly passed to `_claimAndSwapToBase` and `_wethToRdnt` functions. This line of code is not required anymore: `(address[] memory tokens, uint256[] memory amts) = viewPendingRewards(_user);` Consequently, the external function `selfCompound` also needs to accept an array of `SwapData` structs and pass it to `claimCompoud` function. ### BountyManager.sol Besides these changes in `Compounder.sol` contract, a couple of changes are required in `BountyManager.sol` contract: `_getAutoCompoundBounty` function needs to receive an array of `Swapdata` structs and pass it to `claimCompound` function. However, as polymorphism is used and functions `_getChefBounty` and `_getMfdBounty` have the same layout, they also need to receive this array as input param, but they can just ignore it. In their case, this array can be empty. These functions are invoked in `_getAvailableBounty` function that also needs to receive the same array of `SwapData` structs and pass it to these functions as: `bounties[_actionTypeIndex](_user, _execute, _swapData);`. `executeBounty` function is the only function that invokes `_getAvailableBounty`, so it also needs to receive this array as input param and pass it to `_getAvailableBounty` function. If the `_actionType` is different from `_getAutoCompoundBounty`, the array can be empty. Finally, `executeBouty` function is called by two public functions `quote` and `claim`. `quote` function doesn't actually perform any swap, so it doesn't need a change in layout, but it only needs to contruct empty array of structs and pass it to `executeBouty` function. `claim` function, on the other hand, needs to receive the array and pass it to down. ## Zapping Unlike compounding, in the zapping process, `UniswapV2Router` is not called directly, but by encapsulating the swaps in `UniV2Helper` library. These swaps are performed when the asset provided for zapping is different from WETH and this is the flow where DEX aggregator could be used. Besides this, there are functions in `BalancerPoolHelper` and `UniswapPoolHelper` that also perform swaps of WETH to RDNT, but in my opinion, it would make sense to keep them as they are (to be discussed). The major change is made in the internal `_zap` function of `LockZap.sol`. It should receive one `SwapData` struct instead of `_asset` and `assetAmnt` input parameters. Then, wherever the `UniV2Helper._swap` is invoked, `SwapLib.swap` should be invoked with the passed `SwapData` struct. Consequently, all functions that invoke `_zap` need to change the layout in a way to have `SwapData` struct as an input parameter which would replace existing `_asset` and `assetAmnt` input parameters. These functions would then pass the struct to `_zap` function. These functions are public functions `zap`, `zapOnBehalf` and `zapFromVesting`. ## dApp On the frontend side, changes are required whenever any of these functions is invoked for compounding: - `Compounder.claimCompound` - `Compounder.selfCompound` - `BountyManager.executeBounty` - `BountyManager.claim` or zapping: - `LockZap.zap` - `LockZap.zapOnBehalf` - `LockZap.zapFromVesting` Changes in the zapping flow are straightforward: `asset` and `assetAmnt` need to be replaced with the corresponding `SwapData` object. On the other side, changes in the compounding flow are slightly more complex: `Compounder.viewPendingRewards` function should be called to get an array of source tokens and an array of amounts, based on which array of `SwapData` object would be constructed and which is needed for all 4 functions. Finally, the empty array can be passed to all functions (except `Compounder.selfCompound`) if `_execute` param is set to `false` or to `BountyManager` functions if `_actionType` is different from the one used for `_getAutoCompoundBounty`. Frontend should fetch data from 1inch and Paraswap APIs, compare the prices, and determine which one to use. It should also measure gas consumption and determine if using DEX aggregator is worth at all or `SwapData` object should be constructed in a way to use Uniswap V2 router (with `addressToCall` set to zero address). If there are known integrations that might be broken with the change of the function layout or that cannot integrate with APIs, duplicates of functions used in integrations should be created: one implementation that would not have `SwapData` object or array of objects as input parameter and would construct these structs inside to pass to other functions, but form them in a way they end up in the fallback Uniswap V2 Router mechanism. The second implementation would be as described. Finally, the frontend dApp needs to integrate with 1inch and Paraswap APIs in order to construct `SwapData` objects. More specifically, APIs are needed to get `data` and `value` fields as well as gas consumption and destination token amount estimates, while `addressToApprove` and `addressToCall` will be filled by frontend based on the decision whether to use Paraswap, 1inch or Uniswap. ### Paraswap API To get the best route from Paraswap, 2 API calls need to be made to Paraswap API v5 First API call needs to be made to `GET https://apiv5.paraswap.io/prices` as described in their [docs](https://developers.paraswap.network/api/get-rate-for-a-token-pair). The request should include source and destination tokens (with the number of decimals), the swap amount, and the chain id. This API method will return `priceRoute` object. Its `destAmount` field can be used to compare the price against 1inch price and `gasCost` field to compare gas consumption. `tokenTransferProxy` value should be set to `addressToApprove` and `contractAddress` to `addressToCall`. The second call is made to `POST https://apiv5.paraswap.io/transactions/:chainId` as described in their [docs](https://developers.paraswap.network/api/build-parameters-for-transaction). It expects to receive a request payload consisting of `priceRoute` object, source and destination tokens (with their decimals), amount, receiver and the max slippage. The method will respond with the object consisting of `value` and `data`. Here is also the [Swagger page](https://app.swaggerhub.com/apis/paraswapv5/api/1.0#). ### 1inch API In the case of 1inch, one API call is needed to the method `GET https://api.1inch.io/v5.2/{chainId}/swap` with query parameters consisting of source token, destination token, swap amount, max slippage and the address that will initiate the swap. The response will contain `tx` object that will contain `to` (should be assigned to `addressToCall` field), `value` and `data`. It would also contain `gas` allowing us to compare gas consumption. The response object will contain `toAmount` field, allowing to compare the expected price against the Paraswap price. Here is also the [Swagger page](https://docs.1inch.io/docs/aggregation-protocol/api/swagger).