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 committed Feb 12, 2024
1 parent 638d375 commit 638262e
Show file tree
Hide file tree
Showing 13 changed files with 874 additions and 72 deletions.
214 changes: 169 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
5 changes: 2 additions & 3 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
5 changes: 2 additions & 3 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
195 changes: 195 additions & 0 deletions earn/src/components/lend/modal/content/ToUniswapNFTModalContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
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_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],
[ALOE_II_UNISWAP_NFT_MANAGER_ADDRESS[activeChain.id]],
[encodedData],
[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
28 changes: 23 additions & 5 deletions earn/src/data/MarginAccount.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Big from 'big.js';
import { ContractCallContext, Multicall } from 'ethereum-multicall';
import { ethers } from 'ethers';
import JSBI from 'jsbi';
import { borrowerAbi } from 'shared/lib/abis/Borrower';
import { borrowerLensAbi } from 'shared/lib/abis/BorrowerLens';
import { erc20Abi } from 'shared/lib/abis/ERC20';
Expand All @@ -21,6 +22,7 @@ import { Address } from 'wagmi';

import { ContractCallReturnContextEntries, convertBigNumbersForReturnContexts } from '../util/Multicall';
import { TOPIC0_CREATE_BORROWER_EVENT } from './constants/Signatures';
import { UniswapPosition } from './Uniswap';

export type Assets = {
token0Raw: number;
Expand Down Expand Up @@ -51,6 +53,7 @@ export type MarginAccount = {
lender1: Address;
iv: number;
nSigma: number;
uniswapPositions?: UniswapPosition[];
};

/**
Expand Down Expand Up @@ -190,16 +193,16 @@ export async function fetchBorrowerDatas(
contractAddress: ALOE_II_BORROWER_LENS_ADDRESS[chainId],
abi: borrowerLensAbi as any,
calls: [
// {
// reference: 'getLiabilities',
// methodName: 'getLiabilities',
// methodParameters: [accountAddress, true],
// },
{
reference: 'getHealth',
methodName: 'getHealth',
methodParameters: [accountAddress],
},
{
reference: 'getUniswapPositions',
methodName: 'getUniswapPositions',
methodParameters: [accountAddress],
},
],
context: {
fee: fee,
Expand Down Expand Up @@ -282,6 +285,20 @@ export async function fetchBorrowerDatas(
amount1: toImpreciseNumber(liabilitiesData[1], token1.decimals),
};

const uniswapPositionData = lensReturnContexts[1].returnValues;
const uniswapPositionBounds = uniswapPositionData[0] as number[];
const uniswapPositionLiquidity = uniswapPositionData[1] as { hex: `0x${string}` }[];

const uniswapPositions: UniswapPosition[] = [];
uniswapPositionLiquidity.forEach((liquidity, i) => {
uniswapPositions.push({
lower: uniswapPositionBounds[i * 2],
upper: uniswapPositionBounds[i * 2 + 1],
liquidity: JSBI.BigInt(liquidity.hex),
});
});
// const uniswapPositionFees = uniswapPositionData[2];

const lender0 = accountReturnContexts[0].returnValues[0];
const lender1 = accountReturnContexts[1].returnValues[0];
const oracleReturnValues = convertBigNumbersForReturnContexts(oracleResults.callsReturnContext)[0].returnValues;
Expand All @@ -299,6 +316,7 @@ export async function fetchBorrowerDatas(
lender0,
lender1,
nSigma,
uniswapPositions,
};
marginAccounts.push(marginAccount);
});
Expand Down
Loading

0 comments on commit 638262e

Please sign in to comment.