diff --git a/src/abi/CoolerConsolidation.json b/src/abi/CoolerConsolidation.json index 74190aaaad..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" }, @@ -54,6 +29,45 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "ROLES", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ROLESv1" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "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", @@ -67,6 +81,26 @@ ], "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", @@ -108,28 +142,38 @@ }, { "type": "function", - "name": "collector", + "name": "configureDependencies", "inputs": [], "outputs": [ { - "name": "", - "type": "address", - "internalType": "address" + "name": "dependencies", + "type": "bytes5[]", + "internalType": "Keycode[]" } ], - "stateMutability": "view" + "stateMutability": "nonpayable" }, { "type": "function", - "name": "consolidateWithFlashLoan", + "name": "consolidate", "inputs": [ { - "name": "clearinghouse_", + "name": "clearinghouseFrom_", "type": "address", "internalType": "address" }, { - "name": "cooler_", + "name": "clearinghouseTo_", + "type": "address", + "internalType": "address" + }, + { + "name": "coolerFrom_", + "type": "address", + "internalType": "address" + }, + { + "name": "coolerTo_", "type": "address", "internalType": "address" }, @@ -137,16 +181,39 @@ "name": "ids_", "type": "uint256[]", "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "consolidateWithNewOwner", + "inputs": [ + { + "name": "clearinghouseFrom_", + "type": "address", + "internalType": "address" }, { - "name": "useFunds_", - "type": "uint256", - "internalType": "uint256" + "name": "clearinghouseTo_", + "type": "address", + "internalType": "address" }, { - "name": "sdai_", - "type": "bool", - "internalType": "bool" + "name": "coolerFrom_", + "type": "address", + "internalType": "address" + }, + { + "name": "coolerTo_", + "type": "address", + "internalType": "address" + }, + { + "name": "ids_", + "type": "uint256[]", + "internalType": "uint256[]" } ], "outputs": [], @@ -154,24 +221,69 @@ }, { "type": "function", - "name": "dai", + "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" } @@ -199,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" @@ -264,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" }, @@ -308,8 +432,8 @@ }, { "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "address", + "internalType": "address" }, { "name": "", @@ -324,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", @@ -364,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": [] }, { @@ -410,6 +512,11 @@ "name": "OnlyLender", "inputs": [] }, + { + "type": "error", + "name": "OnlyPolicyActive", + "inputs": [] + }, { "type": "error", "name": "OnlyThis", @@ -420,6 +527,11 @@ "name": "Params_FeePercentageOutOfRange", "inputs": [] }, + { + "type": "error", + "name": "Params_InsufficientCoolerCount", + "inputs": [] + }, { "type": "error", "name": "Params_InvalidAddress", @@ -427,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/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/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 new file mode 100644 index 0000000000..5aa7ffaecd --- /dev/null +++ b/src/views/Lending/Cooler/hooks/useGetConsolidationAllowances.tsx @@ -0,0 +1,42 @@ +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); + const totalDebtWithFee = requiredApprovals[3].add(requiredApprovals[4]); + return { + consolidatedLoanCollateral: new DecimalBigNumber(requiredApprovals[1], 18), + totalDebtWithFee: new DecimalBigNumber(totalDebtWithFee, 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 + + + + )} + + ); };