From efcb9ad9648c9b90d2b14a6b54cd07237a3ed4d5 Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Thu, 6 Jan 2022 15:20:24 -0800 Subject: [PATCH] Claim hooks 3 (#2056) * Added new methods to vCow contract's json interface and re-generated the types * Fixing bad merge reverting values back * Hard coding prices fetched from the contract for now * Added hook useDeploymentTimestamp to fetch contract deployment timestamp * Added helper hooks to check whether claim window is still open for each type * Properly calculating ETH value from vCowAmount * Short update to prices constants comment regarding the unit * Typo fix * Turned hasClaims into a boolean rather than number * Adding two dumb checks regarding investment window for testing * Using ms.macro lib for calculating weeks in ms * Using ethers parseUnits function * Refactored hooks to return always a boolean, never null * Made notive token price calculation chain based * Validating claims are still claimable on claim callback * Added new variables to callback hook deps Co-authored-by: Leandro --- src/custom/abis/types/VCow.d.ts | 60 +++++++ .../abis/types/factories/VCow__factory.ts | 52 ++++++ src/custom/abis/vCow.json | 52 ++++++ src/custom/pages/Claim/index.tsx | 52 ++++-- src/custom/state/claim/hooks/index.ts | 149 ++++++++++++++++-- src/custom/state/claim/hooks/utils.ts | 3 +- 6 files changed, 338 insertions(+), 30 deletions(-) diff --git a/src/custom/abis/types/VCow.d.ts b/src/custom/abis/types/VCow.d.ts index 53ef9b8d6..6ca0f4891 100644 --- a/src/custom/abis/types/VCow.d.ts +++ b/src/custom/abis/types/VCow.d.ts @@ -25,6 +25,10 @@ interface VCowInterface extends ethers.utils.Interface { "claimMany(uint256[],uint8[],address[],uint256[],uint256[],bytes32[][],uint256[])": FunctionFragment; "isClaimed(uint256)": FunctionFragment; "merkleRoot()": FunctionFragment; + "deploymentTimestamp()": FunctionFragment; + "gnoPrice()": FunctionFragment; + "usdcPrice()": FunctionFragment; + "wethPrice()": FunctionFragment; }; encodeFunctionData( @@ -58,11 +62,25 @@ interface VCowInterface extends ethers.utils.Interface { functionFragment: "merkleRoot", values?: undefined ): string; + encodeFunctionData( + functionFragment: "deploymentTimestamp", + values?: undefined + ): string; + encodeFunctionData(functionFragment: "gnoPrice", values?: undefined): string; + encodeFunctionData(functionFragment: "usdcPrice", values?: undefined): string; + encodeFunctionData(functionFragment: "wethPrice", values?: undefined): string; decodeFunctionResult(functionFragment: "claim", data: BytesLike): Result; decodeFunctionResult(functionFragment: "claimMany", data: BytesLike): Result; decodeFunctionResult(functionFragment: "isClaimed", data: BytesLike): Result; decodeFunctionResult(functionFragment: "merkleRoot", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "deploymentTimestamp", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "gnoPrice", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "usdcPrice", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "wethPrice", data: BytesLike): Result; events: { "Claimed(uint256,uint8,address,uint256,uint256)": EventFragment; @@ -152,6 +170,14 @@ export class VCow extends BaseContract { ): Promise<[boolean]>; merkleRoot(overrides?: CallOverrides): Promise<[string]>; + + deploymentTimestamp(overrides?: CallOverrides): Promise<[BigNumber]>; + + gnoPrice(overrides?: CallOverrides): Promise<[BigNumber]>; + + usdcPrice(overrides?: CallOverrides): Promise<[BigNumber]>; + + wethPrice(overrides?: CallOverrides): Promise<[BigNumber]>; }; claim( @@ -179,6 +205,14 @@ export class VCow extends BaseContract { merkleRoot(overrides?: CallOverrides): Promise; + deploymentTimestamp(overrides?: CallOverrides): Promise; + + gnoPrice(overrides?: CallOverrides): Promise; + + usdcPrice(overrides?: CallOverrides): Promise; + + wethPrice(overrides?: CallOverrides): Promise; + callStatic: { claim( index: BigNumberish, @@ -204,6 +238,14 @@ export class VCow extends BaseContract { isClaimed(index: BigNumberish, overrides?: CallOverrides): Promise; merkleRoot(overrides?: CallOverrides): Promise; + + deploymentTimestamp(overrides?: CallOverrides): Promise; + + gnoPrice(overrides?: CallOverrides): Promise; + + usdcPrice(overrides?: CallOverrides): Promise; + + wethPrice(overrides?: CallOverrides): Promise; }; filters: { @@ -270,6 +312,14 @@ export class VCow extends BaseContract { ): Promise; merkleRoot(overrides?: CallOverrides): Promise; + + deploymentTimestamp(overrides?: CallOverrides): Promise; + + gnoPrice(overrides?: CallOverrides): Promise; + + usdcPrice(overrides?: CallOverrides): Promise; + + wethPrice(overrides?: CallOverrides): Promise; }; populateTransaction: { @@ -300,5 +350,15 @@ export class VCow extends BaseContract { ): Promise; merkleRoot(overrides?: CallOverrides): Promise; + + deploymentTimestamp( + overrides?: CallOverrides + ): Promise; + + gnoPrice(overrides?: CallOverrides): Promise; + + usdcPrice(overrides?: CallOverrides): Promise; + + wethPrice(overrides?: CallOverrides): Promise; }; } diff --git a/src/custom/abis/types/factories/VCow__factory.ts b/src/custom/abis/types/factories/VCow__factory.ts index c942e62af..4638f69e7 100644 --- a/src/custom/abis/types/factories/VCow__factory.ts +++ b/src/custom/abis/types/factories/VCow__factory.ts @@ -157,6 +157,58 @@ const _abi = [ stateMutability: "view", type: "function", }, + { + inputs: [], + name: "deploymentTimestamp", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "gnoPrice", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "usdcPrice", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "wethPrice", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, ]; export class VCow__factory { diff --git a/src/custom/abis/vCow.json b/src/custom/abis/vCow.json index a9d9a6f5c..246660a65 100644 --- a/src/custom/abis/vCow.json +++ b/src/custom/abis/vCow.json @@ -148,5 +148,57 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [], + "name": "deploymentTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gnoPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "usdcPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "wethPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" } ] diff --git a/src/custom/pages/Claim/index.tsx b/src/custom/pages/Claim/index.tsx index e37f58644..49925a3ea 100644 --- a/src/custom/pages/Claim/index.tsx +++ b/src/custom/pages/Claim/index.tsx @@ -9,6 +9,8 @@ import { FREE_CLAIM_TYPES, ClaimType, useClaimCallback, + useInvestmentStillAvailable, + useAirdropStillAvailable, } from 'state/claim/hooks' import { ButtonPrimary, ButtonSecondary } from 'components/Button' import Circle from 'assets/images/blue-loader.svg' @@ -90,7 +92,7 @@ export default function Claim() { const [claimConfirmed, setClaimConfirmed] = useState(false) const [claimAttempting, setClaimAttempting] = useState(false) const [claimSubmitted, setClaimSubmitted] = useState(false) - const [claimedAmmount, setClaimedAmmount] = useState(0) + const [claimedAmount, setClaimedAmount] = useState(0) // investment const [isInvestFlowActive, setIsInvestFlowActive] = useState(false) @@ -113,7 +115,7 @@ export default function Claim() { // get total unclaimed ammount const unclaimedAmount = useUserUnclaimedAmount(activeClaimAccount) - const hasClaims = useMemo(() => userClaimData.length, [userClaimData]) + const hasClaims = useMemo(() => userClaimData.length > 0, [userClaimData]) const isAirdropOnly = useMemo(() => !hasPaidClaim(userClaimData), [userClaimData]) // handle table select change @@ -124,6 +126,10 @@ export default function Claim() { const typeToCurrencyMap = useMemo(() => getTypeToCurrencyMap(chainId), [chainId]) const typeToPriceMap = useMemo(() => getTypeToPriceMap(), []) + // checks regarding investment time window + const isInvestmentStillAvailable = useInvestmentStillAvailable() + const isAirdropStillAvailable = useAirdropStillAvailable() + // claim callback const { claimCallback } = useClaimCallback(activeClaimAccount) @@ -202,6 +208,15 @@ export default function Claim() { setIsInvestFlowActive(true) } } + console.log( + `Claim/index::`, + `[unclaimedAmount ${unclaimedAmount?.toFixed(2)}]`, + `[hasClaims ${hasClaims}]`, + `[activeClaimAccount ${activeClaimAccount}]`, + `[isAirdropOnly ${isAirdropOnly}]`, + `[isInvestmentStillAvailable ${isInvestmentStillAvailable}]`, + `[isAirdropStillAvailable ${isAirdropStillAvailable}]` + ) // on account change useEffect(() => { @@ -320,16 +335,21 @@ export default function Claim() { {/* START -- IS Airdrop only (simple) ----------------------------------------------------- */} {!!activeClaimAccount && !!hasClaims && !!isAirdropOnly && !claimAttempting && !claimConfirmed && ( - -

- - Thank you for being a supporter of CowSwap and the CoW protocol. As an important member of the CowSwap - Community you may claim vCOW to be used for voting and governance. You can claim your tokens until{' '} - [XX-XX-XXXX - XX:XX GMT] - Read more about vCOW - -

-
+ <> + +

+ + Thank you for being a supporter of CowSwap and the CoW protocol. As an important member of the CowSwap + Community you may claim vCOW to be used for voting and governance. You can claim your tokens until{' '} + [XX-XX-XXXX - XX:XX GMT] + Read more about vCOW + +

+
+ + {/* TODO: this is temporary to show the flag, find a better way to show it */} + {!isAirdropStillAvailable &&

WARNING: investment window is over!!!

} + )} {/* END -- IS Airdrop only (simple) ---------------------------------------- */} @@ -364,7 +384,7 @@ export default function Claim() {

You have successfully claimed

-

{claimedAmmount} vCOW

+

{claimedAmount} vCOW

@@ -406,6 +426,10 @@ export default function Claim() { !(claimAttempting || claimConfirmed) && (

vCOW claim breakdown

+ + {/* TODO: this is temporary to show the flag, find a better way to show it */} + {!isInvestmentStillAvailable &&

WARNING: investment window is over!!!

} + @@ -620,6 +644,7 @@ export default function Claim() { )} {/* END -- Investing vCOW flow (advanced) ----------------------------------------------------- */} + {/* START -- CLAIM button OR other actions */} {/* General claim vCOW button (no invest) */} {!!activeClaimAccount && !!hasClaims && !isInvestFlowActive && !claimAttempting && !claimConfirmed ? ( @@ -668,6 +693,7 @@ export default function Claim() { )} + {/* END -- CLAIM button OR other actions */} ) } diff --git a/src/custom/state/claim/hooks/index.ts b/src/custom/state/claim/hooks/index.ts index a499284f8..4003e6117 100644 --- a/src/custom/state/claim/hooks/index.ts +++ b/src/custom/state/claim/hooks/index.ts @@ -1,7 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import JSBI from 'jsbi' +import ms from 'ms.macro' import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { TransactionResponse } from '@ethersproject/providers' +import { parseUnits } from '@ethersproject/units' import { VCow as VCowType } from 'abis/types' @@ -17,13 +19,29 @@ import { calculateGasMargin } from 'utils/calculateGasMargin' import { isAddress } from 'utils' import { getClaimKey, getClaimsRepoPath, transformRepoClaimsToUserClaims } from 'state/claim/hooks/utils' +import { SupportedChainId } from 'constants/chains' export { useUserClaimData } from '@src/state/claim/hooks' const CLAIMS_REPO_BRANCH = 'main' export const CLAIMS_REPO = `https://raw.githubusercontent.com/gnosis/cow-merkle-drop/${CLAIMS_REPO_BRANCH}/` -export const enum ClaimType { +// TODO: these values came from the test contract, might be different on real deployment +// Network variable price +export const NATIVE_TOKEN_PRICE = { + [SupportedChainId.MAINNET]: '37500000000000', // '0.0000375' WETH (18 decimals) per vCOW, in wei + [SupportedChainId.RINKEBY]: '37500000000000', // assuming Rinkeby has same price as Mainnet + [SupportedChainId.XDAI]: '150000000000000000', // TODO: wild guess, wxDAI is same price as USDC +} +// Same on all networks. Actually, likely available only on Mainnet (and Rinkeby) +export const GNO_PRICE = '375000000000000' // '0.000375' GNO (18 decimals) per vCOW, in atoms +export const USDC_PRICE = '150000' // '0.15' USDC (6 decimals) per vCOW, in atoms + +// Constants regarding investment time windows +const TWO_WEEKS = ms`2 weeks` +const SIX_WEEKS = ms`6 weeks` + +export enum ClaimType { Airdrop, // free, no vesting, can be available on both mainnet and gchain GnoOption, // paid, with vesting, must use GNO, can be available on both mainnet and gchain UserOption, // paid, with vesting, must use Native currency, can be available on both mainnet and gchain @@ -34,16 +52,6 @@ export const enum ClaimType { type RepoClaimType = keyof typeof ClaimType -// TODO: also, is there a smarter way of doing this? -export const REVERSE_CLAIM_TYPE_MAPPING: Record = { - Airdrop: ClaimType.Airdrop, - GnoOption: ClaimType.GnoOption, - UserOption: ClaimType.UserOption, - Investor: ClaimType.Investor, - Team: ClaimType.Team, - Advisor: ClaimType.Advisor, -} - export const FREE_CLAIM_TYPES: ClaimType[] = [ClaimType.Airdrop, ClaimType.Team, ClaimType.Advisor] export const PAID_CLAIM_TYPES: ClaimType[] = [ClaimType.GnoOption, ClaimType.UserOption, ClaimType.Investor] @@ -180,6 +188,78 @@ export function useUserClaims(account: Account): UserClaims | null { return claimKey ? claimInfo[claimKey] : null } +/** + * Fetches from contract the deployment timestamp in ms + * + * Returns null if in there's no network or vCowContract doesn't exist + */ +function useDeploymentTimestamp(): number | null { + const { chainId } = useActiveWeb3React() + const vCowContract = useVCowContract() + const [timestamp, setTimestamp] = useState(null) + + useEffect(() => { + if (!chainId || !vCowContract) { + return + } + + vCowContract.deploymentTimestamp().then((ts) => { + console.log(`Deployment timestamp in seconds: ${ts.toString()}`) + setTimestamp(ts.mul('1000').toNumber()) + }) + }, [chainId, vCowContract]) + + return timestamp +} + +/** + * Returns whether vCOW contract is still open for investments + * Null when not applicable + * + * That is, there has been less than 2 weeks since it was deployed + */ +export function useInvestmentStillAvailable(): boolean { + const deploymentTimestamp = useDeploymentTimestamp() + + return Boolean(deploymentTimestamp && deploymentTimestamp + TWO_WEEKS > Date.now()) +} + +/** + * Returns whether vCOW contract is still open for airdrops + * Null when not applicable + * + * That is, there has been less than 6 weeks since it was deployed + */ +export function useAirdropStillAvailable(): boolean { + const deploymentTimestamp = useDeploymentTimestamp() + + return Boolean(deploymentTimestamp && deploymentTimestamp + SIX_WEEKS > Date.now()) +} + +/** + * Helper function that checks whether selected investment options are still available + * + * Throws when claims are no longer possible + */ +function _validateClaimable( + claims: UserClaims, + input: ClaimInput[], + isInvestmentStillAvailable: boolean, + isAirdropStillAvailable: boolean +): void { + if (!isAirdropStillAvailable) { + throw new Error(`Contract no longer accepts claims`) + } + + input.forEach(({ index }) => { + const claim = claims.find((claim) => claim.index === index) + + if (claim && !isInvestmentStillAvailable && PAID_CLAIM_TYPES.includes(claim.type)) { + throw new Error(`Contract no longer accepts investment type claims`) + } + }) +} + /** * Hook that returns the claimCallback * @@ -196,6 +276,9 @@ export function useClaimCallback(account: string | null | undefined): { const claims = useUserAvailableClaims(account) const vCowContract = useVCowContract() + const isInvestmentStillAvailable = useInvestmentStillAvailable() + const isAirdropStillAvailable = useAirdropStillAvailable() + // used for popup summary const addTransaction = useTransactionAdder() const vCowToken = chainId ? V_COW[chainId] : undefined @@ -214,7 +297,9 @@ export function useClaimCallback(account: string | null | undefined): { throw new Error("Not initialized, can't claim") } - const { args, totalClaimedAmount } = _getClaimManyArgs({ claimInput, claims, account, connectedAccount }) + _validateClaimable(claims, claimInput, isInvestmentStillAvailable, isAirdropStillAvailable) + + const { args, totalClaimedAmount } = _getClaimManyArgs({ claimInput, claims, account, connectedAccount, chainId }) if (!args) { throw new Error('There were no valid claims selected') @@ -240,7 +325,17 @@ export function useClaimCallback(account: string | null | undefined): { }) }) }, - [account, addTransaction, chainId, claims, connectedAccount, vCowContract, vCowToken] + [ + account, + addTransaction, + chainId, + claims, + connectedAccount, + isAirdropStillAvailable, + isInvestmentStillAvailable, + vCowContract, + vCowToken, + ] ) return { claimCallback } @@ -251,6 +346,7 @@ type GetClaimManyArgsParams = { claims: UserClaims account: string connectedAccount: string + chainId: SupportedChainId } type ClaimManyFnArgs = Parameters @@ -268,6 +364,7 @@ function _getClaimManyArgs({ claims, account, connectedAccount, + chainId, }: GetClaimManyArgsParams): GetClaimManyArgsResult { // Arrays are named according to contract parameters // For more info, check https://github.com/gnosis/gp-v2-token/blob/main/src/contracts/mixins/MerkleDistributor.sol#L123 @@ -306,8 +403,8 @@ function _getClaimManyArgs({ claimedAmounts.push(claimedAmount) merkleProofs.push(claim.proof) - // only used on UserOption, equal to claimedAmount - const value = claim.type === ClaimType.UserOption ? claimedAmount : '0' + // only used on UserOption + const value = _getClaimValue(claim, claimedAmount, chainId) sendEth.push(value) // TODO: verify ETH balance < input.amount ? // sum of claimedAmounts for the toast notification @@ -379,6 +476,28 @@ function _hasNoInputOrInputIsGreaterThanClaimAmount( return !input.amount || JSBI.greaterThan(JSBI.BigInt(input.amount), JSBI.BigInt(claim.amount)) } +/** + * Calculates native value based on claim vCowAmount and type + * + * Value will only be != '0' if claim type is UserOption + * Assumes the checks were done previously regarding which amounts are allowed + * + * The calculation is done based on the formula: + * vCowAmount * wethPrice / 10^18 + * See https://github.com/gnosis/gp-v2-token/blob/main/src/contracts/mixins/Claiming.sol#L314-L320 + */ +function _getClaimValue(claim: UserClaimData, vCowAmount: string, chainId: SupportedChainId): string { + if (claim.type !== ClaimType.UserOption) { + return '0' + } + + const price = NATIVE_TOKEN_PRICE[chainId] + + const claimValueInAtoms = JSBI.multiply(JSBI.BigInt(vCowAmount), JSBI.BigInt(price)) + + return parseUnits(claimValueInAtoms.toString(), 18).toString() +} + type LastAddress = string type ClaimAddressMapping = { [firstAddress: string]: LastAddress } const FETCH_CLAIM_MAPPING_PROMISES: Record | null> = {} diff --git a/src/custom/state/claim/hooks/utils.ts b/src/custom/state/claim/hooks/utils.ts index d0172654f..e621ed1b7 100644 --- a/src/custom/state/claim/hooks/utils.ts +++ b/src/custom/state/claim/hooks/utils.ts @@ -7,7 +7,6 @@ import { FREE_CLAIM_TYPES, PAID_CLAIM_TYPES, RepoClaims, - REVERSE_CLAIM_TYPE_MAPPING, UserClaims, } from 'state/claim/hooks/index' @@ -36,7 +35,7 @@ export function hasFreeClaim(claims: UserClaims | null): boolean { * Airdrop -> 0 */ export function transformRepoClaimsToUserClaims(repoClaims: RepoClaims): UserClaims { - return repoClaims.map((claim) => ({ ...claim, type: REVERSE_CLAIM_TYPE_MAPPING[claim.type] })) + return repoClaims.map((claim) => ({ ...claim, type: ClaimType[claim.type] })) } /**