From 1686ac74c7fbd460466774cbfdfb0f981427b789 Mon Sep 17 00:00:00 2001 From: cryptoDev222 Date: Fri, 8 Oct 2021 14:21:02 -0500 Subject: [PATCH] implement sushi connector --- .../mainnet/connectors/sushiswap/events.sol | 41 ++++ .../mainnet/connectors/sushiswap/helpers.sol | 184 +++++++++++++++ .../connectors/sushiswap/interface.sol | 57 +++++ .../mainnet/connectors/sushiswap/main.sol | 196 ++++++++++++++++ test/sushiswap/sushiswap.test.js | 217 ++++++++++++++++++ 5 files changed, 695 insertions(+) create mode 100644 contracts/mainnet/connectors/sushiswap/events.sol create mode 100644 contracts/mainnet/connectors/sushiswap/helpers.sol create mode 100644 contracts/mainnet/connectors/sushiswap/interface.sol create mode 100644 contracts/mainnet/connectors/sushiswap/main.sol create mode 100644 test/sushiswap/sushiswap.test.js diff --git a/contracts/mainnet/connectors/sushiswap/events.sol b/contracts/mainnet/connectors/sushiswap/events.sol new file mode 100644 index 00000000..2717b0a3 --- /dev/null +++ b/contracts/mainnet/connectors/sushiswap/events.sol @@ -0,0 +1,41 @@ +pragma solidity ^0.7.0; + +contract Events { + event LogDepositLiquidity( + address indexed tokenA, + address indexed tokenB, + uint256 amtA, + uint256 amtB, + uint256 uniAmount, + uint256 getId, + uint256 setId + ); + + event LogWithdrawLiquidity( + address indexed tokenA, + address indexed tokenB, + uint256 amountA, + uint256 amountB, + uint256 uniAmount, + uint256 getId, + uint256[] setId + ); + + event LogBuy( + address indexed buyToken, + address indexed sellToken, + uint256 buyAmt, + uint256 sellAmt, + uint256 getId, + uint256 setId + ); + + event LogSell( + address indexed buyToken, + address indexed sellToken, + uint256 buyAmt, + uint256 sellAmt, + uint256 getId, + uint256 setId + ); +} \ No newline at end of file diff --git a/contracts/mainnet/connectors/sushiswap/helpers.sol b/contracts/mainnet/connectors/sushiswap/helpers.sol new file mode 100644 index 00000000..a1baa4c5 --- /dev/null +++ b/contracts/mainnet/connectors/sushiswap/helpers.sol @@ -0,0 +1,184 @@ +pragma solidity ^0.7.0; + +import {TokenInterface} from "../../common/interfaces.sol"; +import {DSMath} from "../../common/math.sol"; +import {Basic} from "../../common/basic.sol"; +import {ISushiSwapRouter, ISushiSwapFactory} from "./interface.sol"; + +abstract contract Helpers is DSMath, Basic { + /** + * @dev ISushiSwapRouter + */ + ISushiSwapRouter internal constant router = + ISushiSwapRouter(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F); + + function getExpectedBuyAmt(address[] memory paths, uint256 sellAmt) + internal + view + returns (uint256 buyAmt) + { + uint256[] memory amts = router.getAmountsOut(sellAmt, paths); + buyAmt = amts[1]; + } + + function getExpectedSellAmt(address[] memory paths, uint256 buyAmt) + internal + view + returns (uint256 sellAmt) + { + uint256[] memory amts = router.getAmountsIn(buyAmt, paths); + sellAmt = amts[0]; + } + + function checkPair(address[] memory paths) internal view { + address pair = ISushiSwapFactory(router.factory()).getPair( + paths[0], + paths[1] + ); + require(pair != address(0), "No-exchange-address"); + } + + function getPaths(address buyAddr, address sellAddr) + internal + pure + returns (address[] memory paths) + { + paths = new address[](2); + paths[0] = address(sellAddr); + paths[1] = address(buyAddr); + } + + function getMinAmount( + TokenInterface token, + uint256 amt, + uint256 slippage + ) internal view returns (uint256 minAmt) { + uint256 _amt18 = convertTo18(token.decimals(), amt); + minAmt = wmul(_amt18, sub(WAD, slippage)); + minAmt = convert18ToDec(token.decimals(), minAmt); + } + + function _addLiquidity( + address tokenA, + address tokenB, + uint256 _amt, + uint256 unitAmt, + uint256 slippage + ) + internal + returns ( + uint256 _amtA, + uint256 _amtB, + uint256 _liquidity + ) + { + (TokenInterface _tokenA, TokenInterface _tokenB) = changeEthAddress( + tokenA, + tokenB + ); + + _amtA = _amt == uint256(-1) + ? getTokenBal(TokenInterface(tokenA)) + : _amt; + _amtB = convert18ToDec( + _tokenB.decimals(), + wmul(unitAmt, convertTo18(_tokenA.decimals(), _amtA)) + ); + + bool isEth = address(_tokenA) == wethAddr; + convertEthToWeth(isEth, _tokenA, _amtA); + + isEth = address(_tokenB) == wethAddr; + convertEthToWeth(isEth, _tokenB, _amtB); + + approve(_tokenA, address(router), _amtA); + approve(_tokenB, address(router), _amtB); + + uint256 minAmtA = getMinAmount(_tokenA, _amtA, slippage); + uint256 minAmtB = getMinAmount(_tokenB, _amtB, slippage); + (_amtA, _amtB, _liquidity) = router.addLiquidity( + address(_tokenA), + address(_tokenB), + _amtA, + _amtB, + minAmtA, + minAmtA, + address(this), + block.timestamp + 1 + ); + } + + function _removeLiquidity( + address tokenA, + address tokenB, + uint256 _amt, + uint256 unitAmtA, + uint256 unitAmtB + ) + internal + returns ( + uint256 _amtA, + uint256 _amtB, + uint256 _uniAmt + ) + { + TokenInterface _tokenA; + TokenInterface _tokenB; + (_tokenA, _tokenB, _uniAmt) = _getRemoveLiquidityData( + tokenA, + tokenB, + _amt + ); + { + uint256 minAmtA = convert18ToDec( + _tokenA.decimals(), + wmul(unitAmtA, _uniAmt) + ); + uint256 minAmtB = convert18ToDec( + _tokenB.decimals(), + wmul(unitAmtB, _uniAmt) + ); + (_amtA, _amtB) = router.removeLiquidity( + address(_tokenA), + address(_tokenB), + _uniAmt, + minAmtA, + minAmtB, + address(this), + block.timestamp + 1 + ); + } + + bool isEth = address(_tokenA) == wethAddr; + convertWethToEth(isEth, _tokenA, _amtA); + + isEth = address(_tokenB) == wethAddr; + convertWethToEth(isEth, _tokenB, _amtB); + } + + function _getRemoveLiquidityData( + address tokenA, + address tokenB, + uint256 _amt + ) + internal + returns ( + TokenInterface _tokenA, + TokenInterface _tokenB, + uint256 _uniAmt + ) + { + (_tokenA, _tokenB) = changeEthAddress(tokenA, tokenB); + address exchangeAddr = ISushiSwapFactory(router.factory()).getPair( + address(_tokenA), + address(_tokenB) + ); + require(exchangeAddr != address(0), "pair-not-found."); + + TokenInterface uniToken = TokenInterface(exchangeAddr); + _uniAmt = _amt == uint256(-1) + ? uniToken.balanceOf(address(this)) + : _amt; + approve(uniToken, address(router), _uniAmt); + } +} diff --git a/contracts/mainnet/connectors/sushiswap/interface.sol b/contracts/mainnet/connectors/sushiswap/interface.sol new file mode 100644 index 00000000..bd3be483 --- /dev/null +++ b/contracts/mainnet/connectors/sushiswap/interface.sol @@ -0,0 +1,57 @@ +pragma solidity ^0.7.0; + +interface ISushiSwapRouter { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} + +interface ISushiSwapFactory { + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint) external view returns (address pair); + function allPairsLength() external view returns (uint); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + + function createPair(address tokenA, address tokenB) external returns (address pair); +} diff --git a/contracts/mainnet/connectors/sushiswap/main.sol b/contracts/mainnet/connectors/sushiswap/main.sol new file mode 100644 index 00000000..bd0078bc --- /dev/null +++ b/contracts/mainnet/connectors/sushiswap/main.sol @@ -0,0 +1,196 @@ +pragma solidity ^0.7.0; + +/** + * @title SushiSwap. + * @dev Decentralized Exchange. + */ + +import { TokenInterface } from "../../common/interfaces.sol"; +import { Helpers } from "./helpers.sol"; +import { Events } from "./events.sol"; + +abstract contract SushipswapResolver is Helpers, Events { + /** + * @dev Deposit Liquidity. + * @notice Deposit Liquidity to a SushiSwap pool. + * @param tokenA The address of token A.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param tokenB The address of token B.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param amtA The amount of A tokens to deposit. + * @param unitAmt The unit amount of of amtB/amtA with slippage. + * @param slippage Slippage amount. + * @param getId ID to retrieve amtA. + * @param setId ID stores the amount of pools tokens received. + */ + function deposit( + address tokenA, + address tokenB, + uint256 amtA, + uint256 unitAmt, + uint256 slippage, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amtA); + + (uint _amtA, uint _amtB, uint _uniAmt) = _addLiquidity( + tokenA, + tokenB, + _amt, + unitAmt, + slippage + ); + setUint(setId, _uniAmt); + + _eventName = "LogDepositLiquidity(address,address,uint256,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(tokenA, tokenB, _amtA, _amtB, _uniAmt, getId, setId); + } + + /** + * @dev Withdraw Liquidity. + * @notice Withdraw Liquidity from a SushiSwap pool. + * @param tokenA The address of token A.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param tokenB The address of token B.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param uniAmt The amount of pool tokens to withdraw. + * @param unitAmtA The unit amount of amtA/uniAmt with slippage. + * @param unitAmtB The unit amount of amtB/uniAmt with slippage. + * @param getId ID to retrieve uniAmt. + * @param setIds Array of IDs to store the amount tokens received. + */ + function withdraw( + address tokenA, + address tokenB, + uint256 uniAmt, + uint256 unitAmtA, + uint256 unitAmtB, + uint256 getId, + uint256[] calldata setIds + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, uniAmt); + + (uint _amtA, uint _amtB, uint _uniAmt) = _removeLiquidity( + tokenA, + tokenB, + _amt, + unitAmtA, + unitAmtB + ); + + setUint(setIds[0], _amtA); + setUint(setIds[1], _amtB); + + _eventName = "LogWithdrawLiquidity(address,address,uint256,uint256,uint256,uint256,uint256[])"; + _eventParam = abi.encode(tokenA, tokenB, _amtA, _amtB, _uniAmt, getId, setIds); + } + + /** + * @dev Buy ETH/ERC20_Token. + * @notice Buy a token using a SushiSwap + * @param buyAddr The address of the token to buy.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param sellAddr The address of the token to sell.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param buyAmt The amount of tokens to buy. + * @param unitAmt The unit amount of sellAmt/buyAmt with slippage. + * @param getId ID to retrieve buyAmt. + * @param setId ID to store the amount of tokens sold. + */ + function buy( + address buyAddr, + address sellAddr, + uint256 buyAmt, + uint256 unitAmt, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _buyAmt = getUint(getId, buyAmt); + (TokenInterface _buyAddr, TokenInterface _sellAddr) = changeEthAddress(buyAddr, sellAddr); + address[] memory paths = getPaths(address(_buyAddr), address(_sellAddr)); + + uint _slippageAmt = convert18ToDec(_sellAddr.decimals(), + wmul(unitAmt, convertTo18(_buyAddr.decimals(), _buyAmt)) + ); + + checkPair(paths); + uint _expectedAmt = getExpectedSellAmt(paths, _buyAmt); + require(_slippageAmt >= _expectedAmt, "Too much slippage"); + + bool isEth = address(_sellAddr) == wethAddr; + convertEthToWeth(isEth, _sellAddr, _expectedAmt); + approve(_sellAddr, address(router), _expectedAmt); + + uint _sellAmt = router.swapTokensForExactTokens( + _buyAmt, + _expectedAmt, + paths, + address(this), + block.timestamp + 1 + )[0]; + + isEth = address(_buyAddr) == wethAddr; + convertWethToEth(isEth, _buyAddr, _buyAmt); + + setUint(setId, _sellAmt); + + _eventName = "LogBuy(address,address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(buyAddr, sellAddr, _buyAmt, _sellAmt, getId, setId); + } + + /** + * @dev Sell ETH/ERC20_Token. + * @notice Sell a token using a SushiSwap + * @param buyAddr The address of the token to buy.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param sellAddr The address of the token to sell.(For ETH: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param sellAmt The amount of the token to sell. + * @param unitAmt The unit amount of buyAmt/sellAmt with slippage. + * @param getId ID to retrieve sellAmt. + * @param setId ID stores the amount of token brought. + */ + function sell( + address buyAddr, + address sellAddr, + uint256 sellAmt, + uint256 unitAmt, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _sellAmt = getUint(getId, sellAmt); + (TokenInterface _buyAddr, TokenInterface _sellAddr) = changeEthAddress(buyAddr, sellAddr); + address[] memory paths = getPaths(address(_buyAddr), address(_sellAddr)); + + if (_sellAmt == uint(-1)) { + _sellAmt = sellAddr == ethAddr ? + address(this).balance : + _sellAddr.balanceOf(address(this)); + } + + uint _slippageAmt = convert18ToDec(_buyAddr.decimals(), + wmul(unitAmt, convertTo18(_sellAddr.decimals(), _sellAmt)) + ); + + checkPair(paths); + uint _expectedAmt = getExpectedBuyAmt(paths, _sellAmt); + require(_slippageAmt <= _expectedAmt, "Too much slippage"); + + bool isEth = address(_sellAddr) == wethAddr; + convertEthToWeth(isEth, _sellAddr, _sellAmt); + approve(_sellAddr, address(router), _sellAmt); + + uint _buyAmt = router.swapExactTokensForTokens( + _sellAmt, + _expectedAmt, + paths, + address(this), + block.timestamp + 1 + )[1]; + + isEth = address(_buyAddr) == wethAddr; + convertWethToEth(isEth, _buyAddr, _buyAmt); + + setUint(setId, _buyAmt); + + _eventName = "LogSell(address,address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(buyAddr, sellAddr, _buyAmt, _sellAmt, getId, setId); + } +} + +contract ConnectV2Sushiswap is SushipswapResolver { + string public constant name = "Sushipswap-v1.1"; +} diff --git a/test/sushiswap/sushiswap.test.js b/test/sushiswap/sushiswap.test.js new file mode 100644 index 00000000..ead9dfd3 --- /dev/null +++ b/test/sushiswap/sushiswap.test.js @@ -0,0 +1,217 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); +const { web3, deployments, waffle, ethers } = hre; +const { provider, deployContract } = waffle + +const deployAndEnableConnector = require("../../scripts/deployAndEnableConnector.js") +const buildDSAv2 = require("../../scripts/buildDSAv2") +const encodeSpells = require("../../scripts/encodeSpells.js") +const encodeFlashcastData = require("../../scripts/encodeFlashcastData.js") +const getMasterSigner = require("../../scripts/getMasterSigner") +const addLiquidity = require("../../scripts/addLiquidity"); + +const addresses = require("../../scripts/constant/addresses"); +const abis = require("../../scripts/constant/abis"); +const constants = require("../../scripts/constant/constant"); +const tokens = require("../../scripts/constant/tokens"); +const { abi: nftManagerAbi } = require("@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json") + +const connectV2SushiswapArtifacts = require("../../artifacts/contracts/mainnet/connectors/sushiswap/main.sol/ConnectV2Sushiswap.json"); +const { eth } = require("../../scripts/constant/tokens"); +const { BigNumber } = require("ethers"); + +const FeeAmount = { + LOW: 500, + MEDIUM: 3000, + HIGH: 10000, +} + +const TICK_SPACINGS = { + 500: 10, + 3000: 60, + 10000: 200 +} + +const USDT_ADDR = "0xdac17f958d2ee523a2206206994597c13d831ec7" +const DAI_ADDR = "0x6b175474e89094c44da98b954eedeac495271d0f" + +let tokenIds = [] +let liquidities = [] +const abiCoder = ethers.utils.defaultAbiCoder + +describe("Sushiswap", function () { + const connectorName = "Sushiswap-v1" + + let dsaWallet0 + let masterSigner; + let instaConnectorsV2; + let connector; + + const wallets = provider.getWallets() + const [wallet0, wallet1, wallet2, wallet3] = wallets + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: hre.config.networks.hardhat.forking.url, + blockNumber: 13005785, + }, + }, + ], + }); + masterSigner = await getMasterSigner(wallet3) + instaConnectorsV2 = await ethers.getContractAt(abis.core.connectorsV2, addresses.core.connectorsV2); + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: connectV2SushiswapArtifacts, + signer: masterSigner, + connectors: instaConnectorsV2 + }) + console.log("Connector address", connector.address) + }) + + it("Should have contracts deployed.", async function () { + expect(!!instaConnectorsV2.address).to.be.true; + expect(!!connector.address).to.be.true; + expect(!!masterSigner.address).to.be.true; + }); + + describe("DSA wallet setup", function () { + it("Should build DSA v2", async function () { + dsaWallet0 = await buildDSAv2(wallet0.address) + expect(!!dsaWallet0.address).to.be.true; + }); + + it("Deposit ETH & DAI into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("10")); + + await addLiquidity("dai", dsaWallet0.address, ethers.utils.parseEther("100000")); + }); + + it("Deposit ETH & USDT into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("10")); + + await addLiquidity("usdt", dsaWallet0.address, ethers.utils.parseEther("100000")); + }); + }); + + describe("Main", function () { + + it("Should deposit successfully", async function () { + const ethAmount = ethers.utils.parseEther("0.1") // 1 ETH + const daiUnitAmount = ethers.utils.parseEther("4000") // 1 ETH + const usdtAmount = ethers.utils.parseEther("400") / Math.pow(10, 12) // 1 ETH + const ethAddress = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + + const getId = "0" + const setId = "0" + + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [ + ethAddress, + DAI_ADDR, + ethAmount, + daiUnitAmount, + "500000000000000000", + getId, + setId + ], + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + let receipt = await tx.wait() + // let castEvent = new Promise((resolve, reject) => { + // dsaWallet0.on('LogCast', (origin, sender, value, targetNames, targets, eventNames, eventParams, event) => { + // const params = abiCoder.decode(["uint256", "uint256", "uint256", "uint256", "int24", "int24"], eventParams[0]); + // const params1 = abiCoder.decode(["uint256", "uint256", "uint256", "uint256", "int24", "int24"], eventParams[2]); + // tokenIds.push(params[0]); + // tokenIds.push(params1[0]); + // liquidities.push(params[1]); + // event.removeListener(); + + // resolve({ + // eventNames, + // }); + // }); + + // setTimeout(() => { + // reject(new Error('timeout')); + // }, 60000) + // }); + + // let event = await castEvent + + // const data = await nftManager.positions(tokenIds[0]) + + // expect(data.liquidity).to.be.equals(liquidities[0]); + }).timeout(10000000000); + + it("Should withdraw successfully", async function () { + const ethAmount = ethers.utils.parseEther("0.1") // 1 ETH + const ethAddress = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + + const getId = "0" + const setIds = ["0", "0"] + + const spells = [ + { + connector: connectorName, + method: "withdraw", + args: [ + ethAddress, + DAI_ADDR, + ethAmount, + 0, + 0, + getId, + setIds + ] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + let receipt = await tx.wait() + }); + + it("Should buy successfully", async function () { + const ethAmount = ethers.utils.parseEther("0.1") // 1 ETH + const daiUnitAmount = ethers.utils.parseEther("4000") // 1 ETH + const ethAddress = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + + const getId = "0" + const setId = "0" + + const spells = [ + { + connector: connectorName, + method: "buy", + args: [ + ethAddress, + DAI_ADDR, + ethAmount, + daiUnitAmount, + getId, + setId + ] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address) + let receipt = await tx.wait() + }); + }); +}) \ No newline at end of file