diff --git a/README.md b/README.md index 9c4282c3..1e27acef 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@ _Expiring Multi Party is UMA's most current financial smart contract template. This UI is a community-made tool to make interfacing with the protocol easier, please use at your own risk._ -Live frontend: https://emp-tools.vercel.app/ +Live frontend: + +- Staging: https://emp-tools.vercel.app/ +- Production: https://tools.umaproject.org/ ## Manual testing diff --git a/containers/EmpSponsors.ts b/containers/EmpSponsors.ts index df355db5..8602ad15 100644 --- a/containers/EmpSponsors.ts +++ b/containers/EmpSponsors.ts @@ -57,17 +57,19 @@ const useEmpSponsors = () => { utils.getAddress(contract.id) === emp.address ); - empData.positions.forEach((position: PositionQuery) => { - const sponsor = utils.getAddress(position.sponsor.id); - - newPositions[sponsor] = { - tokensOutstanding: position.tokensOutstanding, - collateral: position.collateral, - sponsor, - }; - }); - - setActivePositions(newPositions); + if (empData) { + empData.positions.forEach((position: PositionQuery) => { + const sponsor = utils.getAddress(position.sponsor.id); + + newPositions[sponsor] = { + tokensOutstanding: position.tokensOutstanding, + collateral: position.collateral, + sponsor, + }; + }); + + setActivePositions(newPositions); + } } } }; diff --git a/containers/EmpState.ts b/containers/EmpState.ts index 4e858f88..99c7c217 100644 --- a/containers/EmpState.ts +++ b/containers/EmpState.ts @@ -21,6 +21,7 @@ interface ContractState { totalTokensOutstanding: BigNumber | null; liquidationLiveness: BigNumber | null; withdrawalLiveness: BigNumber | null; + currentTime: BigNumber | null; } const initState = { @@ -39,6 +40,7 @@ const initState = { totalTokensOutstanding: null, liquidationLiveness: null, withdrawalLiveness: null, + currentTime: null, }; const useContractState = () => { @@ -70,6 +72,7 @@ const useContractState = () => { emp.totalTokensOutstanding(), emp.liquidationLiveness(), emp.withdrawalLiveness(), + emp.getCurrentTime(), ]); const newState: ContractState = { @@ -88,6 +91,7 @@ const useContractState = () => { totalTokensOutstanding: res[12] as BigNumber, liquidationLiveness: res[13] as BigNumber, withdrawalLiveness: res[14] as BigNumber, + currentTime: res[15] as BigNumber, }; setState(newState); diff --git a/containers/Position.ts b/containers/Position.ts index b159a79d..29357bb9 100644 --- a/containers/Position.ts +++ b/containers/Position.ts @@ -18,6 +18,7 @@ function usePosition() { const [collateral, setCollateral] = useState(null); const [tokens, setTokens] = useState(null); + const [cRatio, setCRatio] = useState(null); const [withdrawAmt, setWithdrawAmt] = useState(null); const [withdrawPassTime, setWithdrawPassTime] = useState(null); const [pendingWithdraw, setPendingWithdraw] = useState(null); @@ -37,6 +38,12 @@ function usePosition() { // format data for storage const collateral: number = weiToNum(collRaw, collDec); const tokens: number = weiToNum(tokensOutstanding, tokenDec); + const cRatio = + collateral !== null && tokens !== null + ? tokens > 0 + ? collateral / tokens + : 0 + : null; const withdrawAmt: number = weiToNum(withdrawReqAmt, collDec); const withdrawPassTime: number = withdrawReqPassTime.toNumber(); const pendingWithdraw: string = @@ -47,6 +54,7 @@ function usePosition() { // set states setCollateral(collateral); setTokens(tokens); + setCRatio(cRatio); setWithdrawAmt(withdrawAmt); setWithdrawPassTime(withdrawPassTime); setPendingWithdraw(pendingWithdraw); @@ -67,6 +75,7 @@ function usePosition() { if (contract === null) { setCollateral(null); setTokens(null); + setCRatio(null); setWithdrawAmt(null); setWithdrawPassTime(null); setPendingWithdraw(null); @@ -78,6 +87,7 @@ function usePosition() { return { collateral, tokens, + cRatio, withdrawAmt, withdrawPassTime, pendingWithdraw, diff --git a/containers/Totals.ts b/containers/Totals.ts index 65c150b1..0c6e8036 100644 --- a/containers/Totals.ts +++ b/containers/Totals.ts @@ -30,7 +30,7 @@ function useTotals() { // use multiplier to find real total collateral in EMP const totalColl = weiToNum(multiplier) * weiToNum(rawColl, collDec); const totalTokens = weiToNum(totalTokensWei, tokenDec); - const gcr = totalColl / totalTokens; + const gcr = totalTokens > 0 ? totalColl / totalTokens : 0; // set states setTotalCollateral(totalColl); diff --git a/features/all-positions/AllPositions.tsx b/features/all-positions/AllPositions.tsx index 0845cfc5..9085cd78 100644 --- a/features/all-positions/AllPositions.tsx +++ b/features/all-positions/AllPositions.tsx @@ -7,11 +7,11 @@ import { TableHead, TableRow, Paper, - Container, Typography, + Tooltip, } from "@material-ui/core"; import styled from "styled-components"; -import { utils, BigNumberish } from "ethers"; +import { utils } from "ethers"; import EmpState from "../../containers/EmpState"; import Collateral from "../../containers/Collateral"; @@ -21,6 +21,10 @@ import EmpContract from "../../containers/EmpContract"; import PriceFeed from "../../containers/PriceFeed"; import Etherscan from "../../containers/Etherscan"; +import { getLiquidationPrice } from "../../utils/getLiquidationPrice"; + +const fromWei = utils.formatUnits; + const Link = styled.a` color: white; font-size: 18px; @@ -30,109 +34,137 @@ const AllPositions = () => { const { empState } = EmpState.useContainer(); const { priceIdentifier: priceId } = empState; const { symbol: tokenSymbol } = Token.useContainer(); - const { symbol: collSymbol } = Collateral.useContainer(); + const { + symbol: collSymbol, + decimals: collDecimals, + } = Collateral.useContainer(); const { activeSponsors } = EmpSponsors.useContainer(); const { contract: emp } = EmpContract.useContainer(); const { latestPrice, sourceUrl } = PriceFeed.useContainer(); const { getEtherscanUrl } = Etherscan.useContainer(); + const { collateralRequirement } = empState; - if (tokenSymbol === null || emp === null) { - return ( - - - - Please first select an EMP from the dropdown above. - - - + if ( + collateralRequirement !== null && + collDecimals !== null && + tokenSymbol !== null && + collSymbol !== null && + emp !== null && + latestPrice !== null && + priceId !== null && + sourceUrl !== undefined + ) { + const collReqFromWei = parseFloat( + fromWei(collateralRequirement, collDecimals) ); - } + const priceIdUtf8 = utils.parseBytes32String(priceId); - const prettyBalance = (x: BigNumberish | null) => { - if (!x) return "N/A"; - x = Number(x).toFixed(4); - return utils.commify(x as string); - }; + const getCollateralRatio = (collateral: number, tokens: number) => { + if (tokens <= 0 || latestPrice <= 0) return 0; + const tokensScaled = tokens * latestPrice; + return collateral / tokensScaled; + }; - const prettyAddress = (x: String | null) => { - if (!x) return "N/A"; - return x.substr(0, 6) + "..." + x.substr(x.length - 6, x.length); - }; + const prettyBalance = (x: number) => { + const x_string = x.toFixed(4); + return utils.commify(x_string); + }; - const getCollateralRatio = ( - collateral: BigNumberish, - tokens: BigNumberish - ) => { - if (!latestPrice) return null; - const tokensScaled = Number(tokens) * Number(latestPrice); - return (Number(collateral) / tokensScaled).toFixed(4); - }; + const prettyAddress = (x: string) => { + return x.substr(0, 6) + "..." + x.substr(x.length - 6, x.length); + }; - return ( - + return ( + + + + {`Estimated price of ${latestPrice} for ${priceIdUtf8} sourced from: `} + + Coinbase Pro. + + + + + {activeSponsors && ( + + + + + Sponsor + + Collateral +
({collSymbol}) +
+ + Synthetics +
({tokenSymbol}) +
+ Collateral Ratio + + Liquidation Price + +
+
+ + {Object.keys(activeSponsors).map((sponsor: string) => { + const activeSponsor = activeSponsors[sponsor]; + return ( + activeSponsor?.collateral && + activeSponsor?.tokensOutstanding && ( + + + + {prettyAddress(sponsor)} + + + + {prettyBalance(Number(activeSponsor.collateral))} + + + {prettyBalance( + Number(activeSponsor.tokensOutstanding) + )} + + + {prettyBalance( + getCollateralRatio( + Number(activeSponsor.collateral), + Number(activeSponsor.tokensOutstanding) + ) + )} + + + {prettyBalance( + getLiquidationPrice( + Number(activeSponsor.collateral), + Number(activeSponsor.tokensOutstanding), + collReqFromWei + ) + )} + + + ) + ); + })} + +
+
+ )} +
+
+ ); + } else { + return ( - - Estimated price of{" "} - {latestPrice ? Number(latestPrice).toFixed(4) : "N/A"} for{" "} - {priceId ? utils.parseBytes32String(priceId) : "N/A"} sourced from{" "} - - - Coinbase Pro. - + Please first connect and select an EMP from the dropdown above. - - {activeSponsors && ( - - - - - Sponsor - Collateral ({collSymbol}) - - Synthetics ({tokenSymbol}) - - Collateral Ratio - - - - {Object.keys(activeSponsors).map((sponsor: string) => { - const activeSponsor = activeSponsors[sponsor]; - return ( - activeSponsor?.collateral && - activeSponsor?.tokensOutstanding && ( - - - - {prettyAddress(sponsor)} - - - - {prettyBalance(activeSponsor.collateral)} - - - {prettyBalance(activeSponsor.tokensOutstanding)} - - - {prettyBalance( - getCollateralRatio( - activeSponsor.collateral, - activeSponsor.tokensOutstanding - ) - )} - - - ) - ); - })} - -
-
- )} -
-
- ); + ); + } }; export default AllPositions; diff --git a/features/contract-state/GeneralInfo.tsx b/features/contract-state/GeneralInfo.tsx index 97520f1e..6aa12f26 100644 --- a/features/contract-state/GeneralInfo.tsx +++ b/features/contract-state/GeneralInfo.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { ethers } from "ethers"; +import { utils } from "ethers"; import { Typography, Box, Tooltip } from "@material-ui/core"; import EmpState from "../../containers/EmpState"; @@ -26,7 +26,8 @@ const Link = styled.a` font-size: 14px; `; -const fromWei = ethers.utils.formatUnits; +const fromWei = utils.formatUnits; +const parseBytes32String = utils.parseBytes32String; const GeneralInfo = () => { const { contract } = EmpContract.useContainer(); @@ -34,7 +35,6 @@ const GeneralInfo = () => { const { gcr } = Totals.useContainer(); const { latestPrice, sourceUrl } = PriceFeed.useContainer(); const { getEtherscanUrl } = Etherscan.useContainer(); - const { expirationTimestamp: expiry, priceIdentifier: priceId, @@ -43,102 +43,131 @@ const GeneralInfo = () => { withdrawalLiveness, } = empState; const { symbol: tokenSymbol } = Token.useContainer(); - - // format nice date - const expiryDate = expiry ? new Date(expiry.toNumber() * 1000) : "N/A"; - - const pricedGcr = gcr && latestPrice ? gcr / Number(latestPrice) : null; - - const withdrawalLivenessInMinutes = withdrawalLiveness - ? Number(withdrawalLiveness) / 60 - : null; - - return ( - - - General Info{" "} - {contract?.address && ( - + + {`General Info `} + {contract?.address && ( + + Etherscan + + )} + + + + + {expiryDate} + + + + + + {priceIdUtf8} + + + + + {`: ${prettyLatestPrice}`} + + + + + {collReqPct} + + + + + {minSponsorTokensSymbol} + + + + + - Etherscan - - )} - - - - {expiry ? ( - - {expiryDate.toString()} + {pricedGcr} - ) : ( - "N/A" - )} - - - - - {priceId ? ethers.utils.parseBytes32String(priceId) : "N/A"} - - - - - {latestPrice ? `${Number(latestPrice).toFixed(4)}` : "N/A"} - - - - - {collReq ? `${parseFloat(fromWei(collReq)) * 100}%` : "N/A"} - - - - - {minSponsorTokens - ? `${fromWei(minSponsorTokens)} ${tokenSymbol}` - : "N/A"} - - - - - - {pricedGcr ? pricedGcr.toFixed(4) : "N/A"} - - - - - - - - {withdrawalLiveness - ? withdrawalLivenessInMinutes?.toFixed(2) - : "N/A"} - - - - - ); + {withdrawalLivenessInMinutes} + + + + ); + } }; export default GeneralInfo; diff --git a/features/contract-state/Totals.tsx b/features/contract-state/Totals.tsx index d0261388..93663aa2 100644 --- a/features/contract-state/Totals.tsx +++ b/features/contract-state/Totals.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Box, Grid, Typography, Tooltip } from "@material-ui/core"; +import { Box, Grid, Typography } from "@material-ui/core"; import TotalsContainer from "../../containers/Totals"; import Collateral from "../../containers/Collateral"; @@ -49,28 +49,60 @@ const Totals = () => { } = Collateral.useContainer(); const { symbol: tokenSymbol, address: tokenAddress } = Token.useContainer(); const { getEtherscanUrl } = Etherscan.useContainer(); - const exchangeInfo = getExchangeInfo(tokenSymbol); + const defaultMissingDataDisplay = "N/A"; + + if ( + totalCollateral !== null && + totalTokens !== null && + collSymbol !== null && + tokenSymbol !== null && + exchangeInfo !== undefined && + collAddress !== null && + tokenAddress !== null + ) { + const prettyTotalCollateral = Number(totalCollateral).toLocaleString(); + const prettyTotalTokens = Number(totalTokens).toLocaleString(); + const prettyCollSymbol = collSymbol; + const prettyTokenSymbol = tokenSymbol; + const getExchangeLinkCollateral = exchangeInfo.getExchangeUrl(collAddress); + const getExchangeLinkToken = exchangeInfo.getExchangeUrl(tokenAddress); + const exchangeName = exchangeInfo.name; + + return renderComponent( + prettyTotalCollateral, + prettyTotalTokens, + prettyCollSymbol, + prettyTokenSymbol, + getExchangeLinkCollateral, + getExchangeLinkToken, + exchangeName + ); + } else { + return renderComponent(); + } - const loading = - !totalCollateral || !totalTokens || !collSymbol || !tokenSymbol; - return ( - <> - - - - - {loading ? "N/A" : Number(totalCollateral).toLocaleString()} - - - {collSymbol} - - - - - {collAddress && ( + function renderComponent( + prettyTotalCollateral: string = defaultMissingDataDisplay, + prettyTotalTokens: string = defaultMissingDataDisplay, + prettyCollSymbol: string = "", + prettyTokenSymbol: string = "", + getExchangeLinkCollateral: string = "https://app.uniswap.org/#/swap", + getExchangeLinkToken: string = "https://app.uniswap.org/#/swap", + exchangeName: string = "Uniswap" + ) { + return ( + <> + + + + {prettyTotalCollateral} + {prettyCollSymbol} + + + { > Etherscan - )} - {exchangeInfo !== undefined && collAddress && ( - {exchangeInfo.name} + {exchangeName} - )} - - - - - - - - - {loading ? "N/A" : Number(totalTokens).toLocaleString()} - - - {tokenSymbol} - - - + + + + + + + + {prettyTotalTokens} + {prettyTokenSymbol} + - - - {tokenAddress && ( + { > Etherscan - )} - {exchangeInfo !== undefined && tokenAddress && ( - {exchangeInfo.name} + {exchangeName} - )} - - - - - ); + + + + + ); + } }; export default Totals; diff --git a/features/core/Header.tsx b/features/core/Header.tsx index 80db04f4..38ca80a7 100644 --- a/features/core/Header.tsx +++ b/features/core/Header.tsx @@ -49,9 +49,7 @@ const Header = () => { return ( - - EMP Tools - + ⚒️⚡️EMP Tools {address && ( diff --git a/features/manage-position/Create.tsx b/features/manage-position/Create.tsx index c5d18bdd..d483c3e2 100644 --- a/features/manage-position/Create.tsx +++ b/features/manage-position/Create.tsx @@ -1,6 +1,13 @@ -import { ethers } from "ethers"; +import { ethers, utils } from "ethers"; import styled from "styled-components"; -import { Box, Button, TextField, Typography } from "@material-ui/core"; +import { + Box, + Button, + TextField, + Typography, + Grid, + Tooltip, +} from "@material-ui/core"; import EmpContract from "../../containers/EmpContract"; import { useState } from "react"; @@ -12,9 +19,8 @@ import Position from "../../containers/Position"; import PriceFeed from "../../containers/PriceFeed"; import Etherscan from "../../containers/Etherscan"; -const Container = styled(Box)` - max-width: 720px; -`; +import { getLiquidationPrice } from "../../utils/getLiquidationPrice"; +import { DOCS_MAP } from "../../utils/getDocLinks"; const Important = styled(Typography)` color: red; @@ -27,7 +33,11 @@ const Link = styled.a` font-size: 14px; `; -const fromWei = ethers.utils.formatUnits; +const { + formatUnits: fromWei, + parseBytes32String: hexToUtf8, + parseUnits: toWei, +} = utils; const Create = () => { const { contract: emp } = EmpContract.useContainer(); @@ -49,260 +59,326 @@ const Create = () => { const { latestPrice } = PriceFeed.useContainer(); const { getEtherscanUrl } = Etherscan.useContainer(); - const [collateral, setCollateral] = useState(""); - const [tokens, setTokens] = useState(""); + const [collateral, setCollateral] = useState("0"); + const [tokens, setTokens] = useState("0"); const [hash, setHash] = useState(null); const [success, setSuccess] = useState(null); const [error, setError] = useState(null); - const { collateralRequirement: collReq, minSponsorTokens } = empState; - const collReqPct = - collReq && collDec - ? `${parseFloat(fromWei(collReq, collDec)) * 100}%` - : "N/A"; - const balanceTooLow = (balance || 0) < (Number(collateral) || 0); + const { + collateralRequirement: collReq, + minSponsorTokens, + priceIdentifier, + } = empState; + const liquidationPriceWarningThreshold = 0.1; - const needAllowance = () => { - if (collAllowance === null || collateral === null) return true; - if (collAllowance === "Infinity") return false; - return collAllowance < parseFloat(collateral); - }; + if ( + collReq !== null && + collDec !== null && + balance !== null && + collAllowance !== null && + emp !== null && + posTokens !== null && + posCollateral !== null && + minSponsorTokens !== null && + tokenDec !== null && + latestPrice !== null && + gcr !== null && + pendingWithdraw !== null && + tokenSymbol !== null && + collSymbol !== null && + priceIdentifier !== null + ) { + const collReqFromWei = parseFloat(fromWei(collReq, collDec)); + const collateralToDeposit = Number(collateral) || 0; + const tokensToCreate = Number(tokens) || 0; + const minSponsorTokensFromWei = parseFloat( + fromWei(minSponsorTokens, tokenDec) + ); + const hasPendingWithdraw = pendingWithdraw === "Yes"; + const priceIdentifierUtf8 = hexToUtf8(priceIdentifier); + const prettyLatestPrice = Number(latestPrice).toFixed(4); - const mintTokens = async () => { - if (collateral && tokens && emp) { - setHash(null); - setSuccess(null); - setError(null); - const collateralWei = ethers.utils.parseUnits(collateral); - const tokensWei = ethers.utils.parseUnits(tokens); - try { - const tx = await emp.create([collateralWei], [tokensWei]); - setHash(tx.hash as string); - await tx.wait(); - setSuccess(true); - } catch (error) { - console.error(error); - setError(error); - } - } else { - setError(new Error("Please check that you are connected.")); - } - }; + // CR of new tokens to create. This must be > GCR according to https://github.com/UMAprotocol/protocol/blob/837869b97edef108fdf68038f54f540ca95cfb44/core/contracts/financial-templates/expiring-multiparty/PricelessPositionManager.sol#L409 + const transactionCR = + tokensToCreate > 0 ? collateralToDeposit / tokensToCreate : 0; + const pricedTransactionCR = + latestPrice !== 0 ? (transactionCR / latestPrice).toFixed(4) : "0"; + // Resultant CR of position if new tokens were created by depositing chosen amount of collateral. + // This is a useful data point for the user but has no effect on the contract's create transaction. + const resultantCollateral = posCollateral + collateralToDeposit; + const resultantTokens = posTokens + tokensToCreate; + const resultantCR = + resultantTokens > 0 ? resultantCollateral / resultantTokens : 0; + const pricedResultantCR = + latestPrice !== 0 ? (resultantCR / latestPrice).toFixed(4) : "0"; + const resultantLiquidationPrice = getLiquidationPrice( + resultantCollateral, + resultantTokens, + collReqFromWei + ).toFixed(4); + const liquidationPriceDangerouslyFarBelowCurrentPrice = + parseFloat(resultantLiquidationPrice) < + (1 - liquidationPriceWarningThreshold) * latestPrice; + // GCR: total contract collateral / total contract tokens. + const pricedGCR = latestPrice !== 0 ? (gcr / latestPrice).toFixed(4) : null; - const handleCreateClick = () => { - const firstPosition = posTokens !== null && posTokens.toString() === "0"; - if (!firstPosition) { - mintTokens(); - } else { - // first time minting, check min sponsor tokens - if (tokens !== null && minSponsorTokens && tokenDec) { - const insufficientMinting = - parseFloat(tokens) < parseFloat(fromWei(minSponsorTokens, tokenDec)); - if (insufficientMinting) { - alert( - `You must mint at least ${fromWei( - minSponsorTokens, - tokenDec - )} token(s).` - ); - } else { - mintTokens(); + // Error conditions for calling create: + const balanceBelowCollateralToDeposit = balance < collateralToDeposit; + const needAllowance = + collAllowance !== "Infinity" && collAllowance < collateralToDeposit; + const resultantTokensBelowMin = resultantTokens < minSponsorTokensFromWei; + const resultantCRBelowRequirement = + parseFloat(pricedResultantCR) >= 0 && + parseFloat(pricedResultantCR) < collReqFromWei; + const transactionCRBelowGCR = transactionCR < gcr; + + const mintTokens = async () => { + if (collateralToDeposit > 0 && tokensToCreate > 0) { + setHash(null); + setSuccess(null); + setError(null); + try { + const collateralWei = toWei(collateral); + const tokensWei = toWei(tokens); + const tx = await emp.create([collateralWei], [tokensWei]); + setHash(tx.hash as string); + await tx.wait(); + setSuccess(true); + } catch (error) { + console.error(error); + setError(error); } + } else { + setError(new Error("Collateral and Token amounts must be positive")); } - } - }; - - const computeCR = () => { - if ( - collateral === null || - tokens === null || - posCollateral === null || - posTokens === null || - latestPrice === null - ) - return null; - - // all values non-null, proceed to calculate - const totalCollateral = posCollateral + parseFloat(collateral); - const totalTokens = posTokens + parseFloat(tokens); - return totalCollateral / totalTokens; - }; - const computedCR = computeCR() || 0; - - const pricedCR = - computedCR && latestPrice ? computedCR / Number(latestPrice) : null; - const pricedGCR = gcr && latestPrice ? gcr / Number(latestPrice) : null; - - // User has not selected an EMP yet. We can detect this by checking if any properties in `empState` are `null`. - if (collReq === null) { - return ( - - - - Please first select an EMP from the dropdown above. - - - - ); - } + }; - if (pendingWithdraw === null || pendingWithdraw === "Yes") { - return ( - + if (hasPendingWithdraw) { + return ( You need to cancel or execute your pending withdrawal request - before creating additional tokens. + before submitting other position management transactions. - - ); - } - - // User has no pending withdrawal requests so they can create tokens. - return ( - - - - - Mint new synthetic tokens ({tokenSymbol}) via this EMP contract. - - - - - - - IMPORTANT! Please read this carefully or you may lose money. - - - - - When minting, your resulting collateralization ratio (collateral / - tokens) must be above the GCR and you need to mint at least{" "} - {minSponsorTokens && tokenDec - ? fromWei(minSponsorTokens, tokenDec) - : "N/A"}{" "} - token(s). - - - - - Ensure that you maintain {collReqPct} collateralization or else you - will get liquidated. Remember to sell your tokens after you mint - them if you want to short the underlying. - - - - - When you're ready, fill in the desired amount of collateral and - tokens below and click the "Create" button. - - - + ); + } else { + return ( + + + + + Mint new synthetic tokens ({tokenSymbol}) via this EMP contract. + + + + + + IMPORTANT! Please read this carefully or you may lose money. + - - ) => - setCollateral(e.target.value) - } - /> - - - ) => - setTokens(e.target.value) - } - /> - - - {needAllowance() && ( - - )} - {tokens && - collateral && - gcr && - !needAllowance() && - computedCR > gcr && - !balanceTooLow ? ( - - ) : ( - - )} - + + + {`When minting, the ratio of new collateral-deposited versus new tokens-created (e.g. the "Transaction CR" value below) + must be above the GCR (${pricedGCR}), and you need to mint at + least ${minSponsorTokensFromWei} ${tokenSymbol}. `} + {`Read more about the GCR `} + + here. + + + + + + Ensure that you keep your position's CR greater than the{" "} + collateral requirement of {collReqFromWei}, or + you will be liquidated. This is the "Resulting CR" value below. + Creating additional tokens can increase or decrease this ratio. + + + - - {tokens && collateral && gcr ? ( - - Resulting CR:{" "} - - {pricedCR?.toFixed(4)} - - - ) : ( - Resulting CR: N/A - )} - Current GCR: {pricedGCR?.toFixed(4) || "N/A"} - + + + ) => + setCollateral(e.target.value) + } + /> + + + ) => + setTokens(e.target.value) + } + /> + + + + {needAllowance && ( + + )} + + + + - {hash && ( - - - Tx Receipt: - {hash ? ( - + + {`Transaction CR: `} + - {hash} - - ) : ( - hash - )} - - - )} - {success && ( - - - Transaction successful! - - - )} - {error && ( - - - {error.message} - + + {pricedTransactionCR} + + + + + {`Resulting liquidation price: `} + 0 && + `This is >${ + liquidationPriceWarningThreshold * 100 + }% below the current price: ${prettyLatestPrice}` + } + > + 0 + ? "red" + : "unset", + }} + > + {resultantLiquidationPrice} ({priceIdentifierUtf8}) + + + + + {`Resulting CR: `} + + + {pricedResultantCR} + + + + {`GCR: ${pricedGCR}`} + + + {hash && ( + + + Tx Receipt: + {hash ? ( + + {hash} + + ) : ( + hash + )} + + + )} + {success && ( + + + Transaction successful! + + + )} + {error && ( + + + {error.message} + + + )} - )} - - ); + ); + } + } else { + return ( + + + Please first connect and select an EMP from the dropdown above. + + + ); + } }; export default Create; diff --git a/features/manage-position/Deposit.tsx b/features/manage-position/Deposit.tsx index adba89cb..2dc4cfe2 100644 --- a/features/manage-position/Deposit.tsx +++ b/features/manage-position/Deposit.tsx @@ -1,182 +1,227 @@ import { useState } from "react"; import styled from "styled-components"; -import { Box, Button, TextField, Typography } from "@material-ui/core"; -import { ethers } from "ethers"; +import { Box, Button, TextField, Typography, Grid } from "@material-ui/core"; +import { utils } from "ethers"; import EmpContract from "../../containers/EmpContract"; +import EmpState from "../../containers/EmpState"; import Collateral from "../../containers/Collateral"; import Position from "../../containers/Position"; import PriceFeed from "../../containers/PriceFeed"; import Etherscan from "../../containers/Etherscan"; -import { hashMessage } from "ethers/lib/utils"; -const Container = styled(Box)` - max-width: 720px; -`; +import { getLiquidationPrice } from "../../utils/getLiquidationPrice"; const Link = styled.a` color: white; font-size: 14px; `; +const { + formatUnits: fromWei, + parseBytes32String: hexToUtf8, + parseUnits: toWei, +} = utils; + const Deposit = () => { const { contract: emp } = EmpContract.useContainer(); - const { symbol: collSymbol, balance } = Collateral.useContainer(); - const { tokens, collateral, pendingWithdraw } = Position.useContainer(); + const { empState } = EmpState.useContainer(); + const { + symbol: collSymbol, + balance: collBalance, + decimals: collDec, + allowance: collAllowance, + setMaxAllowance, + } = Collateral.useContainer(); + const { + tokens: posTokens, + collateral: posColl, + pendingWithdraw, + } = Position.useContainer(); const { latestPrice } = PriceFeed.useContainer(); const { getEtherscanUrl } = Etherscan.useContainer(); + const { collateralRequirement: collReq, priceIdentifier } = empState; - const [collateralToDeposit, setCollateralToDeposit] = useState(""); + const [collateral, setCollateral] = useState("0"); const [hash, setHash] = useState(null); const [success, setSuccess] = useState(null); const [error, setError] = useState(null); - const balanceTooLow = (balance || 0) < (Number(collateralToDeposit) || 0); - - const depositCollateral = async () => { - if (collateralToDeposit && emp) { - setHash(null); - setSuccess(null); - setError(null); - const collateralToDepositWei = ethers.utils.parseUnits( - collateralToDeposit - ); - try { - const tx = await emp.deposit([collateralToDepositWei]); - setHash(tx.hash as string); - await tx.wait(); - setSuccess(true); - } catch (error) { - console.error(error); - setError(error); + if ( + posColl !== null && + posTokens !== null && + pendingWithdraw !== null && + collAllowance !== null && + collBalance !== null && + latestPrice !== null && + emp !== null && + collReq !== null && + collDec !== null && + priceIdentifier !== null && + posColl !== 0 // If position has no collateral, then don't render deposit component. + ) { + const collateralToDeposit = Number(collateral) || 0; + const priceIdentifierUtf8 = hexToUtf8(priceIdentifier); + const hasPendingWithdraw = pendingWithdraw === "Yes"; + const collReqFromWei = parseFloat(fromWei(collReq, collDec)); + const resultantCollateral = posColl + collateralToDeposit; + const resultantCR = posTokens > 0 ? resultantCollateral / posTokens : 0; + const pricedResultantCR = + latestPrice !== 0 ? (resultantCR / latestPrice).toFixed(4) : "0"; + const resultantLiquidationPrice = getLiquidationPrice( + resultantCollateral, + posTokens, + collReqFromWei + ).toFixed(4); + + // Error conditions for calling deposit: + const balanceBelowCollateralToDeposit = collBalance < collateralToDeposit; + const needAllowance = + collAllowance !== "Infinity" && collAllowance < collateralToDeposit; + + const depositCollateral = async () => { + if (collateralToDeposit > 0) { + setHash(null); + setSuccess(null); + setError(null); + try { + const collateralToDepositWei = toWei(collateral); + const tx = await emp.deposit([collateralToDepositWei]); + setHash(tx.hash as string); + await tx.wait(); + setSuccess(true); + } catch (error) { + console.error(error); + setError(error); + } + } else { + setError(new Error("Collateral amount must be positive.")); } - } else { - setError(new Error("Please check that you are connected.")); + }; + + if (hasPendingWithdraw) { + return ( + + + + + You need to cancel or execute your pending withdrawal request + before depositing additional collateral. + + + + + ); } - }; - const handleDepositClick = () => depositCollateral(); - - const startingCR = collateral && tokens ? collateral / tokens : null; - const pricedStartingCR = - startingCR && latestPrice ? startingCR / Number(latestPrice) : null; - - const resultingCR = - collateral && collateralToDeposit && tokens - ? (collateral + parseFloat(collateralToDeposit)) / tokens - : startingCR; - const pricedResultingCR = - resultingCR && latestPrice ? resultingCR / Number(latestPrice) : null; - - // User does not have a position yet. - if (collateral === null || collateral.toString() === "0") { return ( - - + + - Create a position before depositing more collateral. + Adding additional collateral into your position will increase your + collateralization ratio. - - ); - } - if (pendingWithdraw === null || pendingWithdraw === "Yes") { - return ( - - + + + + ) => + setCollateral(e.target.value) + } + /> + + + + + {needAllowance && ( + + )} + + + + + + + {`Resulting CR: ${pricedResultantCR}`} - - You need to cancel or execute your pending withdrawal request - before depositing additional collateral. - + {`Resulting liquidation price: ${resultantLiquidationPrice} (${priceIdentifierUtf8}`} - - ); - } - - // User has a position and no pending withdrawal requests so can deposit more collateral. - return ( - - - - By depositing additional collateral into your position you will - increase your collateralization ratio. - - - - - ) => - setCollateralToDeposit(e.target.value) - } - /> - - - {collateralToDeposit && collateralToDeposit != "0" && !balanceTooLow ? ( - - ) : ( - + {hash && ( + + + Tx Hash: + {hash ? ( + + {hash} + + ) : ( + hash + )} + + + )} + {success && ( + + + Transaction successful! + + + )} + {error && ( + + + {error.message} + + )} - - - - Current CR: {pricedStartingCR?.toFixed(4) || "N/A"} - - - Resulting CR: {pricedResultingCR?.toFixed(4) || "N/A"} - - - - {hash && ( - - - Tx Hash: - {hash ? ( - - {hash} - - ) : ( - hash - )} - - - )} - {success && ( - - - Transaction successful! - - - )} - {error && ( + ); + } else { + return ( + - {error.message} + Create a position before depositing more collateral. - )} - - ); + + ); + } }; export default Deposit; diff --git a/features/manage-position/ManagePosition.tsx b/features/manage-position/ManagePosition.tsx index b3a19cf6..98c219f6 100644 --- a/features/manage-position/ManagePosition.tsx +++ b/features/manage-position/ManagePosition.tsx @@ -10,11 +10,7 @@ import Withdraw from "./Withdraw"; import YourPosition from "./YourPosition"; import YourWallet from "./YourWallet"; -export type Method = "create" | "deposit" | "withdraw" | "redeem" | "transfer"; - -const FalseDoor = () => ( - This feature has not been implemented yet. -); +export type Method = "create" | "deposit" | "withdraw" | "redeem"; const Manager = () => { const { signer } = Connection.useContainer(); @@ -22,11 +18,11 @@ const Manager = () => { const handleChange = (e: React.ChangeEvent<{ value: unknown }>) => setMethod(e.target.value as Method); - if (!signer) { + if (signer === null) { return ( - Please connect first. + Please first connect and select an EMP from the dropdown above. ); diff --git a/features/manage-position/MethodSelector.tsx b/features/manage-position/MethodSelector.tsx index edd24113..b5367f3e 100644 --- a/features/manage-position/MethodSelector.tsx +++ b/features/manage-position/MethodSelector.tsx @@ -74,12 +74,6 @@ const MethodSelector = ({ method, handleChange }: IProps) => { secondary="Redeem synthetic tokens." /> - - - diff --git a/features/manage-position/Redeem.tsx b/features/manage-position/Redeem.tsx index a2d022ee..5bedf11c 100644 --- a/features/manage-position/Redeem.tsx +++ b/features/manage-position/Redeem.tsx @@ -1,7 +1,15 @@ import { useState } from "react"; import styled from "styled-components"; -import { Box, Button, TextField, Typography } from "@material-ui/core"; -import { ethers, BigNumberish } from "ethers"; +import { + Box, + Button, + TextField, + Typography, + InputAdornment, + Grid, + Tooltip, +} from "@material-ui/core"; +import { utils } from "ethers"; import EmpContract from "../../containers/EmpContract"; import EmpState from "../../containers/EmpState"; @@ -10,221 +18,265 @@ import Position from "../../containers/Position"; import Token from "../../containers/Token"; import Etherscan from "../../containers/Etherscan"; -const Container = styled(Box)` - max-width: 720px; -`; - const Link = styled.a` color: white; font-size: 14px; `; -const fromWei = ethers.utils.formatUnits; -const weiToNum = (x: BigNumberish, u = 18) => parseFloat(fromWei(x, u)); +const MaxLink = styled.div` + text-decoration-line: underline; +`; + +const { + formatUnits: fromWei, + parseBytes32String: hexToUtf8, + parseUnits: toWei, +} = utils; const Redeem = () => { const { contract: emp } = EmpContract.useContainer(); const { empState } = EmpState.useContainer(); + const { minSponsorTokens } = empState; const { symbol: collSymbol } = Collateral.useContainer(); const { - tokens: borrowedTokens, - collateral, + tokens: posTokens, + collateral: posColl, pendingWithdraw, } = Position.useContainer(); const { - symbol: syntheticSymbol, - allowance: syntheticAllowance, + symbol: tokenSymbol, + allowance: tokenAllowance, + decimals: tokenDec, setMaxAllowance, - balance: syntheticBalance, + balance: tokenBalance, } = Token.useContainer(); const { getEtherscanUrl } = Etherscan.useContainer(); - const [tokensToRedeem, setTokensToRedeem] = useState(""); + const [tokens, setTokens] = useState("0"); const [hash, setHash] = useState(null); const [success, setSuccess] = useState(null); const [error, setError] = useState(null); - const tokensToRedeemFloat = isNaN(parseFloat(tokensToRedeem)) - ? 0 - : parseFloat(tokensToRedeem); - const tokensAboveMin = - borrowedTokens && empState.minSponsorTokens - ? borrowedTokens - weiToNum(empState.minSponsorTokens) - : 0; - const unwindPosition = - borrowedTokens && - tokensToRedeemFloat && - syntheticBalance && - borrowedTokens === tokensToRedeemFloat && - borrowedTokens <= syntheticBalance; - const maxRedeem = Math.min( - syntheticBalance || 0, - borrowedTokens || 0, - tokensAboveMin || 0 - ); - const isEmpty = tokensToRedeem === ""; - const canSendTxn = - !isNaN(parseFloat(tokensToRedeem)) && - tokensToRedeemFloat >= 0 && - (tokensToRedeemFloat <= maxRedeem || unwindPosition); - - const needAllowance = () => { - if (syntheticAllowance === null || tokensToRedeem === null) return true; - if (syntheticAllowance === "Infinity") return false; - return syntheticAllowance < tokensToRedeemFloat; - }; - - const redeemTokens = async () => { - if (tokensToRedeem && emp) { - setHash(null); - setSuccess(null); - setError(null); - const tokensToRedeemWei = ethers.utils.parseUnits(tokensToRedeem); - try { - const tx = await emp.redeem([tokensToRedeemWei]); - setHash(tx.hash as string); - await tx.wait(); - setSuccess(true); - } catch (error) { - console.error(error); - setError(error); - } - } else { - setError(new Error("Please check that you are connected.")); - } - }; - - const handleRedemptionClick = () => redeemTokens(); - - // User does not have a position yet. if ( - borrowedTokens === null || - borrowedTokens.toString() === "0" || - collateral === null || - collateral.toString() === "0" + posTokens !== null && + posColl !== null && + minSponsorTokens !== null && + tokenBalance !== null && + tokenDec !== null && + tokenSymbol !== null && + tokenAllowance !== null && + emp !== null && + pendingWithdraw !== null && + posColl !== 0 // If position has no collateral, then don't render redeem component. ) { - return ( - - - - You need to borrow tokens before redeeming. - - - + const hasPendingWithdraw = pendingWithdraw === "Yes"; + const tokensToRedeem = + (Number(tokens) || 0) > posTokens ? posTokens : Number(tokens) || 0; + const minSponsorTokensFromWei = parseFloat( + fromWei(minSponsorTokens, tokenDec) ); - } - if (pendingWithdraw === null || pendingWithdraw === "Yes") { - return ( - - - - - You need to cancel or execute your pending withdrawal request - before redeeming tokens. - - - - - ); - } + // If not redeeming full position, then cannot bring position below the minimum sponsor token threshold. + // Amount of collateral received is proportional to percentage of outstanding tokens in position retired. + const maxPartialRedeem = + posTokens > minSponsorTokensFromWei + ? posTokens - minSponsorTokensFromWei + : 0; + const proportionTokensRedeemed = + posTokens > 0 ? tokensToRedeem / posTokens : 0; + const proportionCollateralReceived = + proportionTokensRedeemed <= 1 + ? proportionTokensRedeemed * posColl + : posColl; + const resultantTokens = + posTokens >= tokensToRedeem ? posTokens - tokensToRedeem : 0; + const resultantCollateral = posColl - proportionCollateralReceived; - // User has a position and no withdraw requests, so they can redeem tokens. - return ( - - - - By redeeming your synthetic tokens, you will pay back a portion of - your debt and receive a proportional part of your collateral. Note: - this will not change the collateralization ratio of your position. - - + // Error conditions for calling redeem: (Some of these might be redundant) + const balanceBelowTokensToRedeem = tokenBalance < tokensToRedeem; + const invalidRedeemAmount = + tokensToRedeem < posTokens && tokensToRedeem > maxPartialRedeem; + const needAllowance = + tokenAllowance !== "Infinity" && tokenAllowance < tokensToRedeem; - - ) => - setTokensToRedeem(e.target.value) - } - /> - + const redeemTokens = async () => { + if (tokensToRedeem > 0) { + setHash(null); + setSuccess(null); + setError(null); + try { + const tokensToRedeemWei = toWei(tokensToRedeem.toString()); + const tx = await emp.redeem([tokensToRedeemWei]); + setHash(tx.hash as string); + await tx.wait(); + setSuccess(true); + } catch (error) { + console.error(error); + setError(error); + } + } else { + setError(new Error("Token amounts must be positive")); + } + }; - - {needAllowance() && ( - - )} - {canSendTxn && !needAllowance() ? ( - - ) : ( - - )} - + const setTokensToRedeemToMax = () => { + setTokens(tokenBalance.toString()); + }; - - {`Current borrowed ${syntheticSymbol}: ${borrowedTokens}`} - {`Remaining debt after redemption: ${ - borrowedTokens - tokensToRedeemFloat - }`} - {`Collateral you will receive on redemption: ${ - (tokensToRedeemFloat / borrowedTokens) * collateral - } ${collSymbol}`} - + if (hasPendingWithdraw) { + return ( + + + + + You need to cancel or execute your pending withdrawal request + before redeeming tokens. + + + + + ); + } - {hash && ( - + return ( + + - Tx Hash: - {hash ? ( - - {hash} - - ) : ( - hash - )} + By redeeming your synthetic tokens, you will pay back a portion of + your debt and receive a proportional part of your collateral. +

+

+ Note: this will not change the collateralization + ratio of your position or its liquidation price. +
+

+ + {`When redeeming, you must keep at least ${minSponsorTokensFromWei} ${tokenSymbol} in your position. Currently, you can either redeem exactly ${posTokens} or no more than ${maxPartialRedeem} ${tokenSymbol}`}
- )} - {success && ( - + + + + ) => + setTokens(e.target.value) + } + InputProps={{ + endAdornment: ( + + + + ), + }} + /> + + + + {needAllowance && ( + + )} + + + + + + + {`${collSymbol} you will receive: ${proportionCollateralReceived}`} - Transaction successful! + + + {`Remaining ${tokenSymbol} in your position after redemption: ${resultantTokens}`} + + + {`Remaining ${collSymbol} in your position after redemption: ${resultantCollateral}`} - )} - {error && ( + + {hash && ( + + + Tx Hash: + {hash ? ( + + {hash} + + ) : ( + hash + )} + + + )} + {success && ( + + + Transaction successful! + + + )} + {error && ( + + + {error.message} + + + )} + + ); + } else { + return ( + - {error.message} + You need to borrow tokens before redeeming. - )} -
- ); + + ); + } }; export default Redeem; diff --git a/features/manage-position/Withdraw.tsx b/features/manage-position/Withdraw.tsx index edfef90c..6a293927 100644 --- a/features/manage-position/Withdraw.tsx +++ b/features/manage-position/Withdraw.tsx @@ -1,7 +1,14 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import styled from "styled-components"; -import { Box, Button, TextField, Typography } from "@material-ui/core"; -import { ethers } from "ethers"; +import { + Box, + Button, + TextField, + Typography, + Grid, + Tooltip, +} from "@material-ui/core"; +import { utils } from "ethers"; import EmpState from "../../containers/EmpState"; import EmpContract from "../../containers/EmpContract"; @@ -11,9 +18,8 @@ import Totals from "../../containers/Totals"; import PriceFeed from "../../containers/PriceFeed"; import Etherscan from "../../containers/Etherscan"; -const Container = styled(Box)` - max-width: 720px; -`; +import { getLiquidationPrice } from "../../utils/getLiquidationPrice"; +import { DOCS_MAP } from "../../utils/getDocLinks"; const Important = styled(Typography)` color: red; @@ -26,15 +32,26 @@ const Link = styled.a` font-size: 14px; `; +const { + formatUnits: fromWei, + parseBytes32String: hexToUtf8, + parseUnits: toWei, +} = utils; + const Deposit = () => { const { empState } = EmpState.useContainer(); - const { withdrawalLiveness } = empState; + const { + collateralRequirement: collReq, + withdrawalLiveness, + currentTime, + priceIdentifier, + } = empState; const { contract: emp } = EmpContract.useContainer(); - const { symbol: collSymbol } = Collateral.useContainer(); + const { symbol: collSymbol, decimals: collDec } = Collateral.useContainer(); const { - tokens, - collateral, + tokens: posTokens, + collateral: posColl, withdrawAmt, withdrawPassTime, pendingWithdraw, @@ -42,327 +59,385 @@ const Deposit = () => { const { gcr } = Totals.useContainer(); const { latestPrice } = PriceFeed.useContainer(); const { getEtherscanUrl } = Etherscan.useContainer(); + const liquidationPriceWarningThreshold = 0.1; - const [collateralToWithdraw, setCollateralToWithdraw] = useState(""); + const [collateral, setCollateral] = useState("0"); const [hash, setHash] = useState(null); const [success, setSuccess] = useState(null); const [error, setError] = useState(null); - const withdrawCollateral = async () => { - if (collateralToWithdraw && emp) { - setHash(null); - setSuccess(null); - setError(null); - const collateralToWithdrawWei = ethers.utils.parseUnits( - collateralToWithdraw - ); - try { - if (resultingCRBelowGCR) { - const tx = await emp.requestWithdrawal([collateralToWithdrawWei]); - setHash(tx.hash as string); - await tx.wait(); - } else if (!resultingCRBelowGCR) { - const tx = await emp.withdraw([collateralToWithdrawWei]); - setHash(tx.hash as string); - await tx.wait(); - } - - setSuccess(true); - } catch (error) { - console.error(error); - setError(error); - } - } else { - setError(new Error("Please check that you are connected.")); - } - }; - const executeWithdraw = async () => { - if (pendingWithdrawTimeRemaining && emp) { - setHash(null); - setSuccess(null); - setError(null); + if ( + collateral !== null && + emp !== null && + collDec !== null && + collReq !== null && + posColl !== null && + posTokens !== null && + gcr !== null && + withdrawPassTime !== null && + withdrawalLiveness !== null && + withdrawAmt !== null && + pendingWithdraw !== null && + currentTime !== null && + latestPrice !== null && + priceIdentifier !== null && + posColl !== 0 // If position has no collateral, then don't render withdraw component. + ) { + const collateralToWithdraw = Number(collateral) || 0; + const collReqFromWei = parseFloat(fromWei(collReq, collDec)); + const priceIdentifierUtf8 = hexToUtf8(priceIdentifier); + const prettyLatestPrice = Number(latestPrice).toFixed(4); - try { - const tx = await emp.withdrawPassedRequest(); - setHash(tx.hash as string); - await tx.wait(); + // CR data: + const resultantCollateral = posColl - collateralToWithdraw; + const resultantCR = posTokens > 0 ? resultantCollateral / posTokens : 0; + const resultantCRBelowGCR = resultantCR < gcr; + const pricedResultantCR = + latestPrice !== 0 ? (resultantCR / latestPrice).toFixed(4) : "0"; + const pricedGCR = latestPrice !== 0 ? (gcr / latestPrice).toFixed(4) : null; + const resultantLiquidationPrice = getLiquidationPrice( + resultantCollateral, + posTokens, + collReqFromWei + ).toFixed(4); + const liquidationPriceDangerouslyFarBelowCurrentPrice = + parseFloat(resultantLiquidationPrice) < + (1 - liquidationPriceWarningThreshold) * latestPrice; - setSuccess(true); - } catch (error) { - console.error(error); - setError(error); - } - } else { - setError(new Error("Please check that you are connected.")); - } - }; + // Fast withdrawal amount: can withdraw instantly as long as CR > GCR + const fastWithdrawableCollateral = + posColl > gcr * posTokens ? (posColl - gcr * posTokens).toFixed(4) : "0"; - const cancelWithdraw = async () => { - if (pendingWithdrawTimeRemaining && emp) { - setHash(null); - setSuccess(null); - setError(null); + // Pending withdrawal request information: + const withdrawLivenessString = Math.floor( + Number(withdrawalLiveness) / (60 * 60) + ); + const hasPendingWithdraw = pendingWithdraw === "Yes"; + const pendingWithdrawTimeRemaining = withdrawPassTime - Number(currentTime); + const canExecutePendingWithdraw = + hasPendingWithdraw && pendingWithdrawTimeRemaining <= 0; + const pendingWithdrawTimeString = + pendingWithdrawTimeRemaining > 0 + ? Math.max(0, Math.floor(pendingWithdrawTimeRemaining / 3600)) + + ":" + + Math.max(0, Math.floor((pendingWithdrawTimeRemaining % 3600) / 60)) + + ":" + + Math.max(0, (pendingWithdrawTimeRemaining % 3600) % 60) + : "None"; - try { - const tx = await emp.cancelWithdrawal(); - setHash(tx.hash as string); - await tx.wait(); + // Error conditions for calling withdraw: + const resultantCRBelowRequirement = + parseFloat(pricedResultantCR) >= 0 && + parseFloat(pricedResultantCR) < collReqFromWei; + const withdrawAboveBalance = collateralToWithdraw > posColl; - setSuccess(true); - } catch (error) { - console.error(error); - setError(error); + const withdrawCollateral = async () => { + if (collateralToWithdraw > 0) { + setHash(null); + setSuccess(null); + setError(null); + try { + const collateralToWithdrawWei = toWei(collateral); + if (resultantCRBelowGCR) { + const tx = await emp.requestWithdrawal([collateralToWithdrawWei]); + setHash(tx.hash as string); + await tx.wait(); + } else { + const tx = await emp.withdraw([collateralToWithdrawWei]); + setHash(tx.hash as string); + await tx.wait(); + } + setSuccess(true); + } catch (error) { + console.error(error); + setError(error); + } + } else { + setError(new Error("Collateral amount must be positive.")); } - } else { - setError(new Error("Please check that you are connected.")); - } - }; + }; - const handleWithdrawClick = () => withdrawCollateral(); - const handleExecuteWithdrawClick = () => executeWithdraw(); - const handleCancelWithdrawClick = () => cancelWithdraw(); + const executeWithdraw = async () => { + if (canExecutePendingWithdraw) { + setHash(null); + setSuccess(null); + setError(null); + try { + const tx = await emp.withdrawPassedRequest(); + setHash(tx.hash as string); + await tx.wait(); + setSuccess(true); + } catch (error) { + console.error(error); + setError(error); + } + } else { + setError( + new Error( + "Cannot execute pending withdraw until liveness period has passed." + ) + ); + } + }; - // Calculations using raw collateral ratios: - const startingCR = collateral && tokens ? collateral / tokens : null; - const resultingCR = - collateral && collateralToWithdraw && tokens - ? (collateral - parseFloat(collateralToWithdraw)) / tokens - : startingCR; - const resultingCRBelowGCR = resultingCR && gcr ? resultingCR < gcr : null; - const fastWithdrawableCollateral = - collateral && tokens && gcr ? (collateral - gcr * tokens).toFixed(2) : null; + const cancelWithdraw = async () => { + if (hasPendingWithdraw) { + setHash(null); + setSuccess(null); + setError(null); + try { + const tx = await emp.cancelWithdrawal(); + setHash(tx.hash as string); + await tx.wait(); + setSuccess(true); + } catch (error) { + console.error(error); + setError(error); + } + } else { + setError(new Error("No pending withdraw to cancel.")); + } + }; - // Calculations of collateral ratios using same units as price feed: - const pricedStartingCR = - startingCR && latestPrice ? startingCR / Number(latestPrice) : null; - const pricedResultingCR = - resultingCR && latestPrice ? resultingCR / Number(latestPrice) : null; - const pricedGcr = gcr && latestPrice ? gcr / Number(latestPrice) : null; + if (hasPendingWithdraw) { + return ( + + + + You have a pending withdraw on your position! + + - // Pending withdrawal request information: - const pendingWithdrawTimeRemaining = - collateral && withdrawPassTime && pendingWithdraw === "Yes" - ? withdrawPassTime - Math.floor(Date.now() / 1000) - 7200 - : null; - const pastWithdrawTimeStamp = pendingWithdrawTimeRemaining - ? pendingWithdrawTimeRemaining <= 0 - : null; - const pendingWithdrawTimeString = pendingWithdrawTimeRemaining - ? Math.max(0, Math.floor(pendingWithdrawTimeRemaining / 3600)) + - ":" + - Math.max(0, Math.floor((pendingWithdrawTimeRemaining % 3600) / 60)) + - ":" + - Math.max(0, (pendingWithdrawTimeRemaining % 3600) % 60) - : null; + + + + Once the liveness period has passed you can execute your + withdrawal request. You can cancel the withdraw request at any + time before you execute it. + + + + + Time left until withdrawal: + {pendingWithdrawTimeString} +

+ Requested withdrawal amount: {" "} + {`${withdrawAmt} ${collSymbol}`} +
+
- // User does not have a position yet. - if (collateral === null || collateral.toString() === "0") { - return ( - - - - Create a position before withdrawing collateral. - + + + + + + + + +
- - ); - } + ); + } - // User has a position and a pending withdrawal. - if (collateral !== null && pendingWithdraw === "Yes") { return ( - - + + - You have a pending withdraw on your position! + + By withdrawing collateral from your position you will decrease + your collateralization ratio. + + + IMPORTANT! Please read this carefully or you may lose money. + + + - Once the liveness period has passed you can execute your - withdrawal request. You can also cancel the withdraw request if - you changed your mind. + There are two kinds of withdrawals you can preform: +
    +
  • + "Fast" withdraw: Instantly withdraw collateral + until your collateralization ratio (CR) equals the global + collateralization ratio (GCR). Currently, you can instantly + withdraw {fastWithdrawableCollateral} {collSymbol}. +
  • +
  • + "Slow" withdraw: To withdraw your CR below the + GCR, you will need to wait a liveness period before completing + your withdrawal. For this EMP this is {withdrawLivenessString}{" "} + hours. When performing this kind of withdrawal you should ensure + that your position is sufficiently collateralized above the CR + requirement after the withdrawal or you will risk liquidation. +
  • +
- + - Time left until withdrawal: - {pendingWithdrawTimeString} -

- Requested withdrawal amount: {withdrawAmt}{" "} - {collSymbol} + For more info on the different kinds of withdrawals see the{" "} + + UMA docs + + .
+
- - {pastWithdrawTimeStamp ? ( + + + ) => + setCollateral(e.target.value) + } + /> + + + - ) : ( - - )} - - - -
- ); - } - // User has a position and no pending withdrawal. - return ( - - - - - By withdrawing excess collateral from your position you will - decrease your collateralization ratio. - - - +
+ + - - - - IMPORTANT! Please read this carefully or you may lose money. - - - + - There are two kinds of withdrawals you can preform: + {`Resulting liquidation price: `} + 0 && + `This is >${ + liquidationPriceWarningThreshold * 100 + }% below the current price: ${prettyLatestPrice}` + } + > + 0 + ? "red" + : "unset", + }} + > + {resultantLiquidationPrice} ({priceIdentifierUtf8}) + + -
    -
  • - "Fast" withdrawal: Instantly withdraw collateral - until your positions collateralization ratio is equal to the - global collateralization ratio. For your position you can - instantly withdraw {fastWithdrawableCollateral} {collSymbol}. -
  • -
  • - "Slow" withdrawal: To withdraw past the global - collateralization ratio, you will need to wait a liveness period - before completing your withdrawal. For this EMP this is{" "} - {withdrawalLiveness && - Math.floor(withdrawalLiveness.toNumber() / (60 * 60))}{" "} - hours. When preforming this kind of withdrawal one must ensure - that their position is sufficiently collateralized after the - withdrawal or you risk being liquidated. -
  • -
-
- - For more info on the different kinds of withdrawals see the{" "} - - UMA docs - - . + + {pricedResultantCR} + + + {`GCR: ${pricedGCR}`} -
- - - ) => - setCollateralToWithdraw(e.target.value) - } - /> - - - {collateralToWithdraw && collateralToWithdraw != "0" ? ( - - ) : ( - + {hash && ( + + + Tx Hash: + {hash ? ( + + {hash} + + ) : ( + hash + )} + + )} - - - - - Current global CR: {pricedGcr?.toFixed(4) || "N/A"} - - - Current position CR: {pricedStartingCR?.toFixed(4) || "N/A"} - - - Resulting position CR: {pricedResultingCR?.toFixed(4) || "N/A"} - - {collateralToWithdraw && collateralToWithdraw != "0" ? ( - resultingCRBelowGCR ? ( - - Withdrawal places CR below GCR. Will use slow withdrawal. Ensure - that your final CR is above the EMP CR requirement or you could - risk liquidation. + {success && ( + + + Transaction successful! - ) : ( - - Withdrawal places CR above GCR. Will use fast withdrawal. + + )} + {error && ( + + + {error.message} - ) - ) : ( - "" + )} - - {hash && ( - - - Tx Hash: - {hash ? ( - - {hash} - - ) : ( - hash - )} - - - )} - {success && ( - - - Transaction successful! - - - )} - {error && ( + ); + } else { + return ( + - {error.message} + Create a position before depositing more collateral. - )} - - ); + + ); + } }; export default Deposit; diff --git a/features/manage-position/YourPosition.tsx b/features/manage-position/YourPosition.tsx index de1313a5..03057df9 100644 --- a/features/manage-position/YourPosition.tsx +++ b/features/manage-position/YourPosition.tsx @@ -1,11 +1,17 @@ import styled from "styled-components"; import { Typography } from "@material-ui/core"; +import { utils } from "ethers"; import Position from "../../containers/Position"; import Collateral from "../../containers/Collateral"; import Token from "../../containers/Token"; import Totals from "../../containers/Totals"; import PriceFeed from "../../containers/PriceFeed"; +import EmpState from "../../containers/EmpState"; + +import { getLiquidationPrice } from "../../utils/getLiquidationPrice"; + +const { formatUnits: fromWei, parseBytes32String: hexToUtf8 } = utils; const Label = styled.span` color: #999999; @@ -32,80 +38,129 @@ const YourPosition = () => { const { tokens, collateral, + cRatio, withdrawAmt, pendingWithdraw, pendingTransfer, } = Position.useContainer(); - const { symbol: collSymbol } = Collateral.useContainer(); + const { empState } = EmpState.useContainer(); + const { symbol: collSymbol, decimals: collDec } = Collateral.useContainer(); const { symbol: tokenSymbol } = Token.useContainer(); const { latestPrice, sourceUrl } = PriceFeed.useContainer(); + const { collateralRequirement: collReq, priceIdentifier } = empState; + const defaultMissingDataDisplay = "N/A"; - const ready = + if ( tokens !== null && collateral !== null && + cRatio !== null && + gcr !== null && collSymbol !== null && + collDec !== null && tokenSymbol !== null && latestPrice !== null && - sourceUrl; + collReq !== null && + sourceUrl !== undefined && + priceIdentifier !== null && + withdrawAmt !== null && + pendingWithdraw !== null && + pendingTransfer !== null + ) { + const pricedCR = + latestPrice !== 0 ? (cRatio / latestPrice).toFixed(4) : "0"; + const pricedGCR = latestPrice !== 0 ? (gcr / latestPrice).toFixed(4) : "0"; + const collReqFromWei = parseFloat(fromWei(collReq, collDec)); + const liquidationPrice = getLiquidationPrice( + collateral, + tokens, + collReqFromWei + ).toFixed(4); + const priceIdUtf8 = hexToUtf8(priceIdentifier); - const collateralizationRatio = - collateral !== null && tokens !== null ? collateral / tokens : null; - const pricedCollateralizationRatio = - collateralizationRatio !== null && latestPrice !== null - ? collateralizationRatio / Number(latestPrice) - : null; - const pricedGcr = - gcr !== null && latestPrice !== null ? gcr / Number(latestPrice) : null; + return renderComponent( + pricedCR, + pricedGCR, + collReqFromWei.toFixed(4), + liquidationPrice, + collateral.toFixed(4), + collSymbol, + tokens.toFixed(4), + tokenSymbol, + Number(latestPrice).toFixed(6), + priceIdUtf8, + withdrawAmt.toFixed(4), + pendingWithdraw, + pendingTransfer, + sourceUrl + ); + } else { + return renderComponent(); + } - return ( - - Your Position - - - {ready ? `${collateral} ${collSymbol}` : "N/A"} - - - - {ready ? `${tokens} ${tokenSymbol}` : "N/A"} - - - - {ready ? `${Number(latestPrice).toFixed(4)}` : "N/A"} - - - - {pricedCollateralizationRatio - ? `${pricedCollateralizationRatio?.toFixed( - 4 - )} (${collSymbol} / ${tokenSymbol})` - : "N/A"} - - - - {pricedGcr - ? `${pricedGcr?.toFixed(4)} (${collSymbol} / ${tokenSymbol})` - : "N/A"} - - - - {ready ? `${withdrawAmt} ${collSymbol}` : "N/A"} - - - - {ready ? `${pendingWithdraw}` : "N/A"} - - - - {ready ? pendingTransfer : "N/A"} - - - ); + function renderComponent( + pricedCR: string = defaultMissingDataDisplay, + pricedGCR: string = defaultMissingDataDisplay, + collReqFromWei: string = defaultMissingDataDisplay, + liquidationPrice: string = defaultMissingDataDisplay, + _collateral: string = "0", + _collSymbol: string = "", + _tokens: string = "0", + _tokenSymbol: string = "", + _latestPrice: string = "", + priceIdUtf8: string = defaultMissingDataDisplay, + _withdrawAmt: string = "0", + _pendingWithdraw: string = defaultMissingDataDisplay, + _pendingTransfer: string = defaultMissingDataDisplay, + pricefeedUrl: string = "https://api.pro.coinbase.com/products/" + ) { + return ( + + Your Position + + + {`${_collateral} ${_collSymbol}`} + + + + {`${_tokens} ${_tokenSymbol}`} + + + + {_latestPrice} + + + + {` ${pricedCR} (${_collSymbol} / ${_tokenSymbol})`} + + + + {` ${pricedGCR} (${_collSymbol} / ${_tokenSymbol})`} + + + + {` ${collReqFromWei}`} + + + + {` ${liquidationPrice} (${priceIdUtf8})`} + + + + {` ${_withdrawAmt} ${_collSymbol}`} + + + + {` ${_pendingWithdraw}`} + + + ); + } }; export default YourPosition; diff --git a/features/manage-position/YourWallet.tsx b/features/manage-position/YourWallet.tsx index 547631b4..a5e7e8e1 100644 --- a/features/manage-position/YourWallet.tsx +++ b/features/manage-position/YourWallet.tsx @@ -3,7 +3,6 @@ import { Typography } from "@material-ui/core"; import Collateral from "../../containers/Collateral"; import Token from "../../containers/Token"; -import PriceFeed from "../../containers/PriceFeed"; const Label = styled.span` color: #999999; @@ -26,28 +25,43 @@ const YourWallet = () => { balance: collBalance, } = Collateral.useContainer(); const { symbol: tokenSymbol, balance: tokenBalance } = Token.useContainer(); - const { latestPrice, sourceUrl } = PriceFeed.useContainer(); - const ready = + if ( tokenBalance !== null && collBalance !== null && collSymbol !== null && - tokenSymbol !== null && - latestPrice !== null; - - return ( - - Your Wallet - - - {ready ? `${collBalance} ${collSymbol}` : "N/A"} - - - - {ready ? `${tokenBalance} ${tokenSymbol}` : "N/A"} - - - ); + tokenSymbol !== null + ) { + return renderComponent( + tokenBalance.toFixed(4), + collBalance.toFixed(4), + collSymbol, + tokenSymbol + ); + } else { + return renderComponent(); + } + + function renderComponent( + _tokenBalance: string = "0", + _collBalance: string = "0", + _collSymbol: string = "", + _tokenSymbol: string = "" + ) { + return ( + + Your Wallet + + + {`${_collBalance} ${_collSymbol}`} + + + + {`${_tokenBalance} ${_tokenSymbol}`} + + + ); + } }; export default YourWallet; diff --git a/features/weth/Weth.tsx b/features/weth/Weth.tsx index a7cc8176..568583d4 100644 --- a/features/weth/Weth.tsx +++ b/features/weth/Weth.tsx @@ -4,6 +4,7 @@ import { Button, InputAdornment, Typography, + Grid, } from "@material-ui/core"; import { FormEvent, useState } from "react"; import { BigNumberish, utils } from "ethers"; @@ -59,12 +60,13 @@ const BalanceElement = styled.div` flex-direction: row; justify-content: space-between; margin-top: 20px; + font-size: 16px; `; const InputElement = styled.div` + width: 100%; display: flex; flex-direction: row; - justify-content: space-between; margin-top: 20px; `; @@ -142,99 +144,126 @@ const Weth = () => { return ( - Please connect first. + Please first connect and select an EMP from the dropdown above. ); } return ( - - - - My Wallet - - - - ETH - - {ethBalance} - - - - - WETH - - {wethBalance} - - - - - - -
handleClick(e, ACTION_TYPE.WRAP)} - > - setEthAmount(e.target.value)} - variant="outlined" - helperText="Keep some ETH unwrapped for transaction fees" - InputLabelProps={{ - shrink: true, - }} - InputProps={{ - endAdornment: ( - - - - ), - }} - /> - -
- -
handleClick(e, ACTION_TYPE.UNWRAP)} - > - setWethAmount(e.target.value)} - variant="outlined" - InputLabelProps={{ - shrink: true, - }} - InputProps={{ - startAdornment: ( - - - - ), - endAdornment: ( - - - - ), - }} - /> - -
-
+ + + + Convert your ETH into WETH to be used as contract collateral. This is + needed for yUSD. To learn more about WETH see{" "} + + weth.io + + . Be sure to keep some ETH unwrapped for transaction fees. + + + + + + Your Wallet + + + + ETH + + + Ξ {ethBalance && Number(ethBalance).toFixed(4)} + + + + + + WETH + + + Ξ {wethBalance && Number(wethBalance).toFixed(4)} + + + + + + + + + Wrap and Unwrap + +
handleClick(e, ACTION_TYPE.WRAP)} + > + setEthAmount(e.target.value)} + variant="outlined" + inputProps={{ min: "0" }} + InputLabelProps={{ + shrink: true, + }} + InputProps={{ + endAdornment: ( + + + + ), + }} + /> + +
+ +
handleClick(e, ACTION_TYPE.UNWRAP)} + > + setWethAmount(e.target.value)} + variant="outlined" + inputProps={{ min: "0" }} + InputLabelProps={{ + shrink: true, + }} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: ( + + + + ), + }} + /> + +
+
+
+
+
+ {hash && ( diff --git a/features/yield-calculator/YieldCalculator.tsx b/features/yield-calculator/YieldCalculator.tsx index c02fb9b2..23764c43 100644 --- a/features/yield-calculator/YieldCalculator.tsx +++ b/features/yield-calculator/YieldCalculator.tsx @@ -1,4 +1,4 @@ -import { Box, TextField, Typography } from "@material-ui/core"; +import { Box, TextField, Typography, Grid } from "@material-ui/core"; import { useState, useEffect } from "react"; import styled from "styled-components"; import EmpState from "../../containers/EmpState"; @@ -80,48 +80,73 @@ const YieldCalculator = () => { }, [tokenPrice, daysToExpiry]); return ( - + + + + yUSD is a fixed yielding, expiring token that will be redeemable for + exactly 1 USD worth of ETH at expiry. To use this calculator enter in + the current yUSD price and the Days to expiry. An implied yearly APR + is shown. To learn more about yUSD see the UMA Medium post{" "} + + here + + . + + + yUSD Yield Calculator
- yUSD Yield Calculator - - - setTokenPrice(e.target.value)} - variant="outlined" - InputLabelProps={{ - shrink: true, - }} - /> - - - setDaysToExpiry(e.target.value)} - helperText={`Days to expiry for selected EMP: ${calculateDaysToExpiry()}`} - variant="outlined" - InputLabelProps={{ - shrink: true, - }} - /> - - - - + + + + setTokenPrice(e.target.value)} + variant="outlined" + inputProps={{ min: "0", max: "10", step: "0.01" }} + helperText={`Enter the market observable price`} + InputLabelProps={{ + shrink: true, + }} + /> + + + + + setDaysToExpiry(e.target.value)} + inputProps={{ min: "0", max: "10", step: "1" }} + helperText={ + calculateDaysToExpiry() + ? `Days to expiry for chosen EMP: ${calculateDaysToExpiry()}` + : "" + } + variant="outlined" + InputLabelProps={{ + shrink: true, + }} + /> + + + + + + Yearly APR: {prettyPercentage(yieldAmount)}% + + + +
diff --git a/pages/_document.tsx b/pages/_document.tsx index bdc7314b..e75883e5 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,4 +1,5 @@ import Document from "next/document"; +import Head from "next/head"; import { ServerStyleSheet } from "styled-components"; import { ServerStyleSheets } from "@material-ui/styles"; @@ -22,6 +23,13 @@ export default class MyDocument extends Document { ...initialProps, styles: ( <> + + + {initialProps.styles} {materialSheets.getStyleElement()} {styledComponentsSheet.getStyleElement()} diff --git a/pages/index.tsx b/pages/index.tsx index c00d90d8..e2d4cdf8 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -56,7 +56,15 @@ export default function Index() { easier, please use at your own risk. The source code can be viewed{" "} + here + + . UMA's main Github can be viewed{" "} + @@ -77,7 +85,7 @@ export default function Index() { {tabIndex === 4 && }
- +