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

Allow Uniswap Positions to be used as collateral on the Markets page #807

Merged
merged 5 commits into from
Feb 18, 2024
Merged
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
220 changes: 175 additions & 45 deletions earn/src/components/lend/BorrowingWidget.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion earn/src/components/lend/modal/BorrowModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ export default function BorrowModal(props: BorrowModalProps) {
if (!selectedBorrow) return null;

return (
<Modal isOpen={isOpen} setIsOpen={setIsOpen} title='Borrow'>
<Modal isOpen={isOpen} setIsOpen={setIsOpen} title='Open a new position'>
<div className='w-full flex flex-col gap-4'>
<div>
<Text size='M' weight='bold'>
Expand Down
426 changes: 426 additions & 0 deletions earn/src/components/lend/modal/BorrowModalUniswap.tsx

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion earn/src/components/lend/modal/UpdateCollateralModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { Fragment, useState } from 'react';

import { Tab } from '@headlessui/react';
import { SendTransactionResult } from '@wagmi/core';
import { BigNumber } from 'ethers';
import Modal from 'shared/lib/components/common/Modal';
import { Text } from 'shared/lib/components/common/Typography';
import { GREY_700 } from 'shared/lib/data/constants/Colors';
import styled from 'styled-components';

import { BorrowerNftBorrower } from '../../../data/BorrowerNft';
import { UniswapNFTPosition } from '../../../data/Uniswap';
import AddCollateralModalContent from './content/AddCollateralModalContent';
import RemoveCollateralModalContent from './content/RemoveCollateralModalContent';
import ToUniswapNFTModalContent from './content/ToUniswapNFTModalContent';

export enum ConfirmationType {
DEPOSIT = 'DEPOSIT',
Expand Down Expand Up @@ -49,14 +52,38 @@ const TabButton = styled.button`
export type UpdateCollateralModalProps = {
isOpen: boolean;
borrower: BorrowerNftBorrower;
uniswapPositions: UniswapNFTPosition[];
setIsOpen: (isOpen: boolean) => void;
setPendingTxn: (pendingTxn: SendTransactionResult | null) => void;
};

export default function UpdateCollateralModal(props: UpdateCollateralModalProps) {
const { isOpen, borrower, setIsOpen, setPendingTxn } = props;
const { isOpen, borrower, uniswapPositions, setIsOpen, setPendingTxn } = props;
const [confirmationType, setConfirmationType] = useState<ConfirmationType>(ConfirmationType.DEPOSIT);

if ((borrower.uniswapPositions?.length || 0) > 0) {
const positionToWithdraw = borrower.uniswapPositions![0];
const uniswapNftId = uniswapPositions.find(
(nft) => nft.lower === positionToWithdraw.lower && nft.upper === positionToWithdraw.upper
)?.tokenId;

if (uniswapNftId === undefined) return null;

return (
<Modal isOpen={isOpen} setIsOpen={setIsOpen} title='Withdraw Uniswap NFT'>
<div className='w-full flex flex-col gap-4'>
<ToUniswapNFTModalContent
borrower={borrower}
positionToWithdraw={positionToWithdraw}
uniswapNftId={BigNumber.from(uniswapNftId)}
setIsOpen={setIsOpen}
setPendingTxnResult={setPendingTxn}
/>
</div>
</Modal>
);
}

return (
<Modal isOpen={isOpen} setIsOpen={setIsOpen} title={getConfirmationTypeValue(confirmationType)}>
<div className='w-full flex flex-col gap-4'>
Expand Down
9 changes: 4 additions & 5 deletions earn/src/components/lend/modal/content/BorrowModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,8 @@ export default function BorrowModalContent(props: BorrowModalContentProps) {

const accountEtherBalance = accountEtherBalanceResult && GN.fromBigNumber(accountEtherBalanceResult.value, 18);

// TODO: This assumes that the borrowing token is always the opposite of the collateral token
// and that only one token is borrowed and one token is collateralized
const isBorrowingToken0 = borrower.assets.token1Raw > 0;
// TODO: This assumes that only one token is borrowed and one token is collateralized
const isBorrowingToken0 = borrower.liabilities.amount0 > 0;

// TODO: This logic needs to change once we support more complex borrowing
const borrowToken = isBorrowingToken0 ? borrower.token0 : borrower.token1;
Expand Down Expand Up @@ -233,7 +232,7 @@ export default function BorrowModalContent(props: BorrowModalContentProps) {
const maxBorrowsBasedOnHealth = maxBorrowAndWithdraw(
borrower.assets,
borrower.liabilities,
[], // TODO: add uniswap positions
borrower.uniswapPositions ?? [],
borrower.sqrtPriceX96,
borrower.iv,
borrower.nSigma,
Expand All @@ -256,7 +255,7 @@ export default function BorrowModalContent(props: BorrowModalContentProps) {
const { health: newHealth } = isHealthy(
borrower.assets,
newLiabilities,
[], // TODO: add uniswap positions
borrower.uniswapPositions ?? [],
borrower.sqrtPriceX96,
borrower.iv,
borrower.nSigma,
Expand Down
7 changes: 3 additions & 4 deletions earn/src/components/lend/modal/content/RepayModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,8 @@ export default function RepayModalContent(props: RepayModalContentProps) {
const { address: userAddress } = useAccount();
const { activeChain } = useContext(ChainContext);

// TODO: This assumes that the borrowing token is always the opposite of the collateral token
// and that only one token is borrowed and one token is collateralized
const isRepayingToken0 = borrower.assets.token1Raw > 0;
// TODO: This assumes that only one token is borrowed and one token is collateralized
const isRepayingToken0 = borrower.liabilities.amount0 > 0;

// TODO: This logic needs to change once we support more complex borrowing
const repayToken = isRepayingToken0 ? borrower.token0 : borrower.token1;
Expand Down Expand Up @@ -239,7 +238,7 @@ export default function RepayModalContent(props: RepayModalContentProps) {
const { health: newHealth } = isHealthy(
borrower.assets,
newLiabilities,
[], // TODO: add uniswap positions
borrower.uniswapPositions ?? [],
borrower.sqrtPriceX96,
borrower.iv,
borrower.nSigma,
Expand Down
199 changes: 199 additions & 0 deletions earn/src/components/lend/modal/content/ToUniswapNFTModalContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { useContext, useMemo } from 'react';

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

import { ChainContext } from '../../../../App';
import { isHealthy } from '../../../../data/BalanceSheet';
import { BorrowerNftBorrower } from '../../../../data/BorrowerNft';
import { UniswapPosition, zip } from '../../../../data/Uniswap';
import HealthBar from '../../../borrow/HealthBar';

const GAS_ESTIMATE_WIGGLE_ROOM = 110;
const SECONDARY_COLOR = '#CCDFED';
const TERTIARY_COLOR = '#4b6980';

enum ConfirmButtonState {
CONTRACT_ERROR,
WAITING_FOR_USER,
PENDING,
LOADING,
DISABLED,
READY,
}

function getConfirmButton(state: ConfirmButtonState): { text: string; enabled: boolean } {
switch (state) {
case ConfirmButtonState.CONTRACT_ERROR:
return { text: 'Error', enabled: false };
case ConfirmButtonState.LOADING:
return { text: 'Loading...', enabled: false };
case ConfirmButtonState.PENDING:
return { text: 'Pending', enabled: false };
case ConfirmButtonState.WAITING_FOR_USER:
return { text: 'Check Wallet', enabled: false };
case ConfirmButtonState.READY:
return { text: 'Confirm', enabled: true };
case ConfirmButtonState.DISABLED:
default:
return { text: 'Confirm', enabled: false };
}
}

type ConfirmButtonProps = {
borrower: BorrowerNftBorrower;
positionToWithdraw: UniswapPosition;
uniswapNftId: BigNumber;
setIsOpen: (open: boolean) => void;
setPendingTxnResult: (result: SendTransactionResult | null) => void;
};

function ConfirmButton(props: ConfirmButtonProps) {
const { borrower, positionToWithdraw, uniswapNftId, setIsOpen, setPendingTxnResult } = props;
const { activeChain } = useContext(ChainContext);

const { address: userAddress } = useAccount();

const encodedData = useMemo(() => {
return ethers.utils.defaultAbiCoder.encode(
['uint256', 'int24', 'int24', 'int128', 'uint208'],
[
uniswapNftId,
positionToWithdraw.lower,
positionToWithdraw.upper,
positionToWithdraw.liquidity.toString(10),
zip(
borrower.uniswapPositions!.filter((position) => {
return position.lower !== positionToWithdraw.lower || position.upper !== positionToWithdraw.upper;
}),
'0x83ee755b'
),
]
) as `0x${string}`;
}, [borrower, positionToWithdraw, uniswapNftId]);

const { config: withdrawConfig, error: withdrawError } = usePrepareContractWrite({
address: ALOE_II_BORROWER_NFT_ADDRESS[activeChain.id],
abi: borrowerNftAbi,
functionName: 'modify',
args: [
userAddress ?? '0x',
[borrower.index, borrower.index],
[
ALOE_II_UNISWAP_NFT_MANAGER_ADDRESS[activeChain.id],
ALOE_II_BORROWER_NFT_WITHDRAW_MANAGER_ADDRESS[activeChain.id],
],
[encodedData, '0x'],
[0, 0],
],
enabled: Boolean(userAddress),
chainId: activeChain.id,
});
const gasLimit = withdrawConfig.request?.gasLimit.mul(GAS_ESTIMATE_WIGGLE_ROOM).div(100);
const { write: contractWrite, isLoading: isAskingUserToConfirm } = useContractWrite({
...withdrawConfig,
request: {
...withdrawConfig.request,
gasLimit,
},
onSuccess(data) {
setIsOpen(false);
setPendingTxnResult(data);
},
});

let confirmButtonState = ConfirmButtonState.READY;
if (withdrawError !== null) {
confirmButtonState = ConfirmButtonState.CONTRACT_ERROR;
} else if (contractWrite === undefined || !withdrawConfig.request) {
confirmButtonState = ConfirmButtonState.LOADING;
} else if (isAskingUserToConfirm) {
confirmButtonState = ConfirmButtonState.WAITING_FOR_USER;
}

const confirmButton = getConfirmButton(confirmButtonState);

return (
<FilledStylizedButton size='M' fillWidth={true} disabled={!confirmButton.enabled} onClick={contractWrite}>
{confirmButton.text}
</FilledStylizedButton>
);
}

export type RemoveCollateralModalContentProps = {
borrower: BorrowerNftBorrower;
positionToWithdraw: UniswapPosition;
uniswapNftId: BigNumber;
setIsOpen: (isOpen: boolean) => void;
setPendingTxnResult: (result: SendTransactionResult | null) => void;
};

export default function ToUniswapNFTModalContent(props: RemoveCollateralModalContentProps) {
const { borrower, positionToWithdraw, uniswapNftId, setIsOpen, setPendingTxnResult } = props;

const { health: newHealth } = isHealthy(
borrower.assets,
borrower.liabilities,
[],
borrower.sqrtPriceX96,
borrower.iv,
borrower.nSigma,
borrower.token0.decimals,
borrower.token1.decimals
);

const canExecute = newHealth > 1.0;

// TODO: could provide links (in summary) to both the BorrowerNFT and UniswapNFT on OpenSea
return (
<>
<div className='flex flex-col gap-1 w-full'>
<HealthBar health={newHealth} />
<Text size='M' weight='bold' className='mt-4'>
Summary
</Text>
{canExecute ? (
<Text size='XS' color={SECONDARY_COLOR} className='overflow-hidden text-ellipsis'>
You have an {borrower.token0.symbol}/{borrower.token1.symbol} Uniswap Position in the range{' '}
{positionToWithdraw.lower}&nbsp;⇔&nbsp;{positionToWithdraw.upper}. You're moving it from an Aloe Borrower
NFT back to a plain Uniswap NFT (#{uniswapNftId.toString()}). It will no longer act as collateral.
</Text>
) : (
<Text size='XS' color={SECONDARY_COLOR} className='overflow-hidden text-ellipsis'>
This Uniswap NFT is the only thing keeping your position healthy. Before you can withdraw, you must repay
some (or all) borrows.
</Text>
)}
</div>
{canExecute && (
haydenshively marked this conversation as resolved.
Show resolved Hide resolved
<div className='flex flex-col gap-4 w-full'>
<ConfirmButton
borrower={borrower}
positionToWithdraw={positionToWithdraw}
uniswapNftId={uniswapNftId}
setIsOpen={setIsOpen}
setPendingTxnResult={setPendingTxnResult}
/>
<Text size='XS' color={TERTIARY_COLOR} className='w-full'>
By withdrawing, you agree to our{' '}
<a href={TERMS_OF_SERVICE_URL} className='underline' rel='noreferrer' target='_blank'>
Terms of Service
</a>{' '}
and acknowledge that you may lose your money. Aloe Labs is not responsible for any losses you may incur. It
is your duty to educate yourself and be aware of the risks.
</Text>
</div>
)}
</>
);
}
4 changes: 2 additions & 2 deletions earn/src/data/BorrowerNft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ALOE_II_BORROWER_NFT_ADDRESS,
ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS,
ALOE_II_PERMIT2_MANAGER_ADDRESS,
ALOE_II_SIMPLE_MANAGER_ADDRESS,
ALOE_II_UNISWAP_NFT_MANAGER_ADDRESS,
MULTICALL_ADDRESS,
} from 'shared/lib/data/constants/ChainSpecific';
import { filterNullishValues } from 'shared/lib/util/Arrays';
Expand Down Expand Up @@ -164,9 +164,9 @@ export async function fetchListOfFuse2BorrowNfts(
includeFreshBorrowers: false, // TODO: change later
onlyCheckMostRecentModify: true, // TODO: Hayden has concerns (as usual)
validManagerSet: new Set([
ALOE_II_SIMPLE_MANAGER_ADDRESS[chainId],
ALOE_II_PERMIT2_MANAGER_ADDRESS[chainId],
ALOE_II_BORROWER_NFT_SIMPLE_MANAGER_ADDRESS[chainId],
ALOE_II_UNISWAP_NFT_MANAGER_ADDRESS[chainId],
]),
validUniswapPool: uniswapPool,
});
Expand Down
Loading
Loading