From 23d101b4f53e36be59054db2ce20c16aa3f5a368 Mon Sep 17 00:00:00 2001 From: Micah Alcorn <273937+micahalcorn@users.noreply.github.com> Date: Wed, 5 Jul 2023 23:10:27 -0700 Subject: [PATCH 1/2] Repair labels on staking form --- client/components/vote-escrow/LockupForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/vote-escrow/LockupForm.tsx b/client/components/vote-escrow/LockupForm.tsx index 11d51b7f..964045c4 100644 --- a/client/components/vote-escrow/LockupForm.tsx +++ b/client/components/vote-escrow/LockupForm.tsx @@ -476,7 +476,7 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => {

Your stake summary

- Variable APY + Amount locked
@@ -497,7 +497,7 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => {
- Variable APY + Voting power
From 58faa9779976ebd1de4a629ad4653ec767ea40b4 Mon Sep 17 00:00:00 2001 From: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:19:13 +0530 Subject: [PATCH 2/2] Use `previewPoints` on contract to compute APY (#390) * Use `previewPoints` on contract to compute APY * Fix veOGV Amount --- client/components/Header.tsx | 18 ++---- client/components/claim/Eligibility.tsx | 20 +++---- client/components/claim/claim/ClaimOgv.tsx | 34 ++++++----- client/components/vote-escrow/LockupForm.tsx | 25 +++----- client/components/vote-escrow/YourLockups.tsx | 14 +---- client/pages/claim.tsx | 16 ++--- client/utils/useStakingAPY.tsx | 58 +++++++++++++++++++ 7 files changed, 107 insertions(+), 78 deletions(-) create mode 100644 client/utils/useStakingAPY.tsx diff --git a/client/components/Header.tsx b/client/components/Header.tsx index 694990cc..8c580a61 100644 --- a/client/components/Header.tsx +++ b/client/components/Header.tsx @@ -5,8 +5,7 @@ import Wrapper from "components/Wrapper"; import Link from "components/Link"; import Image from "next/image"; import { navItems } from "../constants"; -import useClaim from "utils/useClaim"; -import { getRewardsApy } from "utils/apy"; +import useStakingAPY from "utils/useStakingAPY"; interface HeaderProps { hideNav?: boolean; @@ -14,16 +13,8 @@ interface HeaderProps { const Header: FunctionComponent = ({ hideNav }) => { const [menuIsOpen, setMenuIsOpen] = useState(false); - const claim = useClaim(); - const totalSupplyVeOgv = claim.staking.totalSupplyVeOgvAdjusted || 0; - - // Standard APY figure, assumes 100 OGV locked for max duration - const stakingApy = getRewardsApy( - 100 * 1.8 ** (48 / 12), - 100, - totalSupplyVeOgv - ); + const { stakingAPY, loading: apyLoading } = useStakingAPY(100, 48); const overlayClassNames = classNames( "bg-black z-20 h-screen w-screen fixed top-0 transition duration-200 lg:hidden", @@ -80,7 +71,8 @@ const Header: FunctionComponent = ({ hideNav }) => { " /> - {stakingApy.toFixed(2)}% vAPY + {apyLoading ? "--.--" : stakingAPY.toFixed(2)}% + vAPY
)} @@ -152,7 +144,7 @@ const Header: FunctionComponent = ({ hideNav }) => { " /> - {stakingApy.toFixed(2)}% vAPY + {apyLoading ? "--.--" : stakingAPY.toFixed(2)}% vAPY
)} diff --git a/client/components/claim/Eligibility.tsx b/client/components/claim/Eligibility.tsx index f825e29c..6bb20ac4 100644 --- a/client/components/claim/Eligibility.tsx +++ b/client/components/claim/Eligibility.tsx @@ -9,9 +9,8 @@ import Icon from "@mdi/react"; import { mdiWallet, mdiCheckCircle, mdiAlertCircle } from "@mdi/js"; import { Loading } from "components/Loading"; import Link from "components/Link"; -import { getRewardsApy } from "utils/apy"; import { filter } from "lodash"; -import { BigNumber } from "ethers"; +import useStakingAPY from "utils/useStakingAPY"; interface EligibilityProps { handleNextStep: () => void; @@ -34,14 +33,8 @@ const Eligibility: FunctionComponent = ({ split.gte(0) ).length; - const totalSupplyVeOgv = claim.staking.totalSupplyVeOgvAdjusted || 0; - // Standard APY figure, assumes 100 OGV locked for max duration - const stakingApy = getRewardsApy( - 100 * 1.8 ** (48 / 12), - 100, - totalSupplyVeOgv - ); + const { stakingAPY, loading: apyLoading } = useStakingAPY(100, 48); const claimValid = (hasClaim && claim.optional && claim.optional.isValid) || @@ -148,8 +141,9 @@ const Eligibility: FunctionComponent = ({

- OGV stakers earn a {stakingApy.toFixed(2)}% variable - APY + OGV stakers earn a{" "} + {apyLoading ? "--.--" : stakingAPY.toFixed(2)}% + variable APY

= ({

- OGV stakers earn a {stakingApy.toFixed(2)}% variable APY + OGV stakers earn a{" "} + {apyLoading ? "--.--" : stakingAPY.toFixed(2)}% variable + APY

= () => { maxLockupDurationInMonths ); const [error, setError] = useState(null); - const totalSupplyVeOgv = claim.staking.totalSupplyVeOgvAdjusted || 0; if (!claim.loaded || !claim.optional.hasClaim) { return <>; } @@ -47,17 +46,14 @@ const ClaimOgv: FunctionComponent = () => { const veOgvFromOgvLockup = claimableOgv * votingDecayFactor ** (lockupDuration / 12); - const ogvLockupRewardApy = getRewardsApy( - veOgvFromOgvLockup, - claimableOgv, - totalSupplyVeOgv - ); - const maxVeOgvFromOgvLockup = claimableOgv * votingDecayFactor ** (48 / 12); - const maxOgvLockupRewardApy = getRewardsApy( - maxVeOgvFromOgvLockup, - claimableOgv, - totalSupplyVeOgv - ); + + const { stakingAPY: ogvLockupRewardApy, loading: ogvLockupRewardLoading } = + useStakingAPY(claimableOgv, lockupDuration); + + const { + stakingAPY: maxOgvLockupRewardApy, + loading: maxOgvLockupRewardLoading, + } = useStakingAPY(claimableOgv, 48); let claimButtonText = ""; if (isValidLockup && claim.optional.state === "ready") { @@ -134,7 +130,11 @@ const ClaimOgv: FunctionComponent = () => {
- {isValidLockup ? ogvLockupRewardApy.toFixed(2) : 0} + {isValidLockup + ? ogvLockupRewardLoading + ? "--.--" + : ogvLockupRewardApy.toFixed(2) + : 0} %
@@ -270,8 +270,10 @@ const ClaimOgv: FunctionComponent = () => { {!isValidLockup && (
If you don't stake your OGV, you'll miss out on the{" "} - {maxOgvLockupRewardApy.toFixed(2)}% variable APY and maximized - voting power.{" "} + {maxOgvLockupRewardLoading + ? "--.--" + : maxOgvLockupRewardApy.toFixed(2)} + % variable APY and maximized voting power.{" "} OGV ( {totalPercentageOfLockedUpOgv.toFixed(2)}% of the total supply) has already been staked by other users. diff --git a/client/components/vote-escrow/LockupForm.tsx b/client/components/vote-escrow/LockupForm.tsx index 964045c4..c71049de 100644 --- a/client/components/vote-escrow/LockupForm.tsx +++ b/client/components/vote-escrow/LockupForm.tsx @@ -20,9 +20,9 @@ import moment from "moment"; import { mdiInformationOutline as InfoIcon } from "@mdi/js"; import Icon from "@mdi/react"; import ApyToolTip from "components/claim/claim/ApyTooltip"; -import { getRewardsApy } from "utils/apy"; import numeral from "numeraljs"; import { decimal18Bn } from "utils"; +import useStakingAPY from "utils/useStakingAPY"; import classnames from "classnames"; interface LockupFormProps { @@ -124,12 +124,9 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => { balances, allowances, blockTimestamp, - totalBalances, } = useStore(); const router = useRouter(); - const { totalSupplyVeOgvAdjusted } = totalBalances; - const [lockupAmount, setLockupAmount] = useState( existingLockup ? Math.floor( @@ -143,17 +140,11 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => { : Math.floor((existingLockup.end - blockTimestamp) / SECONDS_IN_A_MONTH) ); // In months - // as specified here: https://github.com/OriginProtocol/ousd-governance/blob/master/contracts/OgvStaking.sol#L21 - const votingDecayFactor = 1.8; - - const veOgvFromOgvLockup = - lockupAmount * votingDecayFactor ** (lockupDuration / 12); - - const ogvLockupRewardApy = getRewardsApy( - veOgvFromOgvLockup, - lockupAmount, - totalSupplyVeOgvAdjusted - ); + const { + stakingAPY: ogvLockupRewardApy, + loading: apyLoading, + veOgvReceived: veOgvFromOgvLockup, + } = useStakingAPY(lockupAmount, lockupDuration); const validLockup = lockupAmount !== "0" && lockupDuration !== "0"; @@ -463,7 +454,9 @@ const LockupForm: FunctionComponent = ({ existingLockup }) => {
{validLockup - ? ogvLockupRewardApy.toFixed(2) + ? apyLoading + ? "--.--" + : ogvLockupRewardApy.toFixed(2) : (0.0).toFixed(2)} % diff --git a/client/components/vote-escrow/YourLockups.tsx b/client/components/vote-escrow/YourLockups.tsx index ece1c448..068d14b4 100644 --- a/client/components/vote-escrow/YourLockups.tsx +++ b/client/components/vote-escrow/YourLockups.tsx @@ -7,12 +7,11 @@ import { useStore } from "utils/store"; import { Loading } from "components/Loading"; import { toast } from "react-toastify"; import useAccountBalances from "utils/useAccountBalances"; -import useClaim from "utils/useClaim"; -import { getRewardsApy } from "utils/apy"; import Image from "next/image"; import DisabledButtonToolTip from "../DisabledButtonTooltip"; import LockupsTable from "./LockupsTable"; import { Web3Button } from "components/Web3Button"; +import useStakingAPY from "utils/useStakingAPY"; interface YourLockupsProps {} @@ -28,7 +27,6 @@ const YourLockups: FunctionComponent = () => { const { ogv, accruedRewards } = balances; const { totalPercentageOfLockedUpOgv } = totalBalances; const { reloadAccountBalances } = useAccountBalances(); - const claim = useClaim(); const [collectRewardsStatus, setCollectRewardsStatus] = useState("ready"); let collectRewardsButtonText = ""; @@ -40,14 +38,8 @@ const YourLockups: FunctionComponent = () => { collectRewardsButtonText = "Waiting to be mined"; } - const totalSupplyVeOgv = claim.staking.totalSupplyVeOgvAdjusted || 0; - // Standard APY figure, assumes 100 OGV locked for max duration - const stakingApy = getRewardsApy( - 100 * 1.8 ** (48 / 12), - 100, - totalSupplyVeOgv - ); + const { stakingAPY, loading: apyLoading } = useStakingAPY(100, 48); if (!lockups) { return ( @@ -123,7 +115,7 @@ const YourLockups: FunctionComponent = () => { OGV stakers earn{" "} - {stakingApy.toFixed(2)}% + {apyLoading ? "--.--" : stakingAPY.toFixed(2)}% {" "} variable APY diff --git a/client/pages/claim.tsx b/client/pages/claim.tsx index 5a066bb2..39f26cfb 100644 --- a/client/pages/claim.tsx +++ b/client/pages/claim.tsx @@ -3,21 +3,17 @@ import Wrapper from "components/Wrapper"; import Seo from "components/Seo"; import { PageTitle } from "components/PageTitle"; import Card from "components/Card"; +import Button from "components/Button"; import useClaim from "utils/useClaim"; import { getRewardsApy } from "utils/apy"; import Link from "components/Link"; +import useStakingAPY from "utils/useStakingAPY"; interface ClaimPageProps {} const ClaimPage: NextPage = () => { - const claim = useClaim(); - const totalSupplyVeOgv = claim.staking.totalSupplyVeOgvAdjusted || 0; // Standard APY figure, assumes 100 OGV locked for max duration - const stakingApy = getRewardsApy( - 100 * 1.8 ** (48 / 12), - 100, - totalSupplyVeOgv - ); + const { stakingAPY, loading: apyLoading } = useStakingAPY(100, 48); return ( @@ -29,9 +25,9 @@ const ClaimPage: NextPage = () => { The claim period has now expired
- {`All unclaimed OGV was burned after 90 days. You can still buy OGV and stake it for up to four years. Current OGV stakers are earning a vAPY of ${stakingApy.toFixed( - 2 - )}%.`} + {`All unclaimed OGV was burned after 90 days. You can still buy OGV and stake it for up to four years. Current OGV stakers are earning a vAPY of ${ + apyLoading ? "--.--" : stakingAPY.toFixed(2) + }%.`}
{ + const { contracts, totalBalances } = useStore(); + + const [loading, setLoading] = useState(true); + const [veOgvReceived, setVeOGVReceived] = useState(0); + + const { totalSupplyVeOgvAdjusted } = totalBalances; + + useEffect(() => { + if (!contracts.loaded) return; + + let done = false; + + async function go() { + try { + const [val] = await contracts.OgvStaking.previewPoints( + ethers.utils.parseEther(amountStaked.toString()), + duration * SECONDS_IN_A_MONTH + ); + + if (!done) { + setVeOGVReceived(parseFloat(ethers.utils.formatEther(val))); + setLoading(false); + } + } catch (err) { + console.error("Failed to fetch APY", err); + } + } + + go(); + + return () => { + done = true; + }; + }, [amountStaked, duration, contracts.loaded]); + + const stakingAPY = useMemo(() => { + if (!veOgvReceived || !totalSupplyVeOgvAdjusted || !amountStaked) { + return 0; + } + + return getRewardsApy(veOgvReceived, amountStaked, totalSupplyVeOgvAdjusted); + }, [veOgvReceived, amountStaked, totalSupplyVeOgvAdjusted]); + + return { + loading, + veOgvReceived, + stakingAPY, + }; +}; + +export default useStakingAPY;