From de8ad63dda53a0cce903ebfecf1d1c31d95b69f6 Mon Sep 17 00:00:00 2001 From: Shivva <matthieumariejoseph@gmail.com> Date: Tue, 10 Nov 2020 11:43:04 +0100 Subject: [PATCH] Add new Condition, Liq Pool routing and optional vault creation --- contracts/constants/CDebtBridge.sol | 6 + contracts/constants/CInstaDapp.sol | 3 + .../ConnectGelatoDataForFullRefinance.sol | 149 ++++++--- .../ConditionDebtBridgeIsAffordable.sol | 82 +++++ .../functions/gelato/FGelatoDebtBridge.sol | 32 ++ .../resolvers/IInstaPoolResolver.sol | 17 + .../ConditionDebtBridgeIsAffordable.deploy.js | 24 ++ hardhat.config.js | 1 + .../2_Full-Debt-Bridge-Maker-Compound.test.js | 49 ++- ...dge-ETHA-ETHB-With-Vault-Creation.test.js} | 45 ++- test/4_Full-Debt-Bridge-ETHA-ETHB.test.js | 303 ++++++++++++++++++ test/helpers/services/createVaultForETHB.js | 35 ++ test/helpers/services/getConstants.js | 5 +- test/helpers/services/getContracts.js | 10 + .../services/getGasCostForFullRefinance.js | 16 + test/helpers/services/getRoute.js | 10 + ...derWhiteListTaskForMakerETHAToMakerETHB.js | 7 +- ...stTaskForMakerETHAToMakerETHBWithVaultB.js | 80 +++++ ...providerWhiteListTaskForMakerToCompound.js | 5 +- ...RefinanceMakerToMakerWithVaultBCreation.js | 85 +++++ .../4_ConditionDebtBridgeIsAffordable.test.js | 157 +++++++++ 21 files changed, 1060 insertions(+), 61 deletions(-) create mode 100644 contracts/constants/CDebtBridge.sol create mode 100644 contracts/contracts/gelato/conditions/ConditionDebtBridgeIsAffordable.sol create mode 100644 contracts/interfaces/InstaDapp/resolvers/IInstaPoolResolver.sol create mode 100644 deploy/gelato/conditions/ConditionDebtBridgeIsAffordable.deploy.js rename test/{3_Full-Debt-Bridge-ETHA-ETHB.test.js => 3_Full-Debt-Bridge-ETHA-ETHB-With-Vault-Creation.test.js} (86%) create mode 100644 test/4_Full-Debt-Bridge-ETHA-ETHB.test.js create mode 100644 test/helpers/services/createVaultForETHB.js create mode 100644 test/helpers/services/getGasCostForFullRefinance.js create mode 100644 test/helpers/services/getRoute.js create mode 100644 test/helpers/services/providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB.js create mode 100644 test/helpers/setupFullRefinanceMakerToMakerWithVaultBCreation.js create mode 100644 test/unit_tests/4_ConditionDebtBridgeIsAffordable.test.js diff --git a/contracts/constants/CDebtBridge.sol b/contracts/constants/CDebtBridge.sol new file mode 100644 index 0000000..2e4994b --- /dev/null +++ b/contracts/constants/CDebtBridge.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.7.4; + +function GAS_COSTS_FOR_FULL_REFINANCE() pure returns(uint256[4] memory) { + return [uint256(2519000), 3140500, 3971000, 4345000]; +} \ No newline at end of file diff --git a/contracts/constants/CInstaDapp.sol b/contracts/constants/CInstaDapp.sol index b86fab7..f57eaba 100644 --- a/contracts/constants/CInstaDapp.sol +++ b/contracts/constants/CInstaDapp.sol @@ -12,3 +12,6 @@ address constant INSTA_POOL_V2 = 0x3150e5A805577366816A1ddc7330c6Ea17070c05; // Tokens address constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + +// Insta Pool +address constant INSTA_POOL_RESOLVER = 0xa004a5afBa04b74037E9E52bA1f7eb02b5E61509; \ No newline at end of file diff --git a/contracts/contracts/connectors/ConnectGelatoDataForFullRefinance.sol b/contracts/contracts/connectors/ConnectGelatoDataForFullRefinance.sol index 475e5fa..d8514e7 100644 --- a/contracts/contracts/connectors/ConnectGelatoDataForFullRefinance.sol +++ b/contracts/contracts/connectors/ConnectGelatoDataForFullRefinance.sol @@ -3,7 +3,7 @@ pragma solidity 0.7.4; pragma experimental ABIEncoderV2; import {GelatoBytes} from "../../lib/GelatoBytes.sol"; -import {sub} from "../../vendor/DSMath.sol"; +import {sub, wmul} from "../../vendor/DSMath.sol"; import { AccountInterface, ConnectorInterface @@ -39,6 +39,11 @@ import { _encodeBorrowCompound } from "../../functions/InstaDapp/connectors/FConnectCompound.sol"; import {_getGelatoProviderFees} from "../../functions/gelato/FGelato.sol"; +import { + _getFlashLoanRoute, + _getGasCost, + _getBorrowAmountWithDelta +} from "../../functions/gelato/FGelatoDebtBridge.sol"; contract ConnectGelatoDataForFullRefinance is ConnectorInterface { using GelatoBytes for bytes; @@ -67,11 +72,13 @@ contract ConnectGelatoDataForFullRefinance is ConnectorInterface { /// @notice Entry Point for DSA.cast DebtBridge from e.g ETH-A to ETH-B /// @dev payable to be compatible in conjunction with DSA.cast payable target - /// @param _vaultId Id of the unsafe vault of the client. + /// @param _vaultAId Id of the unsafe vault of the client of Vault A Collateral. + /// @param _vaultBId Id of the vault B Collateral of the client. /// @param _token vault's col token address . /// @param _colType colType of the new vault. example : ETH-B, ETH-A. function getDataAndCastForFromMakerToMaker( - uint256 _vaultId, + uint256 _vaultAId, + uint256 _vaultBId, address _token, string calldata _colType ) external payable { @@ -79,7 +86,8 @@ contract ConnectGelatoDataForFullRefinance is ConnectorInterface { address[] memory targets, bytes[] memory datas ) = _execPayloadForFullRefinanceFromMakerToMaker( - _vaultId, + _vaultAId, + _vaultBId, _token, _colType ); @@ -124,48 +132,47 @@ contract ConnectGelatoDataForFullRefinance is ConnectorInterface { /* solhint-disable function-max-lines */ function _execPayloadForFullRefinanceFromMakerToMaker( - uint256 _vaultId, + uint256 _vaultAId, + uint256 _vaultBId, address _token, string calldata _colType ) internal view returns (address[] memory targets, bytes[] memory datas) { targets = new address[](1); targets[0] = INSTA_POOL_V2; - uint256 wDaiDebtToMove = _getMakerVaultDebt(_vaultId); + uint256 wDaiDebtToMove = _getMakerVaultDebt(_vaultAId); + uint256 wDaiToBorrow = _getBorrowAmountWithDelta(wDaiDebtToMove); uint256 wColToWithdrawFromMaker = _getMakerVaultCollateralBalance( - _vaultId + _vaultAId ); - uint256 gasFeesPaidFromCol = _getGelatoProviderFees(GAS_COST); + uint256 route = _getFlashLoanRoute(DAI, wDaiDebtToMove); + uint256 gasCost = _getGasCost(route); + uint256 gasFeesPaidFromCol = _getGelatoProviderFees(gasCost); - address[] memory _targets = new address[](7); - _targets[0] = CONNECT_MAKER; // payback - _targets[1] = CONNECT_MAKER; // withdraw - _targets[2] = CONNECT_MAKER; // open ETH-B vault - _targets[3] = CONNECT_MAKER; // deposit - _targets[4] = CONNECT_MAKER; // borrow - _targets[5] = _connectGelatoProviderPayment; // payProvider - _targets[6] = INSTA_POOL_V2; // flashPayback - - bytes[] memory _datas = new bytes[](7); - _datas[0] = _encodePaybackMakerVault(_vaultId, uint256(-1), 0, 0); - _datas[1] = _encodedWithdrawMakerVault(_vaultId, uint256(-1), 0, 0); - _datas[2] = _encodeOpenMakerVault(_colType); - _datas[3] = _encodedDepositMakerVault( - 0, - sub(wColToWithdrawFromMaker, gasFeesPaidFromCol), - 0, - 0 - ); - _datas[4] = _encodeBorrowDaiMakerVault(0, wDaiDebtToMove, 0, 0); - _datas[5] = _encodePayGelatoProvider(_token, gasFeesPaidFromCol, 0, 0); - _datas[6] = _encodeFlashPayback(DAI, wDaiDebtToMove, 0, 0); + (address[] memory _targets, bytes[] memory _datas) = _vaultBId == 0 + ? _spellsDebtBridgeWithOpenVaultAction( + _vaultAId, + _token, + _colType, + wDaiToBorrow, + wColToWithdrawFromMaker, + gasFeesPaidFromCol + ) + : _spellsDebtBridge( + _vaultAId, + _vaultBId, + _token, + wDaiToBorrow, + wColToWithdrawFromMaker, + gasFeesPaidFromCol + ); datas = new bytes[](1); datas[0] = abi.encodeWithSelector( IConnectInstaPoolV2.flashBorrowAndCast.selector, DAI, - wDaiDebtToMove, - 0, + wDaiToBorrow, + route, abi.encode(_targets, _datas) ); } @@ -181,7 +188,11 @@ contract ConnectGelatoDataForFullRefinance is ConnectorInterface { uint256 wColToWithdrawFromMaker = _getMakerVaultCollateralBalance( _vaultId ); - uint256 gasFeesPaidFromCol = _getGelatoProviderFees(GAS_COST); + uint256 route = _getFlashLoanRoute(DAI, wDaiDebtToMove); + uint256 gasCost = _getGasCost(route); + uint256 gasFeesPaidFromCol = _getGelatoProviderFees(gasCost); + + uint256 wDaiToBorrow = _getBorrowAmountWithDelta(wDaiDebtToMove); address[] memory _targets = new address[](6); _targets[0] = CONNECT_MAKER; // payback @@ -192,7 +203,7 @@ contract ConnectGelatoDataForFullRefinance is ConnectorInterface { _targets[5] = INSTA_POOL_V2; // flashPayback bytes[] memory _datas = new bytes[](6); - _datas[0] = _encodePaybackMakerVault(_vaultId, uint256(-1), 0, 0); + _datas[0] = _encodePaybackMakerVault(_vaultId, uint256(-1), 0, 600); _datas[1] = _encodedWithdrawMakerVault(_vaultId, uint256(-1), 0, 0); _datas[2] = _encodeDepositCompound( _token, @@ -200,19 +211,81 @@ contract ConnectGelatoDataForFullRefinance is ConnectorInterface { 0, 0 ); - _datas[3] = _encodeBorrowCompound(DAI, wDaiDebtToMove, 0, 0); + _datas[3] = _encodeBorrowCompound(DAI, 0, 600, 0); _datas[4] = _encodePayGelatoProvider(_token, gasFeesPaidFromCol, 0, 0); - _datas[5] = _encodeFlashPayback(DAI, wDaiDebtToMove, 0, 0); + _datas[5] = _encodeFlashPayback(DAI, wDaiToBorrow, 0, 0); datas = new bytes[](1); datas[0] = abi.encodeWithSelector( IConnectInstaPoolV2.flashBorrowAndCast.selector, DAI, - wDaiDebtToMove, - 0, + wDaiToBorrow, + route, abi.encode(_targets, _datas) ); } + function _spellsDebtBridgeWithOpenVaultAction( + uint256 _vaultAId, + address _token, + string calldata _colType, + uint256 _wDaiToBorrow, + uint256 _wColToWithdrawFromMaker, + uint256 _gasFeesPaidFromCol + ) internal view returns (address[] memory targets, bytes[] memory datas) { + targets = new address[](7); + targets[0] = CONNECT_MAKER; // payback + targets[1] = CONNECT_MAKER; // withdraw + targets[2] = CONNECT_MAKER; // open ETH-B vault + targets[3] = CONNECT_MAKER; // deposit + targets[4] = CONNECT_MAKER; // borrow + targets[5] = _connectGelatoProviderPayment; // payProvider + targets[6] = INSTA_POOL_V2; // flashPayback + + datas = new bytes[](7); + datas[0] = _encodePaybackMakerVault(_vaultAId, uint256(-1), 0, 600); + datas[1] = _encodedWithdrawMakerVault(_vaultAId, uint256(-1), 0, 0); + datas[2] = _encodeOpenMakerVault(_colType); + datas[3] = _encodedDepositMakerVault( + 0, + sub(_wColToWithdrawFromMaker, _gasFeesPaidFromCol), + 0, + 0 + ); + datas[4] = _encodeBorrowDaiMakerVault(0, 0, 600, 0); + datas[5] = _encodePayGelatoProvider(_token, _gasFeesPaidFromCol, 0, 0); + datas[6] = _encodeFlashPayback(DAI, _wDaiToBorrow, 0, 0); + } + + function _spellsDebtBridge( + uint256 _vaultAId, + uint256 _vaultBId, + address _token, + uint256 _wDaiToBorrow, + uint256 _wColToWithdrawFromMaker, + uint256 _gasFeesPaidFromCol + ) internal view returns (address[] memory targets, bytes[] memory datas) { + targets = new address[](6); + targets[0] = CONNECT_MAKER; // payback + targets[1] = CONNECT_MAKER; // withdraw + targets[2] = CONNECT_MAKER; // deposit + targets[3] = CONNECT_MAKER; // borrow + targets[4] = _connectGelatoProviderPayment; // payProvider + targets[5] = INSTA_POOL_V2; // flashPayback + + datas = new bytes[](6); + datas[0] = _encodePaybackMakerVault(_vaultAId, uint256(-1), 0, 600); + datas[1] = _encodedWithdrawMakerVault(_vaultAId, uint256(-1), 0, 0); + datas[2] = _encodedDepositMakerVault( + _vaultBId, + sub(_wColToWithdrawFromMaker, _gasFeesPaidFromCol), + 0, + 0 + ); + datas[3] = _encodeBorrowDaiMakerVault(_vaultBId, 0, 600, 0); + datas[4] = _encodePayGelatoProvider(_token, _gasFeesPaidFromCol, 0, 0); + datas[5] = _encodeFlashPayback(DAI, _wDaiToBorrow, 0, 0); + } + /* solhint-enable function-max-lines */ } diff --git a/contracts/contracts/gelato/conditions/ConditionDebtBridgeIsAffordable.sol b/contracts/contracts/gelato/conditions/ConditionDebtBridgeIsAffordable.sol new file mode 100644 index 0000000..4274bb7 --- /dev/null +++ b/contracts/contracts/gelato/conditions/ConditionDebtBridgeIsAffordable.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.7.4; +pragma experimental ABIEncoderV2; + +import { + GelatoConditionsStandard +} from "@gelatonetwork/core/contracts/conditions/GelatoConditionsStandard.sol"; +import { + IGelatoCore +} from "@gelatonetwork/core/contracts/gelato_core/interfaces/IGelatoCore.sol"; +import {GelatoBytes} from "../../../lib/GelatoBytes.sol"; +import { + _getMakerVaultDebt, + _getMakerVaultCollateralBalance +} from "../../../functions/dapps/FMaker.sol"; +import { + _getFlashLoanRoute, + _getGasCost +} from "../../../functions/gelato/FGelatoDebtBridge.sol"; +import {_getGelatoProviderFees} from "../../../functions/gelato/FGelato.sol"; +import {DAI} from "../../../constants/CInstaDapp.sol"; +import {wdiv} from "../../../vendor/DSMath.sol"; + +/// @title ConditionDebtBridgeIsAffordable +/// @notice Condition checking if Debt Refinance is affordable. +/// @author Gelato Team +contract ConditionDebtBridgeIsAffordable is GelatoConditionsStandard { + using GelatoBytes for bytes; + + /// @notice Convenience function for off-chain _conditionData encoding + /// @dev Use the return for your Task's Condition.data field off-chain. + /// @dev WARNING _ratioLimit should be in wad standard. + /// @return The encoded payload for your Task's Condition.data field. + function getConditionData(uint256 _vaultId, uint256 _ratioLimit) + public + pure + virtual + returns (bytes memory) + { + return abi.encode(_vaultId, _ratioLimit); + } + + /// @notice Standard GelatoCore system function + /// @dev A standard interface for GelatoCore to read Conditions + /// @param _conditionData The data you get from `getConditionData()` + /// @return OK if the Condition is there, else some error message. + function ok( + uint256, + bytes calldata _conditionData, + uint256 + ) public view virtual override returns (string memory) { + (uint256 _vaultID, uint256 _ratioLimit) = abi.decode( + _conditionData, + (uint256, uint256) + ); + + return isAffordable(_vaultID, _ratioLimit); + } + + /// @notice Specific implementation of this Condition's ok function + /// @dev Check if the debt refinancing action is affordable. + /// @dev WARNING _ratioLimit should be in wad standard. + /// @param _vaultId The id of the Maker vault + /// @param _ratioLimit the maximum limit define by the user up on which + /// the debt is too expensive for him + /// @return OK if the Debt Bridge is affordable, otherwise some error message. + function isAffordable(uint256 _vaultId, uint256 _ratioLimit) + public + view + returns (string memory) + { + uint256 wColToWithdrawFromMaker = _getMakerVaultCollateralBalance( + _vaultId + ); + uint256 gasFeesPaidFromCol = _getGelatoProviderFees( + _getGasCost(_getFlashLoanRoute(DAI, _getMakerVaultDebt(_vaultId))) + ); + if (wdiv(gasFeesPaidFromCol, wColToWithdrawFromMaker) >= _ratioLimit) + return "DebtRefinanceTooExpensive"; + return OK; + } +} diff --git a/contracts/functions/gelato/FGelatoDebtBridge.sol b/contracts/functions/gelato/FGelatoDebtBridge.sol index 1b297c1..5b9c5c4 100644 --- a/contracts/functions/gelato/FGelatoDebtBridge.sol +++ b/contracts/functions/gelato/FGelatoDebtBridge.sol @@ -1,7 +1,13 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.7.4; +pragma experimental ABIEncoderV2; import {sub, wmul, wdiv} from "../../vendor/DSMath.sol"; +import {INSTA_POOL_RESOLVER} from "../../constants/CInstaDapp.sol"; +import {GAS_COSTS_FOR_FULL_REFINANCE} from "../../constants/CDebtBridge.sol"; +import { + IInstaPoolResolver +} from "../../interfaces/InstaDapp/resolvers/IInstaPoolResolver.sol"; function _wCalcCollateralToWithdraw( uint256 _wMinColRatioA, @@ -47,3 +53,29 @@ function _wCalcDebtToRepay( ) ); } + +function _getFlashLoanRoute(address _tokenA, uint256 _wTokenADebtToMove) + view + returns (uint256) +{ + IInstaPoolResolver.RouteData memory rData = IInstaPoolResolver( + INSTA_POOL_RESOLVER + ) + .getTokenLimit(_tokenA); + + if (rData.dydx > _wTokenADebtToMove) return 0; + if (rData.maker > _wTokenADebtToMove) return 1; + if (rData.compound > _wTokenADebtToMove) return 2; + if (rData.aave > _wTokenADebtToMove) return 3; + revert( + "GelateDebtBridge._getRoute: All route have insufficient liquidties." + ); +} + +function _getGasCost(uint256 _route) pure returns (uint256) { + return GAS_COSTS_FOR_FULL_REFINANCE()[_route]; +} + +function _getBorrowAmountWithDelta(uint256 _DebtToMove) pure returns (uint256) { + return wmul(_DebtToMove, 1005e15); +} diff --git a/contracts/interfaces/InstaDapp/resolvers/IInstaPoolResolver.sol b/contracts/interfaces/InstaDapp/resolvers/IInstaPoolResolver.sol new file mode 100644 index 0000000..2f786dc --- /dev/null +++ b/contracts/interfaces/InstaDapp/resolvers/IInstaPoolResolver.sol @@ -0,0 +1,17 @@ +// "SPDX-License-Identifier: UNLICENSED" +pragma solidity 0.7.4; +pragma experimental ABIEncoderV2; + +interface IInstaPoolResolver { + struct RouteData { + uint256 dydx; + uint256 maker; + uint256 compound; + uint256 aave; + } + + function getTokenLimit(address token) + external + view + returns (RouteData memory); +} diff --git a/deploy/gelato/conditions/ConditionDebtBridgeIsAffordable.deploy.js b/deploy/gelato/conditions/ConditionDebtBridgeIsAffordable.deploy.js new file mode 100644 index 0000000..713193e --- /dev/null +++ b/deploy/gelato/conditions/ConditionDebtBridgeIsAffordable.deploy.js @@ -0,0 +1,24 @@ +const {sleep} = require("@gelatonetwork/core"); + +module.exports = async (hre) => { + if (hre.network.name === "mainnet") { + console.log( + "Deploying ConditionDebtBridgeIsAffordable 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 "ConditionMakerVaultUnsafe" + // if the contract was never deployed or if the code changed since last deployment + await deploy("ConditionDebtBridgeIsAffordable", { + from: deployer, + gasPrice: hre.network.config.gasPrice, + log: hre.network.name === "mainnet" ? true : false, + }); +}; + +module.exports.tags = ["ConditionDebtBridgeIsAffordable"]; diff --git a/hardhat.config.js b/hardhat.config.js index 9c385aa..e52d762 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -68,6 +68,7 @@ module.exports = { ConnectInstaPool: "0xCeF5f3c402d4fef76A038e89a4357176963e1464", MakerResolver: "0x0A7008B38E7015F8C36A49eEbc32513ECA8801E5", CompoundResolver: "0x1f22D77365d8BFE3b901C33C83C01B584F946617", + InstaPoolResolver: "0xa004a5afBa04b74037E9E52bA1f7eb02b5E61509", DAI: "0x6b175474e89094c44da98b954eedeac495271d0f", DAI_UNISWAP: "0x2a1530C4C41db0B0b2bB646CB5Eb1A67b7158667", CDAI: "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", diff --git a/test/2_Full-Debt-Bridge-Maker-Compound.test.js b/test/2_Full-Debt-Bridge-Maker-Compound.test.js index d50651e..a1642a4 100644 --- a/test/2_Full-Debt-Bridge-Maker-Compound.test.js +++ b/test/2_Full-Debt-Bridge-Maker-Compound.test.js @@ -5,6 +5,8 @@ const {deployments, ethers} = hre; const GelatoCoreLib = require("@gelatonetwork/core"); const setupFullRefinanceMakerToCompound = require("./helpers/setupFullRefinanceMakerToCompound"); +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 @@ -90,9 +92,20 @@ describe("Full Debt Bridge refinancing loan from Maker to Compound", function () ), }); + const conditionDebtBridgeIsAffordableObj = new GelatoCoreLib.Condition({ + inst: contracts.conditionDebtBridgeIsAffordable.address, + data: await contracts.conditionDebtBridgeIsAffordable.getConditionData( + vaultId, + constants.MAX_FEES_IN_PERCENT + ), + }); + // ======= GELATO TASK SETUP ====== const refinanceIfVaultUnsafe = new GelatoCoreLib.Task({ - conditions: [conditionMakerVaultUnsafeObj], + conditions: [ + conditionMakerVaultUnsafeObj, + conditionDebtBridgeIsAffordableObj, + ], actions: gelatoDebtBridgeSpells, }); @@ -186,7 +199,15 @@ describe("Full Debt Bridge refinancing loan from Maker to Compound", function () vaultId ); - const gasFeesPaidFromCol = ethers.BigNumber.from(1850000).mul( + const route = await getRoute( + contracts.DAI.address, + debtOnMakerBefore, + contracts.instaPoolResolver + ); + + const gasCost = await getGasCostForFullRefinance(route); + + const gasFeesPaidFromCol = ethers.BigNumber.from(gasCost).mul( gelatoGasPrice ); @@ -248,9 +269,21 @@ describe("Full Debt Bridge refinancing loan from Maker to Compound", function () .div(await contracts.cEthToken.totalSupply()); // Estimated amount to borrowed token should be equal to the actual one read on compound contracts - expect(debtOnMakerBefore).to.be.equal( - compoundPosition[0].borrowBalanceStoredUser - ); + if (route === 1) { + expect(debtOnMakerBefore).to.be.lte( + compoundPosition[0].borrowBalanceStoredUser + ); + } else { + expect(debtOnMakerBefore).to.be.equal( + compoundPosition[0].borrowBalanceStoredUser + ); + + // We should not have borrowed DAI on maker + const debtOnMakerAfter = await contracts.makerResolver.getMakerVaultDebt( + vaultId + ); + expect(debtOnMakerAfter).to.be.equal(ethers.constants.Zero); + } // Estimated amount of collateral should be equal to the actual one read on compound contracts expect( @@ -259,15 +292,11 @@ describe("Full Debt Bridge refinancing loan from Maker to Compound", function () ) ).to.be.lt(ethers.utils.parseUnits("1", 12)); - const debtOnMakerAfter = await contracts.makerResolver.getMakerVaultDebt( - vaultId - ); const collateralOnMakerAfter = await contracts.makerResolver.getMakerVaultCollateralBalance( vaultId ); // in Ether. - // We should not have borrowed DAI on maker or deposited ether on it. - expect(debtOnMakerAfter).to.be.equal(ethers.constants.Zero); + // We should not have deposited ether on it. expect(collateralOnMakerAfter).to.be.equal(ethers.constants.Zero); // DSA contain 1000 DAI diff --git a/test/3_Full-Debt-Bridge-ETHA-ETHB.test.js b/test/3_Full-Debt-Bridge-ETHA-ETHB-With-Vault-Creation.test.js similarity index 86% rename from test/3_Full-Debt-Bridge-ETHA-ETHB.test.js rename to test/3_Full-Debt-Bridge-ETHA-ETHB-With-Vault-Creation.test.js index 4204190..fbd2bb0 100644 --- a/test/3_Full-Debt-Bridge-ETHA-ETHB.test.js +++ b/test/3_Full-Debt-Bridge-ETHA-ETHB-With-Vault-Creation.test.js @@ -4,10 +4,12 @@ const {deployments, ethers} = hre; const GelatoCoreLib = require("@gelatonetwork/core"); const setupFullRefinanceMakerToMaker = require("./helpers/setupFullRefinanceMakerToMaker"); +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", function () { +describe("Full Debt Bridge refinancing loan from ETH-A to ETH-B with vault creation for ETH-B ", function () { this.timeout(0); if (hre.network.name !== "hardhat") { console.error("Test Suite is meant to be run on hardhat only"); @@ -92,9 +94,20 @@ describe("Full Debt Bridge refinancing loan from ETH-A to ETH-B", function () { ), }); + 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], + conditions: [ + conditionMakerVaultUnsafeObj, + conditionDebtBridgeIsAffordableObj, + ], actions: gelatoDebtBridgeSpells, }); @@ -188,7 +201,15 @@ describe("Full Debt Bridge refinancing loan from ETH-A to ETH-B", function () { vaultAId ); - const gasFeesPaidFromCol = ethers.BigNumber.from(1850000).mul( + const route = await getRoute( + contracts.DAI.address, + debtOnMakerBefore, + contracts.instaPoolResolver + ); + + const gasCost = await getGasCostForFullRefinance(route); + + const gasFeesPaidFromCol = ethers.BigNumber.from(gasCost).mul( gelatoGasPrice ); @@ -250,20 +271,26 @@ describe("Full Debt Bridge refinancing loan from ETH-A to ETH-B", function () { ); // Estimated amount to borrowed token should be equal to the actual one read on compound contracts - expect(debtOnMakerBefore).to.be.equal(debtOnMakerVaultB); + 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 debtOnMakerOnVaultAAfter = await contracts.makerResolver.getMakerVaultDebt( - vaultAId - ); const collateralOnMakerOnVaultAAfter = await contracts.makerResolver.getMakerVaultCollateralBalance( vaultAId ); // in Ether. - // We should not have borrowed DAI on maker or deposited ether on it. - expect(debtOnMakerOnVaultAAfter).to.be.equal(ethers.constants.Zero); + // 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 diff --git a/test/4_Full-Debt-Bridge-ETHA-ETHB.test.js b/test/4_Full-Debt-Bridge-ETHA-ETHB.test.js new file mode 100644 index 0000000..ee6c493 --- /dev/null +++ b/test/4_Full-Debt-Bridge-ETHA-ETHB.test.js @@ -0,0 +1,303 @@ +const {expect} = require("chai"); +const hre = require("hardhat"); +const {deployments, ethers} = hre; +const GelatoCoreLib = require("@gelatonetwork/core"); + +const setupFullRefinanceMakerToMakerWithVaultBCreation = require("./helpers/setupFullRefinanceMakerToMakerWithVaultBCreation"); +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", 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 setupFullRefinanceMakerToMakerWithVaultBCreation(); + + 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.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 + 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.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 + ); + 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/createVaultForETHB.js b/test/helpers/services/createVaultForETHB.js new file mode 100644 index 0000000..9d54af1 --- /dev/null +++ b/test/helpers/services/createVaultForETHB.js @@ -0,0 +1,35 @@ +const {expect} = require("chai"); +const hre = require("hardhat"); + +const ConnectMaker = require("../../../pre-compiles/ConnectMaker.json"); + +async function createVaultForETHB( + userAddress, + DAI, + dsa, + getCdps, + dssCdpManager +) { + //#region Step 8 User open a Vault, put some ether on it and borrow some dai + + // User open a maker vault + // He deposit 10 Eth on it + // He borrow a 1000 DAI + const openVault = await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "open", + inputs: ["ETH-B"], + }); + + await dsa.cast([hre.network.config.ConnectMaker], [openVault], userAddress); + + const cdps = await getCdps.getCdpsAsc(dssCdpManager.address, dsa.address); + let vaultId = String(cdps.ids[1]); + expect(cdps.ids[1].isZero()).to.be.false; + + //#endregion + + return vaultId; +} + +module.exports = createVaultForETHB; diff --git a/test/helpers/services/getConstants.js b/test/helpers/services/getConstants.js index c14bc8a..035557c 100644 --- a/test/helpers/services/getConstants.js +++ b/test/helpers/services/getConstants.js @@ -2,7 +2,7 @@ const hre = require("hardhat"); const {ethers} = hre; const ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; -const GAS_LIMIT = "4000000"; +const GAS_LIMIT = "5000000"; const GAS_PRICE_CEIL = ethers.utils.parseUnits("1000", "gwei"); const MIN_COL_RATIO_MAKER = ethers.utils.parseUnits("3", 18); @@ -11,6 +11,8 @@ const MIN_COL_RATIO_MAKER = ethers.utils.parseUnits("3", 18); const MAKER_INITIAL_ETH = ethers.utils.parseEther("10"); const MAKER_INITIAL_DEBT = ethers.utils.parseUnits("1000", 18); +const MAX_FEES_IN_PERCENT = ethers.utils.parseUnits("1", 17); + async function getConstants() { return { ETH: ETH, @@ -19,6 +21,7 @@ async function getConstants() { GAS_LIMIT: GAS_LIMIT, MAKER_INITIAL_DEBT: MAKER_INITIAL_DEBT, MAKER_INITIAL_ETH: MAKER_INITIAL_ETH, + MAX_FEES_IN_PERCENT: MAX_FEES_IN_PERCENT, }; } diff --git a/test/helpers/services/getContracts.js b/test/helpers/services/getContracts.js index 3ff71d9..8797fe3 100644 --- a/test/helpers/services/getContracts.js +++ b/test/helpers/services/getContracts.js @@ -17,6 +17,7 @@ const IERC20 = require("../../../pre-compiles/IERC20.json"); const CTokenInterface = require("../../../pre-compiles/CTokenInterface.json"); const CompoundResolver = require("../../../pre-compiles/InstaCompoundResolver.json"); const DsaProviderModuleABI = require("../../../pre-compiles/ProviderModuleDsa_ABI.json"); +const InstaPoolResolver = require("../../../artifacts/contracts/interfaces/InstaDapp/resolvers/IInstaPoolResolver.sol/IInstaPoolResolver.json"); async function getContracts() { const instaMaster = await ethers.provider.getSigner( @@ -85,12 +86,19 @@ async function getContracts() { DsaProviderModuleABI, hre.network.config.ProviderModuleDsa ); + const instaPoolResolver = await ethers.getContractAt( + InstaPoolResolver.abi, + hre.network.config.InstaPoolResolver + ); // ===== Get deployed contracts ================== const priceOracleResolver = await ethers.getContract("PriceOracleResolver"); const conditionMakerVaultUnsafe = await ethers.getContract( "ConditionMakerVaultUnsafe" ); + const conditionDebtBridgeIsAffordable = await ethers.getContract( + "ConditionDebtBridgeIsAffordable" + ); const connectGelatoProviderPayment = await ethers.getContract( "ConnectGelatoProviderPayment" ); @@ -122,7 +130,9 @@ async function getContracts() { priceOracleResolver, dsa: ethers.constants.AddressZero, makerResolver, + instaPoolResolver, dsaProviderModule, + conditionDebtBridgeIsAffordable, }; } diff --git a/test/helpers/services/getGasCostForFullRefinance.js b/test/helpers/services/getGasCostForFullRefinance.js new file mode 100644 index 0000000..1b245c0 --- /dev/null +++ b/test/helpers/services/getGasCostForFullRefinance.js @@ -0,0 +1,16 @@ +async function getGasCostForFullRefinance(route) { + switch (route) { + case 0: + return 2519000; // 2290000 * 1,1 + case 1: + return 3140500; // 2855000 * 1,1 + case 2: + return 3971000; // 3610000 * 1,1 + case 3: + return 4345000; // 3950000 * 1,1 + default: + break; + } +} + +module.exports = getGasCostForFullRefinance; diff --git a/test/helpers/services/getRoute.js b/test/helpers/services/getRoute.js new file mode 100644 index 0000000..69fa728 --- /dev/null +++ b/test/helpers/services/getRoute.js @@ -0,0 +1,10 @@ +async function getRoute(token, tokenDebtToMove, instaPoolResolver) { + const rData = await instaPoolResolver.getTokenLimit(token); + + if (rData.dydx > tokenDebtToMove) return 0; + if (rData.maker > tokenDebtToMove) return 1; + if (rData.compound > tokenDebtToMove) return 2; + if (rData.aave > tokenDebtToMove) return 3; +} + +module.exports = getRoute; diff --git a/test/helpers/services/providerWhiteListTaskForMakerETHAToMakerETHB.js b/test/helpers/services/providerWhiteListTaskForMakerETHAToMakerETHB.js index 88db46f..135cb13 100644 --- a/test/helpers/services/providerWhiteListTaskForMakerETHAToMakerETHB.js +++ b/test/helpers/services/providerWhiteListTaskForMakerETHAToMakerETHB.js @@ -25,7 +25,7 @@ async function providerWhiteListTaskForMakerETHAToMakerETHB( abi: (await deployments.getArtifact("ConnectGelatoDataForFullRefinance")) .abi, functionname: "getDataAndCastForFromMakerToMaker", - inputs: [vaultId, constants.ETH, "ETH-B"], + inputs: [vaultId, 0, constants.ETH, "ETH-B"], }), operation: GelatoCoreLib.Operation.Delegatecall, }); @@ -36,7 +36,10 @@ async function providerWhiteListTaskForMakerETHAToMakerETHB( const connectGelatoFullDebtBridgeFromMakerTaskSpec = new GelatoCoreLib.TaskSpec( { - conditions: [contracts.conditionMakerVaultUnsafe.address], + conditions: [ + contracts.conditionMakerVaultUnsafe.address, + contracts.conditionDebtBridgeIsAffordable.address, + ], actions: spells, gasPriceCeil, } diff --git a/test/helpers/services/providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB.js b/test/helpers/services/providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB.js new file mode 100644 index 0000000..835b463 --- /dev/null +++ b/test/helpers/services/providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB.js @@ -0,0 +1,80 @@ +const {expect} = require("chai"); +const hre = require("hardhat"); +const {deployments, ethers} = hre; +const GelatoCoreLib = require("@gelatonetwork/core"); + +// Instadapp UI should do the same implementation for submitting debt bridge task +async function providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB( + wallets, + contracts, + constants, + vaultAId, + vaultBId +) { + //#region Step 9 Provider should whitelist task + + // By WhiteList task, the provider can constrain the type + // of task the user can submitting. + + //#region Actions + + const spells = []; + + const debtBridgeCalculationForFullRefinance = new GelatoCoreLib.Action({ + addr: contracts.connectGelatoDataForFullRefinance.address, + data: await hre.run("abi-encode-withselector", { + abi: (await deployments.getArtifact("ConnectGelatoDataForFullRefinance")) + .abi, + functionname: "getDataAndCastForFromMakerToMaker", + inputs: [vaultAId, vaultBId, constants.ETH, "ETH-B"], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(debtBridgeCalculationForFullRefinance); + + const gasPriceCeil = ethers.constants.MaxUint256; + + const connectGelatoFullDebtBridgeFromMakerTaskSpec = new GelatoCoreLib.TaskSpec( + { + conditions: [ + contracts.conditionMakerVaultUnsafe.address, + contracts.conditionDebtBridgeIsAffordable.address, + ], + actions: spells, + gasPriceCeil, + } + ); + + await expect( + contracts.gelatoCore + .connect(wallets.gelatoProviderWallet) + .provideTaskSpecs([connectGelatoFullDebtBridgeFromMakerTaskSpec]) + ).to.emit(contracts.gelatoCore, "LogTaskSpecProvided"); + + expect( + await contracts.gelatoCore + .connect(wallets.gelatoProviderWallet) + .isTaskSpecProvided( + wallets.gelatoProviderAddress, + connectGelatoFullDebtBridgeFromMakerTaskSpec + ) + ).to.be.equal("OK"); + + expect( + await contracts.gelatoCore + .connect(wallets.gelatoProviderWallet) + .taskSpecGasPriceCeil( + wallets.gelatoProviderAddress, + await contracts.gelatoCore + .connect(wallets.gelatoProviderWallet) + .hashTaskSpec(connectGelatoFullDebtBridgeFromMakerTaskSpec) + ) + ).to.be.equal(gasPriceCeil); + + //#endregion + + return spells; +} + +module.exports = providerWhiteListTaskForMakerETHAToMakerETHBWithVaultB; diff --git a/test/helpers/services/providerWhiteListTaskForMakerToCompound.js b/test/helpers/services/providerWhiteListTaskForMakerToCompound.js index 409368d..38b64b5 100644 --- a/test/helpers/services/providerWhiteListTaskForMakerToCompound.js +++ b/test/helpers/services/providerWhiteListTaskForMakerToCompound.js @@ -36,7 +36,10 @@ async function providerWhiteListTaskForMakerToCompound( const connectGelatoFullDebtBridgeFromMakerTaskSpec = new GelatoCoreLib.TaskSpec( { - conditions: [contracts.conditionMakerVaultUnsafe.address], + conditions: [ + contracts.conditionMakerVaultUnsafe.address, + contracts.conditionDebtBridgeIsAffordable.address, + ], actions: spells, gasPriceCeil, } diff --git a/test/helpers/setupFullRefinanceMakerToMakerWithVaultBCreation.js b/test/helpers/setupFullRefinanceMakerToMakerWithVaultBCreation.js new file mode 100644 index 0000000..80d5be6 --- /dev/null +++ b/test/helpers/setupFullRefinanceMakerToMakerWithVaultBCreation.js @@ -0,0 +1,85 @@ +const getWallets = require("./services/getWallets"); +const getContracts = require("./services/getContracts"); +const getConstants = require("./services/getConstants"); +const stakeExecutor = require("./services/stakeExecutor"); +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(); + + // Gelato Testing environment setup. + await stakeExecutor(wallets.gelatoExecutorWallet, contracts.gelatoCore); + await provideFunds( + wallets.gelatoProviderWallet, + contracts.gelatoCore, + constants.GAS_LIMIT, + constants.GAS_PRICE_CEIL + ); + await providerAssignsExecutor( + wallets.gelatoProviderWallet, + wallets.gelatoExecutorAddress, + 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; diff --git a/test/unit_tests/4_ConditionDebtBridgeIsAffordable.test.js b/test/unit_tests/4_ConditionDebtBridgeIsAffordable.test.js new file mode 100644 index 0000000..5a4c91f --- /dev/null +++ b/test/unit_tests/4_ConditionDebtBridgeIsAffordable.test.js @@ -0,0 +1,157 @@ +const {expect} = require("chai"); +const hre = require("hardhat"); +const {deployments, ethers} = hre; + +// #region Contracts ABI + +const ConnectMaker = require("../../pre-compiles/ConnectMaker.json"); +const GetCdps = require("../../pre-compiles/GetCdps.json"); +const DssCdpManager = require("../../pre-compiles/DssCdpManager.json"); +const InstaList = require("../../pre-compiles/InstaList.json"); +const InstaAccount = require("../../pre-compiles/InstaAccount.json"); +const InstaIndex = require("../../pre-compiles/InstaIndex.json"); +const IERC20 = require("../../pre-compiles/IERC20.json"); + +// #endregion + +describe("ConditionDebtBridgeIsAffordable Unit Test", 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 userWallet; + let userAddress; + + let getCdps; + let dssCdpManager; + let instaList; + let instaIndex; + let DAI; + + let conditionDebtBridgeIsAffordable; + + let dsa; + let cdpId; + + beforeEach(async function () { + // Deploy contract dependencies + 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") + ); + + instaIndex = await ethers.getContractAt( + InstaIndex.abi, + hre.network.config.InstaIndex + ); + instaList = await ethers.getContractAt( + InstaList.abi, + hre.network.config.InstaList + ); + getCdps = await ethers.getContractAt( + GetCdps.abi, + hre.network.config.GetCdps + ); + dssCdpManager = await ethers.getContractAt( + DssCdpManager.abi, + hre.network.config.DssCdpManager + ); + DAI = await ethers.getContractAt(IERC20.abi, hre.network.config.DAI); + + // ========== Test Setup ============ + conditionDebtBridgeIsAffordable = await ethers.getContract( + "ConditionDebtBridgeIsAffordable" + ); + + // Create DeFi Smart Account + + const dsaAccountCount = await instaList.accounts(); + + await expect(instaIndex.build(userAddress, 1, userAddress)).to.emit( + instaIndex, + "LogAccountCreated" + ); + const dsaID = dsaAccountCount.add(1); + await expect(await instaList.accounts()).to.be.equal(dsaID); + + // Instantiate the DSA + dsa = await ethers.getContractAt( + InstaAccount.abi, + await instaList.accountAddr(dsaID) + ); + + // Create/Deposit/Borrow a Vault + const openVault = await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "open", + inputs: ["ETH-A"], + }); + + await dsa.cast([hre.network.config.ConnectMaker], [openVault], userAddress); + + let cdps = await getCdps.getCdpsAsc(dssCdpManager.address, dsa.address); + cdpId = String(cdps.ids[0]); + + expect(cdps.ids[0].isZero()).to.be.false; + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpId, ethers.utils.parseEther("10"), 0, 0], + }), + ], + userAddress, + { + value: ethers.utils.parseEther("10"), + } + ); + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpId, ethers.utils.parseUnits("1000", 18), 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal( + ethers.utils.parseEther("1000") + ); + }); + + it("#1: ok should return DebtRefinanceTooExpensive when the gas fees exceed a define amount", async function () { + const conditionData = await conditionDebtBridgeIsAffordable.getConditionData( + cdpId, + ethers.utils.parseUnits("5", 15) + ); + + expect( + await conditionDebtBridgeIsAffordable.ok(0, conditionData, 0) + ).to.be.equal("DebtRefinanceTooExpensive"); + }); + + it("#2: ok should return OK when the gas fees not exceed a define amount", async function () { + const conditionData = await conditionDebtBridgeIsAffordable.getConditionData( + cdpId, + ethers.utils.parseUnits("10", 15) + ); + + expect( + await conditionDebtBridgeIsAffordable.ok(0, conditionData, 0) + ).to.be.equal("OK"); + }); +});