From 5e20f8995174a36b83c3000a733cdb76edd86432 Mon Sep 17 00:00:00 2001 From: Shivva Date: Thu, 12 Nov 2020 14:40:12 +0100 Subject: [PATCH] Gas price measurement using Mock contract --- .../contracts/mocks/MockGelatoExecutor.sol | 45 +++ deploy/mocks/FGelatoDebtBridgeMock.deploy.js | 27 ++ deploy/mocks/MockGelatoExecutor.deploy.js | 34 ++ ...ebt-Bridge-ETHA-ETHB-Gas-Cost-Mock.test.js | 303 ++++++++++++++++++ .../services/getGasCostForFullRefinance.js | 8 +- test/helpers/services/getMockGelato.js | 8 + test/helpers/services/stakeExecutorMock.js | 28 ++ ...nanceMakerToMakerWithVaultBCreationMock.js | 96 ++++++ 8 files changed, 545 insertions(+), 4 deletions(-) create mode 100644 contracts/contracts/mocks/MockGelatoExecutor.sol create mode 100644 deploy/mocks/FGelatoDebtBridgeMock.deploy.js create mode 100644 deploy/mocks/MockGelatoExecutor.deploy.js create mode 100644 test/5_Full-Debt-Bridge-ETHA-ETHB-Gas-Cost-Mock.test.js create mode 100644 test/helpers/services/getMockGelato.js create mode 100644 test/helpers/services/stakeExecutorMock.js create mode 100644 test/helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock.js diff --git a/contracts/contracts/mocks/MockGelatoExecutor.sol b/contracts/contracts/mocks/MockGelatoExecutor.sol new file mode 100644 index 0000000..6af5efa --- /dev/null +++ b/contracts/contracts/mocks/MockGelatoExecutor.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.7.4; +pragma experimental ABIEncoderV2; + +// import "hardhat/console.sol"; // Uncomment this line for using gasLeft Method +import { + TaskReceipt +} from "@gelatonetwork/core/contracts/gelato_core/interfaces/IGelatoCore.sol"; +import { + IGelatoCore +} from "@gelatonetwork/core/contracts/gelato_core/interfaces/IGelatoCore.sol"; +import { + IGelatoExecutors +} from "@gelatonetwork/core/contracts/gelato_core/interfaces/IGelatoExecutors.sol"; +import {GelatoBytes} from "../../lib/GelatoBytes.sol"; + +contract MockGelatoExecutor { + using GelatoBytes for bytes; + address public gelatoCore; + + constructor(address _gelatoCore) { + gelatoCore = _gelatoCore; + } + + // solhint-disable-next-line + function exec(TaskReceipt memory _TR) external { + // uint256 gasLeft = gasleft(); // Uncomment this line for using gasleft Method + IGelatoCore(gelatoCore).exec(_TR); + // solhint-disable-next-line + // console.log("Gas Cost for Task Execution %s", gasLeft - gasleft());// Uncomment this line for using gasleft Method + } + + function stakeExecutor() external payable { + IGelatoExecutors(gelatoCore).stakeExecutor{value: msg.value}(); + } + + function canExec( + // solhint-disable-next-line + TaskReceipt calldata _TR, + uint256 _gasLimit, + uint256 _execTxGasPrice + ) external view returns (string memory) { + return IGelatoCore(gelatoCore).canExec(_TR, _gasLimit, _execTxGasPrice); + } +} diff --git a/deploy/mocks/FGelatoDebtBridgeMock.deploy.js b/deploy/mocks/FGelatoDebtBridgeMock.deploy.js new file mode 100644 index 0000000..93a52b5 --- /dev/null +++ b/deploy/mocks/FGelatoDebtBridgeMock.deploy.js @@ -0,0 +1,27 @@ +const {sleep} = require("@gelatonetwork/core"); + +module.exports = async (hre) => { + if (hre.network.name === "mainnet") { + console.log( + "Deploying FGelatoDebtBridgeMock to mainnet. Hit ctrl + c to abort" + ); + await sleep(10000); + } + + const {deployments} = hre; + const {deploy} = deployments; + const {deployer} = await hre.getNamedAccounts(); + + // the following will only deploy "FGelatoDebtBridgeMock" + // if the contract was never deployed or if the code changed since last deployment + await deploy("FGelatoDebtBridgeMock", { + from: deployer, + gasPrice: hre.network.config.gasPrice, + log: hre.network.name === "mainnet" ? true : false, + }); +}; + +module.exports.skip = async (hre) => { + return hre.network.name === "mainnet" ? true : false; +}; +module.exports.tags = ["FGelatoDebtBridgeMock"]; diff --git a/deploy/mocks/MockGelatoExecutor.deploy.js b/deploy/mocks/MockGelatoExecutor.deploy.js new file mode 100644 index 0000000..64e97d5 --- /dev/null +++ b/deploy/mocks/MockGelatoExecutor.deploy.js @@ -0,0 +1,34 @@ +const {sleep} = require("@gelatonetwork/core"); +const hre = require("hardhat"); +const {ethers} = hre; +const GelatoCoreLib = require("@gelatonetwork/core"); + +module.exports = async (hre) => { + if (hre.network.name === "mainnet") { + console.log( + "Deploying MockGelatoExecutor to mainnet. Hit ctrl + c to abort" + ); + await sleep(10000); + } + + const {deployments} = hre; + const {deploy} = deployments; + const {deployer} = await hre.getNamedAccounts(); + const gelatoCore = await ethers.getContractAt( + GelatoCoreLib.GelatoCore.abi, + hre.network.config.GelatoCore + ); + + // the following will only deploy "MockGelatoExecutor" + // if the contract was never deployed or if the code changed since last deployment + await deploy("MockGelatoExecutor", { + from: deployer, + args: [gelatoCore.address], + gasPrice: hre.network.config.gasPrice, + log: hre.network.name === "mainnet" ? true : false, + }); +}; +module.exports.skip = async (hre) => { + return hre.network.name === "mainnet" ? true : false; +}; +module.exports.tags = ["MockGelatoExecutor"]; diff --git a/test/5_Full-Debt-Bridge-ETHA-ETHB-Gas-Cost-Mock.test.js b/test/5_Full-Debt-Bridge-ETHA-ETHB-Gas-Cost-Mock.test.js new file mode 100644 index 0000000..c179516 --- /dev/null +++ b/test/5_Full-Debt-Bridge-ETHA-ETHB-Gas-Cost-Mock.test.js @@ -0,0 +1,303 @@ +const {expect} = require("chai"); +const hre = require("hardhat"); +const {deployments, ethers} = hre; +const GelatoCoreLib = require("@gelatonetwork/core"); + +const setupFullRefinanceMakerToMakerWithVaultBCreationMock = require("./helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock"); +const getRoute = require("./helpers/services/getRoute"); +const getGasCostForFullRefinance = require("./helpers/services/getGasCostForFullRefinance"); + +// This test showcases how to submit a task refinancing a Users debt position from +// Maker to Compound using Gelato +describe("Full Debt Bridge refinancing loan from ETH-A to ETH-B For Gas Measurement", function () { + this.timeout(0); + if (hre.network.name !== "hardhat") { + console.error("Test Suite is meant to be run on hardhat only"); + process.exit(1); + } + + let contracts; + let wallets; + let constants; + let ABI; + + // Payload Params for ConnectGelatoFullDebtBridgeFromMaker and ConditionMakerVaultUnsafe + let vaultAId; + + // For TaskSpec and for Task + let gelatoDebtBridgeSpells = []; + + // Cross test var + let taskReceipt; + + before(async function () { + // Reset back to a fresh forked state during runtime + await deployments.fixture(); + + const result = await setupFullRefinanceMakerToMakerWithVaultBCreationMock(); + + wallets = result.wallets; + contracts = result.contracts; + vaultAId = result.vaultAId; + gelatoDebtBridgeSpells = result.spells; + + ABI = result.ABI; + constants = result.constants; + }); + + it("#1: DSA authorizes Gelato to cast spells.", async function () { + //#region User give authorization to gelato to use his DSA on his behalf. + + // Instadapp DSA contract give the possibility to the user to delegate + // action by giving authorization. + // In this case user give authorization to gelato to execute + // task for him if needed. + + await contracts.dsa.cast( + [hre.network.config.ConnectAuth], + [ + await hre.run("abi-encode-withselector", { + abi: ABI.ConnectAuthABI, + functionname: "add", + inputs: [contracts.gelatoCore.address], + }), + ], + wallets.userAddress + ); + + expect(await contracts.dsa.isAuth(contracts.gelatoCore.address)).to.be.true; + + //#endregion + }); + + it("#2: User submits automated Debt Bridge task to Gelato via DSA", async function () { + //#region User submit a Debt Refinancing task if market move against him + + // User submit the refinancing task if market move against him. + // So in this case if the maker vault go to the unsafe area + // the refinancing task will be executed and the position + // will be split on two position on maker and compound. + // It will be done through a algorithm that will optimize the + // total borrow rate. + + const conditionMakerVaultUnsafeObj = new GelatoCoreLib.Condition({ + inst: contracts.conditionMakerVaultUnsafe.address, + data: await contracts.conditionMakerVaultUnsafe.getConditionData( + vaultAId, + contracts.priceOracleResolver.address, + await hre.run("abi-encode-withselector", { + abi: (await deployments.getArtifact("PriceOracleResolver")).abi, + functionname: "getMockPrice", + inputs: [wallets.userAddress], + }), + constants.MIN_COL_RATIO_MAKER + ), + }); + + const conditionDebtBridgeIsAffordableObj = new GelatoCoreLib.Condition({ + inst: contracts.conditionDebtBridgeIsAffordable.address, + data: await contracts.conditionDebtBridgeIsAffordable.getConditionData( + vaultAId, + constants.MAX_FEES_IN_PERCENT + ), + }); + + // ======= GELATO TASK SETUP ====== + const refinanceFromEthAToBIfVaultUnsafe = new GelatoCoreLib.Task({ + conditions: [ + conditionMakerVaultUnsafeObj, + conditionDebtBridgeIsAffordableObj, + ], + actions: gelatoDebtBridgeSpells, + }); + + const gelatoExternalProvider = new GelatoCoreLib.GelatoProvider({ + addr: wallets.gelatoProviderAddress, // Gelato Provider Address + module: contracts.dsaProviderModule.address, // Gelato DSA module + }); + + const expiryDate = 0; + + await expect( + contracts.dsa.cast( + [contracts.connectGelato.address], // targets + [ + await hre.run("abi-encode-withselector", { + abi: ABI.ConnectGelatoABI, + functionname: "submitTask", + inputs: [ + gelatoExternalProvider, + refinanceFromEthAToBIfVaultUnsafe, + expiryDate, + ], + }), + ], // datas + wallets.userAddress, // origin + { + gasLimit: 5000000, + } + ) + ).to.emit(contracts.gelatoCore, "LogTaskSubmitted"); + + taskReceipt = new GelatoCoreLib.TaskReceipt({ + id: await contracts.gelatoCore.currentTaskReceiptId(), + userProxy: contracts.dsa.address, + provider: gelatoExternalProvider, + tasks: [refinanceFromEthAToBIfVaultUnsafe], + expiryDate, + }); + + //#endregion + }); + + // This test showcases the part which is automatically done by the Gelato Executor Network on mainnet + // Bots constatly check whether the submitted task is executable (by calling canExec) + // If the task becomes executable (returns "OK"), the "exec" function will be called + // which will execute the debt refinancing on behalf of the user + it("#3: Auto-refinance from ETH-A to ETH-B, if the Maker vault became unsafe.", async function () { + // Steps + // Step 1: Market Move against the user (Mock) + // Step 2: Executor execute the user's task + + //#region Step 1 Market Move against the user (Mock) + + // Ether market price went from the current price to 250$ + + const gelatoGasPrice = await hre.run("fetchGelatoGasPrice"); + expect(gelatoGasPrice).to.be.lte(constants.GAS_PRICE_CEIL); + + // TO DO: base mock price off of real price data + await contracts.priceOracleResolver.setMockPrice( + ethers.utils.parseUnits("400", 18) + ); + + expect( + await contracts.mockGelatoExecutor + .connect(wallets.gelatoExecutorWallet) + .canExec(taskReceipt, constants.GAS_LIMIT, gelatoGasPrice) + ).to.be.equal("ConditionNotOk:MakerVaultNotUnsafe"); + + // TO DO: base mock price off of real price data + await contracts.priceOracleResolver.setMockPrice( + ethers.utils.parseUnits("250", 18) + ); + + expect( + await contracts.mockGelatoExecutor + .connect(wallets.gelatoExecutorWallet) + .canExec(taskReceipt, constants.GAS_LIMIT, gelatoGasPrice) + ).to.be.equal("OK"); + + //#endregion + + //#region Step 2 Executor execute the user's task + + // The market move make the vault unsafe, so the executor + // will execute the user's task to make the user position safe + // by a debt refinancing in compound. + + //#region EXPECTED OUTCOME + const debtOnMakerBefore = await contracts.makerResolver.getMakerVaultDebt( + vaultAId + ); + + const route = await getRoute( + contracts.DAI.address, + debtOnMakerBefore, + contracts.instaPoolResolver + ); + + const gasCost = await getGasCostForFullRefinance(route); + + const gasFeesPaidFromCol = ethers.BigNumber.from(gasCost).mul( + gelatoGasPrice + ); + + const pricedCollateral = ( + await contracts.makerResolver.getMakerVaultCollateralBalance(vaultAId) + ).sub(gasFeesPaidFromCol); + + //#endregion + const providerBalanceBeforeExecution = await contracts.gelatoCore.providerFunds( + wallets.gelatoProviderAddress + ); + + await expect( + contracts.mockGelatoExecutor + .connect(wallets.gelatoExecutorWallet) + .exec(taskReceipt, { + gasPrice: gelatoGasPrice, // Exectutor must use gelatoGasPrice (Chainlink fast gwei) + gasLimit: constants.GAS_LIMIT, + }) + ).to.emit(contracts.gelatoCore, "LogExecSuccess"); + + // 🚧 For Debugging: + // const txResponse2 = await contracts.gelatoCore + // .connect(wallets.gelatoExecutorWallet) + // .exec(taskReceipt, { + // gasPrice: gelatoGasPrice, + // gasLimit: constants.GAS_LIMIT, + // }); + // const {blockHash} = await txResponse2.wait(); + // const logs = await ethers.provider.getLogs({blockHash}); + // const iFace = new ethers.utils.Interface(GelatoCoreLib.GelatoCore.abi); + // for (const log of logs) { + // console.log(iFace.parseLog(log).args.reason); + // } + // await GelatoCoreLib.sleep(10000); + + const cdps = await contracts.getCdps.getCdpsAsc( + contracts.dssCdpManager.address, + contracts.dsa.address + ); + let vaultBId = String(cdps.ids[1]); + expect(cdps.ids[1].isZero()).to.be.false; + + const debtOnMakerVaultB = await contracts.makerResolver.getMakerVaultDebt( + vaultBId + ); + const pricedCollateralOnVaultB = await contracts.makerResolver.getMakerVaultCollateralBalance( + vaultBId + ); + + expect( + await contracts.gelatoCore.providerFunds(wallets.gelatoProviderAddress) + ).to.be.gt( + providerBalanceBeforeExecution.sub( + gasFeesPaidFromCol + .mul(await contracts.gelatoCore.totalSuccessShare()) + .div(100) + ) + ); + + // Estimated amount to borrowed token should be equal to the actual one read on compound contracts + if (route === 1) { + expect(debtOnMakerBefore).to.be.lte(debtOnMakerVaultB); + } else { + expect(debtOnMakerBefore).to.be.equal(debtOnMakerVaultB); + + // We should not have borrowed DAI on maker + const debtOnMakerOnVaultAAfter = await contracts.makerResolver.getMakerVaultDebt( + vaultAId + ); + expect(debtOnMakerOnVaultAAfter).to.be.equal(ethers.constants.Zero); + } + + // Estimated amount of collateral should be equal to the actual one read on compound contracts + expect(pricedCollateral).to.be.equal(pricedCollateralOnVaultB); + + const collateralOnMakerOnVaultAAfter = await contracts.makerResolver.getMakerVaultCollateralBalance( + vaultAId + ); // in Ether. + + // We should not have deposited ether on it. + expect(collateralOnMakerOnVaultAAfter).to.be.equal(ethers.constants.Zero); + + // DSA has maximum 2 wei DAI in it due to maths inaccuracies + expect(await contracts.DAI.balanceOf(contracts.dsa.address)).to.be.equal( + constants.MAKER_INITIAL_DEBT + ); + + //#endregion + }); +}); diff --git a/test/helpers/services/getGasCostForFullRefinance.js b/test/helpers/services/getGasCostForFullRefinance.js index 1b245c0..d2ab981 100644 --- a/test/helpers/services/getGasCostForFullRefinance.js +++ b/test/helpers/services/getGasCostForFullRefinance.js @@ -1,13 +1,13 @@ async function getGasCostForFullRefinance(route) { switch (route) { case 0: - return 2519000; // 2290000 * 1,1 + return 2519000; // 2290000 * 1,1 // gas left method measure : 2290000 - 2106637 = 183363 | gas reporter : 2290000 - 1789126 = 500874 case 1: - return 3140500; // 2855000 * 1,1 + return 3140500; // 2855000 * 1,1 // gas left method measure : 2855000 - 2667325 = 187675 | gas reporter : 2855000 - 2244814 = 610186 case 2: - return 3971000; // 3610000 * 1,1 + return 3971000; // 3610000 * 1,1 // gas left method measure : 3610000 - 3423279 = 186721 | gas reporter : 3610000 - 3031103 = 578897 case 3: - return 4345000; // 3950000 * 1,1 + return 4345000; // 3950000 * 1,1 // gas left method measure : 3950000 - 3764004 = 185996 | gas reporter : 3950000 - 3313916 = 636084 default: break; } diff --git a/test/helpers/services/getMockGelato.js b/test/helpers/services/getMockGelato.js new file mode 100644 index 0000000..c3d7c15 --- /dev/null +++ b/test/helpers/services/getMockGelato.js @@ -0,0 +1,8 @@ +const hre = require("hardhat"); +const {ethers} = hre; + +async function getMockGelato() { + return await ethers.getContract("MockGelatoExecutor"); +} + +module.exports = getMockGelato; diff --git a/test/helpers/services/stakeExecutorMock.js b/test/helpers/services/stakeExecutorMock.js new file mode 100644 index 0000000..7393daf --- /dev/null +++ b/test/helpers/services/stakeExecutorMock.js @@ -0,0 +1,28 @@ +const {expect} = require("chai"); + +async function stakeExecutor( + gelatoExecutorWallet, + gelatoExecutorMock, + gelatoCore +) { + //#region Executor Stake on Gelato + + // For task execution provider will ask a executor to watch the + // blockchain for possible execution autorization given by + // the condition that user choose when submitting the task. + // And if all condition are meet executor will execute the task. + // For safety measure Gelato ask the executor to stake a minimum + // amount. + // In our Mock case this executor will be a contract, who will call the gelatoCore smart contract + + await gelatoExecutorMock.connect(gelatoExecutorWallet).stakeExecutor({ + value: await gelatoCore.minExecutorStake(), + }); + + expect(await gelatoCore.isExecutorMinStaked(gelatoExecutorMock.address)).to.be + .true; + + //#endregion +} + +module.exports = stakeExecutor; diff --git a/test/helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock.js b/test/helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock.js new file mode 100644 index 0000000..be16028 --- /dev/null +++ b/test/helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock.js @@ -0,0 +1,96 @@ +const getWallets = require("./services/getWallets"); +const getContracts = require("./services/getContracts"); +const getConstants = require("./services/getConstants"); +const getMockGelato = require("./services/getMockGelato"); +const stakeExecutorMock = require("./services/stakeExecutorMock"); +const provideFunds = require("./services/provideFunds"); +const providerAssignsExecutor = require("./services/providerAssignsExecutor"); +const addProviderModuleDSA = require("./services/addProviderModuleDSA"); +const createDSA = require("./services/createDSA"); +const addETHBGemJoinMapping = require("./services/addETHBGemJoinMapping"); +const initializeMakerCdp = require("./services/initializeMakerCdp"); +const createVaultForETHB = require("./services/createVaultForETHB"); +const providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB = require("./services/providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB"); +const getABI = require("./services/getABI"); + +async function setupFullRefinanceMakerToMakerWithVaultBCreation() { + const wallets = await getWallets(); + const contracts = await getContracts(); + const constants = await getConstants(); + + //#region Mock + + contracts.mockGelatoExecutor = await getMockGelato(); + + //#endregion Mock + + // Gelato Testing environment setup. + await stakeExecutorMock( + wallets.gelatoExecutorWallet, + contracts.mockGelatoExecutor, + contracts.gelatoCore + ); + await provideFunds( + wallets.gelatoProviderWallet, + contracts.gelatoCore, + constants.GAS_LIMIT, + constants.GAS_PRICE_CEIL + ); + await providerAssignsExecutor( + wallets.gelatoProviderWallet, + contracts.mockGelatoExecutor.address, + contracts.gelatoCore + ); + await addProviderModuleDSA( + wallets.gelatoProviderWallet, + contracts.gelatoCore, + contracts.dsaProviderModule.address + ); + contracts.dsa = await createDSA( + wallets.userAddress, + contracts.instaIndex, + contracts.instaList + ); + await addETHBGemJoinMapping( + wallets.userWallet, + contracts.instaMapping, + contracts.instaMaster + ); + const vaultAId = await initializeMakerCdp( + wallets.userAddress, + contracts.DAI, + contracts.dsa, + contracts.getCdps, + contracts.dssCdpManager, + constants.MAKER_INITIAL_ETH, + constants.MAKER_INITIAL_DEBT + ); + const vaultBId = await createVaultForETHB( + wallets.userAddress, + contracts.DAI, + contracts.dsa, + contracts.getCdps, + contracts.dssCdpManager + ); + const spells = await providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB( + wallets, + contracts, + constants, + vaultAId, + vaultBId + ); + + const ABI = getABI(); + + return { + wallets, + contracts, + constants, + vaultAId, + vaultBId, + spells, + ABI, + }; +} + +module.exports = setupFullRefinanceMakerToMakerWithVaultBCreation;