From 2a2158f89b759707d91f0269993c36e8d797921a Mon Sep 17 00:00:00 2001 From: Twin Fish Date: Tue, 1 Dec 2020 12:52:58 +0100 Subject: [PATCH] Add debt ceiling and dust check (#92) * feat: conditions debt ceiling and dust --- .../ConditionBorrowAmountIsDust.sol | 77 +++ .../ConditionDebtBridgeIsAffordable.sol | 3 - .../ConditionDebtCeilingIsReached.sol | 90 +++ contracts/functions/dapps/FMaker.sol | 81 +++ contracts/interfaces/dapps/Maker/IVat.sol | 5 + .../ConditionBorrowAmountIsDust.deploy.js | 27 + .../ConditionDebtCeilingIsReached.deploy.js | 27 + hardhat.config.js | 4 + .../3_ConditionDebtCeilingIsReached.test.js | 547 ++++++++++++++++++ .../4_ConditionBorrowAmountIsDust.test.js | 175 ++++++ 10 files changed, 1033 insertions(+), 3 deletions(-) create mode 100644 contracts/contracts/gelato/conditions/ConditionBorrowAmountIsDust.sol create mode 100644 contracts/contracts/gelato/conditions/ConditionDebtCeilingIsReached.sol create mode 100644 deploy/gelato/conditions/ConditionBorrowAmountIsDust.deploy.js create mode 100644 deploy/gelato/conditions/ConditionDebtCeilingIsReached.deploy.js create mode 100644 test/unit/conditions/3_ConditionDebtCeilingIsReached.test.js create mode 100644 test/unit/conditions/4_ConditionBorrowAmountIsDust.test.js diff --git a/contracts/contracts/gelato/conditions/ConditionBorrowAmountIsDust.sol b/contracts/contracts/gelato/conditions/ConditionBorrowAmountIsDust.sol new file mode 100644 index 0000000..de781ee --- /dev/null +++ b/contracts/contracts/gelato/conditions/ConditionBorrowAmountIsDust.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.7.4; +pragma experimental ABIEncoderV2; + +import { + GelatoConditionsStandard +} from "@gelatonetwork/core/contracts/conditions/GelatoConditionsStandard.sol"; +import {GelatoBytes} from "../../../lib/GelatoBytes.sol"; +import { + _debtIsDustNewVault, + _debtIsDust, + _getMakerVaultDebt, + _isVaultOwner +} from "../../../functions/dapps/FMaker.sol"; + +contract ConditionBorrowAmountIsDust is GelatoConditionsStandard { + using GelatoBytes for bytes; + + function getConditionData( + address _dsa, + uint256 _fromVaultId, + uint256 _destVaultId, + string calldata _destColType + ) public pure virtual returns (bytes memory) { + return + abi.encodeWithSelector( + this.isBorrowAmountDust.selector, + _dsa, + _fromVaultId, + _destVaultId, + _destColType + ); + } + + function ok( + uint256, + bytes calldata _conditionData, + uint256 + ) public view virtual override returns (string memory) { + ( + address _dsa, + uint256 _fromVaultId, + uint256 _destVaultId, + string memory _destColType + ) = abi.decode(_conditionData[4:], (address, uint256, uint256, string)); + + return + isBorrowAmountDust(_dsa, _fromVaultId, _destVaultId, _destColType); + } + + function isBorrowAmountDust( + address _dsa, + uint256 _fromVaultId, + uint256 _destVaultId, + string memory _destColType + ) public view returns (string memory) { + _destVaultId = _isVaultOwner(_destVaultId, _dsa) ? _destVaultId : 0; + + uint256 wDaiToBorrow = _getMakerVaultDebt(_fromVaultId); + + return + borrowAmountIsDustExplicit(_destVaultId, wDaiToBorrow, _destColType) + ? "DebtNotGreaterThanDust" + : OK; + } + + function borrowAmountIsDustExplicit( + uint256 _vaultId, + uint256 _wDaiToBorrow, + string memory _colType + ) public view returns (bool) { + return + _vaultId == 0 + ? _debtIsDustNewVault(_colType, _wDaiToBorrow) + : _debtIsDust(_vaultId, _wDaiToBorrow); + } +} diff --git a/contracts/contracts/gelato/conditions/ConditionDebtBridgeIsAffordable.sol b/contracts/contracts/gelato/conditions/ConditionDebtBridgeIsAffordable.sol index 8f98712..d4f0489 100644 --- a/contracts/contracts/gelato/conditions/ConditionDebtBridgeIsAffordable.sol +++ b/contracts/contracts/gelato/conditions/ConditionDebtBridgeIsAffordable.sol @@ -5,9 +5,6 @@ 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, diff --git a/contracts/contracts/gelato/conditions/ConditionDebtCeilingIsReached.sol b/contracts/contracts/gelato/conditions/ConditionDebtCeilingIsReached.sol new file mode 100644 index 0000000..8ec0cf8 --- /dev/null +++ b/contracts/contracts/gelato/conditions/ConditionDebtCeilingIsReached.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.7.4; +pragma experimental ABIEncoderV2; + +import { + GelatoConditionsStandard +} from "@gelatonetwork/core/contracts/conditions/GelatoConditionsStandard.sol"; +import {GelatoBytes} from "../../../lib/GelatoBytes.sol"; +import { + _debtCeilingIsReachedNewVault, + _debtCeilingIsReached, + _getMakerVaultDebt, + _isVaultOwner +} from "../../../functions/dapps/FMaker.sol"; +import { + _getRealisedDebt +} from "../../../functions/gelato/FGelatoDebtBridge.sol"; + +contract ConditionDebtCeilingIsReached is GelatoConditionsStandard { + using GelatoBytes for bytes; + + function getConditionData( + address _dsa, + uint256 _fromVaultId, + uint256 _destVaultId, + string calldata _destColType + ) public pure virtual returns (bytes memory) { + return + abi.encodeWithSelector( + this.isDebtCeilingReached.selector, + _dsa, + _fromVaultId, + _destVaultId, + _destColType + ); + } + + function ok( + uint256, + bytes calldata _conditionData, + uint256 + ) public view virtual override returns (string memory) { + ( + address _dsa, + uint256 _fromVaultId, + uint256 _destVaultId, + string memory _destColType + ) = abi.decode(_conditionData[4:], (address, uint256, uint256, string)); + + return + isDebtCeilingReached( + _dsa, + _fromVaultId, + _destVaultId, + _destColType + ); + } + + function isDebtCeilingReached( + address _dsa, + uint256 _fromVaultId, + uint256 _destVaultId, + string memory _destColType + ) public view returns (string memory) { + _destVaultId = _isVaultOwner(_destVaultId, _dsa) ? _destVaultId : 0; + + uint256 wDaiToBorrow = + _getRealisedDebt(_getMakerVaultDebt(_fromVaultId)); + + return + debtCeilingIsReachedExplicit( + _destVaultId, + wDaiToBorrow, + _destColType + ) + ? "DebtCeilingReached" + : OK; + } + + function debtCeilingIsReachedExplicit( + uint256 _vaultId, + uint256 _wDaiToBorrow, + string memory _colType + ) public view returns (bool) { + return + _vaultId == 0 + ? _debtCeilingIsReachedNewVault(_colType, _wDaiToBorrow) + : _debtCeilingIsReached(_vaultId, _wDaiToBorrow); + } +} diff --git a/contracts/functions/dapps/FMaker.sol b/contracts/functions/dapps/FMaker.sol index c242fc8..53a1add 100644 --- a/contracts/functions/dapps/FMaker.sol +++ b/contracts/functions/dapps/FMaker.sol @@ -107,6 +107,87 @@ function _newVaultWillBeSafe( return tab <= mul(ink, spot); } +function _debtCeilingIsReachedNewVault( + string memory _colType, + uint256 _amtToBorrow +) view returns (bool) { + IMcdManager manager = IMcdManager(MCD_MANAGER); + IVat vat = IVat(manager.vat()); + + bytes32 ilk = _stringToBytes32(_colType); + + (uint256 Art, uint256 rate, , uint256 line, ) = vat.ilks(ilk); + uint256 Line = vat.Line(); + uint256 debt = vat.debt(); + + uint256 dart = _getBorrowAmt(_amtToBorrow, 0, rate); + uint256 dtab = mul(rate, dart); + + debt = add(debt, dtab); + Art = add(Art, dart); + + return mul(Art, rate) > line || debt > Line; +} + +function _debtCeilingIsReached(uint256 _vaultId, uint256 _amtToBorrow) + view + returns (bool) +{ + IMcdManager manager = IMcdManager(MCD_MANAGER); + IVat vat = IVat(manager.vat()); + + (bytes32 ilk, address urn) = _getVaultData(manager, _vaultId); + + (uint256 Art, uint256 rate, , uint256 line, ) = vat.ilks(ilk); + uint256 dai = vat.dai(urn); + uint256 Line = vat.Line(); + uint256 debt = vat.debt(); + + uint256 dart = _getBorrowAmt(_amtToBorrow, dai, rate); + uint256 dtab = mul(rate, dart); + + debt = add(debt, dtab); + Art = add(Art, dart); + + return mul(Art, rate) > line || debt > Line; +} + +function _debtIsDustNewVault(string memory _colType, uint256 _amtToBorrow) + view + returns (bool) +{ + IMcdManager manager = IMcdManager(MCD_MANAGER); + IVat vat = IVat(manager.vat()); + + bytes32 ilk = _stringToBytes32(_colType); + + (, uint256 rate, , , uint256 dust) = vat.ilks(ilk); + uint256 art = _getBorrowAmt(_amtToBorrow, 0, rate); + + uint256 tab = mul(rate, art); + + return tab < dust; +} + +function _debtIsDust(uint256 _vaultId, uint256 _amtToBorrow) + view + returns (bool) +{ + IMcdManager manager = IMcdManager(MCD_MANAGER); + IVat vat = IVat(manager.vat()); + + (bytes32 ilk, address urn) = _getVaultData(manager, _vaultId); + (, uint256 art) = vat.urns(ilk, urn); + (, uint256 rate, , , uint256 dust) = vat.ilks(ilk); + + uint256 dai = vat.dai(urn); + uint256 dart = _getBorrowAmt(_amtToBorrow, dai, rate); + art = add(art, dart); + uint256 tab = mul(rate, art); + + return tab < dust; +} + function _getVaultData(IMcdManager manager, uint256 vault) view returns (bytes32 ilk, address urn) diff --git a/contracts/interfaces/dapps/Maker/IVat.sol b/contracts/interfaces/dapps/Maker/IVat.sol index cb0449c..d49555e 100644 --- a/contracts/interfaces/dapps/Maker/IVat.sol +++ b/contracts/interfaces/dapps/Maker/IVat.sol @@ -16,4 +16,9 @@ interface IVat { function dai(address) external view returns (uint256); function urns(bytes32, address) external view returns (uint256, uint256); + + function debt() external view returns (uint256); + + // solhint-disable-next-line + function Line() external view returns (uint256); } diff --git a/deploy/gelato/conditions/ConditionBorrowAmountIsDust.deploy.js b/deploy/gelato/conditions/ConditionBorrowAmountIsDust.deploy.js new file mode 100644 index 0000000..67d9042 --- /dev/null +++ b/deploy/gelato/conditions/ConditionBorrowAmountIsDust.deploy.js @@ -0,0 +1,27 @@ +const { sleep } = require("@gelatonetwork/core"); + +module.exports = async (hre) => { + if (hre.network.name === "mainnet") { + console.log( + "\n\n Deploying ConditionBorrowAmountIsDust 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 "ConditionBorrowAmountIsDust" + // if the contract was never deployed or if the code changed since last deployment + await deploy("ConditionBorrowAmountIsDust", { + from: deployer, + gasPrice: hre.network.config.gasPrice, + log: hre.network.name === "mainnet" ? true : false, + }); +}; + +module.exports.skip = async (hre) => { + return hre.network.name === "mainnet" ? true : false; +}; +module.exports.tags = ["ConditionBorrowAmountIsDust"]; diff --git a/deploy/gelato/conditions/ConditionDebtCeilingIsReached.deploy.js b/deploy/gelato/conditions/ConditionDebtCeilingIsReached.deploy.js new file mode 100644 index 0000000..1683779 --- /dev/null +++ b/deploy/gelato/conditions/ConditionDebtCeilingIsReached.deploy.js @@ -0,0 +1,27 @@ +const { sleep } = require("@gelatonetwork/core"); + +module.exports = async (hre) => { + if (hre.network.name === "mainnet") { + console.log( + "\n\n Deploying ConditionDebtCeilingIsReached 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 "ConditionDebtCeilingIsReached" + // if the contract was never deployed or if the code changed since last deployment + await deploy("ConditionDebtCeilingIsReached", { + from: deployer, + gasPrice: hre.network.config.gasPrice, + log: hre.network.name === "mainnet" ? true : false, + }); +}; + +module.exports.skip = async (hre) => { + return hre.network.name === "mainnet" ? true : false; +}; +module.exports.tags = ["ConditionDebtCeilingIsReached"]; diff --git a/hardhat.config.js b/hardhat.config.js index 59e3c50..2c05f25 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -53,6 +53,10 @@ module.exports = { url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_ID}`, blockNumber: 11346751, }, + // Accounts + accounts: { + accountsBalance: "1000000000000000000000000", + }, // Custom ...mainnetDeployments, }, diff --git a/test/unit/conditions/3_ConditionDebtCeilingIsReached.test.js b/test/unit/conditions/3_ConditionDebtCeilingIsReached.test.js new file mode 100644 index 0000000..f21b95e --- /dev/null +++ b/test/unit/conditions/3_ConditionDebtCeilingIsReached.test.js @@ -0,0 +1,547 @@ +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("ConditionDebtCeilingIsReached 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 conditionDebtCeilingIsReached; + + let dsa; + let cdpAId; + let cdpBId; + let amountToBorrow; + + 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("900000") // amount needed to test debt ceiling. + ); + + 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 ============ + conditionDebtCeilingIsReached = await ethers.getContract( + "ConditionDebtCeilingIsReached" + ); + + // 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 for ETH-A + let 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); + cdpAId = String(cdps.ids[0]); + + expect(cdps.ids[0].isZero()).to.be.false; + + // Create/Deposit/Borrow a Vault for ETH-B + openVault = await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "open", + inputs: ["ETH-B"], + }); + + await dsa.cast([hre.network.config.ConnectMaker], [openVault], userAddress); + + cdps = await getCdps.getCdpsAsc(dssCdpManager.address, dsa.address); + cdpBId = String(cdps.ids[1]); + + expect(cdps.ids[1].isZero()).to.be.false; + }); + + it("#1: ok should return Ok if for new vault (to open) the borrow amnount is less than the remaining debt that can be borrowed", async function () { + // Steps : + // 1 - Deposit. + // 2 - Borrow. + // 3 - Test if vault ETH-B will not be able to borrow due to debt ceiling. + + amountToBorrow = ethers.utils.parseUnits("500", 18); + const amountToDeposit = ethers.utils.parseUnits("2", 18); + + //#region Deposit + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpAId, amountToDeposit, 0, 0], + }), + ], + userAddress, + { + value: amountToDeposit, + } + ); + + //#endregion Deposit + + //#region Borrow + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpAId, amountToBorrow, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal(amountToBorrow); + + //#endregion Borrow + + const conditionData = await conditionDebtCeilingIsReached.getConditionData( + dsa.address, + cdpAId, + 0, + "ETH-B" + ); + expect( + await conditionDebtCeilingIsReached.ok(0, conditionData, 0) + ).to.be.equal("OK"); + }); + + it("#2: ok should return DebtCeilingReached if for new vault (to open) the borrow amnount is greater than the remaining debt that can be borrowed", async function () { + // Steps : + // 1 - Deposit. + // 2 - Borrow. + // 3 - Test if vault ETH-B will not be able to borrow due to debt ceiling. + + amountToBorrow = ethers.utils.parseUnits("1000000", 18); + const amountToDeposit = ethers.utils.parseUnits("200000", 18); + + //#region Deposit + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpAId, amountToDeposit, 0, 0], + }), + ], + userAddress, + { + value: amountToDeposit, + } + ); + + //#endregion Deposit + + //#region Borrow + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpAId, amountToBorrow, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal(amountToBorrow); + + //#endregion Borrow + + const conditionData = await conditionDebtCeilingIsReached.getConditionData( + dsa.address, + cdpAId, + 0, + "ETH-B" + ); + expect( + await conditionDebtCeilingIsReached.ok(0, conditionData, 0) + ).to.be.equal("DebtCeilingReached"); + }); + + it("#3: ok should return Ok if for old vault the borrow amnount is less than the remaining debt that can be borrowed", async function () { + // Steps : + // 1 - Deposit. + // 2 - Borrow. + // 3 - Test if vault ETH-B will not be able to borrow due to debt ceiling. + + amountToBorrow = ethers.utils.parseUnits("500", 18); + const amountToDeposit = ethers.utils.parseUnits("2", 18); + + //#region Deposit + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpAId, amountToDeposit, 0, 0], + }), + ], + userAddress, + { + value: amountToDeposit, + } + ); + + //#endregion Deposit + + //#region Borrow + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpAId, amountToBorrow, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal(amountToBorrow); + + //#endregion Borrow + + const conditionData = await conditionDebtCeilingIsReached.getConditionData( + dsa.address, + cdpAId, + cdpBId, + "ETH-B" + ); + expect( + await conditionDebtCeilingIsReached.ok(0, conditionData, 0) + ).to.be.equal("OK"); + }); + + it("#4: ok should return DebtCeilingReached if for old vault the borrow amnount is greater than the remaining debt that can be borrowed", async function () { + // Steps : + // 1 - Deposit. + // 2 - Borrow. + // 3 - Test if vault ETH-B will not be able to borrow due to debt ceiling. + + amountToBorrow = ethers.utils.parseUnits("1000000", 18); + const amountToDeposit = ethers.utils.parseUnits("200000", 18); + + //#region Deposit + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpAId, amountToDeposit, 0, 0], + }), + ], + userAddress, + { + value: amountToDeposit, + } + ); + + //#endregion Deposit + + //#region Borrow + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpAId, amountToBorrow, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal(amountToBorrow); + + //#endregion Borrow + + const conditionData = await conditionDebtCeilingIsReached.getConditionData( + dsa.address, + cdpAId, + cdpBId, + "ETH-B" + ); + expect( + await conditionDebtCeilingIsReached.ok(0, conditionData, 0) + ).to.be.equal("DebtCeilingReached"); + }); + + it("#5: ok should return Ok if for old vault (with debt) the borrow amnount is less than the remaining debt that can be borrowed", async function () { + // Steps : + // 1 - Deposit. + // 2 - Borrow. + // 3 - Test if vault ETH-B will not be able to borrow due to debt ceiling. + + amountToBorrow = ethers.utils.parseUnits("500", 18); + const amountToDeposit = ethers.utils.parseUnits("2", 18); + + const amountToBorrowForVaultB = ethers.utils.parseUnits("500", 18); + const amountToDepositForVaultB = ethers.utils.parseUnits("2", 18); + + //#region Deposit vault ETH-A + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpAId, amountToDeposit, 0, 0], + }), + ], + userAddress, + { + value: amountToDeposit, + } + ); + + //#endregion Deposit vault ETH-A + + //#region Borrow vault ETH-A + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpAId, amountToBorrow, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal(amountToBorrow); + + //#endregion Borrow vault ETH-A + + //#region Deposit vault ETH-B + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpBId, amountToDepositForVaultB, 0, 0], + }), + ], + userAddress, + { + value: amountToDepositForVaultB, + } + ); + + //#endregion Deposit vault ETH-B + + //#region Borrow vault ETH-B + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpBId, amountToBorrowForVaultB, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal( + amountToBorrowForVaultB.add(amountToBorrow) + ); + + //#endregion Borrow vault ETH-B + + const conditionData = await conditionDebtCeilingIsReached.getConditionData( + dsa.address, + cdpAId, + cdpBId, + "ETH-B" + ); + expect( + await conditionDebtCeilingIsReached.ok(0, conditionData, 0) + ).to.be.equal("OK"); + }); + + it("#6: ok should return DebtCeilingReached if for old vault (with debt) the borrow amnount is greater than the remaining debt that can be borrowed", async function () { + // Steps : + // 1 - Deposit. + // 2 - Borrow. + // 3 - Test if vault ETH-B will not be able to borrow due to debt ceiling. + + amountToBorrow = ethers.utils.parseUnits("1000000", 18); + const amountToDeposit = ethers.utils.parseUnits("200000", 18); + + const amountToBorrowForVaultB = ethers.utils.parseUnits("500", 18); + const amountToDepositForVaultB = ethers.utils.parseUnits("2", 18); + + //#region Deposit + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpAId, amountToDeposit, 0, 0], + }), + ], + userAddress, + { + value: amountToDeposit, + } + ); + + //#endregion Deposit + + //#region Borrow + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpAId, amountToBorrow, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal(amountToBorrow); + + //#endregion Borrow + + //#region Deposit vault ETH-B + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpBId, amountToDepositForVaultB, 0, 0], + }), + ], + userAddress, + { + value: amountToDepositForVaultB, + } + ); + + //#endregion Deposit vault ETH-B + + //#region Borrow vault ETH-B + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpBId, amountToBorrowForVaultB, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal( + amountToBorrowForVaultB.add(amountToBorrow) + ); + + //#endregion Borrow vault ETH-B + + const conditionData = await conditionDebtCeilingIsReached.getConditionData( + dsa.address, + cdpAId, + cdpBId, + "ETH-B" + ); + expect( + await conditionDebtCeilingIsReached.ok(0, conditionData, 0) + ).to.be.equal("DebtCeilingReached"); + }); +}); diff --git a/test/unit/conditions/4_ConditionBorrowAmountIsDust.test.js b/test/unit/conditions/4_ConditionBorrowAmountIsDust.test.js new file mode 100644 index 0000000..92e61c3 --- /dev/null +++ b/test/unit/conditions/4_ConditionBorrowAmountIsDust.test.js @@ -0,0 +1,175 @@ +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("ConditionBorrowAmountIsDust 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 conditionBorrowAmountIsDust; + + let dsa; + let cdpAId; + let amountToBorrow; + + 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 ============ + conditionBorrowAmountIsDust = await ethers.getContract( + "ConditionBorrowAmountIsDust" + ); + + // 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 for ETH-A + let 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); + cdpAId = String(cdps.ids[0]); + + expect(cdps.ids[0].isZero()).to.be.false; + }); + + it("#1: ok should return Ok if we borrow more than the dust limit", async function () { + // Steps : + // 1 - Deposit. + // 2 - Borrow. + // 3 - Test if vault ETH-B will not have dust borrow. + + amountToBorrow = ethers.utils.parseUnits("500", 18); + const amountToDeposit = ethers.utils.parseUnits("2", 18); + + //#region Deposit + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "deposit", + inputs: [cdpAId, amountToDeposit, 0, 0], + }), + ], + userAddress, + { + value: amountToDeposit, + } + ); + + //#endregion Deposit + + //#region Borrow + + await dsa.cast( + [hre.network.config.ConnectMaker], + [ + await hre.run("abi-encode-withselector", { + abi: ConnectMaker.abi, + functionname: "borrow", + inputs: [cdpAId, amountToBorrow, 0, 0], + }), + ], + userAddress + ); + + expect(await DAI.balanceOf(dsa.address)).to.be.equal(amountToBorrow); + + //#endregion Borrow + + const conditionData = await conditionBorrowAmountIsDust.getConditionData( + dsa.address, + cdpAId, + 0, + "ETH-B" + ); + expect( + await conditionBorrowAmountIsDust.ok(0, conditionData, 0) + ).to.be.equal("OK"); + }); + + it("#2: borrowAmountIsDustExplicit should return false if we borrow less than the dust limit", async function () { + amountToBorrow = ethers.utils.parseUnits("200", 18); // dust limit is 500 DAI (first Dec 2020) for ETH-B + + expect( + await conditionBorrowAmountIsDust.borrowAmountIsDustExplicit( + 0, + amountToBorrow, + "ETH-B" + ) + ).to.be.true; + }); +});