From 62911881f60fa891d79973b74957dd28ff9cf54c Mon Sep 17 00:00:00 2001 From: brightiron Date: Mon, 14 Oct 2024 22:38:16 -0500 Subject: [PATCH 1/2] add finite consolidation approvals --- src/abi/CoolerConsolidation.json | 52 ------- .../TokenAllowanceGuard.tsx | 2 +- .../hooks/useApproveToken.ts | 7 +- .../hooks/useGetConsolidationAllowances.tsx | 41 ++++++ .../Cooler/positions/ConsolidateLoan.tsx | 137 +++++++++++++++++- 5 files changed, 180 insertions(+), 59 deletions(-) create mode 100644 src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx diff --git a/src/abi/CoolerConsolidation.json b/src/abi/CoolerConsolidation.json index 74190aaaad..42776ba746 100644 --- a/src/abi/CoolerConsolidation.json +++ b/src/abi/CoolerConsolidation.json @@ -54,58 +54,6 @@ ], "stateMutability": "view" }, - { - "type": "function", - "name": "VERSION", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "collateralRequired", - "inputs": [ - { - "name": "clearinghouse_", - "type": "address", - "internalType": "address" - }, - { - "name": "cooler_", - "type": "address", - "internalType": "address" - }, - { - "name": "ids_", - "type": "uint256[]", - "internalType": "uint256[]" - } - ], - "outputs": [ - { - "name": "consolidatedLoanCollateral", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "existingLoanCollateral", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "additionalCollateral", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "collector", diff --git a/src/components/TokenAllowanceGuard/TokenAllowanceGuard.tsx b/src/components/TokenAllowanceGuard/TokenAllowanceGuard.tsx index a11db0984c..fcc533a58c 100644 --- a/src/components/TokenAllowanceGuard/TokenAllowanceGuard.tsx +++ b/src/components/TokenAllowanceGuard/TokenAllowanceGuard.tsx @@ -120,7 +120,7 @@ export const TokenAllowanceGuard: React.FC<{ loading={approveMutation.isLoading} fullWidth className="" - onClick={() => approveMutation.mutate({ spenderAddressMap })} + onClick={() => approveMutation.mutate({ spenderAddressMap, spendAmount })} disabled={approveMutation.isLoading} > {approveMutation.isLoading ? `${approvalPendingText}` : `${approvalText}`} diff --git a/src/components/TokenAllowanceGuard/hooks/useApproveToken.ts b/src/components/TokenAllowanceGuard/hooks/useApproveToken.ts index 66161d7507..c47a967f44 100644 --- a/src/components/TokenAllowanceGuard/hooks/useApproveToken.ts +++ b/src/components/TokenAllowanceGuard/hooks/useApproveToken.ts @@ -3,6 +3,7 @@ import { ContractReceipt } from "@ethersproject/contracts"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import toast from "react-hot-toast"; import { AddressMap } from "src/constants/addresses"; +import { DecimalBigNumber } from "src/helpers/DecimalBigNumber/DecimalBigNumber"; import { useDynamicTokenContract } from "src/hooks/useContract"; import { contractAllowanceQueryKey } from "src/hooks/useContractAllowance"; import { EthersError } from "src/lib/EthersTypes"; @@ -15,14 +16,14 @@ export const useApproveToken = (tokenAddressMap: AddressMap) => { const { chain = { id: 1 } } = useNetwork(); const token = useDynamicTokenContract(tokenAddressMap, true); - return useMutation( - async ({ spenderAddressMap }) => { + return useMutation( + async ({ spenderAddressMap, spendAmount }) => { const contractAddress = spenderAddressMap[chain.id as keyof typeof spenderAddressMap]; if (!token) throw new Error("Token doesn't exist on current network. Please switch networks."); if (!contractAddress) throw new Error("Contract doesn't exist on current network. Please switch networks."); - const transaction = await token.approve(contractAddress, MaxUint256); + const transaction = await token.approve(contractAddress, spendAmount?.toBigNumber() || MaxUint256); return transaction.wait(); }, diff --git a/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx b/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx new file mode 100644 index 0000000000..b1bd54f371 --- /dev/null +++ b/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx @@ -0,0 +1,41 @@ +import { useQuery } from "@tanstack/react-query"; +import { COOLER_CONSOLIDATION_CONTRACT } from "src/constants/contracts"; +import { DecimalBigNumber } from "src/helpers/DecimalBigNumber/DecimalBigNumber"; +import { useTestableNetworks } from "src/hooks/useTestableNetworks"; +import { CoolerConsolidation__factory } from "src/typechain"; +import { useProvider } from "wagmi"; + +export const useGetConsolidationAllowances = ({ + clearingHouseAddress, + coolerAddress, + loanIds, +}: { + clearingHouseAddress: string; + coolerAddress: string; + loanIds: number[]; +}) => { + const provider = useProvider(); + const networks = useTestableNetworks(); + + const { data, isFetched, isLoading } = useQuery( + ["useGetConsolidationAllowances", clearingHouseAddress, coolerAddress], + async () => { + try { + const contractAddress = COOLER_CONSOLIDATION_CONTRACT.addresses[networks.MAINNET]; + const contract = CoolerConsolidation__factory.connect(contractAddress, provider); + const requiredApprovals = await contract.requiredApprovals(clearingHouseAddress, coolerAddress, loanIds); + return { + consolidatedLoanCollateral: new DecimalBigNumber(requiredApprovals[1], 18), + totalDebtWithFee: new DecimalBigNumber(requiredApprovals[2], 18), + }; + } catch { + return { + consolidatedLoanCollateral: new DecimalBigNumber("0", 18), + totalDebtWithFee: new DecimalBigNumber("0", 18), + }; + } + }, + { enabled: !!coolerAddress }, + ); + return { data, isFetched, isLoading }; +}; diff --git a/src/views/Lending/Cooler/positions/ConsolidateLoan.tsx b/src/views/Lending/Cooler/positions/ConsolidateLoan.tsx index 18ddda0a66..e3f955d303 100644 --- a/src/views/Lending/Cooler/positions/ConsolidateLoan.tsx +++ b/src/views/Lending/Cooler/positions/ConsolidateLoan.tsx @@ -1,16 +1,16 @@ import { Box, SvgIcon, Typography } from "@mui/material"; import { InfoNotification, Modal, PrimaryButton } from "@olympusdao/component-library"; -import { BigNumber, ethers } from "ethers"; +import { BigNumber } from "ethers"; import { formatEther } from "ethers/lib/utils.js"; import { useEffect, useState } from "react"; import lendAndBorrowIcon from "src/assets/icons/lendAndBorrow.svg?react"; import { TokenAllowanceGuard } from "src/components/TokenAllowanceGuard/TokenAllowanceGuard"; import { COOLER_CONSOLIDATION_ADDRESSES, DAI_ADDRESSES, GOHM_ADDRESSES } from "src/constants/addresses"; import { formatNumber } from "src/helpers"; -import { DecimalBigNumber } from "src/helpers/DecimalBigNumber/DecimalBigNumber"; import { useBalance } from "src/hooks/useBalance"; import { useTestableNetworks } from "src/hooks/useTestableNetworks"; import { useConsolidateCooler } from "src/views/Lending/Cooler/hooks/useConsolidateCooler"; +import { useGetConsolidationAllowances } from "src/views/Lending/Cooler/hooks/useGetConsolidationAllowances"; import { useGetCoolerLoans } from "src/views/Lending/Cooler/hooks/useGetCoolerLoans"; export const ConsolidateLoans = ({ @@ -39,6 +39,7 @@ export const ConsolidateLoans = ({ }, { principal: BigNumber.from(0), interest: BigNumber.from(0), collateral: BigNumber.from(0) }, ); + const { data: allowances } = useGetConsolidationAllowances({ clearingHouseAddress, coolerAddress, loanIds }); const maturityDate = new Date(); maturityDate.setDate(maturityDate.getDate() + Number(duration || 0)); const { data: daiBalance } = useBalance({ [networks.MAINNET]: debtAddress || "" })[networks.MAINNET]; @@ -56,9 +57,139 @@ export const ConsolidateLoans = ({ } }, [daiBalance, totals.interest]); - console.log("consolidate loans"); return ( <> + setOpen(!open)}>Consolidate Loans + + Consolidate Loans + + } + onClose={() => setOpen(false)} + > + <> + + All existing open loans for this Cooler and Clearinghouse will be repaid and consolidated into a new loan + with a {duration} day duration. You must hold enough DAI in your wallet to cover the interest owed at + consolidation. + + + Loans to Consolidate + + {loans.length} + + + + New Principal Amount + + + {formatNumber(parseFloat(formatEther(totals.principal)), 4)} DAI + + + + + Interest Owed At Consolidation + + + {formatNumber(parseFloat(formatEther(totals.interest)), 4)} DAI + + + + + New Maturity Date + + + {maturityDate.toLocaleDateString([], { + month: "long", + day: "numeric", + year: "numeric", + }) || ""}{" "} + {maturityDate.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + + + + {insufficientCollateral ? ( + + Insufficient DAI Balance + + ) : ( + Approve DAI for Spending on the Consolidation Contract} + spendAmount={allowances?.totalDebtWithFee} + approvalText="Approve DAI for Spending" + > + Approve gOHM for Spending on the Consolidation Contract} + spendAmount={allowances?.consolidatedLoanCollateral} + approvalText="Approve gOHM for Spending" + > + { + coolerMutation.mutate( + { + coolerAddress, + clearingHouseAddress, + loanIds, + }, + { + onSuccess: () => { + setOpen(false); + }, + }, + ); + }} + loading={coolerMutation.isLoading} + disabled={coolerMutation.isLoading} + fullWidth + > + Consolidate Loans + + + + )} + + ); }; From abbd5f96a36ec1eb6afea12ba26d2e26f09bf88f Mon Sep 17 00:00:00 2001 From: brightiron Date: Wed, 30 Oct 2024 11:14:57 -0500 Subject: [PATCH 2/2] update with new interface --- src/abi/CoolerConsolidation.json | 387 +++++++++++++----- .../Cooler/hooks/useConsolidateCooler.tsx | 13 +- .../hooks/useGetConsolidationAllowances.tsx | 3 +- 3 files changed, 301 insertions(+), 102 deletions(-) diff --git a/src/abi/CoolerConsolidation.json b/src/abi/CoolerConsolidation.json index 42776ba746..3c66853a5e 100644 --- a/src/abi/CoolerConsolidation.json +++ b/src/abi/CoolerConsolidation.json @@ -4,32 +4,7 @@ "type": "constructor", "inputs": [ { - "name": "gohm_", - "type": "address", - "internalType": "address" - }, - { - "name": "sdai_", - "type": "address", - "internalType": "address" - }, - { - "name": "dai_", - "type": "address", - "internalType": "address" - }, - { - "name": "owner_", - "type": "address", - "internalType": "address" - }, - { - "name": "lender_", - "type": "address", - "internalType": "address" - }, - { - "name": "collector_", + "name": "kernel_", "type": "address", "internalType": "address" }, @@ -56,20 +31,79 @@ }, { "type": "function", - "name": "collector", + "name": "ROLES", "inputs": [], "outputs": [ { "name": "", "type": "address", - "internalType": "address" + "internalType": "contract ROLESv1" } ], "stateMutability": "view" }, { "type": "function", - "name": "consolidateWithFlashLoan", + "name": "ROLE_ADMIN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "ROLE_EMERGENCY_SHUTDOWN", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "activate", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "changeKernel", + "inputs": [ + { + "name": "newKernel_", + "type": "address", + "internalType": "contract Kernel" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "collateralRequired", "inputs": [ { "name": "clearinghouse_", @@ -85,16 +119,68 @@ "name": "ids_", "type": "uint256[]", "internalType": "uint256[]" + } + ], + "outputs": [ + { + "name": "consolidatedLoanCollateral", + "type": "uint256", + "internalType": "uint256" }, { - "name": "useFunds_", + "name": "existingLoanCollateral", "type": "uint256", "internalType": "uint256" }, { - "name": "sdai_", - "type": "bool", - "internalType": "bool" + "name": "additionalCollateral", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "configureDependencies", + "inputs": [], + "outputs": [ + { + "name": "dependencies", + "type": "bytes5[]", + "internalType": "Keycode[]" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "consolidate", + "inputs": [ + { + "name": "clearinghouseFrom_", + "type": "address", + "internalType": "address" + }, + { + "name": "clearinghouseTo_", + "type": "address", + "internalType": "address" + }, + { + "name": "coolerFrom_", + "type": "address", + "internalType": "address" + }, + { + "name": "coolerTo_", + "type": "address", + "internalType": "address" + }, + { + "name": "ids_", + "type": "uint256[]", + "internalType": "uint256[]" } ], "outputs": [], @@ -102,24 +188,102 @@ }, { "type": "function", - "name": "dai", + "name": "consolidateWithNewOwner", + "inputs": [ + { + "name": "clearinghouseFrom_", + "type": "address", + "internalType": "address" + }, + { + "name": "clearinghouseTo_", + "type": "address", + "internalType": "address" + }, + { + "name": "coolerFrom_", + "type": "address", + "internalType": "address" + }, + { + "name": "coolerTo_", + "type": "address", + "internalType": "address" + }, + { + "name": "ids_", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "consolidatorActive", "inputs": [], "outputs": [ { "name": "", - "type": "address", - "internalType": "contract IERC20" + "type": "bool", + "internalType": "bool" } ], "stateMutability": "view" }, + { + "type": "function", + "name": "deactivate", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "feePercentage", "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "fundsRequired", + "inputs": [ + { + "name": "clearinghouseTo_", + "type": "address", + "internalType": "address" + }, + { + "name": "coolerFrom_", + "type": "address", + "internalType": "address" + }, + { + "name": "ids_", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], "outputs": [ { - "name": "", + "name": "reserveTo", + "type": "address", + "internalType": "address" + }, + { + "name": "interest", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "lenderFee", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "protocolFee", "type": "uint256", "internalType": "uint256" } @@ -147,26 +311,26 @@ }, { "type": "function", - "name": "gohm", + "name": "isActive", "inputs": [], "outputs": [ { "name": "", - "type": "address", - "internalType": "contract IERC20" + "type": "bool", + "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", - "name": "lender", + "name": "kernel", "inputs": [], "outputs": [ { "name": "", "type": "address", - "internalType": "contract IERC3156FlashLender" + "internalType": "contract Kernel" } ], "stateMutability": "view" @@ -212,28 +376,40 @@ }, { "type": "function", - "name": "owner", + "name": "requestPermissions", "inputs": [], "outputs": [ { - "name": "", - "type": "address", - "internalType": "address" + "name": "requests", + "type": "tuple[]", + "internalType": "struct Permissions[]", + "components": [ + { + "name": "keycode", + "type": "bytes5", + "internalType": "Keycode" + }, + { + "name": "funcSelector", + "type": "bytes4", + "internalType": "bytes4" + } + ] } ], - "stateMutability": "view" + "stateMutability": "pure" }, { "type": "function", "name": "requiredApprovals", "inputs": [ { - "name": "clearinghouse_", + "name": "clearinghouseTo_", "type": "address", "internalType": "address" }, { - "name": "cooler_", + "name": "coolerFrom_", "type": "address", "internalType": "address" }, @@ -256,8 +432,8 @@ }, { "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "address", + "internalType": "address" }, { "name": "", @@ -272,32 +448,6 @@ ], "stateMutability": "view" }, - { - "type": "function", - "name": "sdai", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IERC4626" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "setCollector", - "inputs": [ - { - "name": "collector_", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "setFeePercentage", @@ -312,40 +462,44 @@ "stateMutability": "nonpayable" }, { - "type": "function", - "name": "transferOwnership", + "type": "event", + "name": "ConsolidatorActivated", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "ConsolidatorDeactivated", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "FeePercentageSet", "inputs": [ { - "name": "newOwner", - "type": "address", - "internalType": "address" + "name": "feePercentage", + "type": "uint256", + "indexed": false, + "internalType": "uint256" } ], - "outputs": [], - "stateMutability": "nonpayable" + "anonymous": false }, { - "type": "event", - "name": "OwnershipTransferred", + "type": "error", + "name": "KernelAdapter_OnlyKernel", "inputs": [ { - "name": "user", + "name": "caller_", "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "newOwner", - "type": "address", - "indexed": true, "internalType": "address" } - ], - "anonymous": false + ] }, { "type": "error", - "name": "InsufficientCoolerCount", + "name": "OnlyConsolidatorActive", "inputs": [] }, { @@ -358,6 +512,11 @@ "name": "OnlyLender", "inputs": [] }, + { + "type": "error", + "name": "OnlyPolicyActive", + "inputs": [] + }, { "type": "error", "name": "OnlyThis", @@ -368,6 +527,11 @@ "name": "Params_FeePercentageOutOfRange", "inputs": [] }, + { + "type": "error", + "name": "Params_InsufficientCoolerCount", + "inputs": [] + }, { "type": "error", "name": "Params_InvalidAddress", @@ -375,8 +539,35 @@ }, { "type": "error", - "name": "Params_UseFundsOutOfBounds", + "name": "Params_InvalidClearinghouse", + "inputs": [] + }, + { + "type": "error", + "name": "Params_InvalidCooler", "inputs": [] + }, + { + "type": "error", + "name": "Policy_ModuleDoesNotExist", + "inputs": [ + { + "name": "keycode_", + "type": "bytes5", + "internalType": "Keycode" + } + ] + }, + { + "type": "error", + "name": "Policy_WrongModuleVersion", + "inputs": [ + { + "name": "expected_", + "type": "bytes", + "internalType": "bytes" + } + ] } ] } diff --git a/src/views/Lending/Cooler/hooks/useConsolidateCooler.tsx b/src/views/Lending/Cooler/hooks/useConsolidateCooler.tsx index 5b34d0c4f3..f110c5141d 100644 --- a/src/views/Lending/Cooler/hooks/useConsolidateCooler.tsx +++ b/src/views/Lending/Cooler/hooks/useConsolidateCooler.tsx @@ -26,9 +26,16 @@ export const useConsolidateCooler = () => { if (!signer) throw new Error(`Please connect a wallet`); const contractAddress = COOLER_CONSOLIDATION_CONTRACT.addresses[networks.MAINNET]; const contract = CoolerConsolidation__factory.connect(contractAddress, signer); - const cooler = await contract.consolidateWithFlashLoan(clearingHouseAddress, coolerAddress, loanIds, 0, false, { - gasLimit: loanIds.length <= 30 ? loanIds.length * 1000000 : 30000000, - }); + const cooler = await contract.consolidate( + clearingHouseAddress, + clearingHouseAddress, + coolerAddress, + coolerAddress, + loanIds, + { + gasLimit: loanIds.length <= 30 ? loanIds.length * 1000000 : 30000000, + }, + ); const receipt = await cooler.wait(); return receipt; }, diff --git a/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx b/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx index b1bd54f371..5aa7ffaecd 100644 --- a/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx +++ b/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx @@ -24,9 +24,10 @@ export const useGetConsolidationAllowances = ({ const contractAddress = COOLER_CONSOLIDATION_CONTRACT.addresses[networks.MAINNET]; const contract = CoolerConsolidation__factory.connect(contractAddress, provider); const requiredApprovals = await contract.requiredApprovals(clearingHouseAddress, coolerAddress, loanIds); + const totalDebtWithFee = requiredApprovals[3].add(requiredApprovals[4]); return { consolidatedLoanCollateral: new DecimalBigNumber(requiredApprovals[1], 18), - totalDebtWithFee: new DecimalBigNumber(requiredApprovals[2], 18), + totalDebtWithFee: new DecimalBigNumber(totalDebtWithFee, 18), }; } catch { return {