diff --git a/contracts/contracts/connectors/ConnectGelatoDataFullMakerToCompound.sol b/contracts/contracts/connectors/ConnectGelatoDataFullMakerToCompound.sol index 4012bbc..5732016 100644 --- a/contracts/contracts/connectors/ConnectGelatoDataFullMakerToCompound.sol +++ b/contracts/contracts/connectors/ConnectGelatoDataFullMakerToCompound.sol @@ -11,17 +11,16 @@ import { import { IConnectInstaPoolV2 } from "../../interfaces/InstaDapp/connectors/IConnectInstaPoolV2.sol"; -import {IMcdManager} from "../../interfaces/dapps/Maker/IMcdManager.sol"; import { DAI, CONNECT_MAKER, CONNECT_COMPOUND, INSTA_POOL_V2 } from "../../constants/CInstaDapp.sol"; -import {MCD_MANAGER} from "../../constants/CMaker.sol"; import { _getMakerVaultDebt, - _getMakerVaultCollateralBalance + _getMakerVaultCollateralBalance, + _isVaultOwnedBy } from "../../functions/dapps/FMaker.sol"; import { _encodeFlashPayback @@ -93,12 +92,11 @@ contract ConnectGelatoDataFullMakerToCompound is uint256 // cycleId ) public view virtual override returns (string memory) { (uint256 vaultId, ) = abi.decode(_actionData[4:], (uint256, address)); - IMcdManager managerContract = IMcdManager(MCD_MANAGER); if (vaultId == 0) return "ConnectGelatoDataFullETHAToETHB : Vault Id is not valid"; - if (managerContract.owns(vaultId) != _dsa) - return "ConnectGelatoDataFullETHAToETHB : Vault not owns by dsa"; + if (_isVaultOwnedBy(vaultId, _dsa)) + return "ConnectGelatoDataFullETHAToETHB : Vault not owned by dsa"; return OK; } diff --git a/contracts/contracts/connectors/ConnectGelatoDataFullMakerToMaker.sol b/contracts/contracts/connectors/ConnectGelatoDataFullMakerToMaker.sol index a8ddb9e..dba5063 100644 --- a/contracts/contracts/connectors/ConnectGelatoDataFullMakerToMaker.sol +++ b/contracts/contracts/connectors/ConnectGelatoDataFullMakerToMaker.sol @@ -11,17 +11,16 @@ import { import { IConnectInstaPoolV2 } from "../../interfaces/InstaDapp/connectors/IConnectInstaPoolV2.sol"; -import {IMcdManager} from "../../interfaces/dapps/Maker/IMcdManager.sol"; import { DAI, CONNECT_MAKER, CONNECT_COMPOUND, INSTA_POOL_V2 } from "../../constants/CInstaDapp.sol"; -import {MCD_MANAGER} from "../../constants/CMaker.sol"; import { _getMakerVaultDebt, - _getMakerVaultCollateralBalance + _getMakerVaultCollateralBalance, + _isVaultOwnedBy } from "../../functions/dapps/FMaker.sol"; import { _encodeFlashPayback @@ -92,17 +91,13 @@ contract ConnectGelatoDataFullMakerToMaker is uint256, // value uint256 // cycleId ) public view virtual override returns (string memory) { - (uint256 vaultAId, uint256 vaultBId, , ) = + (uint256 vaultAId, , , ) = abi.decode(_actionData[4:], (uint256, uint256, address, string)); - IMcdManager managerContract = IMcdManager(MCD_MANAGER); - if (vaultAId == 0) return "ConnectGelatoDataFullETHAToETHB : Vault A Id is not valid"; - if (managerContract.owns(vaultAId) != _dsa) - return "ConnectGelatoDataFullETHAToETHB : Vault A not owns by dsa"; - if (vaultBId != 0 && managerContract.owns(vaultBId) != _dsa) - return "ConnectGelatoDataFullETHAToETHB : Vault B not owns by dsa"; + if (_isVaultOwnedBy(vaultAId, _dsa)) + return "ConnectGelatoDataFullETHAToETHB : Vault A not owned by dsa"; return OK; } @@ -154,6 +149,8 @@ contract ConnectGelatoDataFullMakerToMaker is targets = new address[](1); targets[0] = INSTA_POOL_V2; + _vaultBId = _isVaultOwnedBy(_vaultBId, address(this)) ? 0 : _vaultBId; + uint256 wDaiToBorrow = _getRealisedDebt(_getMakerVaultDebt(_vaultAId)); uint256 wColToWithdrawFromMaker = _getMakerVaultCollateralBalance(_vaultAId); diff --git a/contracts/functions/dapps/FMaker.sol b/contracts/functions/dapps/FMaker.sol index e875103..a7b30fe 100644 --- a/contracts/functions/dapps/FMaker.sol +++ b/contracts/functions/dapps/FMaker.sol @@ -123,3 +123,8 @@ function _getBorrowAmt( dart = sub(mul(_amt, RAY), _dai) / _rate; dart = mul(dart, _rate) < mul(_amt, RAY) ? dart + 1 : dart; } + +function _isVaultOwnedBy(uint256 _vaultId, address _owner) view returns (bool) { + IMcdManager managerContract = IMcdManager(MCD_MANAGER); + return _vaultId != 0 && managerContract.owns(_vaultId) != _owner; +} diff --git a/test/gas/debt_bridge/full/from_maker/helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock.mock.js b/test/gas/debt_bridge/full/from_maker/helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock.mock.js index 68dba9a..fdcdd0d 100644 --- a/test/gas/debt_bridge/full/from_maker/helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock.mock.js +++ b/test/gas/debt_bridge/full/from_maker/helpers/setupFullRefinanceMakerToMakerWithVaultBCreationMock.mock.js @@ -14,6 +14,7 @@ module.exports = async function (mockRoute) { const wallets = await getWallets(); const contracts = await getContracts(); const constants = await getDebtBridgeFromMakerConstants(); + const ABI = getABI(); // Gelato Testing environment setup. await provideFunds( @@ -44,7 +45,8 @@ module.exports = async function (mockRoute) { contracts.getCdps, contracts.dssCdpManager, constants.MAKER_INITIAL_ETH, - constants.MAKER_INITIAL_DEBT + constants.MAKER_INITIAL_DEBT, + ABI.ConnectMakerABI ); const vaultBId = await createVaultForETHB( wallets.userAddress, @@ -62,8 +64,6 @@ module.exports = async function (mockRoute) { vaultBId ); - const ABI = getABI(); - return { wallets, contracts, diff --git a/test/helpers/services/getABI.js b/test/helpers/services/getABI.js index a265e6e..3534f07 100644 --- a/test/helpers/services/getABI.js +++ b/test/helpers/services/getABI.js @@ -1,10 +1,12 @@ const ConnectGelatoABI = require("../../../pre-compiles/ConnectGelato.json") .abi; const ConnectAuthABI = require("../../../pre-compiles/ConnectAuth.json").abi; +const ConnectMakerABI = require("../../../pre-compiles/ConnectMaker.json").abi; module.exports = function () { return { ConnectGelatoABI: ConnectGelatoABI, ConnectAuthABI: ConnectAuthABI, + ConnectMakerABI: ConnectMakerABI, }; }; diff --git a/test/helpers/services/maker/initializeMakerCdp.js b/test/helpers/services/maker/initializeMakerCdp.js index 009daf3..54bd774 100644 --- a/test/helpers/services/maker/initializeMakerCdp.js +++ b/test/helpers/services/maker/initializeMakerCdp.js @@ -1,8 +1,6 @@ const { expect } = require("chai"); const hre = require("hardhat"); -const ConnectMaker = require("../../../../pre-compiles/ConnectMaker.json"); - module.exports = async function ( userAddress, DAI, @@ -10,7 +8,8 @@ module.exports = async function ( getCdps, dssCdpManager, makerInitialEth, - makerInitialDebt + makerInitialDebt, + connectMakerABI ) { //#region Step 8 User open a Vault, put some ether on it and borrow some dai @@ -18,7 +17,7 @@ module.exports = async function ( // He deposit 10 Eth on it // He borrow a 1000 DAI const openVault = await hre.run("abi-encode-withselector", { - abi: ConnectMaker.abi, + abi: connectMakerABI, functionname: "open", inputs: ["ETH-A"], }); @@ -33,7 +32,7 @@ module.exports = async function ( [hre.network.config.ConnectMaker], [ await hre.run("abi-encode-withselector", { - abi: ConnectMaker.abi, + abi: connectMakerABI, functionname: "deposit", inputs: [vaultId, makerInitialEth, 0, 0], }), @@ -48,7 +47,7 @@ module.exports = async function ( [hre.network.config.ConnectMaker], [ await hre.run("abi-encode-withselector", { - abi: ConnectMaker.abi, + abi: connectMakerABI, functionname: "borrow", inputs: [vaultId, makerInitialDebt, 0, 0], }), diff --git a/test/integration/debt_bridge/from_maker/full/3_ETHA-ETHB-With-Vault-Closing.test.js b/test/integration/debt_bridge/from_maker/full/3_ETHA-ETHB-With-Vault-Closing.test.js new file mode 100644 index 0000000..3925c44 --- /dev/null +++ b/test/integration/debt_bridge/from_maker/full/3_ETHA-ETHB-With-Vault-Closing.test.js @@ -0,0 +1,349 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); +const { deployments, ethers } = hre; +const GelatoCoreLib = require("@gelatonetwork/core"); + +const setupFullRefinanceMakerToMaker = require("./helpers/setupFullRefinanceMakerToMaker"); +const getInstaPoolV2Route = require("../../../../helpers/services/InstaDapp/getInstaPoolV2Route"); +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 with Vault B creation due to the closing of the previous one", 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; + let vaultBId; + + // 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 setupFullRefinanceMakerToMaker(); + + wallets = result.wallets; + contracts = result.contracts; + vaultAId = result.vaultAId; + vaultBId = result.vaultBId; + 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 + ), + }); + + const conditionDestVaultWillBeSafe = new GelatoCoreLib.Condition({ + inst: contracts.conditionDestVaultWillBeSafe.address, + data: await contracts.conditionDestVaultWillBeSafe.getConditionData( + vaultAId, + vaultBId, + "ETH-B" + ), + }); + + // ======= GELATO TASK SETUP ====== + const refinanceFromEthAToBIfVaultUnsafe = new GelatoCoreLib.Task({ + conditions: [ + conditionMakerVaultUnsafeObj, + conditionDebtBridgeIsAffordableObj, + conditionDestVaultWillBeSafe, + ], + 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 + + //#region Closing of the vault B + + await contracts.dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ABI.ConnectMakerABI, + functionname: "close", + inputs: [vaultBId], + }), + ], + wallets.userAddress, // origin + { + gasLimit: 5000000, + } + ); + + //#endregion Closing of the vault B + }); + + // 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.gelatoCore + .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.gelatoCore + .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 + let debtOnMakerBefore = await contracts.makerResolver.getMakerVaultRawDebt( + vaultAId + ); + + const route = await getInstaPoolV2Route( + contracts.DAI.address, + debtOnMakerBefore, + contracts.instaPoolResolver + ); + + if (route !== 1) { + debtOnMakerBefore = await contracts.makerResolver.getMakerVaultDebt( + vaultAId + ); + } + + 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.gelatoCore + .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 + ); + const oldVaultBId = vaultBId; + vaultBId = String(cdps.ids[1]); + expect(cdps.ids[1].isZero()).to.be.false; + + expect(oldVaultBId).to.be.not.equal(vaultBId); + + let debtOnMakerVaultB; + if (route === 1) { + debtOnMakerVaultB = await contracts.makerResolver.getMakerVaultRawDebt( + vaultBId + ); + } else { + 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); + } + + // 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. + const debtOnMakerOnVaultAAfter = await contracts.makerResolver.getMakerVaultRawDebt( + vaultAId + ); + + // We should not have deposited ether or borrowed DAI on maker. + expect(collateralOnMakerOnVaultAAfter).to.be.equal(ethers.constants.Zero); + expect(debtOnMakerOnVaultAAfter).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/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToCompound.js b/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToCompound.js index f2051ca..98eded0 100644 --- a/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToCompound.js +++ b/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToCompound.js @@ -14,6 +14,7 @@ module.exports = async function () { const wallets = await getWallets(); const contracts = await getContracts(); const constants = await getDebtBridgeFromMakerConstants(); + const ABI = getABI(); // Gelato Testing environment setup. await stakeExecutor(wallets.gelatoExecutorWallet, contracts.gelatoCore); @@ -45,7 +46,8 @@ module.exports = async function () { contracts.getCdps, contracts.dssCdpManager, constants.MAKER_INITIAL_ETH, - constants.MAKER_INITIAL_DEBT + constants.MAKER_INITIAL_DEBT, + ABI.ConnectMakerABI ); const spells = await getSpellsToCompound( @@ -55,8 +57,6 @@ module.exports = async function () { vaultId ); - const ABI = getABI(); - return { wallets, contracts, diff --git a/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToMaker.js b/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToMaker.js index 4cd9d58..c1e88cf 100644 --- a/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToMaker.js +++ b/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToMaker.js @@ -15,6 +15,7 @@ module.exports = async function () { const wallets = await getWallets(); const contracts = await getContracts(); const constants = await getDebtBridgeFromMakerConstants(); + const ABI = getABI(); // Gelato Testing environment setup. await stakeExecutor(wallets.gelatoExecutorWallet, contracts.gelatoCore); @@ -46,7 +47,8 @@ module.exports = async function () { contracts.getCdps, contracts.dssCdpManager, constants.MAKER_INITIAL_ETH, - constants.MAKER_INITIAL_DEBT + constants.MAKER_INITIAL_DEBT, + ABI.ConnectMakerABI ); const vaultBId = await createVaultForETHB( wallets.userAddress, @@ -63,8 +65,6 @@ module.exports = async function () { vaultBId ); - const ABI = getABI(); - return { wallets, contracts, diff --git a/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToMakerWithVaultBCreation.js b/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToMakerWithVaultBCreation.js index 9f0e0c9..2ea245e 100644 --- a/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToMakerWithVaultBCreation.js +++ b/test/integration/debt_bridge/from_maker/full/helpers/setupFullRefinanceMakerToMakerWithVaultBCreation.js @@ -14,6 +14,7 @@ module.exports = async function () { const wallets = await getWallets(); const contracts = await getContracts(); const constants = await getDebtBridgeFromMakerConstants(); + const ABI = getABI(); // Gelato Testing environment setup. await stakeExecutor(wallets.gelatoExecutorWallet, contracts.gelatoCore); @@ -45,7 +46,8 @@ module.exports = async function () { contracts.getCdps, contracts.dssCdpManager, constants.MAKER_INITIAL_ETH, - constants.MAKER_INITIAL_DEBT + constants.MAKER_INITIAL_DEBT, + ABI.ConnectMakerABI ); const spells = await getSpellsEthAEthBWithVaultCreation( @@ -55,8 +57,6 @@ module.exports = async function () { vaultAId ); - const ABI = getABI(); - return { wallets, contracts,