From a2fb1c5f44318290d5756ed5c03e6e7d4303781e Mon Sep 17 00:00:00 2001 From: Eugene Rupakov Date: Thu, 12 Aug 2021 19:06:21 +0300 Subject: [PATCH] Added Emiswap adapters --- .../emiswap/EmiswapExchangeAdapter.sol | 38 ++ .../adapters/emiswap/EmiswapTokenAdapter.sol | 83 +++ .../EmiswapAssetInteractiveAdapter.sol | 111 ++++ .../EmiswapExchangeInteractiveAdapter.sol | 126 +++++ contracts/interfaces/IEmiRouter.sol | 124 +++++ contracts/interfaces/IEmiswap.sol | 55 ++ test/adapters/EmiswapLiquidityAdapter.js | 111 ++++ .../EmiswapAssetInteractiveAdapter.js | 483 ++++++++++++++++++ .../EmiswapExchangeInteractiveAdapter.js | 309 +++++++++++ 9 files changed, 1440 insertions(+) create mode 100644 contracts/adapters/emiswap/EmiswapExchangeAdapter.sol create mode 100644 contracts/adapters/emiswap/EmiswapTokenAdapter.sol create mode 100644 contracts/interactiveAdapters/emiswap/EmiswapAssetInteractiveAdapter.sol create mode 100644 contracts/interactiveAdapters/emiswap/EmiswapExchangeInteractiveAdapter.sol create mode 100644 contracts/interfaces/IEmiRouter.sol create mode 100644 contracts/interfaces/IEmiswap.sol create mode 100644 test/adapters/EmiswapLiquidityAdapter.js create mode 100644 test/interactiveAdapters/EmiswapAssetInteractiveAdapter.js create mode 100644 test/interactiveAdapters/EmiswapExchangeInteractiveAdapter.js diff --git a/contracts/adapters/emiswap/EmiswapExchangeAdapter.sol b/contracts/adapters/emiswap/EmiswapExchangeAdapter.sol new file mode 100644 index 00000000..65950271 --- /dev/null +++ b/contracts/adapters/emiswap/EmiswapExchangeAdapter.sol @@ -0,0 +1,38 @@ +// Copyright (C) 2020 Zerion Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity >=0.7.6; +pragma experimental ABIEncoderV2; + +import { ERC20 } from "../../interfaces/ERC20.sol"; +import { ProtocolAdapter } from "../ProtocolAdapter.sol"; + +/** + * @title Adapter for Emiswap protocol (exchange). + * @dev Implementation of ProtocolAdapter abstract contract. + * Base contract for Emiswap exchange adapter. + * @author Eugene Rupakov + */ +contract EmiswapExchangeAdapter is ProtocolAdapter { + /** + * @notice This function is unavailable for exchange adapter. + * @dev Implementation of ProtocolAdapter abstract contract function. + */ + function getBalance(address, address) public override returns (int256) { + revert("EEA: no balance"); + } +} diff --git a/contracts/adapters/emiswap/EmiswapTokenAdapter.sol b/contracts/adapters/emiswap/EmiswapTokenAdapter.sol new file mode 100644 index 00000000..79f04781 --- /dev/null +++ b/contracts/adapters/emiswap/EmiswapTokenAdapter.sol @@ -0,0 +1,83 @@ +// Copyright (C) 2020 Zerion Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity >=0.7.6; +pragma experimental ABIEncoderV2; + +import { ERC20 } from "../../interfaces/ERC20.sol"; +import { Component } from "../../shared/Structs.sol"; +import { TokenAdapter } from "../TokenAdapter.sol"; +import { Helpers } from "../../shared/Helpers.sol"; +import { IEmiswap } from "../../interfaces/IEmiswap.sol"; + +/** + * @title Token adapter for Emiswap Pool Tokens. + * @dev Implementation of TokenAdapter abstract contract. + * @author Eugene Rupakov + */ +contract EmiswapTokenAdapter is TokenAdapter { + using Helpers for bytes32; + + /** + * @return Array of Component structs with underlying tokens rates for the given token. + * @dev Implementation of TokenAdapter abstract contract function. + */ + function getComponents(address token) external override returns (Component[] memory) { + address[] memory tokens = new address[](2); + tokens[0] = address(IEmiswap(token).tokens(0)); + tokens[1] = address(IEmiswap(token).tokens(1)); + uint256 totalSupply = ERC20(token).totalSupply(); + + Component[] memory components = new Component[](2); + + for (uint256 i = 0; i < 2; i++) { + components[i] = Component({ + token: tokens[i], + rate: int256((ERC20(tokens[i]).balanceOf(token) * 1e18) / totalSupply) + }); + } + + return components; + } + + /** + * @return Pool name. + */ + function getName(address token) internal view override returns (string memory) { + return + string( + abi.encodePacked( + getUnderlyingSymbol(address(IEmiswap(token).tokens(0))), + "/", + getUnderlyingSymbol(address(IEmiswap(token).tokens(1))), + " Pool" + ) + ); + } + + function getUnderlyingSymbol(address token) internal view returns (string memory) { + (, bytes memory returnData) = token.staticcall( + abi.encodeWithSelector(ERC20(token).symbol.selector) + ); + + if (returnData.length == 32) { + return abi.decode(returnData, (bytes32)).toString(); + } else { + return abi.decode(returnData, (string)); + } + } +} diff --git a/contracts/interactiveAdapters/emiswap/EmiswapAssetInteractiveAdapter.sol b/contracts/interactiveAdapters/emiswap/EmiswapAssetInteractiveAdapter.sol new file mode 100644 index 00000000..51c85b33 --- /dev/null +++ b/contracts/interactiveAdapters/emiswap/EmiswapAssetInteractiveAdapter.sol @@ -0,0 +1,111 @@ +// Copyright (C) 2020 Zerion Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity >=0.7.6; +pragma experimental ABIEncoderV2; + +import { ERC20 } from "../../interfaces/ERC20.sol"; +import { SafeERC20 } from "../../shared/SafeERC20.sol"; +import { TokenAmount } from "../../shared/Structs.sol"; +import { ERC20ProtocolAdapter } from "../../adapters/ERC20ProtocolAdapter.sol"; +import { InteractiveAdapter } from "../InteractiveAdapter.sol"; +import "../../interfaces/IEmiswap.sol"; + +/** + * @title Interactive adapter for Emiswap protocol (liquidity). + * @dev Implementation of InteractiveAdapter abstract contract. + * @author Igor Sobolev + */ +contract EmiswapV2AssetInteractiveAdapter is InteractiveAdapter, ERC20ProtocolAdapter { + using SafeERC20 for ERC20; + + address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + /** + * @notice Deposits tokens to the Uniswap pool (pair). + * @param tokenAmounts Array with one element - TokenAmount struct with + * underlying tokens addresses, underlying tokens amounts to be deposited, and amount types. + * @param data ABI-encoded additional parameters: + * - pair - pair address. + * @return tokensToBeWithdrawn Array with one element - UNI-token (pair) address. + * @dev Implementation of InteractiveAdapter function. + */ + function deposit(TokenAmount[] calldata tokenAmounts, bytes calldata data) + external + payable + override + returns (address[] memory tokensToBeWithdrawn) + { + require(tokenAmounts.length == 2, "ELIA: should be 2 tokenAmounts"); + + address pair = abi.decode(data, (address)); + + uint256 [] memory a = new uint256[](2); + uint256 [] memory minA = new uint256[](2); + + a[0] = getAbsoluteAmountDeposit(tokenAmounts[0]); + a[1] = getAbsoluteAmountDeposit(tokenAmounts[1]); + + tokensToBeWithdrawn = new address[](2); + tokensToBeWithdrawn[0] = tokenAmounts[0].token; + tokensToBeWithdrawn[1] = tokenAmounts[1].token; + + // solhint-disable-next-line no-empty-blocks + + try IEmiswap(pair).deposit(a, minA, address(0)) returns (uint256) {} catch Error( + string memory reason + ) { + revert(reason); + } catch { + revert("ELIA: deposit fail"); + } + } + + /** + * @notice Withdraws tokens from the Uniswap pool. + * @param tokenAmounts Array with one element - TokenAmount struct with + * UNI token address, UNI token amount to be redeemed, and amount type. + * @return tokensToBeWithdrawn Array with two elements - underlying tokens. + * @dev Implementation of InteractiveAdapter function. + */ + function withdraw(TokenAmount[] calldata tokenAmounts, bytes calldata) + external + payable + override + returns (address[] memory tokensToBeWithdrawn) + { + require(tokenAmounts.length == 1, "ELIA: should be 1 tokenAmount"); + + address token = tokenAmounts[0].token; + uint256 amount = getAbsoluteAmountWithdraw(tokenAmounts[0]); + + tokensToBeWithdrawn = new address[](2); + tokensToBeWithdrawn[0] = address(IEmiswap(token).tokens(0)); + tokensToBeWithdrawn[1] = address(IEmiswap(token).tokens(1)); + + // solhint-disable-next-line no-empty-blocks + uint256 [] memory minReturns = new uint256[](2); + + try IEmiswap(token).withdraw(amount, minReturns) {} catch Error( + string memory reason + ) { + revert(reason); + } catch { + revert("ELIA: withdraw fail"); + } + } +} diff --git a/contracts/interactiveAdapters/emiswap/EmiswapExchangeInteractiveAdapter.sol b/contracts/interactiveAdapters/emiswap/EmiswapExchangeInteractiveAdapter.sol new file mode 100644 index 00000000..86fa6b99 --- /dev/null +++ b/contracts/interactiveAdapters/emiswap/EmiswapExchangeInteractiveAdapter.sol @@ -0,0 +1,126 @@ +// Copyright (C) 2020 Zerion Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity >=0.7.6; +pragma experimental ABIEncoderV2; + +import { ERC20 } from "../../interfaces/ERC20.sol"; +import { SafeERC20 } from "../../shared/SafeERC20.sol"; +import { TokenAmount, AmountType } from "../../shared/Structs.sol"; +import { EmiswapExchangeAdapter } from "../../adapters/emiswap/EmiswapExchangeAdapter.sol"; +import { InteractiveAdapter } from "../InteractiveAdapter.sol"; +import "../../interfaces/IEmiRouter.sol"; + +/** + * @title Interactive adapter for Emiswap protocol (exchange). + * @dev Implementation of InteractiveAdapter abstract contract. + * @author Eugene Rupakov + */ +contract EmiswapExchangeInteractiveAdapter is InteractiveAdapter, EmiswapExchangeAdapter { + using SafeERC20 for ERC20; + + address internal constant ROUTER = 0xf164fC0Ec4E93095b804a4795bBe1e041497b92a; + + /** + * @notice Exchange tokens using Uniswap pool. + * @param tokenAmounts Array with one element - TokenAmount struct with + * "from" token address, "from" token amount, and amount type. + * @param data Uniswap exchange path starting from tokens[0] (ABI-encoded). + * @return tokensToBeWithdrawn Array with one element - token address to be exchanged to. + * @dev Implementation of InteractiveAdapter function. + */ + function deposit(TokenAmount[] calldata tokenAmounts, bytes calldata data) + external + payable + override + returns (address[] memory tokensToBeWithdrawn) + { + require(tokenAmounts.length == 1, "EEIA: should be 1 tokenAmount"); + + address[] memory path = abi.decode(data, (address[])); + address token = tokenAmounts[0].token; + require(token == path[0], "EEIA: bad path[0]"); + uint256 amount = getAbsoluteAmountDeposit(tokenAmounts[0]); + + tokensToBeWithdrawn = new address[](1); + tokensToBeWithdrawn[0] = path[path.length - 1]; + + ERC20(token).safeApprove(ROUTER, amount, "EEIA[1]"); + + try + IEmiRouter(ROUTER).swapExactTokensForTokens( + amount, + 0, + path, + address(this), + // solhint-disable-next-line not-rely-on-time + address(0) + ) + returns (uint256[] memory) {} catch Error(string memory reason) { + //solhint-disable-previous-line no-empty-blocks + revert(reason); + } catch { + revert("EEIA: deposit fail"); + } + } + + /** + * @notice Exchange tokens using Uniswap pool. + * @param tokenAmounts Array with one element - TokenAmount struct with + * "to" token address, "to" token amount, and amount type (must be absolute). + * @param data Uniswap exchange path ending with tokens[0] (ABI-encoded). + * @return tokensToBeWithdrawn Array with one element - token address to be changed to. + * @dev Implementation of InteractiveAdapter function. + */ + function withdraw(TokenAmount[] calldata tokenAmounts, bytes calldata data) + external + payable + override + returns (address[] memory tokensToBeWithdrawn) + { + require(tokenAmounts.length == 1, "EEIA: should be 1 tokenAmount"); + require(tokenAmounts[0].amountType == AmountType.Absolute, "EEIA: bad type"); + + address[] memory path = abi.decode(data, (address[])); + address token = tokenAmounts[0].token; + require(token == path[path.length - 1], "EEIA: bad path[path.length - 1]"); + uint256 amount = tokenAmounts[0].amount; + + tokensToBeWithdrawn = new address[](1); + tokensToBeWithdrawn[0] = token; + + ERC20(path[0]).safeApprove(ROUTER, ERC20(path[0]).balanceOf(address(this)), "EEIA[2]"); + + try + IEmiRouter(ROUTER).swapTokensForExactTokens( + amount, + type(uint256).max, + path, + address(this), + // solhint-disable-next-line not-rely-on-time + address(0) + ) + returns (uint256[] memory) {} catch Error(string memory reason) { + //solhint-disable-previous-line no-empty-blocks + revert(reason); + } catch { + revert("EEIA: withdraw fail"); + } + + ERC20(path[0]).safeApprove(ROUTER, 0, "EEIA[3]"); + } +} diff --git a/contracts/interfaces/IEmiRouter.sol b/contracts/interfaces/IEmiRouter.sol new file mode 100644 index 00000000..8fa8cea1 --- /dev/null +++ b/contracts/interfaces/IEmiRouter.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity >=0.7.6; + +import "./ERC20.sol"; + +interface IEmiRouter { + function factory() external pure returns (address); + + function WETH() external pure returns (address); + + function getReserves(ERC20 token0, ERC20 token1) + external + view + returns ( + uint256 _reserve0, + uint256 _reserve1, + address poolAddresss + ); + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address ref + ) + external + returns ( + uint256 amountA, + uint256 amountB, + uint256 liquidity + ); + + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + address ref + ) external returns (uint256[] memory amounts); + + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + address ref + ) external returns (uint256[] memory amounts); + + function swapExactETHForTokens( + uint256 amountOutMin, + address[] calldata path, + address to, + address ref + ) external payable returns (uint256[] memory amounts); + + function swapTokensForExactETH( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + address ref + ) external returns (uint256[] memory amounts); + + function swapExactTokensForETH( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + address ref + ) external returns (uint256[] memory amounts); + + function swapETHForExactTokens( + uint256 amountOut, + address[] calldata path, + address to, + address ref + ) external payable returns (uint256[] memory amounts); + + function getAmountOut( + uint256 amountIn, + uint256 reserveIn, + uint256 reserveOut + ) external view returns (uint256 amountOut); + + function getAmountIn( + uint256 amountOut, + uint256 reserveIn, + uint256 reserveOut + ) external view returns (uint256 amountIn); + + function getAmountsOut(uint256 amountIn, address[] calldata path) + external + view + returns (uint256[] memory amounts); + + function getAmountsIn(uint256 amountOut, address[] calldata path) + external + view + returns (uint256[] memory amounts); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address[] calldata pathDAI + ) external; + + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint256 amountOutMin, + address[] calldata path, + address[] calldata pathDAI + ) external payable; + + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address[] calldata pathDAI + ) external; +} diff --git a/contracts/interfaces/IEmiswap.sol b/contracts/interfaces/IEmiswap.sol new file mode 100644 index 00000000..e63d334f --- /dev/null +++ b/contracts/interfaces/IEmiswap.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity >=0.7.6; + +import "./ERC20.sol"; + +interface IEmiswapRegistry { + function pools(ERC20 token1, ERC20 token2) + external + view + returns (IEmiswap); + + function isPool(address addr) external view returns (bool); + + function deploy(ERC20 tokenA, ERC20 tokenB) external returns (IEmiswap); + function getAllPools() external view returns (IEmiswap[] memory); +} + +interface IEmiswap { + function fee() external view returns (uint256); + + function tokens(uint256 i) external view returns (ERC20); + + function deposit( + uint256[] calldata amounts, + uint256[] calldata minAmounts, + address referral + ) external payable returns (uint256 fairSupply); + + function withdraw(uint256 amount, uint256[] calldata minReturns) external; + + function getBalanceForAddition(ERC20 token) + external + view + returns (uint256); + + function getBalanceForRemoval(ERC20 token) external view returns (uint256); + + function getReturn( + ERC20 fromToken, + ERC20 destToken, + uint256 amount + ) external view returns (uint256, uint256); + + function swap( + ERC20 fromToken, + ERC20 destToken, + uint256 amount, + uint256 minReturn, + address to, + address referral + ) external payable returns (uint256 returnAmount); + + function initialize(ERC20[] calldata assets) external; +} diff --git a/test/adapters/EmiswapLiquidityAdapter.js b/test/adapters/EmiswapLiquidityAdapter.js new file mode 100644 index 00000000..eb76ef25 --- /dev/null +++ b/test/adapters/EmiswapLiquidityAdapter.js @@ -0,0 +1,111 @@ +import displayToken from '../helpers/displayToken'; + +const ASSET_ADAPTER = '01'; + +const ProtocolAdapterRegistry = artifacts.require('ProtocolAdapterRegistry'); +const TokenAdapterRegistry = artifacts.require('TokenAdapterRegistry'); +const ProtocolAdapter = artifacts.require('ERC20ProtocolAdapter'); +const TokenAdapter = artifacts.require('EmiswapTokenAdapter'); +const ERC20TokenAdapter = artifacts.require('ERC20TokenAdapter'); + +contract('EmiswapAssetAdapter', () => { + const daiWethUniAddress = '0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11'; + const testAddress = '0x42b9dF65B219B3dD36FF330A4dD8f327A6Ada990'; + + let accounts; + let adapterRegistry; + let protocolAdapterAddress; + let tokenAdapterAddress; + let erc20TokenAdapterAddress; + const daiWethUni = [ + 'DAI/WETH Pool', + 'UNI-V2', + '18', + ]; + const dai = [ + 'Dai Stablecoin', + 'DAI', + '18', + ]; + const weth = [ + 'Wrapped Ether', + 'WETH', + '18', + ]; + + beforeEach(async () => { + accounts = await web3.eth.getAccounts(); + await ProtocolAdapter.new({ from: accounts[0] }) + .then((result) => { + protocolAdapterAddress = result.address; + }); + await TokenAdapter.new({ from: accounts[0] }) + .then((result) => { + tokenAdapterAddress = result.address; + }); + await ERC20TokenAdapter.new({ from: accounts[0] }) + .then((result) => { + erc20TokenAdapterAddress = result.address; + }); + await ProtocolAdapterRegistry.new({ from: accounts[0] }) + .then((result) => { + adapterRegistry = result.contract; + }); + await TokenAdapterRegistry.new({ from: accounts[0] }) + .then((result) => { + tokenAdapterRegistry = result.contract; + }); + await adapterRegistry.methods.addProtocolAdapters( + [ + `${web3.eth.abi.encodeParameter( + 'bytes32', + web3.utils.toHex('Emiswap'), + ) + .slice(0, -2)}${ASSET_ADAPTER}`, + ], + [ + protocolAdapterAddress, + ], + [[ + daiWethUniAddress, + ]], + ) + .send({ + from: accounts[0], + gas: '1000000', + }); + await tokenAdapterRegistry.methods.addTokenAdapters( + [ + web3.utils.toHex('ERC20'), + web3.utils.toHex('Emiswap LP Token'), + ], + [erc20TokenAdapterAddress, tokenAdapterAddress], + ) + .send({ + from: accounts[0], + gas: '1000000', + }); + }); + + it('should return correct balances', async () => { + await adapterRegistry.methods.getBalances(testAddress) + .call() + .then(async (result) => { + await displayToken(adapterRegistry, result[0].tokenBalances[0]); + }); + await adapterRegistry.methods.getFullTokenBalances( + [ + web3.utils.toHex('Emiswap LP Token'), + ], + [ + daiWethUniAddress, + ], + ) + .call() + .then((result) => { + assert.deepEqual(result[0].base.erc20metadata, daiWethUni); + assert.deepEqual(result[0].underlying[0].erc20metadata, dai); + assert.deepEqual(result[0].underlying[1].erc20metadata, weth); + }); + }); +}); diff --git a/test/interactiveAdapters/EmiswapAssetInteractiveAdapter.js b/test/interactiveAdapters/EmiswapAssetInteractiveAdapter.js new file mode 100644 index 00000000..a865d243 --- /dev/null +++ b/test/interactiveAdapters/EmiswapAssetInteractiveAdapter.js @@ -0,0 +1,483 @@ +// import displayToken from '../helpers/displayToken'; +import convertToShare from '../helpers/convertToShare'; +import expectRevert from '../helpers/expectRevert'; + +const EMISWAP_ADAPTER = web3.eth.abi.encodeParameter( + 'bytes32', + web3.utils.toHex('Emiswap LP'), +).slice(0, -2); +const WETH_ADAPTER = web3.eth.abi.encodeParameter( + 'bytes32', + web3.utils.toHex('Weth'), +).slice(0, -2); +const ASSET_ADAPTER = '01'; +const EXCHANGE_ADAPTER = '03'; +const EMISWAP_ASSET_ADAPTER = `${EMISWAP_ADAPTER}${ASSET_ADAPTER}`; +const EMISWAP_EXCHANGE_ADAPTER = `${EMISWAP_ADAPTER}${EXCHANGE_ADAPTER}`; +const WETH_ASSET_ADAPTER = `${WETH_ADAPTER}${ASSET_ADAPTER}`; + +const ACTION_DEPOSIT = 1; +const ACTION_WITHDRAW = 2; +const AMOUNT_RELATIVE = 1; +const AMOUNT_ABSOLUTE = 2; +const EMPTY_BYTES = '0x'; + +const ZERO = '0x0000000000000000000000000000000000000000'; + +const ProtocolAdapterRegistry = artifacts.require('./ProtocolAdapterRegistry'); +const EmiswapAdapter = artifacts.require('./EmiswapAssetInteractiveAdapter'); +const EmiswapExchangeAdapter = artifacts.require('./EmiswapExchangeInteractiveAdapter'); +const WethAdapter = artifacts.require('./WethInteractiveAdapter'); +const Core = artifacts.require('./Core'); +const Router = artifacts.require('./Router'); +const ERC20 = artifacts.require('./ERC20'); + +contract('EmiswapAssetInteractiveAdapter', () => { + const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; + const wethDaiAddress = '0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11'; + const ethAddress = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + const wethAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; + + let accounts; + let core; + let router; + let protocolAdapterRegistry; + let protocolAdapterAddress; + let emiswapAdapterAddress; + let wethAdapterAddress; + let DAI; + let WETH; + let WETHDAI; + + beforeEach(async () => { + accounts = await web3.eth.getAccounts(); + await EmiswapAdapter.new({ from: accounts[0] }) + .then((result) => { + protocolAdapterAddress = result.address; + }); + await EmiswapExchangeAdapter.new({ from: accounts[0] }) + .then((result) => { + emiswapAdapterAddress = result.address; + }); + await WethAdapter.new({ from: accounts[0] }) + .then((result) => { + wethAdapterAddress = result.address; + }); + await ProtocolAdapterRegistry.new({ from: accounts[0] }) + .then((result) => { + protocolAdapterRegistry = result.contract; + }); + await protocolAdapterRegistry.methods.addProtocolAdapters( + [ + EMISWAP_ASSET_ADAPTER, + EMISWAP_EXCHANGE_ADAPTER, + WETH_ASSET_ADAPTER, + ], + [ + protocolAdapterAddress, + emiswapAdapterAddress, + wethAdapterAddress, + ], + [ + [wethDaiAddress], + [], + [], + ], + ) + .send({ + from: accounts[0], + gas: '1000000', + }); + await Core.new( + protocolAdapterRegistry.options.address, + { from: accounts[0] }, + ) + .then((result) => { + core = result.contract; + }); + await Router.new( + core.options.address, + { from: accounts[0] }, + ) + .then((result) => { + router = result.contract; + }); + await ERC20.at(daiAddress) + .then((result) => { + DAI = result.contract; + }); + await ERC20.at(wethAddress) + .then((result) => { + WETH = result.contract; + }); + await ERC20.at(wethDaiAddress) + .then((result) => { + WETHDAI = result.contract; + }); + // exchange 1 ETH to WETH like we had WETH initially + await router.methods.execute( + // actions + [ + [ + WETH_ASSET_ADAPTER, + ACTION_DEPOSIT, + [ + [ethAddress, web3.utils.toWei('1', 'ether'), AMOUNT_ABSOLUTE], + ], + EMPTY_BYTES, + ], + ], + // inputs + [], + // fee + [0, ZERO], + // outputs + [], + ) + .send({ + from: accounts[0], + gas: 10000000, + value: web3.utils.toWei('1', 'ether'), + }); + }); + + describe('Uniswap V2 asset tests', () => { + it('should prepare for tests buyng dai for WETH', async () => { + await WETH.methods.approve(router.options.address, web3.utils.toWei('0.3', 'ether')) + .send({ + from: accounts[0], + gas: 1000000, + }); + await router.methods.execute( + // actions + [ + [ + EMISWAP_EXCHANGE_ADAPTER, + ACTION_DEPOSIT, + [ + [wethAddress, web3.utils.toWei('0.3', 'ether'), AMOUNT_ABSOLUTE], + ], + web3.eth.abi.encodeParameter('address[]', [wethAddress, daiAddress]), + ], + ], + // inputs + [ + [wethAddress, web3.utils.toWei('0.3', 'ether'), AMOUNT_ABSOLUTE], + ], + // fee + [0, ZERO], + // outputs + [], + ) + .send({ + gas: 10000000, + from: accounts[0], + }); + }); + + it('should not buy 1 UNI-V2 with existing DAI and WETH with wrong tokens', async () => { + let daiAmount; + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + daiAmount = result; + }); + await DAI.methods.approve(router.options.address, daiAmount.toString()) + .send({ + from: accounts[0], + gas: 1000000, + }); + let wethAmount; + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + wethAmount = result; + }); + await WETH.methods.approve(router.options.address, wethAmount.toString()) + .send({ + from: accounts[0], + gas: 1000000, + }); + await expectRevert(router.methods.execute( + [ + [ + EMISWAP_ASSET_ADAPTER, + ACTION_DEPOSIT, + [ + [daiAddress, convertToShare(1), AMOUNT_RELATIVE], + [daiAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + web3.eth.abi.encodeParameters('address', wethDaiAddress), + ], + ], + [ + [daiAddress, convertToShare(50), AMOUNT_ABSOLUTE], + [wethAddress, convertToShare(0.3), AMOUNT_ABSOLUTE], + ], + [0, ZERO], + [ + [wethDaiAddress, 0], + ], + ) + .send({ + from: accounts[0], + gas: 1000000, + })); + }); + + it('should buy 1 UNI-V2 with existing DAI and WETH', async () => { + await WETH.methods.approve(router.options.address, web3.utils.toWei('0.3', 'ether')) + .send({ + from: accounts[0], + gas: 1000000, + }); + await router.methods.execute( + // actions + [ + [ + EMISWAP_EXCHANGE_ADAPTER, + ACTION_DEPOSIT, + [ + [wethAddress, web3.utils.toWei('0.3', 'ether'), AMOUNT_ABSOLUTE], + ], + web3.eth.abi.encodeParameter('address[]', [wethAddress, daiAddress]), + ], + ], + // inputs + [ + [wethAddress, web3.utils.toWei('0.3', 'ether'), AMOUNT_ABSOLUTE], + ], + // fee + [0, ZERO], + // outputs + [], + ) + .send({ + gas: 10000000, + from: accounts[0], + }); + let daiAmount; + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`dai amount before is ${web3.utils.fromWei(result, 'ether')}`); + daiAmount = result; + }); + await DAI.methods.approve(router.options.address, daiAmount.toString()) + .send({ + from: accounts[0], + gas: 1000000, + }); + let wethAmount; + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`weth amount before is ${web3.utils.fromWei(result, 'ether')}`); + wethAmount = result; + }); + await WETH.methods.approve(router.options.address, wethAmount.toString()) + .send({ + from: accounts[0], + gas: 1000000, + }); + await WETHDAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`wethdai amount before is ${web3.utils.fromWei(result, 'ether')}`); + }); + await router.methods.execute( + [ + [ + EMISWAP_ASSET_ADAPTER, + ACTION_DEPOSIT, + [ + [daiAddress, convertToShare(1), AMOUNT_RELATIVE], + [wethAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + web3.eth.abi.encodeParameters('address', wethDaiAddress), + ], + ], + [ + [daiAddress, convertToShare(30), AMOUNT_ABSOLUTE], + [wethAddress, convertToShare(0.1), AMOUNT_ABSOLUTE], + ], + [0, ZERO], + [ + [wethDaiAddress, 0], + ], + ) + .send({ + from: accounts[0], + gas: 1000000, + }) + .then((receipt) => { + console.log(`called router for ${receipt.cumulativeGasUsed} gas`); + }); + await router.methods.execute( + [ + [ + EMISWAP_ASSET_ADAPTER, + ACTION_DEPOSIT, + [ + [daiAddress, convertToShare(1), AMOUNT_RELATIVE], + [wethAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + web3.eth.abi.encodeParameters('address', wethDaiAddress), + ], + ], + [ + [daiAddress, convertToShare(10), AMOUNT_ABSOLUTE], + [wethAddress, convertToShare(0.1), AMOUNT_ABSOLUTE], + ], + [0, ZERO], + [ + [wethDaiAddress, 0], + ], + ) + .send({ + from: accounts[0], + gas: 1000000, + }) + .then((receipt) => { + console.log(`called router for ${receipt.cumulativeGasUsed} gas`); + }); + await router.methods.execute( + [ + [ + EMISWAP_ASSET_ADAPTER, + ACTION_DEPOSIT, + [ + [wethAddress, convertToShare(1), AMOUNT_RELATIVE], + [daiAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + web3.eth.abi.encodeParameters('address', wethDaiAddress), + ], + ], + [ + [daiAddress, convertToShare(10), AMOUNT_ABSOLUTE], + [wethAddress, convertToShare(0.1), AMOUNT_ABSOLUTE], + ], + [0, ZERO], + [ + [wethDaiAddress, 0], + ], + ) + .send({ + from: accounts[0], + gas: 1000000, + }) + .then((receipt) => { + console.log(`called router for ${receipt.cumulativeGasUsed} gas`); + }); + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`dai amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`weth amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETHDAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`wethdai amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await DAI.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + await WETH.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + await WETHDAI.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + }); + + it('should sell 100% DAIUNI', async () => { + let wethDaiAmount; + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(` dai amount before is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(` weth amount before is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETHDAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then(async (result) => { + console.log(`weth/dai uni amount before is ${web3.utils.fromWei(result, 'ether')}`); + wethDaiAmount = result; + }); + await WETHDAI.methods.approve(router.options.address, wethDaiAmount.toString()) + .send({ + from: accounts[0], + gas: 1000000, + }); + await router.methods.execute( + [ + [ + EMISWAP_ASSET_ADAPTER, + ACTION_WITHDRAW, + [ + [wethDaiAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + web3.eth.abi.encodeParameter('address', daiAddress), + ], + ], + [ + [wethDaiAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + [0, ZERO], + [], + ) + .send({ + from: accounts[0], + gas: 1000000, + }) + .then((receipt) => { + console.log(`called router for ${receipt.cumulativeGasUsed} gas`); + }); + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`dai amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`weth amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETHDAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`wethdai amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await DAI.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + await WETH.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + await WETHDAI.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + }); + }); +}); diff --git a/test/interactiveAdapters/EmiswapExchangeInteractiveAdapter.js b/test/interactiveAdapters/EmiswapExchangeInteractiveAdapter.js new file mode 100644 index 00000000..3c7e19eb --- /dev/null +++ b/test/interactiveAdapters/EmiswapExchangeInteractiveAdapter.js @@ -0,0 +1,309 @@ +// import displayToken from '../helpers/displayToken'; +import convertToShare from '../helpers/convertToShare'; +import expectRevert from '../helpers/expectRevert'; + +const EMISWAP_V2_ADAPTER = web3.eth.abi.encodeParameter( + 'bytes32', + web3.utils.toHex('Emiswap LP'), +).slice(0, -2); +const WETH_ADAPTER = web3.eth.abi.encodeParameter( + 'bytes32', + web3.utils.toHex('Weth'), +).slice(0, -2); +const ASSET_ADAPTER = '01'; +const EXCHANGE_ADAPTER = '03'; +const WETH_ASSET_ADAPTER = `${WETH_ADAPTER}${ASSET_ADAPTER}`; +const EMISWAP_EXCHANGE_ADAPTER = `${EMISWAP_V2_ADAPTER}${EXCHANGE_ADAPTER}`; + +const ACTION_DEPOSIT = 1; +const ACTION_WITHDRAW = 2; +const AMOUNT_RELATIVE = 1; +const AMOUNT_ABSOLUTE = 2; +const EMPTY_BYTES = '0x'; + +const ZERO = '0x0000000000000000000000000000000000000000'; + +const ProtocolAdapterRegistry = artifacts.require('./ProtocolAdapterRegistry'); +const InteractiveAdapter = artifacts.require('./EmiswapExchangeInteractiveAdapter'); +const WethAdapter = artifacts.require('./WethInteractiveAdapter'); +const Core = artifacts.require('./Core'); +const Router = artifacts.require('./Router'); +const ERC20 = artifacts.require('./ERC20'); + +contract('EmiswapExchangeInteractiveAdapter', () => { + const ethAddress = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + const daiAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; + const wethAddress = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; + + let accounts; + let core; + let router; + let protocolAdapterRegistry; + let protocolAdapterAddress; + let wethAdapterAddress; + let DAI; + let WETH; + + beforeEach(async () => { + accounts = await web3.eth.getAccounts(); + await InteractiveAdapter.new({ from: accounts[0] }) + .then((result) => { + protocolAdapterAddress = result.address; + }); + await WethAdapter.new({ from: accounts[0] }) + .then((result) => { + wethAdapterAddress = result.address; + }); + await ProtocolAdapterRegistry.new({ from: accounts[0] }) + .then((result) => { + protocolAdapterRegistry = result.contract; + }); + await protocolAdapterRegistry.methods.addProtocolAdapters( + [ + EMISWAP_EXCHANGE_ADAPTER, + WETH_ASSET_ADAPTER, + ], + [ + protocolAdapterAddress, + wethAdapterAddress, + ], + [ + [], + [], + ], + ) + .send({ + from: accounts[0], + gas: '1000000', + }); + await Core.new( + protocolAdapterRegistry.options.address, + { from: accounts[0] }, + ) + .then((result) => { + core = result.contract; + }); + await Router.new( + core.options.address, + { from: accounts[0] }, + ) + .then((result) => { + router = result.contract; + }); + await ERC20.at(daiAddress) + .then((result) => { + DAI = result.contract; + }); + await ERC20.at(wethAddress) + .then((result) => { + WETH = result.contract; + }); + }); + + describe('ETH <-> DAI exchange', () => { + it('should prepare for exchanges (generate 1 WETH)', async () => { + // exchange 1 ETH to WETH like we had WETH initially + await router.methods.execute( + // actions + [ + [ + WETH_ASSET_ADAPTER, + ACTION_DEPOSIT, + [ + [ethAddress, web3.utils.toWei('1', 'ether'), AMOUNT_RELATIVE], + ], + EMPTY_BYTES, + ], + ], + // inputs + [], + // fee + [0, ZERO], + // outputs + [], + ) + .send({ + from: accounts[0], + gas: 10000000, + value: web3.utils.toWei('1', 'ether'), + }); + }); + + it('should be correct one-side exchange deposit-like', async () => { + let wethAmount; + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`dai amount before is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`weth amount before is ${web3.utils.fromWei(result, 'ether')}`); + wethAmount = result; + }); + await WETH.methods.approve(router.options.address, wethAmount.toString()) + .send({ + from: accounts[0], + gas: 1000000, + }); + await router.methods.execute( + // actions + [ + [ + EMISWAP_EXCHANGE_ADAPTER, + ACTION_DEPOSIT, + [ + [wethAddress, web3.utils.toWei('0.5', 'ether'), AMOUNT_ABSOLUTE], + ], + web3.eth.abi.encodeParameter('address[]', [wethAddress, daiAddress]), + ], + ], + // inputs + [ + [wethAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + // fee + [0, ZERO], + // outputs + [ + [daiAddress, web3.utils.toWei('90', 'ether')], + ], + ) + .send({ + gas: 10000000, + from: accounts[0], + }) + .then((receipt) => { + console.log(`called router for ${receipt.cumulativeGasUsed} gas`); + }); + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`dai amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`weth amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await DAI.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + await WETH.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + }); + + it('should not be correct reverse exchange withdraw-like if amount is relative', async () => { + let daiAmount; + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + daiAmount = result; + }); + await DAI.methods.approve(router.options.address, daiAmount.toString()) + .send({ + from: accounts[0], + gas: 1000000, + }); + await expectRevert(router.methods.execute( + [ + [ + EMISWAP_EXCHANGE_ADAPTER, + ACTION_WITHDRAW, + [ + [wethAddress, web3.utils.toWei('0.3', 'ether'), AMOUNT_RELATIVE], + ], + web3.eth.abi.encodeParameter('address[]', [daiAddress, wethAddress]), + ], + ], + [ + [daiAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + [0, ZERO], + [ + [wethAddress, web3.utils.toWei('0.3', 'ether')], + ], + ) + .send({ + gas: 10000000, + from: accounts[0], + })); + }); + + it('should be correct reverse exchange withdraw-like', async () => { + let daiAmount; + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`dai amount before is ${web3.utils.fromWei(result, 'ether')}`); + daiAmount = result; + }); + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`weth amount before is ${web3.utils.fromWei(result, 'ether')}`); + }); + await DAI.methods.approve(router.options.address, daiAmount.toString()) + .send({ + from: accounts[0], + gas: 1000000, + }); + await web3.eth.getBalance(accounts[0]) + .then((result) => { + console.log(`eth amount before is ${web3.utils.fromWei(result, 'ether')}`); + }); + await router.methods.execute( + [ + [ + EMISWAP_EXCHANGE_ADAPTER, + ACTION_WITHDRAW, + [ + [wethAddress, web3.utils.toWei('0.3', 'ether'), AMOUNT_ABSOLUTE], + ], + web3.eth.abi.encodeParameter('address[]', [daiAddress, wethAddress]), + ], + ], + [ + [daiAddress, convertToShare(1), AMOUNT_RELATIVE], + ], + [0, ZERO], + [ + [wethAddress, web3.utils.toWei('0.3', 'ether')], + ], + ) + .send({ + gas: 10000000, + from: accounts[0], + }) + .then((receipt) => { + console.log(`called router for ${receipt.cumulativeGasUsed} gas`); + }); + await DAI.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`dai amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await WETH.methods['balanceOf(address)'](accounts[0]) + .call() + .then((result) => { + console.log(`weth amount after is ${web3.utils.fromWei(result, 'ether')}`); + }); + await DAI.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + await WETH.methods['balanceOf(address)'](core.options.address) + .call() + .then((result) => { + assert.equal(result, 0); + }); + }); + }); +});