Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Laying the groundwork for chained operations #806

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion earn/src/components/lend/BorrowingWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import styled from 'styled-components';
import { computeLTV } from '../../data/BalanceSheet';
import { BorrowerNftBorrower } from '../../data/BorrowerNft';
import { LendingPair, LendingPairBalancesMap } from '../../data/LendingPair';
import MulticallOperator from '../../data/operations/MulticallOperator';
import { rgba } from '../../util/Colors';
import HealthGauge from '../common/HealthGauge';
import BorrowModal from './modal/BorrowModal';
Expand Down Expand Up @@ -120,6 +121,7 @@ export type BorrowingWidgetProps = {
tokenBalances: LendingPairBalancesMap;
tokenQuotes: Map<string, number>;
tokenColors: Map<string, string>;
multicallOperator: MulticallOperator;
setPendingTxn: (pendingTxn: SendTransactionResult | null) => void;
};

Expand All @@ -142,7 +144,7 @@ function filterBySelection(lendingPairs: LendingPair[], selection: Token | null)
}

export default function BorrowingWidget(props: BorrowingWidgetProps) {
const { borrowers, lendingPairs, tokenBalances, tokenColors, setPendingTxn } = props;
const { borrowers, lendingPairs, tokenBalances, tokenColors, multicallOperator, setPendingTxn } = props;

// selection/hover state for Available Table
const [selectedCollateral, setSelectedCollateral] = useState<Token | null>(null);
Expand Down Expand Up @@ -442,6 +444,7 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) {
selectedCollateral={selectedCollateral}
selectedBorrow={selectedBorrows}
userBalance={tokenBalances.get(selectedCollateral.address)?.gn ?? GN.zero(selectedCollateral.decimals)}
multicallOperator={multicallOperator}
setIsOpen={() => {
setSelectedBorrows(null);
setSelectedCollateral(null);
Expand All @@ -454,6 +457,7 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) {
isOpen={selectedBorrower != null}
borrower={selectedBorrower.borrower}
lendingPair={lendingPairs.find((pair) => pair.uniswapPool === selectedBorrower.borrower.uniswapPool)}
multicallOperator={multicallOperator}
setIsOpen={() => {
setSelectedBorrower(null);
}}
Expand All @@ -464,6 +468,7 @@ export default function BorrowingWidget(props: BorrowingWidgetProps) {
<UpdateCollateralModal
isOpen={selectedBorrower != null}
borrower={selectedBorrower.borrower}
multicallOperator={multicallOperator}
setIsOpen={() => {
setSelectedBorrower(null);
}}
Expand Down
91 changes: 26 additions & 65 deletions earn/src/components/lend/modal/BorrowModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { borrowerNftAbi } from 'shared/lib/abis/BorrowerNft';
import { factoryAbi } from 'shared/lib/abis/Factory';
import { permit2Abi } from 'shared/lib/abis/Permit2';
import { volatilityOracleAbi } from 'shared/lib/abis/VolatilityOracle';
import { FilledGradientButton } from 'shared/lib/components/common/Buttons';
import { FilledStylizedButton } from 'shared/lib/components/common/Buttons';
import { SquareInputWithMax } from 'shared/lib/components/common/Input';
import Modal from 'shared/lib/components/common/Modal';
import TokenAmountInput from 'shared/lib/components/common/TokenAmountInput';
Expand All @@ -26,11 +26,12 @@ import { Permit2State, usePermit2 } from 'shared/lib/data/hooks/UsePermit2';
import { Token } from 'shared/lib/data/Token';
import { formatNumberInput } from 'shared/lib/util/Numbers';
import { generateBytes12Salt } from 'shared/lib/util/Salt';
import { useAccount, useBalance, useContractRead, useContractWrite, usePrepareContractWrite } from 'wagmi';
import { useAccount, useBalance, useContractRead } from 'wagmi';

import { ChainContext } from '../../../App';
import { computeLTV } from '../../../data/BalanceSheet';
import { LendingPair } from '../../../data/LendingPair';
import MulticallOperator from '../../../data/operations/MulticallOperator';
import { RateModel, yieldPerSecondToAPR } from '../../../data/RateModel';

const MAX_BORROW_PERCENTAGE = 0.8;
Expand Down Expand Up @@ -64,7 +65,7 @@ const permit2StateToButtonStateMap = {
function getConfirmButton(state: ConfirmButtonState, token: Token): { text: string; enabled: boolean } {
switch (state) {
case ConfirmButtonState.READY:
return { text: 'Confirm', enabled: true };
return { text: 'Add Action', enabled: true };
case ConfirmButtonState.LOADING:
return { text: 'Loading', enabled: false };
case ConfirmButtonState.PERMIT_ASSET:
Expand All @@ -85,7 +86,7 @@ function getConfirmButton(state: ConfirmButtonState, token: Token): { text: stri
return { text: 'Connect Wallet', enabled: false };
case ConfirmButtonState.DISABLED:
default:
return { text: 'Confirm', enabled: false };
return { text: 'Add Action', enabled: false };
}
}

Expand All @@ -95,17 +96,18 @@ export type BorrowModalProps = {
selectedCollateral: Token;
selectedBorrow: Token;
userBalance: GN;
multicallOperator: MulticallOperator;
setIsOpen: (isOpen: boolean) => void;
setPendingTxn: (pendingTxn: SendTransactionResult | null) => void;
};

export default function BorrowModal(props: BorrowModalProps) {
const { isOpen, selectedLendingPair, selectedCollateral, selectedBorrow, userBalance, setIsOpen, setPendingTxn } =
const { isOpen, selectedLendingPair, selectedCollateral, selectedBorrow, userBalance, multicallOperator, setIsOpen } =
props;
const [collateralAmountStr, setCollateralAmountStr] = useState<string>('');
const [borrowAmountStr, setBorrowAmountStr] = useState<string>('');
const { activeChain } = useContext(ChainContext);

const { activeChain } = useContext(ChainContext);
const { address: userAddress } = useAccount();

const { data: consultData } = useContractRead({
Expand Down Expand Up @@ -265,17 +267,6 @@ export default function BorrowModal(props: BorrowModalProps) {
);
}, [permit2Result, predictedAddress, selectedCollateral.address, userAddress]);

// Prepare for actual import/mint transaction
const borrowerNft = useMemo(() => new ethers.utils.Interface(borrowerNftAbi), []);
// First, we `mint` so that they have a `Borrower` to put stuff in
const encodedMint = useMemo(() => {
if (!userAddress || selectedLendingPair?.uniswapPool === undefined) return null;
const to = userAddress;
const pools = [selectedLendingPair.uniswapPool ?? '0x'];
const salts = [generatedSalt];
return borrowerNft.encodeFunctionData('mint', [to, pools, salts]) as `0x${string}`;
}, [userAddress, selectedLendingPair?.uniswapPool, generatedSalt, borrowerNft]);

const encodedBorrowCall = useMemo(() => {
if (!userAddress || !selectedLendingPair || !selectedBorrow) return null;
const borrower = new ethers.utils.Interface(borrowerAbi);
Expand All @@ -287,50 +278,12 @@ export default function BorrowModal(props: BorrowModalProps) {
return borrower.encodeFunctionData('borrow', [amount0.toBigNumber(), amount1.toBigNumber(), userAddress]);
}, [borrowAmount, selectedBorrow, selectedLendingPair, userAddress]);

const encodedModify = useMemo(() => {
if (!userAddress || nextNftPtrIdx === undefined || ante === undefined || !encodedPermit2 || !encodedBorrowCall)
return null;
const owner = userAddress;
const indices = [nextNftPtrIdx];
const managers = [ALOE_II_PERMIT2_MANAGER_ADDRESS[activeChain.id]];
const datas = [encodedPermit2.concat(encodedBorrowCall.slice(2))];
const antes = [ante.toBigNumber().div(1e13)];
return borrowerNft.encodeFunctionData('modify', [owner, indices, managers, datas, antes]) as `0x${string}`;
}, [userAddress, nextNftPtrIdx, ante, activeChain.id, encodedPermit2, encodedBorrowCall, borrowerNft]);

const {
config: configMulticallOps,
isError: isUnableToMulticallOps,
isLoading: isCheckingIfAbleToMulticallOps,
} = usePrepareContractWrite({
address: ALOE_II_BORROWER_NFT_ADDRESS[activeChain.id],
abi: borrowerNftAbi,
functionName: 'multicall',
args: [[encodedMint ?? '0x', encodedModify ?? '0x']],
overrides: { value: ante?.toBigNumber() },
chainId: activeChain.id,
enabled: userAddress && Boolean(encodedMint) && Boolean(encodedModify) && parameterData !== undefined,
});
const gasLimit = configMulticallOps.request?.gasLimit.mul(110).div(100);
const { write: borrow, isLoading: isAskingUserToMulticallOps } = useContractWrite({
...configMulticallOps,
request: {
...configMulticallOps.request,
gasLimit,
},
onSuccess(data) {
setIsOpen(false);
setPendingTxn(data);
},
});

let confirmButtonState: ConfirmButtonState;
if (!userAddress) {
confirmButtonState = ConfirmButtonState.CONNECT_WALLET;
} else if (ante === undefined || maxBorrowSupplyConstraint == null || maxBorrowHealthConstraint == null) {
confirmButtonState = ConfirmButtonState.LOADING;
} else if (
isAskingUserToMulticallOps ||
permit2State === Permit2State.ASKING_USER_TO_SIGN ||
permit2State === Permit2State.ASKING_USER_TO_APPROVE
) {
Expand Down Expand Up @@ -427,26 +380,34 @@ export default function BorrowModal(props: BorrowModalProps) {
</div>
</div>
</div>
<FilledGradientButton
<FilledStylizedButton
size='M'
fillWidth={true}
disabled={!confirmButton.enabled}
onClick={() => {
// TODO: clean this up
if (permit2State !== Permit2State.DONE) {
permit2Action?.();
} else if (
confirmButton.enabled &&
!isUnableToMulticallOps &&
!isCheckingIfAbleToMulticallOps &&
configMulticallOps
) {
borrow?.();
return;
}
if (!userAddress || encodedPermit2 == null || encodedBorrowCall == null || ante === undefined) return;
multicallOperator
.addMintOperation({
to: userAddress,
pools: [selectedLendingPair.uniswapPool],
salts: [generatedSalt],
})
.addModifyOperation({
owner: userAddress,
indices: [nextNftPtrIdx?.toNumber() ?? 0],
managers: [ALOE_II_PERMIT2_MANAGER_ADDRESS[activeChain.id]],
data: [encodedPermit2.concat(encodedBorrowCall.slice(2)) as `0x${string}`],
antes: [ante],
});
setIsOpen(false);
}}
>
{confirmButton.text}
</FilledGradientButton>
</FilledStylizedButton>
</div>
<Text size='XS' color={TERTIARY_COLOR} className='w-full mt-2'>
By borrowing, you agree to our{' '}
Expand Down
113 changes: 113 additions & 0 deletions earn/src/components/lend/modal/OperationsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useContext, useMemo } from 'react';

import { SendTransactionResult } from '@wagmi/core';
import { ethers } from 'ethers';
import { borrowerNftAbi } from 'shared/lib/abis/BorrowerNft';
import { FilledStylizedButton } from 'shared/lib/components/common/Buttons';
import Modal from 'shared/lib/components/common/Modal';
import { Text } from 'shared/lib/components/common/Typography';
import { ALOE_II_BORROWER_NFT_ADDRESS } from 'shared/lib/data/constants/ChainSpecific';
import { useAccount, useContractWrite, usePrepareContractWrite } from 'wagmi';

import { ChainContext } from '../../../App';
import MulticallOperator from '../../../data/operations/MulticallOperator';

export type OperationsModalProps = {
multicallOperator: MulticallOperator;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
setPendingTxn: (pendingTxn: SendTransactionResult | null) => void;
};

export default function OperationsModal(props: OperationsModalProps) {
const { multicallOperator, isOpen, setIsOpen, setPendingTxn } = props;

const { address: userAddress } = useAccount();
const { activeChain } = useContext(ChainContext);

const mintOperation = multicallOperator.combineMintOperations();
const modifyOperation = multicallOperator.combineModifyOperations();
const combinedAnte = multicallOperator.getCombinedAnte();

const borrowerNft = useMemo(() => new ethers.utils.Interface(borrowerNftAbi), []);
const encodedMint =
mintOperation &&
(borrowerNft.encodeFunctionData('mint', [
mintOperation.to,
mintOperation.pools,
mintOperation.salts,
]) as `0x${string}`);

const encodedModify =
modifyOperation &&
(borrowerNft.encodeFunctionData('modify', [
modifyOperation.owner,
modifyOperation.indices,
modifyOperation.managers,
modifyOperation.data,
modifyOperation.antes.map((ante) => ante.toBigNumber().div(1e13)),
]) as `0x${string}`);

const functionName = mintOperation ? 'multicall' : 'modify';

const args = mintOperation
? [[encodedMint ?? '0x', encodedModify]]
: [
modifyOperation.owner,
modifyOperation.indices,
modifyOperation.managers,
modifyOperation.data,
modifyOperation.antes.map((ante) => ante.toBigNumber().div(1e13)),
];

const {
config: configMulticallOps,
isError: isUnableToMulticallOps,
isLoading: isCheckingIfAbleToMulticallOps,
} = usePrepareContractWrite({
address: ALOE_II_BORROWER_NFT_ADDRESS[activeChain.id],
abi: borrowerNftAbi,
functionName: functionName,
args: args as any,
overrides: {
value: combinedAnte.toBigNumber(),
},
chainId: activeChain.id,
enabled: userAddress !== undefined,
});
const gasLimit = configMulticallOps.request?.gasLimit.mul(110).div(100);
const { write: call, isLoading: isAskingUserToMulticallOps } = useContractWrite({
...configMulticallOps,
request: {
...configMulticallOps.request,
gasLimit,
},
onSuccess(data) {
setIsOpen(false);
setPendingTxn(data);
},
});

return (
<Modal isOpen={isOpen} setIsOpen={setIsOpen} title='Operations'>
<div className='w-full flex flex-col gap-4'>
<Text size='M'>
You are performing {mintOperation?.salts.length ?? 0} mint operations and{' '}
{modifyOperation?.indices.length ?? 0} modify operations in a single transaction. This will save you gas fees.
</Text>
<FilledStylizedButton
size='M'
fillWidth={true}
onClick={() => {
call?.();
}}
disabled={
isAskingUserToMulticallOps || isCheckingIfAbleToMulticallOps || isUnableToMulticallOps || !userAddress
}
>
Confirm
</FilledStylizedButton>
</div>
</Modal>
);
}
5 changes: 4 additions & 1 deletion earn/src/components/lend/modal/UpdateBorrowerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import styled from 'styled-components';

import { BorrowerNftBorrower } from '../../../data/BorrowerNft';
import { LendingPair } from '../../../data/LendingPair';
import MulticallOperator from '../../../data/operations/MulticallOperator';
import BorrowModalContent from './content/BorrowModalContent';
import RepayModalContent from './content/RepayModalContent';

Expand Down Expand Up @@ -50,13 +51,14 @@ const TabButton = styled.button`
export type UpdateBorrowerModalProps = {
isOpen: boolean;
borrower: BorrowerNftBorrower;
multicallOperator: MulticallOperator;
lendingPair?: LendingPair;
setIsOpen: (isOpen: boolean) => void;
setPendingTxn: (pendingTxn: SendTransactionResult | null) => void;
};

export default function UpdateBorrowerModal(props: UpdateBorrowerModalProps) {
const { isOpen, borrower, lendingPair, setIsOpen, setPendingTxn } = props;
const { isOpen, borrower, lendingPair, multicallOperator, setIsOpen, setPendingTxn } = props;
const [confirmationType, setConfirmationType] = useState<ConfirmationType>(ConfirmationType.BORROW);

return (
Expand Down Expand Up @@ -85,6 +87,7 @@ export default function UpdateBorrowerModal(props: UpdateBorrowerModalProps) {
<Tab.Panel className='w-full px-2'>
<BorrowModalContent
borrower={borrower}
multicallOperator={multicallOperator}
setIsOpen={setIsOpen}
lendingPair={lendingPair}
setPendingTxnResult={setPendingTxn}
Expand Down
Loading
Loading