diff --git a/contracts/mainnet/common/interfaces.sol b/contracts/mainnet/common/interfaces.sol index 24a4eb47..53e848d3 100644 --- a/contracts/mainnet/common/interfaces.sol +++ b/contracts/mainnet/common/interfaces.sol @@ -8,6 +8,7 @@ interface TokenInterface { function withdraw(uint) external; function balanceOf(address) external view returns (uint); function decimals() external view returns (uint); + function totalSupply() external view returns (uint); } interface MemoryInterface { diff --git a/contracts/mainnet/connectors/sushi-incentive/events.sol b/contracts/mainnet/connectors/sushi-incentive/events.sol new file mode 100644 index 00000000..ebf68ebd --- /dev/null +++ b/contracts/mainnet/connectors/sushi-incentive/events.sol @@ -0,0 +1,36 @@ +pragma solidity ^0.7.0; + +contract Events { + event LogDeposit( + address indexed user, + uint256 indexed pid, + uint256 indexed version, + uint256 amount + ); + event LogWithdraw( + address indexed user, + uint256 indexed pid, + uint256 indexed version, + uint256 amount + ); + event LogEmergencyWithdraw( + address indexed user, + uint256 indexed pid, + uint256 indexed version, + uint256 lpAmount, + uint256 rewardsAmount + ); + event LogHarvest( + address indexed user, + uint256 indexed pid, + uint256 indexed version, + uint256 amount + ); + event LogWithdrawAndHarvest( + address indexed user, + uint256 indexed pid, + uint256 indexed version, + uint256 widrawAmount, + uint256 harvestAmount + ); +} diff --git a/contracts/mainnet/connectors/sushi-incentive/helpers.sol b/contracts/mainnet/connectors/sushi-incentive/helpers.sol new file mode 100644 index 00000000..97522f22 --- /dev/null +++ b/contracts/mainnet/connectors/sushi-incentive/helpers.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import {DSMath} from "../../common/math.sol"; +import {Basic} from "../../common/basic.sol"; +import "./interface.sol"; + +contract Helpers is DSMath, Basic { + IMasterChefV2 immutable masterChefV2 = + IMasterChefV2(0xEF0881eC094552b2e128Cf945EF17a6752B4Ec5d); + IMasterChef immutable masterChef = + IMasterChef(0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd); + ISushiSwapFactory immutable factory = + ISushiSwapFactory(0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac); + + function _deposit(uint256 _pid, uint256 _amount, uint256 _version) internal { + if(_version == 2) + masterChefV2.deposit(_pid, _amount, address(this)); + else + masterChef.deposit(_pid, _amount); + } + + function _withdraw(uint256 _pid, uint256 _amount, uint256 _version) internal { + if(_version == 2) + masterChefV2.withdraw(_pid, _amount, address(this)); + else + masterChef.withdraw(_pid, _amount); + } + + function _harvest(uint256 _pid) internal { + masterChefV2.harvest(_pid, address(this)); + } + + function _withdrawAndHarvest(uint256 _pid, uint256 _amount, uint256 _version) internal { + if(_version == 2) + masterChefV2.withdrawAndHarvest(_pid, _amount, address(this)); + else _withdraw(_pid, _amount, _version); + } + + function _emergencyWithdraw(uint256 _pid, uint256 _version) internal { + if(_version == 2) + masterChefV2.emergencyWithdraw(_pid, address(this)); + else + masterChef.emergencyWithdraw(_pid, address(this)); + } + + function _getPoolId(address tokenA, address tokenB) + internal + view + returns (uint256 poolId, uint256 version, address lpToken) + { + address pair = factory.getPair(tokenA, tokenB); + uint256 length = masterChefV2.poolLength(); + version = 2; + poolId = uint256(-1); + + for (uint256 i = 0; i < length; i++) { + lpToken = masterChefV2.lpToken(i); + if (pair == lpToken) { + poolId = i; + break; + } + } + + uint256 lengthV1 = masterChef.poolLength(); + for (uint256 i = 0; i < lengthV1; i++) { + (lpToken, , , ) = masterChef.poolInfo(i); + if (pair == lpToken) { + poolId = i; + version = 1; + break; + } + } + } + + function _getUserInfo(uint256 _pid, uint256 _version) + internal + view + returns (uint256 lpAmount, uint256 rewardsAmount) + { + if(_version == 2) + (lpAmount, rewardsAmount) = masterChefV2.userInfo(_pid, address(this)); + else + (lpAmount, rewardsAmount) = masterChef.userInfo(_pid, address(this)); + } +} diff --git a/contracts/mainnet/connectors/sushi-incentive/interface.sol b/contracts/mainnet/connectors/sushi-incentive/interface.sol new file mode 100644 index 00000000..865ec361 --- /dev/null +++ b/contracts/mainnet/connectors/sushi-incentive/interface.sol @@ -0,0 +1,94 @@ +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; +import "./libraries/IERC20.sol"; + +struct UserInfo { + uint256 amount; + uint256 rewardDebt; +} + +struct PoolInfo { + IERC20 lpToken; // Address of LP token contract. + uint256 allocPoint; // How many allocation points assigned to this pool. SUSHIs to distribute per block. + uint256 lastRewardBlock; // Last block number that SUSHIs distribution occurs. + uint256 accSushiPerShare; // Accumulated SUSHIs per share, times 1e12. See below. +} + +interface IMasterChef { + function poolLength() external view returns (uint256); + + function updatePool(uint256 pid) external returns (PoolInfo memory); + + function poolInfo(uint256 pid) external view returns (address, uint256, uint256, uint256); + + function userInfo(uint256 _pid, address _user) + external + view + returns (uint256, uint256); + + function deposit( + uint256 pid, + uint256 amount + ) external; + + function withdraw( + uint256 pid, + uint256 amount + ) external; + + function emergencyWithdraw(uint256 pid, address to) external; +} + +interface IMasterChefV2 { + function poolLength() external view returns (uint256); + + function updatePool(uint256 pid) external returns (PoolInfo memory); + + function lpToken(uint256 pid) external view returns (address); + + function userInfo(uint256 _pid, address _user) + external + view + returns (uint256, uint256); + + function deposit( + uint256 pid, + uint256 amount, + address to + ) external; + + function withdraw( + uint256 pid, + uint256 amount, + address to + ) external; + + function emergencyWithdraw(uint256 pid, address to) external; + + function harvest(uint256 pid, address to) external; + + function withdrawAndHarvest( + uint256 pid, + uint256 amount, + address to + ) external; +} + +interface ISushiSwapFactory { + function getPair(address tokenA, address tokenB) + external + view + returns (address pair); + + function allPairs(uint256) external view returns (address pair); + + function allPairsLength() external view returns (uint256); + + 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/sushi-incentive/main.sol b/contracts/mainnet/connectors/sushi-incentive/main.sol new file mode 100644 index 00000000..a60e6940 --- /dev/null +++ b/contracts/mainnet/connectors/sushi-incentive/main.sol @@ -0,0 +1,146 @@ +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +/** + * @title SushiSwap Double Incentive. + * @dev Decentralized Exchange. + */ + +import {TokenInterface} from "../../common/interfaces.sol"; +import {Helpers} from "./helpers.sol"; +import {Events} from "./events.sol"; + +abstract contract SushipswapIncentiveResolver is Helpers, Events { + /** + * @dev deposit LP token to masterChef + * @param token1 token1 of LP token + * @param token2 token2 of LP token + * @param amount amount of LP token + * @param getId ID to retrieve amount + * @param setId ID stores Pool ID + */ + function deposit( + address token1, + address token2, + uint256 amount, + uint256 getId, + uint256 setId + ) external { + amount = getUint(getId, amount); + (uint256 _pid, uint256 _version, address lpTokenAddr) = _getPoolId( + token1, + token2 + ); + setUint(setId, _pid); + require(_pid != uint256(-1), "pool-does-not-exist"); + TokenInterface lpToken = TokenInterface(lpTokenAddr); + lpToken.approve(address(masterChef), amount); + _deposit(_pid, amount, _version); + emit LogDeposit(address(this), _pid, _version, amount); + } + + /** + * @dev withdraw LP token from masterChef + * @param token1 token1 of LP token + * @param token2 token2 of LP token + * @param amount amount of LP token + * @param getId ID to retrieve amount + * @param setId ID stores Pool ID + */ + function withdraw( + address token1, + address token2, + uint256 amount, + uint256 getId, + uint256 setId + ) external { + amount = getUint(getId, amount); + (uint256 _pid, uint256 _version, ) = _getPoolId(token1, token2); + setUint(setId, _pid); + require(_pid != uint256(-1), "pool-does-not-exist"); + _withdraw(_pid, amount, _version); + emit LogWithdraw(address(this), _pid, _version, amount); + } + + /** + * @dev harvest from masterChef + * @param token1 token1 deposited of LP token + * @param token2 token2 deposited LP token + * @param setId ID stores Pool ID + */ + function harvest( + address token1, + address token2, + uint256 setId + ) external { + (uint256 _pid, uint256 _version, ) = _getPoolId(token1, token2); + setUint(setId, _pid); + require(_pid != uint256(-1), "pool-does-not-exist"); + (, uint256 rewardsAmount) = _getUserInfo(_pid, _version); + if (_version == 2) _harvest(_pid); + else _withdraw(_pid, 0, _version); + emit LogHarvest(address(this), _pid, _version, rewardsAmount); + } + + /** + * @dev withdraw LP token and harvest from masterChef + * @param token1 token1 of LP token + * @param token2 token2 of LP token + * @param amount amount of LP token + * @param getId ID to retrieve amount + * @param setId ID stores Pool ID + */ + function withdrawAndHarvest( + address token1, + address token2, + uint256 amount, + uint256 getId, + uint256 setId + ) external { + amount = getUint(getId, amount); + (uint256 _pid, uint256 _version, ) = _getPoolId(token1, token2); + setUint(setId, _pid); + require(_pid != uint256(-1), "pool-does-not-exist"); + (, uint256 rewardsAmount) = _getUserInfo(_pid, _version); + _withdrawAndHarvest(_pid, amount, _version); + emit LogWithdrawAndHarvest( + address(this), + _pid, + _version, + amount, + rewardsAmount + ); + } + + /** + * @dev emergency withdraw from masterChef + * @param token1 token1 deposited of LP token + * @param token2 token2 deposited LP token + * @param setId ID stores Pool ID + */ + function emergencyWithdraw( + address token1, + address token2, + uint256 setId + ) external { + (uint256 _pid, uint256 _version, ) = _getPoolId(token1, token2); + setUint(setId, _pid); + require(_pid != uint256(-1), "pool-does-not-exist"); + (uint256 lpAmount, uint256 rewardsAmount) = _getUserInfo( + _pid, + _version + ); + _emergencyWithdraw(_pid, _version); + emit LogEmergencyWithdraw( + address(this), + _pid, + _version, + lpAmount, + rewardsAmount + ); + } +} + +contract ConnectV2SushiswapIncentive is SushipswapIncentiveResolver { + string public constant name = "SushipswapIncentive-v1.1"; +} diff --git a/hardhat.config.js b/hardhat.config.js new file mode 100644 index 00000000..7d88c978 --- /dev/null +++ b/hardhat.config.js @@ -0,0 +1,90 @@ +require("@nomiclabs/hardhat-waffle"); +require("@nomiclabs/hardhat-ethers"); +require("@tenderly/hardhat-tenderly"); +require("@nomiclabs/hardhat-etherscan"); +require("@nomiclabs/hardhat-web3"); +require("hardhat-deploy"); +require("hardhat-deploy-ethers"); +require("dotenv").config(); + +const { utils } = require("ethers"); + +const PRIVATE_KEY = process.env.PRIVATE_KEY; +const ALCHEMY_ID = process.env.ALCHEMY_ID; +const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; + +if (!process.env.ALCHEMY_ID) { + throw new Error("ENV Variable ALCHEMY_ID not set!"); +} + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +module.exports = { + solidity: { + compilers: [ + { + version: "0.7.6", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + { + version: "0.6.0", + }, + { + version: "0.6.2", + }, + { + version: "0.6.12", + }, + { + version: "0.6.5", + }, + ], + }, + networks: { + // defaultNetwork: "hardhat", + kovan: { + url: `https://eth-kovan.alchemyapi.io/v2/${ALCHEMY_ID}`, + accounts: [`0x${PRIVATE_KEY}`], + }, + mainnet: { + url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_ID}`, + accounts: [`0x${PRIVATE_KEY}`], + timeout: 150000, + gasPrice: parseInt(utils.parseUnits("30", "gwei")), + }, + rinkeby: { + url: `https://eth-rinkeby.alchemyapi.io/v2/${ALCHEMY_ID}`, + accounts: [`0x${PRIVATE_KEY}`], + timeout: 150000, + }, + hardhat: { + forking: { + url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_ID}`, + blockNumber: 12696000, + }, + blockGasLimit: 12000000, + }, + matic: { + url: "https://rpc-mainnet.maticvigil.com/", + accounts: [`0x${PRIVATE_KEY}`], + timeout: 150000, + gasPrice: parseInt(utils.parseUnits("1", "gwei")), + }, + }, + etherscan: { + apiKey: ETHERSCAN_API_KEY, + }, + tenderly: { + project: process.env.TENDERLY_PROJECT, + username: process.env.TENDERLY_USERNAME, + }, + mocha: { + timeout: 100 * 1000, + }, +}; \ No newline at end of file diff --git a/test/sushiswap/sushiswap.test.js b/test/sushiswap/sushiswap.test.js index ead9dfd3..baaf3876 100644 --- a/test/sushiswap/sushiswap.test.js +++ b/test/sushiswap/sushiswap.test.js @@ -1,44 +1,21 @@ const { expect } = require("chai"); const hre = require("hardhat"); -const { web3, deployments, waffle, ethers } = hre; -const { provider, deployContract } = waffle +const { waffle, ethers } = hre; +const { provider } = 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" @@ -134,30 +111,6 @@ describe("Sushiswap", function () { 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 () { diff --git a/test/sushiswapIncentive/sushiIncentive.test.js b/test/sushiswapIncentive/sushiIncentive.test.js new file mode 100644 index 00000000..1b2fd01b --- /dev/null +++ b/test/sushiswapIncentive/sushiIncentive.test.js @@ -0,0 +1,237 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); +const { waffle, ethers } = hre; +const { provider } = waffle + +const deployAndEnableConnector = require("../../scripts/deployAndEnableConnector.js") +const buildDSAv2 = require("../../scripts/buildDSAv2") +const encodeSpells = require("../../scripts/encodeSpells.js") +const getMasterSigner = require("../../scripts/getMasterSigner") +const addLiquidity = require("../../scripts/addLiquidity"); + +const addresses = require("../../scripts/constant/addresses"); +const abis = require("../../scripts/constant/abis"); + +const connectV2SushiswapArtifacts = require("../../artifacts/contracts/mainnet/connectors/sushiswap/main.sol/ConnectV2Sushiswap.json"); +const connectV2SushiswapIncentiveArtifacts = require("../../artifacts/contracts/mainnet/connectors/sushi-incentive/main.sol/ConnectV2SushiswapIncentive.json"); + +const DAI_ADDR = "0x6b175474e89094c44da98b954eedeac495271d0f" +const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + +describe("Sushiswap", function () { + const connectorName = "Sushiswap-v1" + const incentiveConnectorName = "Sushiswp-Incentive-v1" + + let dsaWallet0 + let masterSigner; + let instaConnectorsV2; + let connector, connectorIncentive; + + 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) + + connectorIncentive = await deployAndEnableConnector({ + connectorName: incentiveConnectorName, + contractArtifact: connectV2SushiswapIncentiveArtifacts, + signer: masterSigner, + connectors: instaConnectorsV2 + }) + console.log("Incentive Connector address", connectorIncentive.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("2") // 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) + await tx.wait() + + describe("Incentive", () => { + it("Should deposit successfully", async () => { + const getId = 0 + const setId = 0 + const spells = [ + { + connector: incentiveConnectorName, + method: "deposit", + args: [ + WETH_ADDR, + DAI_ADDR, + ethers.utils.parseEther("10"), + getId, + setId + ] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet0.address) + await tx.wait(); + }) + + it("Should harvest successfully", async () => { + const setId = 0 + const spells = [ + { + connector: incentiveConnectorName, + method: "harvest", + args: [ + WETH_ADDR, + DAI_ADDR, + setId + ] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet0.address) + await tx.wait(); + }) + + it("Should harvest and withdraw successfully", async () => { + const getId = 0 + const setId = 0 + const spells = [ + { + connector: incentiveConnectorName, + method: "withdrawAndHarvest", + args: [ + WETH_ADDR, + DAI_ADDR, + ethers.utils.parseEther("1"), + getId, + setId + ] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet0.address) + await tx.wait(); + }) + + it("Should withdraw successfully", async () => { + const getId = 0 + const setId = 0 + const spells = [ + { + connector: incentiveConnectorName, + method: "withdraw", + args: [ + WETH_ADDR, + DAI_ADDR, + ethers.utils.parseEther("1"), + getId, + setId + ] + } + ] + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet0.address) + await tx.wait(); + }) + }) + }).timeout(10000000000); + + 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