// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IMetaAggregatorSwapContract {
// Struct to hold parameters for swap functions
struct SwapETHParams {
address tokenIn; // Address of the input token (must be the native token for swapETH)
IERC20 tokenOut; // ERC20 token to swap to
address aggregator; // Address of the aggregator to use for the swap
address sender; // Address of the sender initiating the swap
address receiver; // Address to receive the tokenOut
address feeRecipient; // Address to receive the fee
uint256 amountIn; // Amount of tokenIn to swap
uint256 minAmountOut; // Minimum amount of tokenOut expected
uint256 feeBps; // Fee basis points sent from amountIn
bytes swapData; // data required for the swap call
bool isDelegate; // Indicates if the swap is being executed by a delegate
}
struct SwapERC20Params {
IERC20 tokenIn; // The ERC20 token being swapped from
IERC20 tokenOut; // The ERC20 token being swapped to
address aggregator; // The address of the aggregator to facilitate the swap
address sender; // The address of the sender initiating the swap
address receiver; // The address that will receive the tokenOut
address feeRecipient; // The address that will receive the fee from the swap
uint256 amountIn; // The amount of tokenIn to swap
uint256 minAmountOut; // The minimum amount of tokenOut expected from the swap
uint256 feeBps; // The fee in basis points (1/100th of a percent) taken from amountIn
bytes swapData; // data required for the swap
bool isDelegate; // Indicates if the swap is being executed by a delegate
}
function swapERC20(SwapERC20Params calldata params) external;
function swapETH(SwapETHParams calldata params) external payable;
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IMetaAggregatorSwapContract} from "./interfaces/IMetaAggregatorSwapContract.sol";
import {TransferHelper} from "./libraries/TransferHelper.sol";
/**
* @title MetaAggregatorSwapContract
* @dev Facilitates swapping between ETH and ERC20 tokens or between two ERC20 tokens using an aggregator.
*/
contract MetaAggregatorSwapContract is IMetaAggregatorSwapContract {
address constant nativeToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; // Represent native ETH token
address immutable usdt; // Address of USDT token
address immutable SWAP_TARGET; // Address of the swap target for delegatecall operations
address immutable _this; // Address of this contract instance
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
// Custom error messages for efficient error handling
error CannotSwapTokens();
error AmountInMustBeGreaterThanZero();
error MinAmountOutMustBeGreaterThanZero();
error TokenInAndTokenOutCannotBeSame();
error IncorrectEtherAmountSent();
error CannotSwapETHToETH();
error InvalidReceiver();
error InvalidENSOAddress();
error InvalidUSDTAddress();
error InsufficientOutputBalance();
error InsufficientETHOutAmount();
error InsufficientTokenOutAmount();
error SwapFailed();
error CannotSwapETH();
error FeeTransferFailed();
// Event emitted when Tokens are swapped
event TokenSwapped(
address indexed sender,
address indexed tokenIn,
address receiver,
address tokenOut,
address feeReceiver,
address aggregator,
uint256 indexed amountIn,
uint256 amountOut,
uint256 minAmountOut,
uint256 fee,
bool isDelegate
);
/**
* @dev Initializes the contract with the swap target and USDT addresses.
* @param _ensoSwapContract The address of the swap target contract.
* @param _usdt The address of the USDT token.
*/
constructor(address _ensoSwapContract, address _usdt) {
if (_ensoSwapContract == address(0)) revert InvalidENSOAddress();
if (_usdt == address(0)) revert InvalidUSDTAddress();
SWAP_TARGET = _ensoSwapContract;
usdt = _usdt;
_this = address(this);
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
/**
* @dev only checks for re-entrancy when the call is not delegate.
*/
function _nonReentrantBefore() private {
if (address(this) == _this) {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
}
/**
* @dev only checks for re-entrancy when the call is not delegate.
*/
function _nonReentrantAfter() private {
if (address(this) == _this) {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}
/**
* @dev Swaps ETH for an ERC20 token.
* @param params SwapETHParams
*/
function swapETH(
SwapETHParams calldata params
) external payable nonReentrant {
if (address(params.tokenIn) != nativeToken) {
revert CannotSwapTokens();
}
(uint256 amountOut, uint256 fee) = _swapETH(params);
emit TokenSwapped(
address(params.sender),
address(params.tokenIn),
address(params.receiver),
address(params.tokenOut),
params.feeRecipient,
params.aggregator,
params.amountIn,
amountOut,
params.minAmountOut,
fee,
params.isDelegate
);
}
/**
* @dev Swaps one ERC20 token for another ERC20 token or native ETH.
* @param params SwapERC20Params
*/
function swapERC20(SwapERC20Params calldata params) external nonReentrant {
(uint256 amountOut, uint256 fee) = _swapERC20(params);
emit TokenSwapped(
address(params.sender),
address(params.tokenIn),
address(params.receiver),
address(params.tokenOut),
params.feeRecipient,
params.aggregator,
params.amountIn,
amountOut,
params.minAmountOut,
fee,
params.isDelegate
);
}
/**
* @dev Internal function to perform the swap from ETH to ERC20.
* @param params SwapETHParams
*/
function _swapETH(
SwapETHParams memory params
) internal returns (uint256, uint256) {
IERC20 tokenOut = params.tokenOut;
uint256 amountIn = params.amountIn;
uint256 minAmountOut = params.minAmountOut;
address receiver = params.receiver;
address feeRecipient = params.feeRecipient;
uint256 feeBps = params.feeBps;
_validateInputs(
params.tokenIn,
address(tokenOut),
amountIn,
minAmountOut,
receiver
);
if (msg.value < amountIn) revert IncorrectEtherAmountSent();
uint256 fee;
if (feeRecipient != address(0) || feeBps != 0) {
fee = (amountIn * feeBps) / 10000;
amountIn -= fee;
(bool success, ) = payable(feeRecipient).call{value: fee}("");
if (!success) revert FeeTransferFailed();
}
uint256 balanceBefore = tokenOut.balanceOf(address(this));
_executeAggregatorCall(
params.swapData,
params.isDelegate,
params.aggregator,
amountIn
);
uint256 amountOut = tokenOut.balanceOf(address(this)) - balanceBefore;
if (amountOut < minAmountOut) revert InsufficientOutputBalance();
if (receiver != address(this)) {
TransferHelper.safeTransfer(address(tokenOut), receiver, amountOut);
}
return (amountOut, fee);
}
/**
* @dev Internal function to swap ERC20 tokens or ERC20 to native ETH.
* @param params SwapERC20Params
*/
function _swapERC20(
SwapERC20Params memory params
) internal returns (uint256, uint256) {
IERC20 tokenIn = params.tokenIn;
IERC20 tokenOut = params.tokenOut;
address aggregator = params.aggregator;
address receiver = params.receiver;
address feeRecipient = params.feeRecipient;
uint256 amountIn = params.amountIn;
uint256 minAmountOut = params.minAmountOut;
uint256 feeBps = params.feeBps;
bytes memory swapData = params.swapData;
bool isDelegate = params.isDelegate;
_validateInputs(
address(tokenIn),
address(tokenOut),
amountIn,
minAmountOut,
receiver
);
if (!isDelegate) {
if (address(tokenIn) == usdt)
TransferHelper.safeApprove(address(tokenIn), aggregator, 0);
TransferHelper.safeApprove(address(tokenIn), aggregator, amountIn);
}
uint256 fee;
if (feeRecipient != address(0) || feeBps != 0) {
fee = (amountIn * feeBps) / 10000;
amountIn -= fee;
TransferHelper.safeTransfer(address(tokenIn), feeRecipient, fee);
}
uint256 amountOut;
if (address(tokenOut) == nativeToken) {
uint256 balanceBefore = address(this).balance;
_executeAggregatorCall(swapData, isDelegate, aggregator, 0);
amountOut = address(this).balance - balanceBefore;
if (amountOut < minAmountOut) revert InsufficientETHOutAmount();
if (receiver != address(this)) {
(bool success, ) = receiver.call{value: amountOut}("");
if (!success) revert SwapFailed();
}
} else {
uint256 balanceBefore = tokenOut.balanceOf(address(this));
_executeAggregatorCall(swapData, isDelegate, aggregator, 0);
amountOut = tokenOut.balanceOf(address(this)) - balanceBefore;
if (amountOut < minAmountOut) revert InsufficientTokenOutAmount();
if (receiver != address(this)) {
TransferHelper.safeTransfer(
address(tokenOut),
receiver,
amountOut
);
}
}
return (amountOut, fee);
}
/**
* @dev Executes a swap call via the aggregator or delegatecall context.
* @param swapData The data required for the swap.
* @param isDelegate Indicates if the swap is in a delegatecall context.
* @param aggregator The address of the aggregator to use for the swap.
* @param value The amount of ETH to send with the call (if applicable).
*/
function _executeAggregatorCall(
bytes memory swapData,
bool isDelegate,
address aggregator,
uint256 value
) internal {
(bool success, bytes memory returnData) = isDelegate
? SWAP_TARGET.delegatecall(swapData)
: aggregator.call{value: value}(swapData);
if (!success) {
assembly {
let size := mload(returnData)
revert(add(32, returnData), size)
}
}
}
/**
* @dev Validates the swap inputs for consistency and correctness.
* @param tokenIn address of tokenIn
* @param tokenOut address of tokenIn
* @param amountIn The amount of tokenIn to swap.
* @param minAmountOut The minimum amount of tokenOut expected.
* @param receiver The address to receive the tokenOut.
*/
function _validateInputs(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut,
address receiver
) internal pure {
if (receiver == address(0)) revert InvalidReceiver();
if (amountIn == 0) revert AmountInMustBeGreaterThanZero();
if (minAmountOut == 0) revert MinAmountOutMustBeGreaterThanZero();
if (tokenIn == tokenOut) revert TokenInAndTokenOutCannotBeSame();
}
/**
* @dev Allows the contract to receive ETH.
*/
receive() external payable {}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
// Helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
library TransferHelper {
// Custom errors
error ApprovalFailed(address token, address to, uint256 value);
error TransferFailed(address token, address to, uint256 value);
error TransferFromFailed(address token, address from, address to, uint256 value);
error ETHTransferFailed(address to, uint256 value);
/**
* @dev Safely approves a token for spending.
* @param token The address of the token contract.
* @param to The address to approve.
* @param value The amount to approve.
*/
function safeApprove(
address token,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('approve(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) {
revert ApprovalFailed(token, to, value);
}
}
/**
* @dev Safely transfers tokens.
* @param token The address of the token contract.
* @param to The address to transfer to.
* @param value The amount to transfer.
*/
function safeTransfer(
address token,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transfer(address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) {
revert TransferFailed(token, to, value);
}
}
/**
* @dev Safely transfers tokens from one address to another.
* @param token The address of the token contract.
* @param from The address to transfer from.
* @param to The address to transfer to.
* @param value The amount to transfer.
*/
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) {
revert TransferFromFailed(token, from, to, value);
}
}
}
{
"compilationTarget": {
"contracts/MetaAggregatorSwapContract.sol": "MetaAggregatorSwapContract"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs",
"useLiteralContent": true
},
"optimizer": {
"enabled": true,
"runs": 125
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"_ensoSwapContract","type":"address"},{"internalType":"address","name":"_usdt","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AmountInMustBeGreaterThanZero","type":"error"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"ApprovalFailed","type":"error"},{"inputs":[],"name":"CannotSwapETH","type":"error"},{"inputs":[],"name":"CannotSwapETHToETH","type":"error"},{"inputs":[],"name":"CannotSwapTokens","type":"error"},{"inputs":[],"name":"FeeTransferFailed","type":"error"},{"inputs":[],"name":"IncorrectEtherAmountSent","type":"error"},{"inputs":[],"name":"InsufficientETHOutAmount","type":"error"},{"inputs":[],"name":"InsufficientOutputBalance","type":"error"},{"inputs":[],"name":"InsufficientTokenOutAmount","type":"error"},{"inputs":[],"name":"InvalidENSOAddress","type":"error"},{"inputs":[],"name":"InvalidReceiver","type":"error"},{"inputs":[],"name":"InvalidUSDTAddress","type":"error"},{"inputs":[],"name":"MinAmountOutMustBeGreaterThanZero","type":"error"},{"inputs":[],"name":"SwapFailed","type":"error"},{"inputs":[],"name":"TokenInAndTokenOutCannotBeSame","type":"error"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"TransferFailed","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"tokenIn","type":"address"},{"indexed":false,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"address","name":"tokenOut","type":"address"},{"indexed":false,"internalType":"address","name":"feeReceiver","type":"address"},{"indexed":false,"internalType":"address","name":"aggregator","type":"address"},{"indexed":true,"internalType":"uint256","name":"amountIn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOut","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"minAmountOut","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"},{"indexed":false,"internalType":"bool","name":"isDelegate","type":"bool"}],"name":"TokenSwapped","type":"event"},{"inputs":[{"components":[{"internalType":"contract IERC20","name":"tokenIn","type":"address"},{"internalType":"contract IERC20","name":"tokenOut","type":"address"},{"internalType":"address","name":"aggregator","type":"address"},{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"feeRecipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"minAmountOut","type":"uint256"},{"internalType":"uint256","name":"feeBps","type":"uint256"},{"internalType":"bytes","name":"swapData","type":"bytes"},{"internalType":"bool","name":"isDelegate","type":"bool"}],"internalType":"struct IMetaAggregatorSwapContract.SwapERC20Params","name":"params","type":"tuple"}],"name":"swapERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"contract IERC20","name":"tokenOut","type":"address"},{"internalType":"address","name":"aggregator","type":"address"},{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"feeRecipient","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"minAmountOut","type":"uint256"},{"internalType":"uint256","name":"feeBps","type":"uint256"},{"internalType":"bytes","name":"swapData","type":"bytes"},{"internalType":"bool","name":"isDelegate","type":"bool"}],"internalType":"struct IMetaAggregatorSwapContract.SwapETHParams","name":"params","type":"tuple"}],"name":"swapETH","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]