Skip to content

Commit

Permalink
Allow Uniswap Positions to be used as collateral on the Markets page (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenshively authored Feb 18, 2024
1 parent ac434cb commit c8b26e4
Show file tree
Hide file tree
Showing 13 changed files with 892 additions and 75 deletions.
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 && (
<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

0 comments on commit c8b26e4

Please sign in to comment.