diff --git a/contracts/mainnet/connectors/euler/import/events.sol b/contracts/mainnet/connectors/euler/import/events.sol new file mode 100644 index 00000000..8d7db584 --- /dev/null +++ b/contracts/mainnet/connectors/euler/import/events.sol @@ -0,0 +1,16 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +contract Events { + event LogEulerImport( + address user, + uint256 sourceId, + uint256 targetId, + address[] supplyTokens, + uint256[] supplyAmounts, + address[] borrowTokens, + uint256[] borrowAmounts, + bool[] enterMarket + ); +} diff --git a/contracts/mainnet/connectors/euler/import/helpers.sol b/contracts/mainnet/connectors/euler/import/helpers.sol new file mode 100644 index 00000000..09f355b6 --- /dev/null +++ b/contracts/mainnet/connectors/euler/import/helpers.sol @@ -0,0 +1,111 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; +import { TokenInterface, AccountInterface } from "../../../common/interfaces.sol"; +import { Basic } from "../../../common/basic.sol"; +import "./interface.sol"; + +contract EulerHelpers is Basic { + /** + * @dev Euler's Market Module + */ + IEulerMarkets internal constant markets = + IEulerMarkets(0x3520d5a913427E6F0D6A83E07ccD4A4da316e4d3); + + /** + * @dev Euler's Execution Module + */ + IEulerExecute internal constant eulerExec = + IEulerExecute(0x59828FdF7ee634AaaD3f58B19fDBa3b03E2D9d80); + + /** + * @dev Compute sub account address. + * @notice Compute sub account address from sub-account id + * @param primary primary address + * @param subAccountId sub-account id whose address needs to be computed + */ + function getSubAccountAddress(address primary, uint256 subAccountId) + public + pure + returns (address) + { + require(subAccountId < 256, "sub-account-id-too-big"); + return address(uint160(primary) ^ uint160(subAccountId)); + } + + struct ImportInputData { + address[] _supplyTokens; + address[] _borrowTokens; + bool[] _enterMarket; + } + + struct ImportData { + address[] supplyTokens; + address[] borrowTokens; + EulerTokenInterface[] eTokens; + EulerTokenInterface[] dTokens; + uint256[] supplyAmts; + uint256[] borrowAmts; + } + + struct ImportHelper { + uint256 supplylength; + uint256 borrowlength; + uint256 totalExecutions; + address sourceAccount; + address targetAccount; + } + + function getSupplyAmounts( + address userAccount, // user's EOA sub-account address + ImportInputData memory inputData, + ImportData memory data + ) internal view returns (ImportData memory) { + data.supplyAmts = new uint256[](inputData._supplyTokens.length); + data.supplyTokens = new address[](inputData._supplyTokens.length); + data.eTokens = new EulerTokenInterface[]( + inputData._supplyTokens.length + ); + uint256 length_ = inputData._supplyTokens.length; + + for (uint256 i = 0; i < length_; i++) { + address token_ = inputData._supplyTokens[i] == ethAddr + ? wethAddr + : inputData._supplyTokens[i]; + data.supplyTokens[i] = token_; + data.eTokens[i] = EulerTokenInterface( + markets.underlyingToEToken(token_) + ); + data.supplyAmts[i] = data.eTokens[i].balanceOf(userAccount); //All 18 dec + } + + return data; + } + + function getBorrowAmounts( + address userAccount, // user's EOA sub-account address + ImportInputData memory inputData, + ImportData memory data + ) internal view returns (ImportData memory) { + uint256 borrowTokensLength_ = inputData._borrowTokens.length; + + if (borrowTokensLength_ > 0) { + data.borrowTokens = new address[](borrowTokensLength_); + data.dTokens = new EulerTokenInterface[](borrowTokensLength_); + data.borrowAmts = new uint256[](borrowTokensLength_); + + for (uint256 i = 0; i < borrowTokensLength_; i++) { + address _token = inputData._borrowTokens[i] == ethAddr + ? wethAddr + : inputData._borrowTokens[i]; + + data.borrowTokens[i] = _token; + data.dTokens[i] = EulerTokenInterface( + markets.underlyingToDToken(_token) + ); + data.borrowAmts[i] = data.dTokens[i].balanceOf(userAccount); + } + } + return data; + } +} diff --git a/contracts/mainnet/connectors/euler/import/interface.sol b/contracts/mainnet/connectors/euler/import/interface.sol new file mode 100644 index 00000000..3cf7144f --- /dev/null +++ b/contracts/mainnet/connectors/euler/import/interface.sol @@ -0,0 +1,56 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +interface EulerTokenInterface { + function balanceOf(address _user) external view returns (uint256); + + function transferFrom( + address, + address, + uint256 + ) external returns (bool); + + function allowance(address, address) external returns (uint256); +} + +interface IEulerMarkets { + function enterMarket(uint256 subAccountId, address newMarket) external; + + function getEnteredMarkets(address account) + external + view + returns (address[] memory); + + function exitMarket(uint256 subAccountId, address oldMarket) external; + + function underlyingToEToken(address underlying) + external + view + returns (address); + + function underlyingToDToken(address underlying) + external + view + returns (address); +} + +interface IEulerExecute { + struct EulerBatchItem { + bool allowError; + address proxyAddr; + bytes data; + } + + struct EulerBatchItemResponse { + bool success; + bytes result; + } + + function batchDispatch( + EulerBatchItem[] calldata items, + address[] calldata deferLiquidityChecks + ) external; + + function deferLiquidityCheck(address account, bytes memory data) external; +} diff --git a/contracts/mainnet/connectors/euler/import/main.sol b/contracts/mainnet/connectors/euler/import/main.sol new file mode 100644 index 00000000..c6e1724b --- /dev/null +++ b/contracts/mainnet/connectors/euler/import/main.sol @@ -0,0 +1,155 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; +import "./helpers.sol"; +import "./interface.sol"; +import "./events.sol"; + +contract EulerImport is EulerHelpers { + /** + * @dev Import Euler position . + * @notice Import EOA's Euler position to DSA's Euler position + * @param userAccount EOA address + * @param sourceId Sub-account id of "EOA" from which the funds will be transferred + * @param targetId Sub-account id of "DSA" to which the funds will be transferred + * @param inputData The struct containing all the neccessary input data + */ + function importEuler( + address userAccount, + uint256 sourceId, + uint256 targetId, + ImportInputData memory inputData + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + require(sourceId < 256 && targetId < 256, "Id should be less than 256"); + + (_eventName, _eventParam) = _importEuler( + userAccount, + sourceId, + targetId, + inputData + ); + } + + /** + * @dev Import Euler position . + * @notice Import EOA's Euler position to DSA's Euler position + * @param userAccount EOA address + * @param sourceId Sub-account id of "EOA" from which the funds will be transferred + * @param targetId Sub-account id of "DSA" to which the funds will be transferred + * @param inputData The struct containing all the neccessary input data + */ + function _importEuler( + address userAccount, + uint256 sourceId, + uint256 targetId, + ImportInputData memory inputData + ) internal returns (string memory _eventName, bytes memory _eventParam) { + require(inputData._supplyTokens.length > 0, "0-length-not-allowed"); + require( + AccountInterface(address(this)).isAuth(userAccount), + "user-account-not-auth" + ); + require( + inputData._enterMarket.length == inputData._supplyTokens.length, + "lengths-not-same" + ); + + ImportData memory data; + ImportHelper memory helper; + + helper.sourceAccount = getSubAccountAddress(userAccount, sourceId); + helper.targetAccount = getSubAccountAddress(address(this), targetId); + + // BorrowAmts will be in underlying token decimals + data = getBorrowAmounts(helper.sourceAccount, inputData, data); + + // SupplyAmts will be in 18 decimals + data = getSupplyAmounts(helper.sourceAccount, inputData, data); + + helper.supplylength = data.supplyTokens.length; + helper.borrowlength = data.borrowTokens.length; + uint16 enterMarketsLength = 0; + + for (uint16 i = 0; i < inputData._enterMarket.length; i++) { + if (inputData._enterMarket[i]) { + ++enterMarketsLength; + } + } + + helper.totalExecutions = + helper.supplylength + + enterMarketsLength + + helper.borrowlength; + + IEulerExecute.EulerBatchItem[] + memory items = new IEulerExecute.EulerBatchItem[]( + helper.totalExecutions + ); + + uint16 k = 0; + + for (uint16 i = 0; i < helper.supplylength; i++) { + items[k++] = IEulerExecute.EulerBatchItem({ + allowError: false, + proxyAddr: address(data.eTokens[i]), + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + helper.sourceAccount, + helper.targetAccount, + data.supplyAmts[i] + ) + }); + + if (inputData._enterMarket[i]) { + items[k++] = IEulerExecute.EulerBatchItem({ + allowError: false, + proxyAddr: address(markets), + data: abi.encodeWithSignature( + "enterMarket(uint256,address)", + targetId, + data.supplyTokens[i] + ) + }); + } + } + + for (uint16 j = 0; j < helper.borrowlength; j++) { + items[k++] = IEulerExecute.EulerBatchItem({ + allowError: false, + proxyAddr: address(data.dTokens[j]), + data: abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + helper.sourceAccount, + helper.targetAccount, + data.borrowAmts[j] + ) + }); + } + + address[] memory deferLiquidityChecks = new address[](2); + deferLiquidityChecks[0] = helper.sourceAccount; + deferLiquidityChecks[1] = helper.targetAccount; + + eulerExec.batchDispatch(items, deferLiquidityChecks); + + _eventName = "LogEulerImport(address,uint256,uint256,address[],uint256[],address[],uint256[],bool[])"; + _eventParam = abi.encode( + userAccount, + sourceId, + targetId, + inputData._supplyTokens, + data.supplyAmts, + inputData._borrowTokens, + data.borrowAmts, + inputData._enterMarket + ); + } +} + +contract ConnectV2EulerImport is EulerImport { + string public constant name = "Euler-Import-v1.0"; +} diff --git a/test/mainnet/euler-import/euler-import.test.ts b/test/mainnet/euler-import/euler-import.test.ts new file mode 100644 index 00000000..b50500f1 --- /dev/null +++ b/test/mainnet/euler-import/euler-import.test.ts @@ -0,0 +1,268 @@ +import { expect } from "chai"; +import hre from "hardhat"; +import { abis } from "../../../scripts/constant/abis"; +import { addresses } from "../../../scripts/tests/mainnet/addresses"; +import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; +import { getMasterSigner } from "../../../scripts/tests/getMasterSigner"; +import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2"; +import { ConnectV2EulerImport__factory, IERC20__factory } from "../../../typechain"; +import { parseEther, parseUnits } from "@ethersproject/units"; +import { encodeSpells } from "../../../scripts/tests/encodeSpells"; +const { ethers } = hre; +import type { Signer, Contract } from "ethers"; +import { BigNumber } from "bignumber.js"; +import { Address } from "@project-serum/anchor"; + +const DAI = '0x6b175474e89094c44da98b954eedeac495271d0f' +const Dai = parseUnits('50', 18) + +const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' +const ACC_WETH = '0x05547D4e1A2191B91510Ea7fA8555a2788C70030' +const Weth = parseUnits('50', 18) + +const ETH = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' + +const token_weth = new ethers.Contract( + WETH, + [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}], + // IERC20__factory.abi, + ethers.provider, +) + +const token_dai = new ethers.Contract( + DAI, + IERC20__factory.abi, + ethers.provider, +) + +const eTokensABI = [ + "function approve(address, uint256) public", + "function balanceOf(address account) public view returns (uint256)", + "function allowance(address, address) public returns (uint256)", + "function deposit(uint256,uint256) public", + "function balanceOfUnderlying(address) public view returns (uint256)", + "function mint(uint256,uint256) public", + "function approveSubAccount(uint256, address, uint256) public" +]; + +const dTokensABI = [ + "function balanceOf(address account) public view returns (uint256)", + "function borrow(uint256,uint256) public" +]; + +const marketsABI = [ + "function enterMarket(uint256,address) public", + "function underlyingToEToken(address) public view returns (address)", + "function underlyingToDToken(address) public view returns (address)" +] + +const eWethAddress = '0x1b808F49ADD4b8C6b5117d9681cF7312Fcf0dC1D'; +const eWethContract = new ethers.Contract(eWethAddress, eTokensABI); + +const dWethAddress = '0x62e28f054efc24b26A794F5C1249B6349454352C' +const dWethContract = new ethers.Contract(dWethAddress, dTokensABI); + +const dDaiAddress = '0x6085Bc95F506c326DCBCD7A6dd6c79FBc18d4686'; +const dDaiContract = new ethers.Contract(dDaiAddress, dTokensABI); + +const euler_mainnet = '0x27182842E098f60e3D576794A5bFFb0777E025d3' +const euler_markets = '0x3520d5a913427E6F0D6A83E07ccD4A4da316e4d3' +const marketsContract = new ethers.Contract(euler_markets, marketsABI); + + +describe("Euler", function () { + const connectorName = "EULER-IMPORT-TEST-A"; + let connector: any; + + let wallet0: Signer, wallet1:Signer; + let dsaWallet0: any; + let instaConnectorsV2: Contract; + let masterSigner: Signer; + let walletAddr: Address; + let subAcc1: Address; + let subAcc2DSA: Address; + + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + // @ts-ignore + jsonRpcUrl: hre.config.networks.hardhat.forking.url, + blockNumber: 15379000, + }, + }, + ], + }); + [wallet0, wallet1] = await ethers.getSigners(); + + await hre.network.provider.send("hardhat_setBalance", [ACC_WETH, ethers.utils.parseEther("10").toHexString()]); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [ACC_WETH] + }); + + const signer_weth = await ethers.getSigner(ACC_WETH) + await token_weth.connect(signer_weth).transfer(wallet0.getAddress(), ethers.utils.parseEther("8")); + console.log("WETH transferred to wallet0"); + + await hre.network.provider.request({ + method: 'hardhat_stopImpersonatingAccount', + params: [ACC_WETH], + }) + + masterSigner = await getMasterSigner(); + instaConnectorsV2 = await ethers.getContractAt( + abis.core.connectorsV2, + addresses.core.connectorsV2 + ); + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: ConnectV2EulerImport__factory, + signer: masterSigner, + connectors: instaConnectorsV2, + }); + console.log("Connector address", connector.address); + walletAddr = (await wallet0.getAddress()).toString() + console.log("walletAddr: ", walletAddr) + subAcc1 = ethers.BigNumber.from(walletAddr).xor(1).toHexString() + console.log("subAcc1: ", subAcc1) + }); + + it("should have contracts deployed", async () => { + expect(!!instaConnectorsV2.address).to.be.true; + expect(!!connector.address).to.be.true; + expect(!!(await masterSigner.getAddress())).to.be.true; + }); + + describe("DSA wallet setup", function () { + it("Should build DSA v2", async function () { + dsaWallet0 = await buildDSAv2(wallet0.getAddress()); + expect(!!dsaWallet0.address).to.be.true; + + subAcc2DSA = ethers.BigNumber.from(dsaWallet0.address).xor(2).toHexString() + console.log("subAcc2DSA: ", subAcc2DSA) + }); + + it("Deposit ETH into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: parseEther("10"), + }); + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte( + parseEther("10") + ); + }); + + describe("Create Euler position in SUBACCOUNT 0", async () => { + it("Should create Euler position of WETH(collateral) and DAI(debt)", async () => { + // approve WETH to euler + await token_weth.connect(wallet0).approve(euler_mainnet, Weth); + console.log("Approved WETH"); + + // deposit WETH in euler + await eWethContract.connect(wallet0).deposit("0", parseEther("2")); + expect(await eWethContract.connect(wallet0).balanceOfUnderlying(walletAddr)).to.be.gte(parseEther("1.9")); + console.log("Supplied WETH on Euler"); + + // enter WETH market + await marketsContract.connect(wallet0).enterMarket("0", WETH); + console.log("Entered market for WETH"); + + // borrow DAI from Euler + await dDaiContract.connect(wallet0).borrow("0", Dai); + console.log("Borrowed DAI from Euler"); + }); + + it("Should check created position of user", async () => { + expect(await token_dai.connect(wallet0).balanceOf(walletAddr)).to.be.gte( + parseUnits('50', 18) + ); + }); + }); + + describe("Create Euler self-position in SUBACCOUNT 1", async () => { + it("Should create Euler self-position of WETH(collateral) and WETH(debt)", async () => { + // approve WETH to euler + await token_weth.connect(wallet0).approve(euler_mainnet, Weth); + console.log("Approved WETH"); + + // deposit WETH in euler + await eWethContract.connect(wallet0).deposit("1", parseEther("2")); + expect(await eWethContract.connect(wallet0).balanceOfUnderlying(subAcc1)).to.be.gte(parseEther("1.9")); + console.log("Supplied WETH on Euler"); + + // enter WETH market + await marketsContract.connect(wallet0).enterMarket("1", WETH); + console.log("Entered market for WETH"); + + // mint WETH from Euler + await eWethContract.connect(wallet0).mint("1", parseEther("1")); + expect(await eWethContract.connect(wallet0).balanceOfUnderlying(subAcc1)).to.be.gte(parseEther("2.9")); + console.log("Minted WETH from Euler"); + }); + + it("Should check created position of user", async () => { + expect(await eWethContract.connect(wallet0).balanceOfUnderlying(subAcc1)).to.be.gte(parseEther("2.9")); + }); + }); + + describe("Euler position migration", async () => { + it("Approve sub-account0 eTokens for import to DSA sub-account 0", async () => { + let balance = await eWethContract.connect(wallet0).balanceOf(walletAddr) + await eWethContract.connect(wallet0).approve(dsaWallet0.address, balance); + }); + + it("Approve sub-account1 eTokens for import to DSA sub-account 2", async () => { + let balance = await eWethContract.connect(wallet0).balanceOf(subAcc1) + await eWethContract.connect(wallet0).approveSubAccount("1", dsaWallet0.address, balance); + }); + + it("Should migrate euler position of sub-account 0 to DSA sub-account 0", async () => { + const spells = [ + { + connector: "EULER-IMPORT-TEST-A", + method: "importEuler", + args: [ + walletAddr, + "0", + "0", + [[ETH],[DAI],["true"]] + ] + }, + ]; + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet0.getAddress()); + const receipt = await tx.wait(); + }); + + it("Should check migration", async () => { + expect(await eWethContract.connect(wallet0).balanceOfUnderlying(dsaWallet0.address)).to.be.gte(parseEther("2")); + expect(await dDaiContract.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte(parseEther("50")); + }); + + it("Should migrate euler position of sub-account 1 to DSA sub-account 2", async () => { + const spells = [ + { + connector: "EULER-IMPORT-TEST-A", + method: "importEuler", + args: [ + walletAddr, + "1", + "2", + [[ETH],[ETH],["true"]] + ] + }, + ]; + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet0.getAddress()); + const receipt = await tx.wait(); + }); + + it("Should check migration", async () => { + expect(await eWethContract.connect(wallet0).balanceOfUnderlying(subAcc2DSA)).to.be.gte(parseEther("3")); + expect(await dWethContract.connect(wallet0).balanceOf(subAcc2DSA)).to.be.gte(parseEther("1")); + }); + }) +}); +})