packages/contracts-rfq/contracts/legacy/router/adapters/DefaultAdapter.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {IDefaultPool, IDefaultExtendedPool} from "../interfaces/IDefaultExtendedPool.sol";
import {IRouterAdapter} from "../interfaces/IRouterAdapter.sol";
import {IWETH9} from "../interfaces/IWETH9.sol";
import {MsgValueIncorrect, PoolNotFound, TokenAddressMismatch, TokensIdentical} from "../libs/Errors.sol";
import {Action, DefaultParams} from "../libs/Structs.sol";
import {UniversalTokenLib} from "../libs/UniversalToken.sol";
import {SafeERC20, IERC20} from "@openzeppelin/contracts-4.5.0/token/ERC20/utils/SafeERC20.sol";
contract DefaultAdapter is IRouterAdapter {
using SafeERC20 for IERC20;
using UniversalTokenLib for address;
/// @notice Enable this contract to receive Ether when withdrawing from WETH.
/// @dev Consider implementing rescue functions to withdraw Ether from this contract.
receive() external payable {}
/// @inheritdoc IRouterAdapter
function adapterSwap(
address recipient,
address tokenIn,
uint256 amountIn,
address tokenOut,
bytes memory rawParams
) external payable returns (uint256 amountOut) {
return _adapterSwap(recipient, tokenIn, amountIn, tokenOut, rawParams);
}
/// @dev Internal logic for doing a tokenIn -> tokenOut swap.
/// Note: `tokenIn` is assumed to have already been transferred to this contract.
function _adapterSwap(
address recipient,
address tokenIn,
uint256 amountIn,
address tokenOut,
bytes memory rawParams
) internal virtual returns (uint256 amountOut) {
// We define a few phases for the whole Adapter's swap process.
// (?) means the phase is optional.
// (!) means the phase is mandatory.
// PHASE 0(!): CHECK ALL THE PARAMS
DefaultParams memory params = _checkParams(tokenIn, tokenOut, rawParams);
// PHASE 1(?): WRAP RECEIVED ETH INTO WETH
tokenIn = _wrapReceivedETH(tokenIn, amountIn, tokenOut, params);
// After PHASE 1 this contract has `amountIn` worth of `tokenIn`, tokenIn != ETH_ADDRESS
// PHASE 2(?): PREPARE TO UNWRAP SWAPPED WETH
address tokenSwapTo = _deriveTokenSwapTo(tokenIn, tokenOut, params);
// We need to perform tokenIn -> tokenSwapTo action in PHASE 3.
// if tokenOut == ETH_ADDRESS, we need to unwrap WETH in PHASE 4.
// Recipient will receive `tokenOut` in PHASE 5.
// PHASE 3(?): PERFORM A REQUESTED SWAP
amountOut = _performPoolAction(tokenIn, amountIn, tokenSwapTo, params);
// After PHASE 3 this contract has `amountOut` worth of `tokenSwapTo`, tokenSwapTo != ETH_ADDRESS
// PHASE 4(?): UNWRAP SWAPPED WETH
// Check if the final token is native ETH
if (tokenOut == UniversalTokenLib.ETH_ADDRESS) {
// PHASE 2: WETH address was stored as `tokenSwapTo`
_unwrapETH(tokenSwapTo, amountOut);
}
// PHASE 5(!): TRANSFER SWAPPED TOKENS TO RECIPIENT
// Note: this will transfer native ETH, if tokenOut == ETH_ADDRESS
// Note: this is a no-op if recipient == address(this)
tokenOut.universalTransfer(recipient, amountOut);
}
/// @dev Checks the params and decodes them into a struct.
function _checkParams(
address tokenIn,
address tokenOut,
bytes memory rawParams
) internal pure returns (DefaultParams memory params) {
if (tokenIn == tokenOut) revert TokensIdentical();
// Decode params for swapping via a Default pool
params = abi.decode(rawParams, (DefaultParams));
// Swap pool should exist, if action other than HandleEth was requested
if (params.pool == address(0) && params.action != Action.HandleEth) revert PoolNotFound();
}
/// @dev Wraps native ETH into WETH, if requested.
/// Returns the address of the token this contract ends up with.
function _wrapReceivedETH(
address tokenIn,
uint256 amountIn,
address tokenOut,
DefaultParams memory params
) internal returns (address wrappedTokenIn) {
// tokenIn was already transferred to this contract, check if we start from native ETH
if (tokenIn == UniversalTokenLib.ETH_ADDRESS) {
// Determine WETH address: this is either tokenOut (if no swap is needed),
// or a pool token with index `tokenIndexFrom` (if swap is needed).
wrappedTokenIn = _deriveWethAddress({token: tokenOut, params: params, isTokenFromWeth: true});
// Wrap ETH into WETH and leave it in this contract
_wrapETH(wrappedTokenIn, amountIn);
} else {
wrappedTokenIn = tokenIn;
// For ERC20 tokens msg.value should be zero
if (msg.value != 0) revert MsgValueIncorrect();
}
}
/// @dev Derives the address of token to be received after an action defined in `params`.
function _deriveTokenSwapTo(
address tokenIn,
address tokenOut,
DefaultParams memory params
) internal view returns (address tokenSwapTo) {
// Check if swap to native ETH was requested
if (tokenOut == UniversalTokenLib.ETH_ADDRESS) {
// Determine WETH address: this is either tokenIn (if no swap is needed),
// or a pool token with index `tokenIndexTo` (if swap is needed).
tokenSwapTo = _deriveWethAddress({token: tokenIn, params: params, isTokenFromWeth: false});
} else {
tokenSwapTo = tokenOut;
}
}
/// @dev Performs an action defined in `params` and returns the amount of `tokenSwapTo` received.
function _performPoolAction(
address tokenIn,
uint256 amountIn,
address tokenSwapTo,
DefaultParams memory params
) internal returns (uint256 amountOut) {
// Determine if we need to perform a swap
if (params.action == Action.HandleEth) {
// If no swap is required, amountOut doesn't change
amountOut = amountIn;
} else {
// Record balance before the swap
amountOut = IERC20(tokenSwapTo).balanceOf(address(this));
// Approve the pool for spending exactly `amountIn` of `tokenIn`
IERC20(tokenIn).safeIncreaseAllowance(params.pool, amountIn);
if (params.action == Action.Swap) {
_swap(params.pool, params, amountIn, tokenSwapTo);
} else if (params.action == Action.AddLiquidity) {
_addLiquidity(params.pool, params, amountIn, tokenSwapTo);
} else {
// The only remaining action is RemoveLiquidity
_removeLiquidity(params.pool, params, amountIn, tokenSwapTo);
}
// Use the difference between the balance after the swap and the recorded balance as `amountOut`
amountOut = IERC20(tokenSwapTo).balanceOf(address(this)) - amountOut;
}
}
// ═══════════════════════════════════════ INTERNAL LOGIC: SWAP ACTIONS ════════════════════════════════════════════
/// @dev Performs a swap through the given pool.
/// Note: The pool should be already approved for spending `tokenIn`.
function _swap(
address pool,
DefaultParams memory params,
uint256 amountIn,
address tokenOut
) internal {
// tokenOut should match the "swap to" token
if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert TokenAddressMismatch();
// amountOut and deadline are not checked in RouterAdapter
IDefaultPool(pool).swap({
tokenIndexFrom: params.tokenIndexFrom,
tokenIndexTo: params.tokenIndexTo,
dx: amountIn,
minDy: 0,
deadline: type(uint256).max
});
}
/// @dev Adds liquidity in a form of a single token to the given pool.
/// Note: The pool should be already approved for spending `tokenIn`.
function _addLiquidity(
address pool,
DefaultParams memory params,
uint256 amountIn,
address tokenOut
) internal {
uint256 numTokens = _getPoolNumTokens(pool);
address lpToken = _getPoolLPToken(pool);
// tokenOut should match the LP token
if (lpToken != tokenOut) revert TokenAddressMismatch();
uint256[] memory amounts = new uint256[](numTokens);
amounts[params.tokenIndexFrom] = amountIn;
// amountOut and deadline are not checked in RouterAdapter
IDefaultExtendedPool(pool).addLiquidity({amounts: amounts, minToMint: 0, deadline: type(uint256).max});
}
/// @dev Removes liquidity in a form of a single token from the given pool.
/// Note: The pool should be already approved for spending `tokenIn`.
function _removeLiquidity(
address pool,
DefaultParams memory params,
uint256 amountIn,
address tokenOut
) internal {
// tokenOut should match the "swap to" token
if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert TokenAddressMismatch();
// amountOut and deadline are not checked in RouterAdapter
IDefaultExtendedPool(pool).removeLiquidityOneToken({
tokenAmount: amountIn,
tokenIndex: params.tokenIndexTo,
minAmount: 0,
deadline: type(uint256).max
});
}
// ═════════════════════════════════════════ INTERNAL LOGIC: POOL LENS ═════════════════════════════════════════════
/// @dev Returns the LP token address of the given pool.
function _getPoolLPToken(address pool) internal view returns (address lpToken) {
(, , , , , , lpToken) = IDefaultExtendedPool(pool).swapStorage();
}
/// @dev Returns the number of tokens in the given pool.
function _getPoolNumTokens(address pool) internal view returns (uint256 numTokens) {
// Iterate over all tokens in the pool until the end is reached
for (uint8 index = 0; ; ++index) {
try IDefaultPool(pool).getToken(index) returns (address) {} catch {
// End of pool reached
numTokens = index;
break;
}
}
}
/// @dev Returns the tokens in the given pool.
function _getPoolTokens(address pool) internal view returns (address[] memory tokens) {
uint256 numTokens = _getPoolNumTokens(pool);
tokens = new address[](numTokens);
for (uint8 i = 0; i < numTokens; ++i) {
// This will not revert because we already know the number of tokens in the pool
tokens[i] = IDefaultPool(pool).getToken(i);
}
}
/// @dev Returns the quote for a swap through the given pool.
/// Note: will return 0 on invalid swaps.
function _getPoolSwapQuote(
address pool,
uint8 tokenIndexFrom,
uint8 tokenIndexTo,
uint256 amountIn
) internal view returns (uint256 amountOut) {
try IDefaultPool(pool).calculateSwap(tokenIndexFrom, tokenIndexTo, amountIn) returns (uint256 dy) {
amountOut = dy;
} catch {
// Return 0 instead of reverting
amountOut = 0;
}
}
// ════════════════════════════════════════ INTERNAL LOGIC: ETH <> WETH ════════════════════════════════════════════
/// @dev Wraps ETH into WETH.
function _wrapETH(address weth, uint256 amount) internal {
if (amount != msg.value) revert MsgValueIncorrect();
// Deposit in order to have WETH in this contract
IWETH9(weth).deposit{value: amount}();
}
/// @dev Unwraps WETH into ETH.
function _unwrapETH(address weth, uint256 amount) internal {
// Withdraw ETH to this contract
IWETH9(weth).withdraw(amount);
}
/// @dev Derives WETH address from swap parameters.
function _deriveWethAddress(
address token,
DefaultParams memory params,
bool isTokenFromWeth
) internal view returns (address weth) {
if (params.action == Action.HandleEth) {
// If we only need to wrap/unwrap ETH, WETH address should be specified as the other token
weth = token;
} else {
// Otherwise, we need to get WETH address from the liquidity pool
weth = address(
IDefaultPool(params.pool).getToken(isTokenFromWeth ? params.tokenIndexFrom : params.tokenIndexTo)
);
}
}
}