From ff551cf52bda13cd2f115d43d4279844abb03097 Mon Sep 17 00:00:00 2001 From: Shivva Date: Fri, 23 Oct 2020 15:02:32 +0200 Subject: [PATCH] chore: rebased Full refinancing on new master --- .prettierignore | 4 +- ConnectGelatoDebtBridge.sol | 531 +++++++++++ README.md | 15 + ...rtial-Refinance-External-Provider.test.js} | 0 ...t.js => 6_ConnectGelatoDebtBridge.test.js} | 0 ...SA.test.js => 7_ProviderModuleDSA.test.js} | 0 ...2_Full-Refinance-External-Provider.test.js | 831 ++++++++++++++++++ 7 files changed, 1379 insertions(+), 2 deletions(-) create mode 100644 ConnectGelatoDebtBridge.sol rename test/{2_Debt-Bridge-External-Provider.test.js => 3_Partial-Refinance-External-Provider.test.js} (100%) rename test/{7_ConnectGelatoDebtBridge.test.js => 6_ConnectGelatoDebtBridge.test.js} (100%) rename test/{6_ProviderModuleDSA.test.js => 7_ProviderModuleDSA.test.js} (100%) create mode 100644 test_temp/2_Full-Refinance-External-Provider.test.js diff --git a/.prettierignore b/.prettierignore index d0e41a6..da5fa0f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,5 @@ -# Ignore artifacts: -#artifacts +# Ignore: +artifacts cache assets coverage diff --git a/ConnectGelatoDebtBridge.sol b/ConnectGelatoDebtBridge.sol new file mode 100644 index 0000000..9ca81da --- /dev/null +++ b/ConnectGelatoDebtBridge.sol @@ -0,0 +1,531 @@ +// "SPDX-License-Identifier: UNLICENSED" +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import "./IMemoryInterface.sol"; +import "./DSMath.sol"; + +interface ConnectorInterface { + function connectorID() external view returns (uint256 _type, uint256 _id); + + function name() external view returns (string memory); +} + +interface OracleAggregator { + function getMakerTokenPrice(string memory _pair) + external + view + returns (uint256); +} + +interface GelatoGasPriceOracle { + function latestAnswer() external view returns (int256); +} + +interface IMakerResolver { + struct VaultData { + uint256 id; + address owner; + string colType; + uint256 collateral; + uint256 art; + uint256 debt; + uint256 liquidatedCol; + uint256 borrowRate; + uint256 colPrice; + uint256 liquidationRatio; + address vaultAddress; + } + + function getVaultById(uint256 id) external view returns (VaultData memory); +} + +interface ICompoundResolver { + struct CompData { + uint256 tokenPriceInEth; + uint256 tokenPriceInUsd; + uint256 exchangeRateStored; + uint256 balanceOfUser; + uint256 borrowBalanceStoredUser; + uint256 supplyRatePerBlock; + uint256 borrowRatePerBlock; + } + + function getCompoundData(address owner, address[] memory cAddress) + external + view + returns (CompData[] memory); +} + +interface IAaveResolver { + struct AaveTokenData { + uint256 ltv; + uint256 threshold; + bool usageAsCollEnabled; + bool borrowEnabled; + bool stableBorrowEnabled; + bool isActive; + } + + struct AaveUserTokenData { + uint256 tokenPriceInEth; + uint256 tokenPriceInUsd; + uint256 supplyBalance; + uint256 borrowBalance; + uint256 borrowFee; + uint256 supplyRate; + uint256 borrowRate; + uint256 borrowModal; + AaveTokenData aaveTokenData; + } + + struct AaveUserData { + uint256 totalSupplyETH; + uint256 totalCollateralETH; + uint256 totalBorrowsETH; + uint256 totalFeesETH; + uint256 availableBorrowsETH; + uint256 currentLiquidationThreshold; + uint256 ltv; + uint256 healthFactor; + uint256 ethPriceInUsd; + } + + function getPosition(address user, address[] memory tokens) + external + view + returns (AaveUserTokenData[] memory, AaveUserData memory); +} + +abstract contract Helpers is ConnectorInterface, DSMath { + uint256 internal _id; + + /** + * @dev Return ethereum address + */ + function _getAddressETH() internal pure returns (address) { + return 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; // ETH Address + } + + /** + * @dev Return Memory Variable Address + */ + function _getMemoryAddr() internal pure returns (address) { + return 0x8a5419CfC711B2343c17a6ABf4B2bAFaBb06957F; // InstaMemory Address + } + + /** + * @dev Set Uint value in InstaMemory Contract. + */ + function _setUint(uint256 setId, uint256 val) internal { + if (setId != 0) IMemoryInterface(_getMemoryAddr()).setUint(setId, val); + } + + /** + * @dev Get Uint value from InstaMemory Contract. + */ + function _getUint(uint256 getId, uint256 val) + internal + returns (uint256 returnVal) + { + returnVal = getId == 0 + ? val + : IMemoryInterface(_getMemoryAddr()).getUint(getId); + } + + /** + * @dev Connector Details + */ + function connectorID() + public + view + override + returns (uint256 _type, uint256 _iD) + { + (_type, _iD) = (1, _id); // Should put specific value. + } + + function _stringToBytes32(string memory str) + internal + pure + returns (bytes32 result) + { + require(bytes(str).length != 0, "String-Empty"); + // solium-disable-next-line security/no-inline-assembly + assembly { + result := mload(add(str, 32)) + } + } +} + +abstract contract ConnectGelatoDebtBridgeHelpers is Helpers { + function _getMakerResolver() internal pure returns (address) { + return 0x0A7008B38E7015F8C36A49eEbc32513ECA8801E5; + } + + function _getCompoundResolver() internal pure returns (address) { + return 0x1f22D77365d8BFE3b901C33C83C01B584F946617; + } + + function _getAaveResolver() internal pure returns (address) { + return 0xe04Cd009fF68628BC663058dDAA7E5Bf7979BEaF; + } + + function _getGelatoGasPriceOracle() internal pure returns (address) { + return 0x169E633A2D1E6c10dD91238Ba11c4A708dfEF37C; + } + + function _getGasPrice() internal view returns (uint256) { + return + uint256( + GelatoGasPriceOracle(_getGelatoGasPriceOracle()).latestAnswer() + ); + } +} + +abstract contract ConnectGelatoDebtBridgeResolver is + ConnectGelatoDebtBridgeHelpers +{ + mapping(address => address) internal _priceFeeds; + + function getMakerVault(uint256 _vaultID) + public + view + returns (IMakerResolver.VaultData memory) + { + // call maker resolver. + return IMakerResolver(_getMakerResolver()).getVaultById(_vaultID); + } + + function getMakerVaultDebt(uint256 _vaultID) public view returns (uint256) { + return getMakerVault(_vaultID).debt; + } + + function getMakerVaultCollateralBalance(uint256 _vaultID) + public + view + returns (uint256) + { + return getMakerVault(_vaultID).collateral; + } + + function getMakerVaultCollateralType(uint256 _vaultID) + public + view + returns (string memory) + { + return getMakerVault(_vaultID).colType; + } + + function getCompoundData(address _owner, address _cAddress) + public + view + returns (ICompoundResolver.CompData memory) + { + address[] memory cAddressArray; + cAddressArray[0] = _cAddress; + return + ICompoundResolver(_getCompoundResolver()).getCompoundData( + _owner, + cAddressArray + )[0]; + } + + function getCompoundDebt(address _owner, address _cAddress) + public + view + returns (uint256) + { + return getCompoundData(_owner, _cAddress).borrowBalanceStoredUser; + } + + function getCompoundCollateralBalance(address _owner, address _cAddress) + public + view + returns (uint256) + { + return getCompoundData(_owner, _cAddress).balanceOfUser; + } + + function getAaveTokenData(address _owner, address _atoken) + public + view + returns ( + IAaveResolver.AaveUserTokenData memory, + IAaveResolver.AaveUserData memory + ) + { + address[] memory aTokenArray; + aTokenArray[0] = _atoken; + ( + IAaveResolver.AaveUserTokenData[] memory tokensData, + IAaveResolver.AaveUserData memory etherUserData + ) = IAaveResolver(_getAaveResolver()).getPosition(_owner, aTokenArray); + return (tokensData[0], etherUserData); + } + + function getAaveTokenDebt(address _owner, address _atoken) + public + view + returns (uint256) + { + (IAaveResolver.AaveUserTokenData memory tokenData, ) = getAaveTokenData( + _owner, + _atoken + ); + return tokenData.supplyBalance; + } + + function getAaveTokenCollateralBalance(address _owner, address _atoken) + public + view + returns (uint256) + { + (IAaveResolver.AaveUserTokenData memory tokenData, ) = getAaveTokenData( + _owner, + _atoken + ); + return tokenData.borrowBalance; + } +} + +contract ConnectGelatoDebtBridge is ConnectGelatoDebtBridgeResolver { + // Constant name must be in capitalized SNAKE_CASE + // solhint-disable-next-line + string public constant override name = "GelatoDebtBridge-v1.0"; + uint256 public constant GASLIMIT = 1933090 + (19331 * 2); // 1933080 + ~2% (Estimated Value) + address public immutable oracleAggregator; + + constructor(uint256 _iD, address _oracleAggregator) public { + _id = _iD; + oracleAggregator = _oracleAggregator; + } + + /// @notice Write in instaMemory the needed values for doing full refinancing between makerDAO and Compound. + /// @param _vaultID is the id of the makerDAO vault. + /// @param _getID Id for writting in instaMemory. + /// @param _setID Id for loading from instaMemory. + function fullRefinanceMakerToCompound( + uint256 _vaultID, + uint256 _getID, + uint256 _setID + ) external payable { + uint256 fees = _getFees(); // get Fees + uint256 paybackAmount = getMakerVaultDebt(_vaultID); + uint256 collateralToWithdraw = getMakerVaultCollateralBalance(_vaultID); + + _setUint(100, paybackAmount); + _setUint(101, paybackAmount); // payback maker + _setUint(102, collateralToWithdraw); // withdraw maker + _setUint(103, _sub(collateralToWithdraw, fees)); // deposit compound + _setUint(104, paybackAmount); // borrow compound + _setUint(105, fees); // pay the provider + } + + /// @notice Write in instaMemory the needed values for doing partial refinancing between makerDAO and Compound. + /// @param _vaultID is the id of the makerDAO vault. + /// @param _vaultCollateralizationRatio is the collateralization ratio wanted by the client. + /// @param _compPosCollateralizationRatio is the collateralization ratio wanted by the client. + /// @param _pair crypto currency pair used (collateral token/ borrowed token). + /// @param _getID Id for writting in instaMemory. + /// @param _setID Id for loading from instaMemory. + function partialRefinanceMakerToCompound( + uint256 _vaultID, + uint256 _vaultCollateralizationRatio, // should be in ray because maker use ray standard + uint256 _compPosCollateralizationRatio, // should be in wad because compound use wad standard + string memory _pair, + uint256 _getID, + uint256 _setID + ) external payable { + ( + uint256 paybackAmount, + uint256 collateralToWithdraw, + uint256 fees + ) = debtBridgeCompute( + _vaultID, + _vaultCollateralizationRatio, + _compPosCollateralizationRatio, + _pair + ); + + _setUint(100, paybackAmount); + _setUint(101, paybackAmount); // payback maker + _setUint(102, _add(collateralToWithdraw, fees)); // withdraw maker + _setUint(103, collateralToWithdraw); // deposit compound + _setUint(104, paybackAmount); // borrow compound + _setUint(105, fees); // pay the provider + } + + // Price Oracle + + function debtBridgeCompute( + uint256 _vaultID, + uint256 _vaultLiquidationRatio, // should be in ray because maker use ray standard + uint256 _compPosLiquidationRatio, // should be in wad because compound use wad standard + string memory _pair + ) + public + view + returns ( + uint256 paybackAmount, + uint256 collateralToWithdraw, + uint256 fees + ) + { + uint256 latestPrice = _getLatestPrice(_pair); + // uint256 fees = mul(GASLIMIT, wmul(_getGasPrice(), latestPrice)); + fees = _getFees(); + + uint256 debt = getMakerVaultDebt(_vaultID); + uint256 collateral = _wmul( + _sub(getMakerVaultCollateralBalance(_vaultID), fees), + latestPrice + ); + + collateralToWithdraw = wcollateralToWithdraw( + _vaultLiquidationRatio, + _compPosLiquidationRatio, + collateral, + debt, + latestPrice + ); + paybackAmount = wborrowedTokenToPayback( + _vaultLiquidationRatio, + _compPosLiquidationRatio, + collateral, + debt + ); + } + + function _getFees() internal view returns (uint256 gasCost) { + gasCost = _mul(GASLIMIT, _getGasPrice()); + } + + function _getLatestPrice(string memory _pair) + internal + view + returns (uint256) + { + return OracleAggregator(oracleAggregator).getMakerTokenPrice(_pair); + } + + /// Computation in ray + /// @notice return the amount of collateral we need to withdraw during the debt refinancing in ray standard. + /// @param _p1LiqRatio the liquidation ratio of protocol 1. + /// @param _p2LiqRatio the liquidation ratio of protocol 2. + /// @param _col token1 collateral to put on protocol 1. + /// @param _bor amount of borrowed token2 on protocol 1. + /// @param _colPrice price of the collateral. + /// @return collateral to withdraw in ray standard + function _rcollateralToWithdraw( + uint256 _p1LiqRatio, + uint256 _p2LiqRatio, + uint256 _col, + uint256 _bor, + uint256 _colPrice + ) internal pure returns (uint256) { + return + _rdiv( + _sub( + _col, + _rdiv( + _sub( + _rmul(_p1LiqRatio, _col), + _rmul(_p1LiqRatio, _rmul(_p2LiqRatio, _bor)) + ), + _sub(_p1LiqRatio, _p2LiqRatio) + ) + ), + _colPrice + ); + } + + /// Computation in ray + /// @notice return the amount of borrowed token we need to payback during the debt refinancing in ray standard. + /// @param _p1LiqRatio the liquidation ratio of protocol 1. + /// @param _p2LiqRatio the liquidation ratio of protocol 2. + /// @param _col token1 collateral to put on protocol 1. + /// @param _bor amount of borrowed token2 on protocol 1. + /// @return amount of borrowed token to pay back in ray standard + function _rborrowedTokenToPayback( + uint256 _p1LiqRatio, + uint256 _p2LiqRatio, + uint256 _col, + uint256 _bor + ) internal pure returns (uint256) { + return + _sub( + _bor, + _rmul( + _rdiv(1e18, _p1LiqRatio), + _rdiv( + _sub( + _rmul(_p1LiqRatio, _col), + _rmul(_p1LiqRatio, _rmul(_p2LiqRatio, _bor)) + ), + _sub(_p1LiqRatio, _p2LiqRatio) + ) + ) + ); + } + + /// Computation in wad + /// @notice return the amount of collateral we need to withdraw during the debt refinancing in wad standard. + /// @param _p1LiqRatio the liquidation ratio of protocol 1. + /// @param _p2LiqRatio the liquidation ratio of protocol 2. + /// @param _col token1 collateral to put on protocol 1. + /// @param _bor amount of borrowed token2 on protocol 1. + /// @param _colPrice price of the collateral. + /// @return collateral to withdraw in wad standard + function wcollateralToWithdraw( + uint256 _p1LiqRatio, + uint256 _p2LiqRatio, + uint256 _col, + uint256 _bor, + uint256 _colPrice + ) public pure returns (uint256) { + return + _wdiv( + _sub( + _col, + _wdiv( + _sub( + _wmul(_p1LiqRatio, _col), + _wmul(_p1LiqRatio, _wmul(_p2LiqRatio, _bor)) + ), + _sub(_p1LiqRatio, _p2LiqRatio) + ) + ), + _colPrice + ); + } + + /// Computation in wad + /// @notice return the amount of borrowed token we need to payback during the debt refinancing in wad standard. + /// @param _p1LiqRatio the liquidation ratio of protocol 1. + /// @param _p2LiqRatio the liquidation ratio of protocol 2. + /// @param _col token1 collateral to put on protocol 1. + /// @param _bor amount of borrowed token2 on protocol 1. + /// @return amount of borrowed token to pay back in wad standard + function wborrowedTokenToPayback( + uint256 _p1LiqRatio, + uint256 _p2LiqRatio, + uint256 _col, + uint256 _bor + ) public pure returns (uint256) { + return + _sub( + _bor, + _wmul( + _wdiv(1e18, _p1LiqRatio), + _wdiv( + _sub( + _wmul(_p1LiqRatio, _col), + _wmul(_p1LiqRatio, _wmul(_p2LiqRatio, _bor)) + ), + _sub(_p1LiqRatio, _p2LiqRatio) + ) + ) + ); + } +} diff --git a/README.md b/README.md index abd8b9c..bfde4f9 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,21 @@ Furtheremore the following contracts were added to showcase the automation of th To see for yourself check out the [contracts](./contracts) folder and make sure to check out `test/mv-DAI-DSR-Compound.test.js`, to see an end-to-end test showcasing the prototype. To do so follow the steps below: +#### Debt Bridge + +Debt Bridge is a finance process that aim to make the user position safer. InstaDapp DSA could use Gelato to automate this process. + +**Full Refinancing from Maker's Vault to Compound.** +Based on the [debt bridge](https://docs.instadapp.io/usecases/debt-bridge/) documentation of Instadapp, we automated this process by adding two connectors `ConnectGelatoDebtBridge`, `ConnectGelatoProviderPayment` and a Gelato condition contract. + +- `ConditionMakerVaultIsSafe.sol`: determine if a specific vault is on an unsafe position. + +- `ConnectGelatoDebtBridge.sol`: use Instamemory to assign values to the differents inputs needed by `ConnectMaker` and `ConnectCompound` like the amount of DAI to pay back, the amount of Ether to withdraw from Maker etc... + +- `ConnectGelatoProviderPayment.sol`: will pay the Gelato provider (the Ethereum account paying the transaction). + +For understanding the entire process of a full refinancing, from the opening of the vault on maker to the execution of the refinancing on InstaDapp, take a look at `test/2_Full-Refinance-External-Provider.test.js` test. + 1. Clone this repo 2. Put your Alchemy ID in .env 3. yarn install diff --git a/test/2_Debt-Bridge-External-Provider.test.js b/test/3_Partial-Refinance-External-Provider.test.js similarity index 100% rename from test/2_Debt-Bridge-External-Provider.test.js rename to test/3_Partial-Refinance-External-Provider.test.js diff --git a/test/7_ConnectGelatoDebtBridge.test.js b/test/6_ConnectGelatoDebtBridge.test.js similarity index 100% rename from test/7_ConnectGelatoDebtBridge.test.js rename to test/6_ConnectGelatoDebtBridge.test.js diff --git a/test/6_ProviderModuleDSA.test.js b/test/7_ProviderModuleDSA.test.js similarity index 100% rename from test/6_ProviderModuleDSA.test.js rename to test/7_ProviderModuleDSA.test.js diff --git a/test_temp/2_Full-Refinance-External-Provider.test.js b/test_temp/2_Full-Refinance-External-Provider.test.js new file mode 100644 index 0000000..8d46827 --- /dev/null +++ b/test_temp/2_Full-Refinance-External-Provider.test.js @@ -0,0 +1,831 @@ +const {expect} = require("chai"); +const bre = require("@nomiclabs/buidler"); +const {constants} = require("ethers"); +const {ethers} = bre; +const GelatoCoreLib = require("@gelatonetwork/core"); + +// #region Contracts ABI + +const InstaIndex = require("../pre-compiles/InstaIndex.json"); +const InstaList = require("../pre-compiles/InstaList.json"); +const InstaAccount = require("../pre-compiles/InstaAccount.json"); +const ConnectGelato = require("../pre-compiles/ConnectGelato.json"); +const ConnectMaker = require("../pre-compiles/ConnectMaker.json"); +const ConnectCompound = require("../pre-compiles/ConnectCompound.json"); +const ConnectInstaPool = require("../pre-compiles/ConnectInstaPool.json"); +const ConnectAuth = require("../pre-compiles/ConnectAuth.json"); +const ConnectGelatoDebtBridgeABI = require("../artifacts/ConnectGelatoDebtBridge.json"); +const ConnectGelatoProviderPaymentABI = require("../artifacts/ConnectGelatoProviderPayment.json"); +const InstaConnector = require("../pre-compiles/InstaConnectors.json"); +const DssCdpManager = require("../pre-compiles/DssCdpManager.json"); +const GetCdps = require("../pre-compiles/GetCdps.json"); +const IERC20 = require("../pre-compiles/IERC20.json"); +const CTokenInterface = require("../pre-compiles/CTokenInterface.json"); +const GelatoGasPriceOracle = require("../pre-compiles/GelatoGasPriceOracle.json"); +const CompoundResolver = require("../pre-compiles/InstaCompoundResolver.json"); + +const ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +const GAS_LIMIT = "4000000"; +const GAS_PRICE_CEIL = ethers.utils.parseUnits("1000", "gwei"); + +// #endregion + +describe("Debt Bridge with External Provider", function () { + this.timeout(0); + if (bre.network.name !== "ganache") { + console.error("Test Suite is meant to be run on ganache only"); + process.exit(1); + } + + // Wallet to use for local testing + let userWallet; + let userAddress; + let providerWallet; + let providerAddress; + + // Deployed instances + let connectGelato; + let connectMaker; + let connectInstaPool; + let connectCompound; + let instaIndex; + let instaList; + let dssCdpManager; + let getCdps; + let daiToken; + let gelatoCore; + let cDaiToken; + let cEthToken; + let instaMaster; + let instaConnectors; + let gelatoGasPriceOracle; + let compoundResolver; + + // Contracts to deploy and use for local testing + let conditionMakerVaultIsSafe; + let connectGelatoDebtBridge; + let connectGelatoProviderPayment; + let oracleAggregator; + let dsaProviderModule; + + // Creation during test + let dsa; + let connectedGelatoCore; + + before(async function () { + // Get Test Wallet for local testnet + [userWallet] = await ethers.getSigners(); + userAddress = await userWallet.getAddress(); + + [, providerWallet] = await ethers.getSigners(); + providerAddress = await providerWallet.getAddress(); + + instaMaster = await ethers.provider.getSigner( + bre.network.config.InstaMaster + ); + + // Ganache default accounts prefilled with 100 ETH + expect(await userWallet.getBalance()).to.be.gt( + ethers.utils.parseEther("10") + ); + + // ===== Get Deployed Contract Instance ================== + instaIndex = await ethers.getContractAt( + InstaIndex.abi, + bre.network.config.InstaIndex + ); + instaList = await ethers.getContractAt( + InstaList.abi, + bre.network.config.InstaList + ); + connectGelato = await ethers.getContractAt( + ConnectGelato.abi, + bre.network.config.ConnectGelato + ); + connectMaker = await ethers.getContractAt( + ConnectMaker.abi, + bre.network.config.ConnectMaker + ); + connectInstaPool = await ethers.getContractAt( + ConnectInstaPool.abi, + bre.network.config.ConnectInstaPool + ); + connectCompound = await ethers.getContractAt( + ConnectCompound.abi, + bre.network.config.ConnectCompound + ); + dssCdpManager = await ethers.getContractAt( + DssCdpManager.abi, + bre.network.config.DssCdpManager + ); + getCdps = await ethers.getContractAt( + GetCdps.abi, + bre.network.config.GetCdps + ); + daiToken = await ethers.getContractAt(IERC20.abi, bre.network.config.DAI); + gelatoCore = await ethers.getContractAt( + GelatoCoreLib.GelatoCore.abi, + bre.network.config.GelatoCore + ); + cDaiToken = await ethers.getContractAt( + CTokenInterface.abi, + bre.network.config.CDAI + ); + cEthToken = await ethers.getContractAt( + CTokenInterface.abi, + bre.network.config.CETH + ); + instaConnectors = await ethers.getContractAt( + InstaConnector.abi, + bre.network.config.InstaConnectors + ); + gelatoGasPriceOracle = await ethers.getContractAt( + GelatoGasPriceOracle.abi, + bre.network.config.GelatoGasPriceOracle + ); + compoundResolver = await ethers.getContractAt( + CompoundResolver.abi, + bre.network.config.CompoundResolver + ); + // instaEvent = await ethers.getContractAt( + // InstaEvent.abi, + // bre.network.config.InstaEvent + // ) + + // ===== Deploy Needed Contract ================== + + const OracleAggregator = await ethers.getContractFactory( + "OracleAggregator" + ); + oracleAggregator = await OracleAggregator.deploy(); + await oracleAggregator.deployed(); + + const ConditionMakerVaultIsSafe = await ethers.getContractFactory( + "ConditionMakerVaultIsSafe" + ); + conditionMakerVaultIsSafe = await ConditionMakerVaultIsSafe.deploy( + oracleAggregator.address + ); + await conditionMakerVaultIsSafe.deployed(); + + const connectorLength = await instaConnectors.connectorLength(); + const connectorId = connectorLength.add(1); + + const ConnectGelatoDebtBridge = await ethers.getContractFactory( + "ConnectGelatoDebtBridge" + ); + connectGelatoDebtBridge = await ConnectGelatoDebtBridge.deploy( + connectorId, + oracleAggregator.address + ); + await connectGelatoDebtBridge.deployed(); + + const ConnectGelatoProviderPayment = await ethers.getContractFactory( + "ConnectGelatoProviderPayment" + ); + connectGelatoProviderPayment = await ConnectGelatoProviderPayment.deploy( + connectorId.add(1) + ); + await connectGelatoProviderPayment.deployed(); + + const ProviderModuleDSA = await ethers.getContractFactory( + "ProviderModuleDSA" + ); + dsaProviderModule = await ProviderModuleDSA.deploy( + bre.network.config.InstaIndex, + bre.network.config.GelatoCore, + connectGelatoProviderPayment.address + ); + await dsaProviderModule.deployed(); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////// After Contracts Deployement : Setup /////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////////////////////////// + + // Gelato Testing environment setup. + // Step 1 : Add EUR/USD Maker Medianizer in the Oracle Aggregator + // Step 2 : Enable Debt Bridge Connector and Gelato Provider Payment Connector + // Step 3 : Executor Staking on Gelato + // Step 4 : Provider put some fund on gelato for paying future tasks executions + // Step 5 : Provider choose a executor + // Step 6 : Provider will add a module + // Step 7 : Provider should whitelist task + + //#region Step 1 Add EUR/USD Maker Medianizer in the Oracle Aggregator + + // Oracle Aggregator is a price feeder aggregator + // You will be able to query price from multiple source through this aggregator + // For the demo we add the ETH/USD Medianizer to the aggregator + // MakerDAO price oracle are called Medianizer + + await oracleAggregator.addOracle( + "ETH/USD", + "0x729D19f657BD0614b4985Cf1D82531c67569197B" + ); + + //#endregion + + //#region Step 2 Enable Debt Bridge Connector and Gelato Provider Payment Connector + + // Debt Bridge Connector is used during refinancing of debt + // This Connect help the user to split a position in one protocol. + // to 2 protocol in a safe way. Both debt position will be safe. + + // Gelato Provider Payment Connector is used for paying the provider + // for task execution. So when futur task will be executed, through a self financing + // transaction (user will pay during the execution of the task) task will + // be executed. Improvind user experience. + + await userWallet.sendTransaction({ + to: bre.network.config.InstaMaster, + value: ethers.utils.parseEther("0.1"), + }); + await instaConnectors + .connect(instaMaster) + .enable(connectGelatoDebtBridge.address); + await instaConnectors + .connect(instaMaster) + .enable(connectGelatoProviderPayment.address); + + expect( + await instaConnectors.isConnector([connectGelatoDebtBridge.address]) + ).to.be.true; + expect( + await instaConnectors.isConnector([connectGelatoProviderPayment.address]) + ).to.be.true; + + //#endregion + + //#region Step 3 Executor Staking 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. + + connectedGelatoCore = gelatoCore.connect(providerWallet); + gelatoCore = gelatoCore.connect(userWallet); + await connectedGelatoCore.stakeExecutor({ + from: providerAddress, + value: await connectedGelatoCore.minExecutorStake(), + }); + + expect( + await connectedGelatoCore.isExecutorMinStaked(providerAddress) + ).to.be.true; + + //#endregion + + //#region Step 4 Provider put some fund on gelato for paying future tasks executions + + // Provider put some funds in gelato system for paying the + // Executor when this one will execute task on behalf of the + // Provider. At each provider's task execution, some funds (approximatively + // the gas cost value) will be transfered to the Executor stake. + + const TASK_AUTOMATION_FUNDS = await gelatoCore.minExecProviderFunds( + GAS_LIMIT, + GAS_PRICE_CEIL + ); + + await expect( + connectedGelatoCore.provideFunds(providerAddress, { + value: TASK_AUTOMATION_FUNDS, + }) + ).to.emit(gelatoCore, "LogFundsProvided"); + + expect( + await connectedGelatoCore.providerFunds(providerAddress) + ).to.be.equal(TASK_AUTOMATION_FUNDS); + + //#endregion + + //#region Step 5 Provider choose a executor + + // Provider choose a executor who will execute futur task + // for the provider, it will be compensated by the provider. + + await expect( + connectedGelatoCore.providerAssignsExecutor(providerAddress) + ).to.emit(gelatoCore, "LogProviderAssignedExecutor"); + + expect( + await connectedGelatoCore.executorByProvider(providerAddress) + ).to.be.equal(providerAddress); + + //#endregion + + //#region Step 6 Provider will add a module + + // By adding a module the provider will format future task's + // payload by adding some specificity like his address to the + // Payment connector for receiving payment of User. + + await expect( + connectedGelatoCore.addProviderModules([dsaProviderModule.address]) + ).to.emit(gelatoCore, "LogProviderModuleAdded"); + + expect( + await connectedGelatoCore.isModuleProvided( + providerAddress, + dsaProviderModule.address + ) + ).to.be.true; + + //#endregion + + //#region Step 7 Provider should whitelist task + + // By WhiteList task, the provider can constrain the type + // of task the user can submitting. + + //#region Actions + + const spells = []; + + let debtBridge = new GelatoCoreLib.Action({ + addr: connectGelatoDebtBridge.address, + data: constants.HashZero, + operation: GelatoCoreLib.Operation.Delegatecall, + dataFlow: GelatoCoreLib.DataFlow.None, + termsOkCheck: false, + value: 0, + }); + + spells.push(debtBridge); + + let flashBorrow = new GelatoCoreLib.Action({ + addr: connectInstaPool.address, + data: constants.HashZero, + operation: GelatoCoreLib.Operation.Delegatecall, + dataFlow: GelatoCoreLib.DataFlow.None, + termsOkCheck: false, + value: 0, + }); + + spells.push(flashBorrow); + + let paybackMaker = new GelatoCoreLib.Action({ + addr: connectMaker.address, + data: constants.HashZero, + operation: GelatoCoreLib.Operation.Delegatecall, + dataFlow: GelatoCoreLib.DataFlow.None, + termsOkCheck: false, + value: 0, + }); + + spells.push(paybackMaker); + + let withdrawMaker = new GelatoCoreLib.Action({ + addr: connectMaker.address, + data: constants.HashZero, + operation: GelatoCoreLib.Operation.Delegatecall, + dataFlow: GelatoCoreLib.DataFlow.None, + termsOkCheck: false, + value: 0, + }); + + spells.push(withdrawMaker); + + let depositCompound = new GelatoCoreLib.Action({ + addr: connectCompound.address, + data: constants.HashZero, + operation: GelatoCoreLib.Operation.Delegatecall, + dataFlow: GelatoCoreLib.DataFlow.None, + termsOkCheck: false, + value: 0, + }); + + spells.push(depositCompound); + + let borrowCompound = new GelatoCoreLib.Action({ + addr: connectCompound.address, + data: constants.HashZero, + operation: GelatoCoreLib.Operation.Delegatecall, + dataFlow: GelatoCoreLib.DataFlow.None, + termsOkCheck: false, + value: 0, + }); + + spells.push(borrowCompound); + + let flashPayBack = new GelatoCoreLib.Action({ + addr: connectInstaPool.address, + data: constants.HashZero, + operation: GelatoCoreLib.Operation.Delegatecall, + dataFlow: GelatoCoreLib.DataFlow.None, + termsOkCheck: false, + value: 0, + }); + + spells.push(flashPayBack); + + let payProvider = new GelatoCoreLib.Action({ + addr: connectGelatoProviderPayment.address, + data: constants.HashZero, + operation: GelatoCoreLib.Operation.Delegatecall, + dataFlow: GelatoCoreLib.DataFlow.None, + termsOkCheck: false, + value: 0, + }); + + spells.push(payProvider); + + const gasPriceCeil = constants.MaxUint256; + + const gelatoFlashLoanTaskSpec = new GelatoCoreLib.TaskSpec({ + conditions: [conditionMakerVaultIsSafe.address], + actions: spells, + gasPriceCeil, + }); + + await expect( + connectedGelatoCore.provideTaskSpecs([gelatoFlashLoanTaskSpec]) + ).to.emit(gelatoCore, "LogTaskSpecProvided"); + + expect( + await connectedGelatoCore.isTaskSpecProvided( + providerAddress, + gelatoFlashLoanTaskSpec + ) + ).to.be.equal("OK"); + + expect( + await connectedGelatoCore.taskSpecGasPriceCeil( + providerAddress, + await connectedGelatoCore.hashTaskSpec(gelatoFlashLoanTaskSpec) + ) + ).to.be.equal(gasPriceCeil); + + //#endregion + + //#endregion + }); + + it("#1: Use Maker Compound refinancing if the maker vault become unsafe after a market move.", async function () { + // User Actions + // Step 1 : User create a DeFi Smart Account + // Step 2 : User open a Vault, put some ether on it and borrow some dai + // Step 3 : User give authorization to gelato to use his DSA on his behalf. + // Step 4 : User submit a Debt Refinancing task if market move against him + // Step 5 : Market Move against the user (Mock) + // Step 6 : Executor execute the user's task + + //#region Step 1 User create a DeFi Smart Account + + // User create a Instadapp DeFi Smart Account + // who give him the possibility to interact + // with a large list of DeFi protocol through one + // Proxy 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) + ); + + //#endregion + + //#region Step 2 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 bre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "open", + inputs: ["ETH-A"], + }); + + await dsa.cast([bre.network.config.ConnectMaker], [openVault], userAddress); + + let cdps = await getCdps.getCdpsAsc(dssCdpManager.address, dsa.address); + let cdpId = String(cdps.ids[0]); + + expect(cdps.ids[0].isZero()).to.be.false; + + await dsa.cast( + [bre.network.config.ConnectMaker], + [ + await bre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpId, ethers.utils.parseEther("10"), 0, 0], + }), + ], + userAddress, + { + value: ethers.utils.parseEther("10"), + } + ); + + let makerVaultInitialBorrow = ethers.utils.parseUnits("1000", 18); + + await dsa.cast( + [bre.network.config.ConnectMaker], + [ + await bre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpId, makerVaultInitialBorrow, 0, 0], + }), + ], + userAddress + ); + + expect(await daiToken.balanceOf(dsa.address)).to.be.equal( + ethers.utils.parseEther("1000") + ); + + //#endregion + + //#region Step 3 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 dsa.cast( + [bre.network.config.ConnectAuth], + [ + await bre.run("abi-encode-withselector", { + abi: ConnectAuth.abi, + functionname: "add", + inputs: [gelatoCore.address], + }), + ], + userAddress + ); + + expect(await dsa.isAuth(gelatoCore.address)).to.be.true; + + //#endregion + + //#region Step 4 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 debtBridgeCondition = new GelatoCoreLib.Condition({ + inst: conditionMakerVaultIsSafe.address, + data: await conditionMakerVaultIsSafe.getConditionData( + cdpId, + "ETH/USD", + ethers.utils.parseUnits("40", 17) + ), + }); + + // ======= Action/Spells setup ====== + const spells = []; + + let debtBridgeCalculation = new GelatoCoreLib.Action({ + addr: connectGelatoDebtBridge.address, + data: await bre.run("abi-encode-withselector", { + abi: ConnectGelatoDebtBridgeABI.abi, + functionname: "fullRefinanceMakerToCompound", + inputs: [cdpId, 0, 0], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(debtBridgeCalculation); + + let flashBorrow = new GelatoCoreLib.Action({ + addr: connectInstaPool.address, + data: await bre.run("abi-encode-withselector", { + abi: ConnectInstaPool.abi, + functionname: "flashBorrow", + inputs: [bre.network.config.DAI, 0, "100", 0], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(flashBorrow); + + let paybackMaker = new GelatoCoreLib.Action({ + addr: connectMaker.address, + data: await bre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "payback", + inputs: [cdpId, 0, "101", 0], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(paybackMaker); + + let withdrawMaker = new GelatoCoreLib.Action({ + addr: connectMaker.address, + data: await bre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "withdraw", + inputs: [cdpId, 0, "102", 0], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(withdrawMaker); + + let depositCompound = new GelatoCoreLib.Action({ + addr: connectCompound.address, + data: await bre.run("abi-encode-withselector", { + abi: ConnectCompound.abi, + functionname: "deposit", + inputs: [ETH, 0, "103", 0], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(depositCompound); + + let borrowCompound = new GelatoCoreLib.Action({ + addr: connectCompound.address, + data: await bre.run("abi-encode-withselector", { + abi: ConnectCompound.abi, + functionname: "borrow", + inputs: [bre.network.config.DAI, 0, "104", 0], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(borrowCompound); + + let flashPayBack = new GelatoCoreLib.Action({ + addr: connectInstaPool.address, + data: await bre.run("abi-encode-withselector", { + abi: ConnectInstaPool.abi, + functionname: "flashPayback", + inputs: [bre.network.config.DAI, 0, 0], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(flashPayBack); + + let payProvider = new GelatoCoreLib.Action({ + addr: connectGelatoProviderPayment.address, + data: await bre.run("abi-encode-withselector", { + abi: ConnectGelatoProviderPaymentABI.abi, + functionname: "payProvider", + inputs: [ethers.constants.AddressZero, ETH, 0, "105", 0], + }), + operation: GelatoCoreLib.Operation.Delegatecall, + }); + + spells.push(payProvider); + + const refinanceIfCompoundBorrowIsBetter = new GelatoCoreLib.Task({ + conditions: [debtBridgeCondition], + actions: spells, + }); + + const gelatoExternalProvider = new GelatoCoreLib.GelatoProvider({ + addr: providerAddress, + module: dsaProviderModule.address, + }); + + const expiryDate = 0; + await expect( + dsa.cast( + [connectGelato.address], // targets + [ + await bre.run("abi-encode-withselector", { + abi: ConnectGelato.abi, + functionname: "submitTask", + inputs: [ + gelatoExternalProvider, + refinanceIfCompoundBorrowIsBetter, + expiryDate, + ], + }), + ], // datas + userAddress, // origin + { + gasLimit: 5000000, + } + ) + ).to.emit(gelatoCore, "LogTaskSubmitted"); + + const taskReceipt = new GelatoCoreLib.TaskReceipt({ + id: await gelatoCore.currentTaskReceiptId(), + userProxy: dsa.address, + provider: gelatoExternalProvider, + tasks: [refinanceIfCompoundBorrowIsBetter], + expiryDate, + }); + + //#endregion + + //#region Step 5 Market Move against the user (Mock) + + // Ether market price went from the current price to 250$ + + const gelatoGasPrice = await bre.run("fetchGelatoGasPrice"); + expect(gelatoGasPrice).to.be.lte(GAS_PRICE_CEIL); + + await oracleAggregator.mock(true, ethers.utils.parseUnits("400", 18)); + + expect( + await connectedGelatoCore.canExec(taskReceipt, GAS_LIMIT, gelatoGasPrice) + ).to.be.equal("ConditionNotOk:NotOKMakerVaultIsSafe"); + + await oracleAggregator.mock(true, ethers.utils.parseUnits("380", 18)); + + expect( + await connectedGelatoCore.canExec(taskReceipt, GAS_LIMIT, gelatoGasPrice) + ).to.be.equal("OK"); + + //#endregion + + //#region Step 6 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 fees = ethers.utils + .parseUnits(String(1933090 + 19331 * 2), 0) + .mul(await gelatoGasPriceOracle.latestAnswer()); + let debt = await connectGelatoDebtBridge.getMakerVaultDebt(cdpId); + let collateral = ( + await connectGelatoDebtBridge.getMakerVaultCollateralBalance(cdpId) + ).sub(fees); + + //#endregion + + let providerBalanceBeforeExecution = await providerWallet.getBalance(); + + await expect( + connectedGelatoCore.exec(taskReceipt, { + gasPrice: gelatoGasPrice, // Exectutor must use gelatoGasPrice (Chainlink fast gwei) + gasLimit: GAS_LIMIT, + }) + ).to.emit(gelatoCore, "LogExecSuccess"); + + let providerBalanceAfterExecution = await providerWallet.getBalance(); + + expect(providerBalanceAfterExecution).to.be.gt( + providerBalanceBeforeExecution + ); + + // compound position of DSA on cDai and cEth + let compoundPosition = await compoundResolver.getCompoundData(dsa.address, [ + cDaiToken.address, + cEthToken.address, + ]); + + // https://compound.finance/docs/ctokens#exchange-rate + // calculate cEth/ETH rate to convert back cEth to ETH + // for comparing with the withdrew Ether to the deposited one. + let exchangeRateCethToEth = (await cEthToken.getCash()) + .add(await cEthToken.totalBorrows()) + .sub(await cEthToken.totalReserves()) + .div(await cEthToken.totalSupply()); + + // Estimated amount to borrowed token should be equal to the actual one read on compound contracts + expect(debt).to.be.equal(compoundPosition[0].borrowBalanceStoredUser); + + // Estimated amount of collateral should be equal to the actual one read on compound contracts + expect( + collateral.sub( + compoundPosition[1].balanceOfUser.mul(exchangeRateCethToEth) + ) + ).to.be.lt(ethers.utils.parseUnits("1", 12)); + + debt = await connectGelatoDebtBridge.getMakerVaultDebt(cdpId); + collateral = await connectGelatoDebtBridge.getMakerVaultCollateralBalance( + cdpId + ); // in Ether. + + expect(debt).to.be.equal(ethers.constants.Zero); + expect(collateral).to.be.equal(ethers.constants.Zero); + + // DSA contain 1000 DAI + expect(await daiToken.balanceOf(dsa.address)).to.be.equal( + makerVaultInitialBorrow + ); + + //#endregion + }); +});