// running `npx hardhat test` automatically makes use of hardhat-waffle plugin // => only dependency we need is "chai" const {expect} = require("chai"); const hre = require("hardhat"); const {deployments, ethers} = hre; const GelatoCoreLib = require("@gelatonetwork/core"); //const { sleep } = GelatoCoreLib; // Constants const DAI_100 = ethers.utils.parseUnits("100", 18); // Contracts const InstaIndex = require("../pre-compiles/InstaIndex.json"); const InstaList = require("../pre-compiles/InstaList.json"); const InstaAccount = require("../pre-compiles/InstaAccount.json"); const ConnectAuth = require("../pre-compiles/ConnectAuth.json"); const ConnectGelato_ABI = require("../pre-compiles/ConnectGelato_ABI.json"); const ConnectMaker = require("../pre-compiles/ConnectMaker.json"); const ConnectCompound = require("../pre-compiles/ConnectCompound.json"); const IERC20 = require("../pre-compiles/IERC20.json"); const IUniswapExchange = require("../pre-compiles/IUniswapExchange.json"); describe("Move DAI lending from DSR to Compound", function () { this.timeout(0); if (hre.network.name !== "hardhat") { console.error("Test Suite is meant to be run on hardhat only"); process.exit(1); } // Wallet to use for local testing let userWallet; let userAddress; let dsaAddress; // Deployed instances let connectMaker; let connectCompound; let gelatoCore; let dai; // Contracts to deploy and use for local testing let dsa; let mockDSR; let mockCDAI; let conditionCompareUints; before(async function () { // Reset back to a fresh forked state during runtime await deployments.fixture(); // Get Test Wallet for local testnet [userWallet] = await ethers.getSigners(); userAddress = await userWallet.getAddress(); // Hardhat default accounts prefilled with 100 ETH expect(await userWallet.getBalance()).to.be.gt( ethers.utils.parseEther("10") ); // ===== DSA SETUP ================== const instaIndex = await ethers.getContractAt( InstaIndex.abi, hre.network.config.InstaIndex ); const instaList = await ethers.getContractAt( InstaList.abi, hre.network.config.InstaList ); connectMaker = await ethers.getContractAt( ConnectMaker.abi, hre.network.config.ConnectMaker ); connectCompound = await ethers.getContractAt( ConnectCompound.abi, hre.network.config.ConnectCompound ); // Deploy DSA and get and verify ID of newly deployed DSA const dsaIDPrevious = await instaList.accounts(); await expect(instaIndex.build(userAddress, 1, userAddress)).to.emit( instaIndex, "LogAccountCreated" ); const dsaID = dsaIDPrevious.add(1); await expect(await instaList.accounts()).to.be.equal(dsaID); // Instantiate the DSA dsaAddress = await instaList.accountAddr(dsaID); dsa = await ethers.getContractAt(InstaAccount.abi, dsaAddress); // ===== GELATO SETUP ================== gelatoCore = await ethers.getContractAt( GelatoCoreLib.GelatoCore.abi, hre.network.config.GelatoCore ); // Add GelatoCore as auth on DSA const addAuthData = await hre.run("abi-encode-withselector", { abi: ConnectAuth.abi, functionname: "add", inputs: [gelatoCore.address], }); await dsa.cast( [hre.network.config.ConnectAuth], [addAuthData], userAddress ); expect(await dsa.isAuth(gelatoCore.address)).to.be.true; // Deployed Mocks for Testing mockCDAI = await ethers.getContract("MockCDAI"); mockDSR = await ethers.getContract("MockDSR"); // Deployed Gelato Conditions for Testing conditionCompareUints = await ethers.getContract( "ConditionCompareUintsFromTwoSources" ); // ===== Dapp Dependencies SETUP ================== // This test assumes our user has 100 DAI deposited in Maker DSR dai = await ethers.getContractAt(IERC20.abi, hre.network.config.DAI); expect(await dai.balanceOf(userAddress)).to.be.equal(0); // Let's get the test user 100 DAI++ from Kyber const daiUniswapExchange = await ethers.getContractAt( IUniswapExchange.abi, hre.network.config.DAI_UNISWAP ); await daiUniswapExchange.ethToTokenTransferInput( 1, 2525644800, // random timestamp in the future (year 2050) userAddress, { value: ethers.utils.parseEther("2"), } ); expect(await dai.balanceOf(userAddress)).to.be.gte(DAI_100); // Next we transfer the 100 DAI into our DSA await dai.transfer(dsa.address, DAI_100); expect(await dai.balanceOf(dsa.address)).to.be.eq(DAI_100); // Next we deposit the 100 DAI into the DSR const depositDai = await hre.run("abi-encode-withselector", { abi: ConnectMaker.abi, functionname: "depositDai", inputs: [DAI_100, 0, 0], }); await expect( dsa.cast([hre.network.config.ConnectMaker], [depositDai], userAddress) ) .to.emit(dsa, "LogCast") .withArgs(userAddress, userAddress, 0); expect(await dai.balanceOf(dsa.address)).to.be.eq(0); }); it("#1: Gelato refinances DAI from DSR=>Compound, if better rate", async function () { // ======= Condition setup ====== // We instantiate the Rebalance Condition: // Compound APY needs to be 10000000 per second points higher than DSR const MIN_SPREAD = "10000000"; const rebalanceCondition = new GelatoCoreLib.Condition({ inst: conditionCompareUints.address, data: await conditionCompareUints.getConditionData( mockCDAI.address, // We are in DSR so we compare against CDAI => SourceA=CDAI mockDSR.address, // SourceB=DSR await hre.run("abi-encode-withselector", { abi: (await hre.artifacts.readArtifact("MockCDAI")).abi, functionname: "supplyRatePerSecond", }), // CDAI data feed first (sourceAData) await hre.run("abi-encode-withselector", { abi: (await hre.artifacts.readArtifact("MockDSR")).abi, functionname: "dsr", }), // DSR data feed second (sourceBData) MIN_SPREAD ), }); // ======= Action/Spells setup ====== // To assimilate to DSA SDK const spells = []; // We instantiate target1: Withdraw DAI from DSR and setId 1 for // target2 Compound deposit to fetch DAI amount. const connectorWithdrawFromDSR = new GelatoCoreLib.Action({ addr: connectMaker.address, data: await hre.run("abi-encode-withselector", { abi: ConnectMaker.abi, functionname: "withdrawDai", inputs: [ethers.constants.MaxUint256, 0, 1], }), operation: GelatoCoreLib.Operation.Delegatecall, }); spells.push(connectorWithdrawFromDSR); // We instantiate target2: Deposit DAI to CDAI and getId 1 const connectorDepositCompound = new GelatoCoreLib.Action({ addr: connectCompound.address, data: await hre.run("abi-encode-withselector", { abi: ConnectCompound.abi, functionname: "deposit", inputs: [dai.address, 0, 1, 0], }), operation: GelatoCoreLib.Operation.Delegatecall, }); spells.push(connectorDepositCompound); // ======= Gelato Task Setup ========= // A Gelato Task just combines Conditions with Actions // You also specify how much GAS a Task consumes at max and the ceiling // gas price under which you are willing to auto-transact. There is only // one gas price in the current Gelato system: fast gwei read from Chainlink. const GAS_LIMIT = "4000000"; const GAS_PRICE_CEIL = ethers.utils.parseUnits("1000", "gwei"); const taskRebalanceDSRToCDAIifBetter = new GelatoCoreLib.Task({ conditions: [rebalanceCondition], actions: spells, selfProviderGasLimit: GAS_LIMIT, selfProviderGasPriceCeil: GAS_PRICE_CEIL, }); // ======= Gelato Provider setup ====== // Someone needs to pay for gas for automatic Task execution on Gelato. // Gelato has the concept of a "Provider" to denote who is providing (depositing) // ETH on Gelato in order to pay for automation gas. In our case, the User // is paying for his own automation gas. Therefore, the User is a "Self-Provider". // But since Gelato only talks to smart contract accounts, the User's DSA proxy // plays the part of the "Self-Provider" on behalf of the User behind the DSA. // A GelatoProvider is an object with the address of the provider - in our case // the DSA address - and the address of the "ProviderModule". This module // fulfills certain functions like encoding the execution payload for the Gelato // protocol. Check out ./contracts/ProviderModuleDsa.sol to see what it does. const gelatoSelfProvider = new GelatoCoreLib.GelatoProvider({ addr: dsa.address, module: hre.network.config.ProviderModuleDsa, }); // ======= Executor Setup ========= // For local Testing purposes our test User account will play the role of the Gelato // Executor network because this logic is non-trivial to fork into a local instance await gelatoCore.stakeExecutor({ value: await gelatoCore.minExecutorStake(), }); expect(await gelatoCore.isExecutorMinStaked(userAddress)).to.be.true; // ======= Gelato Task Provision ========= // Gelato requires some initial setup via its multiProvide API // We must 1) provide ETH to pay for future automation gas, 2) we must // assign an Executor network to the Task, 3) we must tell Gelato what // "ProviderModule" we want to use for our Task. // Since our DSA proxy is the one through which we interact with Gelato, // we must do this setup via the DSA proxy by using ConnectGelato const TASK_AUTOMATION_FUNDS = await gelatoCore.minExecProviderFunds( GAS_LIMIT, GAS_PRICE_CEIL ); await dsa.cast( [hre.network.config.ConnectGelato], // targets [ await hre.run("abi-encode-withselector", { abi: ConnectGelato_ABI, functionname: "multiProvide", inputs: [ userAddress, [], [hre.network.config.ProviderModuleDsa], TASK_AUTOMATION_FUNDS, 0, // _getId 0, // _setId ], }), ], // datas userAddress, // origin { value: TASK_AUTOMATION_FUNDS, gasLimit: 5000000, } ); expect(await gelatoCore.providerFunds(dsa.address)).to.be.gte( TASK_AUTOMATION_FUNDS ); expect( await gelatoCore.isProviderLiquid(dsa.address, GAS_LIMIT, GAS_PRICE_CEIL) ); expect(await gelatoCore.executorByProvider(dsa.address)).to.be.equal( userAddress ); expect( await gelatoCore.isModuleProvided( dsa.address, hre.network.config.ProviderModuleDsa ) ).to.be.true; // ======= 📣 TASK SUBMISSION 📣 ========= // In Gelato world our DSA is the User. So we must submit the Task // to Gelato via our DSA and hence use ConnectGelato again. const expiryDate = 0; await expect( dsa.cast( [hre.network.config.ConnectGelato], // targets [ await hre.run("abi-encode-withselector", { abi: ConnectGelato_ABI, functionname: "submitTask", inputs: [ gelatoSelfProvider, taskRebalanceDSRToCDAIifBetter, expiryDate, ], }), ], // datas userAddress, // origin { gasLimit: 5000000, } ) ).to.emit(gelatoCore, "LogTaskSubmitted"); // Task Receipt: a successfully submitted Task in Gelato // is wrapped in a TaskReceipt. For testing we instantiate the TaskReceipt // for our to be submitted Task. const taskReceiptId = await gelatoCore.currentTaskReceiptId(); const taskReceipt = new GelatoCoreLib.TaskReceipt({ id: taskReceiptId, userProxy: dsa.address, provider: gelatoSelfProvider, tasks: [taskRebalanceDSRToCDAIifBetter], expiryDate, }); // ======= 📣 TASK EXECUTION 📣 ========= // This stuff is normally automated by the Gelato Network and Dapp Developers // and their Users don't have to take care of it. However, for local testing // we simulate the Gelato Execution logic. // First we fetch the gelatoGasPrice as fed by ChainLink oracle. Gelato // allows Users to specify a maximum fast gwei gas price for their Tasks // to remain executable up until. const gelatoGasPrice = await hre.run("fetchGelatoGasPrice"); expect(gelatoGasPrice).to.be.lte( taskRebalanceDSRToCDAIifBetter.selfProviderGasPriceCeil ); // Let's first check if our Task is executable. Since both MockDSR and MockCDAI // are deployed with a normalized per second rate of APY_2_PERCENT_IN_SECONDS // (1000000000627937192491029810 in 10**27 precision) in both of them, we // expect ConditionNotOk because ANotGreaterOrEqualToBbyMinspread. // Check out contracts/ConditionCompareUintsFromTwoSources.sol to see how // how the comparison of MockDSR and MockCDAI is implemented in Condition code. expect( await gelatoCore.canExec( taskReceipt, taskRebalanceDSRToCDAIifBetter.selfProviderGasLimit, gelatoGasPrice ) ).to.be.equal("ConditionNotOk:ANotGreaterOrEqualToBbyMinspread"); // We defined a MIN_SPREAD of 10000000 points in the per second rate // for our ConditionCompareUintsFromTwoSources. So we now // set the CDAI.supplyRatePerSecond to be 10000000 higher than MockDSR.dsr // and expect it to mean that our Task becomes executable. await mockCDAI.setSupplyRatePerSecond( (await mockDSR.dsr()).add(MIN_SPREAD) ); expect( await gelatoCore.canExec( taskReceipt, taskRebalanceDSRToCDAIifBetter.selfProviderGasLimit, gelatoGasPrice ) ).to.be.equal("OK"); // To verify whether the execution of DSR=>CDAI has been successful in this Testing // we look at changes in the CDAI balance of the DSA const cDAI = await ethers.getContractAt( IERC20.abi, hre.network.config.CDAI ); const dsaCDAIBefore = await cDAI.balanceOf(dsa.address); // For testing we now simulate automatic Task Execution ❗ await expect( gelatoCore.exec(taskReceipt, { gasPrice: gelatoGasPrice, // Exectutor must use gelatoGasPrice (Chainlink fast gwei) gasLimit: taskRebalanceDSRToCDAIifBetter.selfProviderGasLimit, }) ).to.emit(gelatoCore, "LogExecSuccess"); // Since the Execution was successful, we now expect our DSA to hold more // CDAI then before. This concludes our testing. expect(await cDAI.balanceOf(dsa.address)).to.be.gt(dsaCDAIBefore); }); });