From 8d5466db80b447a0e5de32ca1999e6249648a657 Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Wed, 30 Oct 2024 16:46:41 -0700 Subject: [PATCH 01/78] first commit working on de-prize --- ui/components/layout/Sidebar/Navigation.ts | 8 +- ui/components/nance/DePrize.tsx | 389 +++++++++++++++++++++ ui/const/abis/Competitor.json | 1 + ui/const/config.ts | 11 + ui/lib/tokens/hooks/useTokenBalances.tsx | 26 ++ ui/lib/utils/voting.ts | 6 +- ui/pages/_app.tsx | 2 + ui/pages/deprize.tsx | 66 ++++ 8 files changed, 505 insertions(+), 4 deletions(-) create mode 100644 ui/components/nance/DePrize.tsx create mode 100644 ui/const/abis/Competitor.json create mode 100644 ui/lib/tokens/hooks/useTokenBalances.tsx create mode 100644 ui/pages/deprize.tsx diff --git a/ui/components/layout/Sidebar/Navigation.ts b/ui/components/layout/Sidebar/Navigation.ts index b06f7dd04..92b7120d7 100644 --- a/ui/components/layout/Sidebar/Navigation.ts +++ b/ui/components/layout/Sidebar/Navigation.ts @@ -6,6 +6,7 @@ import { RocketLaunchIcon, Squares2X2Icon, UserGroupIcon, + MoonIcon, } from '@heroicons/react/24/outline' import IconOrg from '../../assets/IconOrg' @@ -19,6 +20,11 @@ export const navigation = [ { name: 'Create a Team', href: '/team' }, ], }, + { + name: 'De-Prize', + href: '/deprize', + icon: MoonIcon, + }, { name: 'Network', href: '/network', @@ -37,7 +43,7 @@ export const navigation = [ }, { name: 'Events', href: '/events' }, { name: 'Analytics', href: '/analytics' }, - { name: 'Current Projects', href: '/current-projects'} + { name: 'Current Projects', href: '/current-projects' }, ], }, { diff --git a/ui/components/nance/DePrize.tsx b/ui/components/nance/DePrize.tsx new file mode 100644 index 000000000..d0f42a4d0 --- /dev/null +++ b/ui/components/nance/DePrize.tsx @@ -0,0 +1,389 @@ +import { Arbitrum, Sepolia } from '@thirdweb-dev/chains' +import { useAddress, useContract } from '@thirdweb-dev/react' +import ERC20 from 'const/abis/ERC20.json' +import { + DEPRIZE_DISTRIBUTION_TABLE_ADDRESSES, + SNAPSHOT_RETROACTIVE_REWARDS_ID, + PRIZE_TOKEN_ADDRESSES, + PRIZE_DECIMALS, +} from 'const/config' +import _ from 'lodash' +import { useState, useEffect } from 'react' +import toast from 'react-hot-toast' +import { useCitizens } from '@/lib/citizen/useCitizen' +import { useAssets } from '@/lib/dashboard/hooks' +import toastStyle from '@/lib/marketplace/marketplace-utils/toastConfig' +import { SNAPSHOT_SPACE_NAME } from '@/lib/nance/constants' +import { useVotingPowers } from '@/lib/snapshot' +import useWindowSize from '@/lib/team/use-window-size' +import useTokenBalances from '@/lib/tokens/hooks/useTokenBalances' +import useWatchTokenBalance from '@/lib/tokens/hooks/useWatchTokenBalance' +import { getBudget, getPayouts } from '@/lib/utils/rewards' +import { runQuadraticVoting } from '@/lib/utils/voting' +import Asset from '@/components/dashboard/treasury/balance/Asset' +import Container from '@/components/layout/Container' +import ContentLayout from '@/components/layout/ContentLayout' +import Head from '@/components/layout/Head' +import { NoticeFooter } from '@/components/layout/NoticeFooter' +import StandardButton from '../layout/StandardButton' + +export type Competitor = { + id: string + deprize: number + title: string + treasury: string +} +export type Distribution = { + deprize: number + year: number + quarter: number + address: string + distribution: { [key: string]: number } +} + +export type DePrizeProps = { + competitors: Competitor[] + distributions: Distribution[] + refreshRewards: () => void +} + +export function DePrize({ + competitors, + distributions, + refreshRewards, +}: DePrizeProps) { + console.log('competitors', competitors) + const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia + const { isMobile } = useWindowSize() + + const userAddress = useAddress() + const year = new Date().getFullYear() + const quarter = Math.floor((new Date().getMonth() + 3) / 3) - 1 + const deprize = 1 + + const [edit, setEdit] = useState(false) + const [distribution, setDistribution] = useState<{ [key: string]: number }>( + {} + ) + // Check if the user already has a distribution for the current quarter + useEffect(() => { + if (distributions && userAddress) { + for (const d of distributions) { + if ( + d.year === year && + d.quarter === quarter && + d.address.toLowerCase() === userAddress.toLowerCase() + ) { + setDistribution(d.distribution) + setEdit(true) + break + } + } + } + }, [userAddress, distributions]) + const handleDistributionChange = (competitorId: string, value: number) => { + setDistribution((prev) => ({ + ...prev, + [competitorId]: Math.min(100, Math.max(1, value)), + })) + } + + const addresses = distributions ? distributions.map((d) => d.address) : [] + + const { data: _vps } = useVotingPowers( + addresses, + SNAPSHOT_SPACE_NAME, + SNAPSHOT_RETROACTIVE_REWARDS_ID + ) + const { contract: prizeContract } = useContract( + PRIZE_TOKEN_ADDRESSES[chain.slug], + ERC20.abi + ) + const prizeBalance = useWatchTokenBalance(prizeContract, PRIZE_DECIMALS) + console.log('prizeBalance', prizeBalance) + const tokenBalances = useTokenBalances( + prizeContract, + PRIZE_DECIMALS, + addresses + ) + console.log('tokenBalances', tokenBalances) + const addressToQuadraticVotingPower = Object.fromEntries( + addresses.map((address, i) => [address, Math.sqrt(tokenBalances[i])]) + ) + console.log('userAddress', userAddress) + console.log('addressToQuadraticVotingPower', addressToQuadraticVotingPower) + const votingPowerSumIsNonZero = + _.sum(Object.values(addressToQuadraticVotingPower)) > 0 + const userHasVotingPower = + userAddress && + (userAddress.toLowerCase() in addressToQuadraticVotingPower || + userAddress in addressToQuadraticVotingPower) && + addressToQuadraticVotingPower[userAddress.toLowerCase()] > 0 + console.log('userHasVotingPower', userHasVotingPower) + + // All competitors need at least one citizen distribution to do iterative normalization + const isCitizens = useCitizens(chain, addresses) + const citizenDistributions = distributions?.filter((_, i) => isCitizens[i]) + const nonCitizenDistributions = distributions?.filter( + (_, i) => !isCitizens[i] + ) + const allCompetitorsHaveCitizenDistribution = competitors.every(({ id }) => + citizenDistributions.some(({ distribution }) => id in distribution) + ) + console.log( + 'allCompetitorsHaveCitizenDistribution', + allCompetitorsHaveCitizenDistribution + ) + //const readyToRunVoting = + //allCompetitorsHaveCitizenDistribution && votingPowerSumIsNonZero + const readyToRunVoting = votingPowerSumIsNonZero + + console.log('distributions', distributions) + const budgetPercent = 100 + const competitorIdToEstimatedPercentage: { [key: string]: number } = + runQuadraticVoting( + distributions, + addressToQuadraticVotingPower, + budgetPercent + ) + + const { contract: distributionTableContract } = useContract( + DEPRIZE_DISTRIBUTION_TABLE_ADDRESSES[chain.slug] + ) + const { tokens } = useAssets() + const { ethBudget, usdBudget, mooneyBudget, ethPrice } = getBudget( + tokens, + year, + quarter + ) + const prizeBudget = 2_500_000 + const prizePrice = 1 + const competitorIdToPrizePayout = {} + for (const competitor of competitors) { + competitorIdToPrizePayout[competitor.id] = + (prizeBudget * competitorIdToEstimatedPercentage[competitor.id]) / 100 + } + console.log('competitorIdToPrizePayout', competitorIdToPrizePayout) + + const handleSubmit = async () => { + const totalPercentage = Object.values(distribution).reduce( + (sum, value) => sum + value, + 0 + ) + if (totalPercentage !== 100) { + toast.error('Total distribution must equal 100%', { + style: toastStyle, + }) + return + } + try { + if (edit) { + await distributionTableContract?.call('updateTableCol', [ + deprize, + quarter, + year, + JSON.stringify(distribution), + ]) + toast.success('Distribution edited successfully!', { + style: toastStyle, + }) + setTimeout(() => { + refreshRewards() + }, 5000) + } else { + await distributionTableContract?.call('insertIntoTable', [ + deprize, + quarter, + year, + JSON.stringify(distribution), + ]) + toast.success('Distribution submitted successfully!', { + style: toastStyle, + }) + setTimeout(() => { + refreshRewards() + }, 5000) + } + } catch (error) { + console.error('Error submitting distribution:', error) + toast.error('Error submitting distribution. Please try again.', { + style: toastStyle, + }) + } + } + const handleDelete = async () => { + try { + await distributionTableContract?.call('deleteFromTable', [ + deprize, + quarter, + year, + ]) + toast.success('Distribution deleted successfully!', { + style: toastStyle, + }) + setTimeout(() => { + refreshRewards() + }, 5000) + } catch (error) { + console.error('Error deleting distribution:', error) + toast.error('Error deleting distribution. Please try again.', { + style: toastStyle, + }) + } + } + + return ( +
+ + + } + mainPadding + mode="compact" + popOverEffect={false} + isProfile + > +
+
+

+ Total Q{quarter} Rewards +

+ +
+ {userAddress && ( +
+

+ Voting Power +

+ +
+ )} +
+
+
+

+ Distribute +

+ {readyToRunVoting && ( +

+ Estimated Rewards +

+ )} +
+
+ {competitors && + competitors.map((competitor, i: number) => ( +
+
+ + handleDistributionChange( + competitor.id, + parseInt(e.target.value) + ) + } + className="border rounded px-2 py-1 w-20" + min="1" + max="100" + disabled={!userAddress || !userHasVotingPower} + /> + % +
+      +
+ + Competitor {competitor.id}: + + {competitor.name} +
+ {readyToRunVoting && tokens && tokens[0] && ( + <> +
+ {competitorIdToEstimatedPercentage[ + competitor.id + ].toFixed(2)} + % +
+
+ {Number( + competitorIdToPrizePayout[ + competitor.id + ].toPrecision(3) + ).toLocaleString()}{' '} + PRIZE +
+ + )} +
+ ))} +
+ {competitors && userHasVotingPower ? ( + + + {edit ? 'Edit Distribution' : 'Submit Distribution'} + + {edit && ( + + Delete Distribution + + )} + + ) : ( + + + Get Voting Power + + + )} +
+
+
+
+ ) +} diff --git a/ui/const/abis/Competitor.json b/ui/const/abis/Competitor.json new file mode 100644 index 000000000..a8ee1306e --- /dev/null +++ b/ui/const/abis/Competitor.json @@ -0,0 +1 @@ +[{"type":"constructor","inputs":[{"name":"_table_prefix","type":"string","internalType":"string"}],"stateMutability":"nonpayable"},{"type":"function","name":"currId","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPolicy","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"tuple","internalType":"struct TablelandPolicy","components":[{"name":"allowInsert","type":"bool","internalType":"bool"},{"name":"allowUpdate","type":"bool","internalType":"bool"},{"name":"allowDelete","type":"bool","internalType":"bool"},{"name":"whereClause","type":"string","internalType":"string"},{"name":"withCheck","type":"string","internalType":"string"},{"name":"updatableColumns","type":"string[]","internalType":"string[]"}]}],"stateMutability":"payable"},{"type":"function","name":"getPolicy","inputs":[{"name":"caller","type":"address","internalType":"address"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct TablelandPolicy","components":[{"name":"allowInsert","type":"bool","internalType":"bool"},{"name":"allowUpdate","type":"bool","internalType":"bool"},{"name":"allowDelete","type":"bool","internalType":"bool"},{"name":"whereClause","type":"string","internalType":"string"},{"name":"withCheck","type":"string","internalType":"string"},{"name":"updatableColumns","type":"string[]","internalType":"string[]"}]}],"stateMutability":"payable"},{"type":"function","name":"getTableId","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getTableName","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"insertIntoTable","inputs":[{"name":"name","type":"string","internalType":"string"},{"name":"deprize","type":"uint256","internalType":"uint256"},{"name":"treasury","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"renounceOwnership","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setAccessControl","inputs":[{"name":"controller","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"transferOwnership","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"OwnershipTransferred","inputs":[{"name":"previousOwner","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"ChainNotSupported","inputs":[{"name":"chainid","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"OwnableInvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"OwnableUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"}]}] diff --git a/ui/const/config.ts b/ui/const/config.ts index c1eb5468c..7c7d27256 100644 --- a/ui/const/config.ts +++ b/ui/const/config.ts @@ -94,10 +94,21 @@ export const PROJECT_TABLE_ADDRESSES: Index = { arbitrum: '0x4c2d6567D81A34117E894b6fDC97C9824f80D961', 'arbitrum-sepolia': '0xF0A3DB6161D1Ee7B99197CDeD4EdFc462EAE80e0', } +export const COMPETITOR_TABLE_ADDRESSES: Index = { + sepolia: '0xb5D65d5867eBF99556a0F79fF6eB5A1F5680bFBc', +} export const DISTRIBUTION_TABLE_ADDRESSES: Index = { arbitrum: '0xabD8D3693439A72393220d87aee159952261Ad1f', 'arbitrum-sepolia': '0xd1D57F18252D06a6b28DE96B6cbF7F4283A4F205', } +export const DEPRIZE_DISTRIBUTION_TABLE_ADDRESSES: Index = { + sepolia: '0xA441f20115c868dc66bC1977E1c17D4B9A0189c7', +} +// TODO don't hard code, pull from d-prize contract +export const PRIZE_TOKEN_ADDRESSES: Index = { + sepolia: '0xf2a29F67fb5e6d7B9682591c0fD100d357dA85A7', +} +export const PRIZE_DECIMALS = 18 export const CITIZEN_WHITELIST_ADDRESSES: Index = { arbitrum: '0xd594DBF360D666c94615Fb186AF3cB1018Be1616', diff --git a/ui/lib/tokens/hooks/useTokenBalances.tsx b/ui/lib/tokens/hooks/useTokenBalances.tsx new file mode 100644 index 000000000..744fe3a60 --- /dev/null +++ b/ui/lib/tokens/hooks/useTokenBalances.tsx @@ -0,0 +1,26 @@ +import { useContext, useEffect, useState } from 'react' +import PrivyWalletContext from '../privy/privy-wallet-context' +import { useWallets } from './useWallets' + +export default function useTokenBalances( + tokenContract: any, + decimals: number, + addresses: string[] +) { + const [tokenBalances, setTokenBalances] = useState([]) + + useEffect(() => { + async function getBalances() { + const balances = await Promise.all( + addresses.map(async (address) => { + const balance = await tokenContract.call('balanceOf', [address]) + return +balance.toString() / 10 ** decimals + }) + ) + setTokenBalances(balances) + } + + getBalances() + }, []) + return tokenBalances +} diff --git a/ui/lib/utils/voting.ts b/ui/lib/utils/voting.ts index f65aff4bd..8f09c191f 100644 --- a/ui/lib/utils/voting.ts +++ b/ui/lib/utils/voting.ts @@ -174,9 +174,10 @@ export function runIterativeNormalization(distributions: any, projects: any) { return return_tuple } -function runQuadraticVoting( +export function runQuadraticVoting( distributions: Distribution[], - addressToQuadraticVotingPower: any + addressToQuadraticVotingPower: any, + budgetPercentMinusCommunityFund = 90 ) { const projectIdToEstimatedPercentage: { [key: string]: number } = {} const projectIdToListOfPercentage: { [key: string]: number[] } = {} @@ -202,7 +203,6 @@ function runQuadraticVoting( ) ) / votingPowerSum } - const budgetPercentMinusCommunityFund = 90 // normalize projectIdToEstimatedPercentage const sum = _.sum(Object.values(projectIdToEstimatedPercentage)) for (const [projectId, percentage] of Object.entries( diff --git a/ui/pages/_app.tsx b/ui/pages/_app.tsx index 8c54d9bf9..626e7cc4d 100644 --- a/ui/pages/_app.tsx +++ b/ui/pages/_app.tsx @@ -14,6 +14,8 @@ function App({ Component, pageProps: { session, ...pageProps } }: any) { const [selectedChain, setSelectedChain]: any = useState( process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia ) + console.log('selectedChain') + console.log(selectedChain) const [lightMode, setLightMode] = useLightMode() diff --git a/ui/pages/deprize.tsx b/ui/pages/deprize.tsx new file mode 100644 index 000000000..9e523bf7c --- /dev/null +++ b/ui/pages/deprize.tsx @@ -0,0 +1,66 @@ +import { Arbitrum, Sepolia } from '@thirdweb-dev/chains' +import CompetitorABI from 'const/abis/Competitor.json' +import { + COMPETITOR_TABLE_ADDRESSES, + DEPRIZE_DISTRIBUTION_TABLE_ADDRESSES, + TABLELAND_ENDPOINT, +} from 'const/config' +import { useRouter } from 'next/router' +import { initSDK } from '@/lib/thirdweb/thirdweb' +import { DePrize, DePrizeProps } from '../components/nance/DePrize' + +export default function Rewards({ competitors, distributions }: DePrizeProps) { + const router = useRouter() + return ( + router.reload()} + /> + ) +} + +export async function getStaticProps() { + const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia + const sdk = initSDK(chain) + + const competitorTableContract = await sdk.getContract( + COMPETITOR_TABLE_ADDRESSES[chain.slug], + CompetitorABI + ) + + const distributionTableContract = await sdk.getContract( + DEPRIZE_DISTRIBUTION_TABLE_ADDRESSES[chain.slug] + ) + + const competitorBoardTableName = await competitorTableContract.call( + 'getTableName' + ) + const distributionTableName = await distributionTableContract.call( + 'getTableName' + ) + + // TODO don't hardcode + const dePrizeId = 1 + const currentYear = new Date().getFullYear() + const currentQuarter = Math.floor((new Date().getMonth() + 3) / 3) - 1 + const competitorStatement = `SELECT * FROM ${competitorBoardTableName} WHERE deprize = ${dePrizeId}` + const competitorsRes = await fetch( + `${TABLELAND_ENDPOINT}?statement=${competitorStatement}` + ) + const competitors = await competitorsRes.json() + + const distributionStatement = `SELECT * FROM ${distributionTableName} WHERE deprize = ${dePrizeId} AND year = ${currentYear} AND quarter = ${currentQuarter}` + const distributionsRes = await fetch( + `${TABLELAND_ENDPOINT}?statement=${distributionStatement}` + ) + const distributions = await distributionsRes.json() + + return { + props: { + competitors, + distributions, + }, + revalidate: 60, + } +} From 64c2e0f80dfc4da668c0e3ce65fe079f1d9626dd Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Wed, 30 Oct 2024 16:58:42 -0700 Subject: [PATCH 02/78] fix type errors --- ui/components/nance/DePrize.tsx | 31 +++++++++++-------------------- ui/pages/deprize.tsx | 2 +- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/ui/components/nance/DePrize.tsx b/ui/components/nance/DePrize.tsx index d0f42a4d0..3e40c9456 100644 --- a/ui/components/nance/DePrize.tsx +++ b/ui/components/nance/DePrize.tsx @@ -27,11 +27,15 @@ import Head from '@/components/layout/Head' import { NoticeFooter } from '@/components/layout/NoticeFooter' import StandardButton from '../layout/StandardButton' +export type Metadata = { + social: string +} export type Competitor = { id: string deprize: number title: string treasury: string + metadata: Metadata } export type Distribution = { deprize: number @@ -52,7 +56,6 @@ export function DePrize({ distributions, refreshRewards, }: DePrizeProps) { - console.log('competitors', competitors) const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia const { isMobile } = useWindowSize() @@ -100,18 +103,14 @@ export function DePrize({ ERC20.abi ) const prizeBalance = useWatchTokenBalance(prizeContract, PRIZE_DECIMALS) - console.log('prizeBalance', prizeBalance) const tokenBalances = useTokenBalances( prizeContract, PRIZE_DECIMALS, addresses ) - console.log('tokenBalances', tokenBalances) const addressToQuadraticVotingPower = Object.fromEntries( addresses.map((address, i) => [address, Math.sqrt(tokenBalances[i])]) ) - console.log('userAddress', userAddress) - console.log('addressToQuadraticVotingPower', addressToQuadraticVotingPower) const votingPowerSumIsNonZero = _.sum(Object.values(addressToQuadraticVotingPower)) > 0 const userHasVotingPower = @@ -119,7 +118,6 @@ export function DePrize({ (userAddress.toLowerCase() in addressToQuadraticVotingPower || userAddress in addressToQuadraticVotingPower) && addressToQuadraticVotingPower[userAddress.toLowerCase()] > 0 - console.log('userHasVotingPower', userHasVotingPower) // All competitors need at least one citizen distribution to do iterative normalization const isCitizens = useCitizens(chain, addresses) @@ -130,15 +128,8 @@ export function DePrize({ const allCompetitorsHaveCitizenDistribution = competitors.every(({ id }) => citizenDistributions.some(({ distribution }) => id in distribution) ) - console.log( - 'allCompetitorsHaveCitizenDistribution', - allCompetitorsHaveCitizenDistribution - ) - //const readyToRunVoting = - //allCompetitorsHaveCitizenDistribution && votingPowerSumIsNonZero const readyToRunVoting = votingPowerSumIsNonZero - console.log('distributions', distributions) const budgetPercent = 100 const competitorIdToEstimatedPercentage: { [key: string]: number } = runQuadraticVoting( @@ -158,12 +149,12 @@ export function DePrize({ ) const prizeBudget = 2_500_000 const prizePrice = 1 - const competitorIdToPrizePayout = {} - for (const competitor of competitors) { - competitorIdToPrizePayout[competitor.id] = - (prizeBudget * competitorIdToEstimatedPercentage[competitor.id]) / 100 - } - console.log('competitorIdToPrizePayout', competitorIdToPrizePayout) + const competitorIdToPrizePayout = Object.fromEntries( + competitors.map(({ id }) => [ + id, + (prizeBudget * competitorIdToEstimatedPercentage[id]) / 100, + ]) + ) const handleSubmit = async () => { const totalPercentage = Object.values(distribution).reduce( @@ -264,7 +255,7 @@ export function DePrize({ name="PRIZE" amount={Number(prizeBudget.toPrecision(3)).toLocaleString()} usd={Number( - prizeBudget.toPrecision(3) * prizePrice + (prizeBudget * prizePrice).toPrecision(3) ).toLocaleString()} /> diff --git a/ui/pages/deprize.tsx b/ui/pages/deprize.tsx index 9e523bf7c..5203ba246 100644 --- a/ui/pages/deprize.tsx +++ b/ui/pages/deprize.tsx @@ -53,7 +53,7 @@ export async function getStaticProps() { const distributionStatement = `SELECT * FROM ${distributionTableName} WHERE deprize = ${dePrizeId} AND year = ${currentYear} AND quarter = ${currentQuarter}` const distributionsRes = await fetch( `${TABLELAND_ENDPOINT}?statement=${distributionStatement}` - ) + j const distributions = await distributionsRes.json() return { From 111213484e87a61328b49d724bb9d56180c0784c Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Wed, 30 Oct 2024 17:13:29 -0700 Subject: [PATCH 03/78] some fixes --- ui/lib/tokens/hooks/useTokenBalances.tsx | 3 ++- ui/pages/deprize.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/lib/tokens/hooks/useTokenBalances.tsx b/ui/lib/tokens/hooks/useTokenBalances.tsx index 744fe3a60..72e26b8c5 100644 --- a/ui/lib/tokens/hooks/useTokenBalances.tsx +++ b/ui/lib/tokens/hooks/useTokenBalances.tsx @@ -11,6 +11,7 @@ export default function useTokenBalances( useEffect(() => { async function getBalances() { + if (!tokenContract) return const balances = await Promise.all( addresses.map(async (address) => { const balance = await tokenContract.call('balanceOf', [address]) @@ -21,6 +22,6 @@ export default function useTokenBalances( } getBalances() - }, []) + }, [tokenContract, addresses]) return tokenBalances } diff --git a/ui/pages/deprize.tsx b/ui/pages/deprize.tsx index 5203ba246..9e523bf7c 100644 --- a/ui/pages/deprize.tsx +++ b/ui/pages/deprize.tsx @@ -53,7 +53,7 @@ export async function getStaticProps() { const distributionStatement = `SELECT * FROM ${distributionTableName} WHERE deprize = ${dePrizeId} AND year = ${currentYear} AND quarter = ${currentQuarter}` const distributionsRes = await fetch( `${TABLELAND_ENDPOINT}?statement=${distributionStatement}` - j + ) const distributions = await distributionsRes.json() return { From 808d88d9996219ee634ac1fdec428c340803e3df Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Wed, 30 Oct 2024 17:17:18 -0700 Subject: [PATCH 04/78] fixes types --- ui/components/nance/DePrize.tsx | 1 + ui/lib/tokens/hooks/useTokenBalances.tsx | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/components/nance/DePrize.tsx b/ui/components/nance/DePrize.tsx index 3e40c9456..118305da0 100644 --- a/ui/components/nance/DePrize.tsx +++ b/ui/components/nance/DePrize.tsx @@ -32,6 +32,7 @@ export type Metadata = { } export type Competitor = { id: string + name: string deprize: number title: string treasury: string diff --git a/ui/lib/tokens/hooks/useTokenBalances.tsx b/ui/lib/tokens/hooks/useTokenBalances.tsx index 72e26b8c5..4e8f29a4c 100644 --- a/ui/lib/tokens/hooks/useTokenBalances.tsx +++ b/ui/lib/tokens/hooks/useTokenBalances.tsx @@ -1,6 +1,4 @@ import { useContext, useEffect, useState } from 'react' -import PrivyWalletContext from '../privy/privy-wallet-context' -import { useWallets } from './useWallets' export default function useTokenBalances( tokenContract: any, From ec48c26a5363277eb177392a7f3cac2318df7551 Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Wed, 30 Oct 2024 17:22:31 -0700 Subject: [PATCH 05/78] hard code sepolia' --- ui/pages/deprize.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/pages/deprize.tsx b/ui/pages/deprize.tsx index 9e523bf7c..7ca3d083a 100644 --- a/ui/pages/deprize.tsx +++ b/ui/pages/deprize.tsx @@ -21,7 +21,9 @@ export default function Rewards({ competitors, distributions }: DePrizeProps) { } export async function getStaticProps() { - const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia + // TODO uncomment + //const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia + const chain = process.env.NEXT_PUBLIC_CHAIN === Sepolia const sdk = initSDK(chain) const competitorTableContract = await sdk.getContract( From 71a894a13aac97748006ece575d8b4f73cf663b8 Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Wed, 30 Oct 2024 17:25:14 -0700 Subject: [PATCH 06/78] fix typo --- ui/pages/deprize.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/pages/deprize.tsx b/ui/pages/deprize.tsx index 7ca3d083a..f896c4de5 100644 --- a/ui/pages/deprize.tsx +++ b/ui/pages/deprize.tsx @@ -23,7 +23,7 @@ export default function Rewards({ competitors, distributions }: DePrizeProps) { export async function getStaticProps() { // TODO uncomment //const chain = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia - const chain = process.env.NEXT_PUBLIC_CHAIN === Sepolia + const chain = Sepolia const sdk = initSDK(chain) const competitorTableContract = await sdk.getContract( From d82f852942901ac646e4e3f54cd2617844669c20 Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Wed, 30 Oct 2024 17:30:26 -0700 Subject: [PATCH 07/78] fix error --- ui/components/nance/DePrize.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ui/components/nance/DePrize.tsx b/ui/components/nance/DePrize.tsx index 118305da0..5da29c299 100644 --- a/ui/components/nance/DePrize.tsx +++ b/ui/components/nance/DePrize.tsx @@ -150,12 +150,14 @@ export function DePrize({ ) const prizeBudget = 2_500_000 const prizePrice = 1 - const competitorIdToPrizePayout = Object.fromEntries( - competitors.map(({ id }) => [ - id, - (prizeBudget * competitorIdToEstimatedPercentage[id]) / 100, - ]) - ) + const competitorIdToPrizePayout = competitors + ? Object.fromEntries( + competitors.map(({ id }) => [ + id, + (prizeBudget * competitorIdToEstimatedPercentage[id]) / 100, + ]) + ) + : {} const handleSubmit = async () => { const totalPercentage = Object.values(distribution).reduce( From 215693fbc6efd0e9c1bd61e96cfe1d84285a22b6 Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Thu, 7 Nov 2024 11:14:23 -0800 Subject: [PATCH 08/78] wip --- contracts/README.md | 5 +- contracts/curve.py | 671 ++++++++++++++++++ contracts/foundry.toml | 10 +- contracts/scripts/dev-deploy.ts | 60 +- contracts/scripts/helpers.ts | 19 +- .../src/governance/SmartWalletChecker.sol | 63 ++ contracts/src/governance/VotingEscrow.vy | 4 +- .../src/governance/VotingEscrowDepositor.sol | 25 + contracts/src/sweepstakes/mooney_mumbai.sol | 35 - contracts/src/sweepstakes/tts2_mumbai.sol | 250 ------- contracts/src/test/MerkleDistributorV2.t.sol | 112 --- .../src/test/VotingEscrowDepositor.t.sol | 76 ++ ui/components/nance/DePrize.tsx | 81 ++- ui/const/abis/REVDeployer.json | 1 + ui/const/config.ts | 10 + ui/lib/revnet/hooks/useIsOperator.tsx | 21 + ui/lib/tokens/hooks/useTokenBalances.tsx | 2 +- ui/lib/tokens/hooks/useTokenSupply.tsx | 16 + ui/pages/_app.tsx | 2 - ui/public/sitemap-0.xml | 57 +- 20 files changed, 1046 insertions(+), 474 deletions(-) create mode 100644 contracts/curve.py create mode 100644 contracts/src/governance/SmartWalletChecker.sol create mode 100644 contracts/src/governance/VotingEscrowDepositor.sol delete mode 100644 contracts/src/sweepstakes/mooney_mumbai.sol delete mode 100644 contracts/src/sweepstakes/tts2_mumbai.sol delete mode 100644 contracts/src/test/MerkleDistributorV2.t.sol create mode 100644 contracts/src/test/VotingEscrowDepositor.t.sol create mode 100644 ui/const/abis/REVDeployer.json create mode 100644 ui/lib/revnet/hooks/useIsOperator.tsx create mode 100644 ui/lib/tokens/hooks/useTokenSupply.tsx diff --git a/contracts/README.md b/contracts/README.md index 84426e88e..52d3dbbc1 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -33,7 +33,10 @@ yarn install cp .env.sample .env # Install Vyper -pip install vyper==0.2.4 +pip install vyper==0.3.1 + +# Add vyper to path +export PATH="$PATH:$HOME/Library/Python/3.9/bin" # Install Git submodules forge install diff --git a/contracts/curve.py b/contracts/curve.py new file mode 100644 index 000000000..d92c57af6 --- /dev/null +++ b/contracts/curve.py @@ -0,0 +1,671 @@ +# @version 0.3.0 +""" +@title Voting Escrow +@author Curve Finance +@license MIT +@notice Votes have a weight depending on time, so that users are + committed to the future of (whatever they are voting for) +@dev Vote weight decays linearly over time. Lock time cannot be + more than `MAXTIME` (4 years). +""" + +# Voting escrow to have time-weighted votes +# Votes have a weight depending on time, so that users are committed +# to the future of (whatever they are voting for). +# The weight in this implementation is linear, and lock cannot be more than maxtime: +# w ^ +# 1 + / +# | / +# | / +# | / +# |/ +# 0 +--------+------> time +# maxtime (4 years?) + +struct Point: + bias: int128 + slope: int128 # - dweight / dt + ts: uint256 + blk: uint256 # block +# We cannot really do block numbers per se b/c slope is per time, not per block +# and per block could be fairly bad b/c Ethereum changes blocktimes. +# What we can do is to extrapolate ***At functions + +struct LockedBalance: + amount: int128 + end: uint256 + + +interface ERC20: + def decimals() -> uint256: view + def name() -> String[64]: view + def symbol() -> String[32]: view + def transfer(to: address, amount: uint256) -> bool: nonpayable + def transferFrom(spender: address, to: address, amount: uint256) -> bool: nonpayable + + +# Interface for checking whether address belongs to a whitelisted +# type of a smart wallet. +# When new types are added - the whole contract is changed +# The check() method is modifying to be able to use caching +# for individual wallet addresses +interface SmartWalletChecker: + def check(addr: address) -> bool: nonpayable + +DEPOSIT_FOR_TYPE: constant(int128) = 0 +CREATE_LOCK_TYPE: constant(int128) = 1 +INCREASE_LOCK_AMOUNT: constant(int128) = 2 +INCREASE_UNLOCK_TIME: constant(int128) = 3 + + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Deposit: + provider: indexed(address) + value: uint256 + locktime: indexed(uint256) + type: int128 + ts: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + ts: uint256 + +event Supply: + prevSupply: uint256 + supply: uint256 + + +WEEK: constant(uint256) = 7 * 86400 # all future times are rounded by week +MAXTIME: constant(uint256) = 4 * 365 * 86400 # 4 years +MULTIPLIER: constant(uint256) = 10 ** 18 + +token: public(address) +supply: public(uint256) + +locked: public(HashMap[address, LockedBalance]) + +epoch: public(uint256) +point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point +user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch] +user_point_epoch: public(HashMap[address, uint256]) +slope_changes: public(HashMap[uint256, int128]) # time -> signed slope change + +# Aragon's view methods for compatibility +controller: public(address) +transfersEnabled: public(bool) + +name: public(String[64]) +symbol: public(String[32]) +version: public(String[32]) +decimals: public(uint256) + +# Checker for whitelisted (smart contract) wallets which are allowed to deposit +# The goal is to prevent tokenizing the escrow +future_smart_wallet_checker: public(address) +smart_wallet_checker: public(address) + +admin: public(address) # Can and will be a smart contract +future_admin: public(address) + + +@external +def __init__(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]): + """ + @notice Contract constructor + @param token_addr `ERC20CRV` token address + @param _name Token name + @param _symbol Token symbol + @param _version Contract version - required for Aragon compatibility + """ + self.admin = msg.sender + self.token = token_addr + self.point_history[0].blk = block.number + self.point_history[0].ts = block.timestamp + self.controller = msg.sender + self.transfersEnabled = True + + _decimals: uint256 = ERC20(token_addr).decimals() + assert _decimals <= 255 + self.decimals = _decimals + + self.name = _name + self.symbol = _symbol + self.version = _version + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of VotingEscrow contract to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def apply_transfer_ownership(): + """ + @notice Apply ownership transfer + """ + assert msg.sender == self.admin # dev: admin only + _admin: address = self.future_admin + assert _admin != ZERO_ADDRESS # dev: admin not set + self.admin = _admin + log ApplyOwnership(_admin) + + +@external +def commit_smart_wallet_checker(addr: address): + """ + @notice Set an external contract to check for approved smart contract wallets + @param addr Address of Smart contract checker + """ + assert msg.sender == self.admin + self.future_smart_wallet_checker = addr + + +@external +def apply_smart_wallet_checker(): + """ + @notice Apply setting external contract to check approved smart contract wallets + """ + assert msg.sender == self.admin + self.smart_wallet_checker = self.future_smart_wallet_checker + + +@internal +def assert_not_contract(addr: address): + """ + @notice Check if the call is from a whitelisted smart contract, revert if not + @param addr Address to be checked + """ + if addr != tx.origin: + checker: address = self.smart_wallet_checker + if checker != ZERO_ADDRESS: + if SmartWalletChecker(checker).check(addr): + return + raise "Smart contract depositors not allowed" + + +@external +@view +def get_last_user_slope(addr: address) -> int128: + """ + @notice Get the most recently recorded rate of voting power decrease for `addr` + @param addr Address of the user wallet + @return Value of the slope + """ + uepoch: uint256 = self.user_point_epoch[addr] + return self.user_point_history[addr][uepoch].slope + + +@external +@view +def user_point_history__ts(_addr: address, _idx: uint256) -> uint256: + """ + @notice Get the timestamp for checkpoint `_idx` for `_addr` + @param _addr User wallet address + @param _idx User epoch number + @return Epoch time of the checkpoint + """ + return self.user_point_history[_addr][_idx].ts + + +@external +@view +def locked__end(_addr: address) -> uint256: + """ + @notice Get timestamp when `_addr`'s lock finishes + @param _addr User wallet + @return Epoch time of the lock end + """ + return self.locked[_addr].end + + +@internal +def _checkpoint(addr: address, old_locked: LockedBalance, new_locked: LockedBalance): + """ + @notice Record global and per-user data to checkpoint + @param addr User's wallet address. No user checkpoint if 0x0 + @param old_locked Pevious locked amount / end lock time for the user + @param new_locked New locked amount / end lock time for the user + """ + u_old: Point = empty(Point) + u_new: Point = empty(Point) + old_dslope: int128 = 0 + new_dslope: int128 = 0 + _epoch: uint256 = self.epoch + + if addr != ZERO_ADDRESS: + # Calculate slopes and biases + # Kept at zero when they have to + if old_locked.end > block.timestamp and old_locked.amount > 0: + u_old.slope = old_locked.amount / MAXTIME + u_old.bias = u_old.slope * convert(old_locked.end - block.timestamp, int128) + if new_locked.end > block.timestamp and new_locked.amount > 0: + u_new.slope = new_locked.amount / MAXTIME + u_new.bias = u_new.slope * convert(new_locked.end - block.timestamp, int128) + + # Read values of scheduled changes in the slope + # old_locked.end can be in the past and in the future + # new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros + old_dslope = self.slope_changes[old_locked.end] + if new_locked.end != 0: + if new_locked.end == old_locked.end: + new_dslope = old_dslope + else: + new_dslope = self.slope_changes[new_locked.end] + + last_point: Point = Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number}) + if _epoch > 0: + last_point = self.point_history[_epoch] + last_checkpoint: uint256 = last_point.ts + # initial_last_point is used for extrapolation to calculate block number + # (approximately, for *At methods) and save them + # as we cannot figure that out exactly from inside the contract + initial_last_point: Point = last_point + block_slope: uint256 = 0 # dblock/dt + if block.timestamp > last_point.ts: + block_slope = MULTIPLIER * (block.number - last_point.blk) / (block.timestamp - last_point.ts) + # If last point is already recorded in this block, slope=0 + # But that's ok b/c we know the block in such case + + # Go over weeks to fill history and calculate what the current point is + t_i: uint256 = (last_checkpoint / WEEK) * WEEK + for i in range(255): + # Hopefully it won't happen that this won't get used in 5 years! + # If it does, users will be able to withdraw but vote weight will be broken + t_i += WEEK + d_slope: int128 = 0 + if t_i > block.timestamp: + t_i = block.timestamp + else: + d_slope = self.slope_changes[t_i] + last_point.bias -= last_point.slope * convert(t_i - last_checkpoint, int128) + last_point.slope += d_slope + if last_point.bias < 0: # This can happen + last_point.bias = 0 + if last_point.slope < 0: # This cannot happen - just in case + last_point.slope = 0 + last_checkpoint = t_i + last_point.ts = t_i + last_point.blk = initial_last_point.blk + block_slope * (t_i - initial_last_point.ts) / MULTIPLIER + _epoch += 1 + if t_i == block.timestamp: + last_point.blk = block.number + break + else: + self.point_history[_epoch] = last_point + + self.epoch = _epoch + # Now point_history is filled until t=now + + if addr != ZERO_ADDRESS: + # If last point was in this block, the slope change has been applied already + # But in such case we have 0 slope(s) + last_point.slope += (u_new.slope - u_old.slope) + last_point.bias += (u_new.bias - u_old.bias) + if last_point.slope < 0: + last_point.slope = 0 + if last_point.bias < 0: + last_point.bias = 0 + + # Record the changed point into history + self.point_history[_epoch] = last_point + + if addr != ZERO_ADDRESS: + # Schedule the slope changes (slope is going down) + # We subtract new_user_slope from [new_locked.end] + # and add old_user_slope to [old_locked.end] + if old_locked.end > block.timestamp: + # old_dslope was - u_old.slope, so we cancel that + old_dslope += u_old.slope + if new_locked.end == old_locked.end: + old_dslope -= u_new.slope # It was a new deposit, not extension + self.slope_changes[old_locked.end] = old_dslope + + if new_locked.end > block.timestamp: + if new_locked.end > old_locked.end: + new_dslope -= u_new.slope # old slope disappeared at this point + self.slope_changes[new_locked.end] = new_dslope + # else: we recorded it already in old_dslope + + # Now handle user history + user_epoch: uint256 = self.user_point_epoch[addr] + 1 + + self.user_point_epoch[addr] = user_epoch + u_new.ts = block.timestamp + u_new.blk = block.number + self.user_point_history[addr][user_epoch] = u_new + + +@internal +def _deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128): + """ + @notice Deposit and lock tokens for a user + @param _addr User's wallet address + @param _value Amount to deposit + @param unlock_time New time when to unlock the tokens, or 0 if unchanged + @param locked_balance Previous locked amount / timestamp + """ + _locked: LockedBalance = locked_balance + supply_before: uint256 = self.supply + + self.supply = supply_before + _value + old_locked: LockedBalance = _locked + # Adding to existing lock, or if a lock is expired - creating a new one + _locked.amount += convert(_value, int128) + if unlock_time != 0: + _locked.end = unlock_time + self.locked[_addr] = _locked + + # Possibilities: + # Both old_locked.end could be current or expired (>/< block.timestamp) + # value == 0 (extend lock) or value > 0 (add to lock or extend lock) + # _locked.end > block.timestamp (always) + self._checkpoint(_addr, old_locked, _locked) + + if _value != 0: + assert ERC20(self.token).transferFrom(_addr, self, _value) + + log Deposit(_addr, _value, _locked.end, type, block.timestamp) + log Supply(supply_before, supply_before + _value) + + +@external +def checkpoint(): + """ + @notice Record global data to checkpoint + """ + self._checkpoint(ZERO_ADDRESS, empty(LockedBalance), empty(LockedBalance)) + + +@external +@nonreentrant('lock') +def deposit_for(_addr: address, _value: uint256): + """ + @notice Deposit `_value` tokens for `_addr` and add to the lock + @dev Anyone (even a smart contract) can deposit for someone else, but + cannot extend their locktime and deposit for a brand new user + @param _addr User's wallet address + @param _value Amount to add to user's lock + """ + _locked: LockedBalance = self.locked[_addr] + + assert _value > 0 # dev: need non-zero value + assert _locked.amount > 0, "No existing lock found" + assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw" + + self._deposit_for(_addr, _value, 0, self.locked[_addr], DEPOSIT_FOR_TYPE) + + +@external +@nonreentrant('lock') +def create_lock(_value: uint256, _unlock_time: uint256): + """ + @notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time` + @param _value Amount to deposit + @param _unlock_time Epoch time when tokens unlock, rounded down to whole weeks + """ + self.assert_not_contract(msg.sender) + unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks + _locked: LockedBalance = self.locked[msg.sender] + + assert _value > 0 # dev: need non-zero value + assert _locked.amount == 0, "Withdraw old tokens first" + assert unlock_time > block.timestamp, "Can only lock until time in the future" + assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max" + + self._deposit_for(msg.sender, _value, unlock_time, _locked, CREATE_LOCK_TYPE) + + +@external +@nonreentrant('lock') +def increase_amount(_value: uint256): + """ + @notice Deposit `_value` additional tokens for `msg.sender` + without modifying the unlock time + @param _value Amount of tokens to deposit and add to the lock + """ + self.assert_not_contract(msg.sender) + _locked: LockedBalance = self.locked[msg.sender] + + assert _value > 0 # dev: need non-zero value + assert _locked.amount > 0, "No existing lock found" + assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw" + + self._deposit_for(msg.sender, _value, 0, _locked, INCREASE_LOCK_AMOUNT) + + +@external +@nonreentrant('lock') +def increase_unlock_time(_unlock_time: uint256): + """ + @notice Extend the unlock time for `msg.sender` to `_unlock_time` + @param _unlock_time New epoch time for unlocking + """ + self.assert_not_contract(msg.sender) + _locked: LockedBalance = self.locked[msg.sender] + unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks + + assert _locked.end > block.timestamp, "Lock expired" + assert _locked.amount > 0, "Nothing is locked" + assert unlock_time > _locked.end, "Can only increase lock duration" + assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max" + + self._deposit_for(msg.sender, 0, unlock_time, _locked, INCREASE_UNLOCK_TIME) + + +@external +@nonreentrant('lock') +def withdraw(): + """ + @notice Withdraw all tokens for `msg.sender` + @dev Only possible if the lock has expired + """ + _locked: LockedBalance = self.locked[msg.sender] + assert block.timestamp >= _locked.end, "The lock didn't expire" + value: uint256 = convert(_locked.amount, uint256) + + old_locked: LockedBalance = _locked + _locked.end = 0 + _locked.amount = 0 + self.locked[msg.sender] = _locked + supply_before: uint256 = self.supply + self.supply = supply_before - value + + # old_locked can have either expired <= timestamp or zero end + # _locked has only 0 end + # Both can have >= 0 amount + self._checkpoint(msg.sender, old_locked, _locked) + + assert ERC20(self.token).transfer(msg.sender, value) + + log Withdraw(msg.sender, value, block.timestamp) + log Supply(supply_before, supply_before - value) + + +# The following ERC20/minime-compatible methods are not real balanceOf and supply! +# They measure the weights for the purpose of voting, so they don't represent +# real coins. + +@internal +@view +def find_block_epoch(_block: uint256, max_epoch: uint256) -> uint256: + """ + @notice Binary search to estimate timestamp for block number + @param _block Block to find + @param max_epoch Don't go beyond this epoch + @return Approximate timestamp for block + """ + # Binary search + _min: uint256 = 0 + _max: uint256 = max_epoch + for i in range(128): # Will be always enough for 128-bit numbers + if _min >= _max: + break + _mid: uint256 = (_min + _max + 1) / 2 + if self.point_history[_mid].blk <= _block: + _min = _mid + else: + _max = _mid - 1 + return _min + + +@external +@view +def balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256: + """ + @notice Get the current voting power for `msg.sender` + @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility + @param addr User wallet address + @param _t Epoch time to return voting power at + @return User voting power + """ + _epoch: uint256 = self.user_point_epoch[addr] + if _epoch == 0: + return 0 + else: + last_point: Point = self.user_point_history[addr][_epoch] + last_point.bias -= last_point.slope * convert(_t - last_point.ts, int128) + if last_point.bias < 0: + last_point.bias = 0 + return convert(last_point.bias, uint256) + + +@external +@view +def balanceOfAt(addr: address, _block: uint256) -> uint256: + """ + @notice Measure voting power of `addr` at block height `_block` + @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime + @param addr User's wallet address + @param _block Block to calculate the voting power at + @return Voting power + """ + # Copying and pasting totalSupply code because Vyper cannot pass by + # reference yet + assert _block <= block.number + + # Binary search + _min: uint256 = 0 + _max: uint256 = self.user_point_epoch[addr] + for i in range(128): # Will be always enough for 128-bit numbers + if _min >= _max: + break + _mid: uint256 = (_min + _max + 1) / 2 + if self.user_point_history[addr][_mid].blk <= _block: + _min = _mid + else: + _max = _mid - 1 + + upoint: Point = self.user_point_history[addr][_min] + + max_epoch: uint256 = self.epoch + _epoch: uint256 = self.find_block_epoch(_block, max_epoch) + point_0: Point = self.point_history[_epoch] + d_block: uint256 = 0 + d_t: uint256 = 0 + if _epoch < max_epoch: + point_1: Point = self.point_history[_epoch + 1] + d_block = point_1.blk - point_0.blk + d_t = point_1.ts - point_0.ts + else: + d_block = block.number - point_0.blk + d_t = block.timestamp - point_0.ts + block_time: uint256 = point_0.ts + if d_block != 0: + block_time += d_t * (_block - point_0.blk) / d_block + + upoint.bias -= upoint.slope * convert(block_time - upoint.ts, int128) + if upoint.bias >= 0: + return convert(upoint.bias, uint256) + else: + return 0 + + +@internal +@view +def supply_at(point: Point, t: uint256) -> uint256: + """ + @notice Calculate total voting power at some point in the past + @param point The point (bias/slope) to start search from + @param t Time to calculate the total voting power at + @return Total voting power at that time + """ + last_point: Point = point + t_i: uint256 = (last_point.ts / WEEK) * WEEK + for i in range(255): + t_i += WEEK + d_slope: int128 = 0 + if t_i > t: + t_i = t + else: + d_slope = self.slope_changes[t_i] + last_point.bias -= last_point.slope * convert(t_i - last_point.ts, int128) + if t_i == t: + break + last_point.slope += d_slope + last_point.ts = t_i + + if last_point.bias < 0: + last_point.bias = 0 + return convert(last_point.bias, uint256) + + +@external +@view +def totalSupply(t: uint256 = block.timestamp) -> uint256: + """ + @notice Calculate total voting power + @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility + @return Total voting power + """ + _epoch: uint256 = self.epoch + last_point: Point = self.point_history[_epoch] + return self.supply_at(last_point, t) + + +@external +@view +def totalSupplyAt(_block: uint256) -> uint256: + """ + @notice Calculate total voting power at some point in the past + @param _block Block to calculate the total voting power at + @return Total voting power at `_block` + """ + assert _block <= block.number + _epoch: uint256 = self.epoch + target_epoch: uint256 = self.find_block_epoch(_block, _epoch) + + point: Point = self.point_history[target_epoch] + dt: uint256 = 0 + if target_epoch < _epoch: + point_next: Point = self.point_history[target_epoch + 1] + if point.blk != point_next.blk: + dt = (_block - point.blk) * (point_next.ts - point.ts) / (point_next.blk - point.blk) + else: + if point.blk != block.number: + dt = (_block - point.blk) * (block.timestamp - point.ts) / (block.number - point.blk) + # Now dt contains info on how far are we beyond point + + return self.supply_at(point, point.ts + dt) + + +# Dummy methods for compatibility with Aragon + +@external +def changeController(_newController: address): + """ + @dev Dummy method required for Aragon compatibility + """ + assert msg.sender == self.controller + self.controller = _newController diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 8d2dd1062..2fe11be02 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -1,4 +1,12 @@ -[default] +[profile.default] src = "src" out = "out" libs = ["lib"] +evm_version = "berlin" + +#[profile.default.compiler] +#version = "0.8.17" + +# Vyper compiler settings +[profile.default.vyper] +vyper_version = "0.3.1" diff --git a/contracts/scripts/dev-deploy.ts b/contracts/scripts/dev-deploy.ts index 971d79fb4..b3ef3ee6c 100644 --- a/contracts/scripts/dev-deploy.ts +++ b/contracts/scripts/dev-deploy.ts @@ -1,22 +1,33 @@ import { wallet, dec, save } from "./helpers"; import { ethers, BigNumber, Contract } from "ethers"; -import { formatUnits } from "@ethersproject/units" +import { formatUnits } from "@ethersproject/units"; -import MyToken from '../out/MyToken.sol/MyToken.json'; -import VotingEscrow from '../out/VotingEscrow.vy/VotingEscrow.json'; -import MerkleDistributorV2 from '../out/MerkleDistributorV2.sol/MerkleDistributorV2.json'; +import MyToken from "../out/MyToken.sol/MyToken.json"; +import VotingEscrow from "../out/VotingEscrow.vy/VotingEscrow.json"; const getContractFactory = (artifact: any) => { return new ethers.ContractFactory(artifact.abi, artifact.bytecode.object, wallet); -} - -const deployContract = async ({ name, deployer, factory, args }: { name: string, deployer: ethers.Wallet, factory: ethers.ContractFactory, args: Array}) => { - console.log(`Deploying ${name}..`) - const contract = await factory.connect(deployer).deploy(...args); +}; + +const deployContract = async ({ + name, + deployer, + factory, + args, +}: { + name: string; + deployer: ethers.Wallet; + factory: ethers.ContractFactory; + args: Array; +}) => { + console.log(`Deploying ${name}..`); + const contract = await factory.connect(deployer).deploy(...args, { + gasLimit: BigNumber.from(8000000), + }); await contract.deployed(); - console.log(`Deployed ${name} to: ${contract.address}`) + console.log(`Deployed ${name} to: ${contract.address}`); return contract; -} +}; const deployMOONEY = async () => { const supply: BigNumber = BigNumber.from(process.env.MOONEY_SUPPLY ?? dec(42069, 18)); @@ -26,14 +37,14 @@ const deployMOONEY = async () => { name: "MOONEY", deployer: wallet, factory: Factory, - args: [] + args: ["Mooney", "MOONEY"], }); - await MOONEYToken.connect(wallet).mint(wallet.address, supply); - console.log(`Minted ${formatUnits(supply, 18)} tokens to deployer address`) + //await MOONEYToken.connect(wallet).mint(wallet.address, supply); + console.log(`Minted ${formatUnits(supply, 18)} tokens to deployer address`); return MOONEYToken; -} +}; const deployVMOONEY = async (MOONEYToken: Contract) => { const Factory = getContractFactory(VotingEscrow); @@ -42,11 +53,11 @@ const deployVMOONEY = async (MOONEYToken: Contract) => { name: "vMOONEY", deployer: wallet, factory: Factory, - args: [MOONEYToken.address, "Vote-escrowed MOONEY", "vMOONEY", "vMOONEY_1.0.0"] - }) + args: [MOONEYToken.address, "Vote-escrowed MOONEY", "vMOONEY", "vMOONEY_1.0.0"], + }); return vMOONEY; -} +}; // const deployAirdropDistributor = async (MOONEYToken: Contract, root: string) => { // const Factory = getContractFactory(MerkleDistributorV2); @@ -119,7 +130,7 @@ const deployVMOONEY = async (MOONEYToken: Contract) => { // factory: passportIssuerFactory, // args: [] // }) - + // await passportToken.connect(wallet).transferControl(passportIssuer.address); // // TODO: Set renderer @@ -139,16 +150,15 @@ const main = async () => { const vMOONEY = await deployVMOONEY(MOONEY); const deployment = { - "MOONEYToken": MOONEY.address, - "vMOONEYToken": vMOONEY.address, - } + MOONEYToken: MOONEY.address, + vMOONEYToken: vMOONEY.address, + }; const manifestFile = "./deployments/local.json"; save(deployment, manifestFile); - console.log(`Deployment manifest saved to ${manifestFile}`) -} - + console.log(`Deployment manifest saved to ${manifestFile}`); +}; main().catch((error) => { console.error(error); diff --git a/contracts/scripts/helpers.ts b/contracts/scripts/helpers.ts index dbbe5c4ea..7cec616e4 100644 --- a/contracts/scripts/helpers.ts +++ b/contracts/scripts/helpers.ts @@ -1,7 +1,7 @@ import fs from "fs"; import { ethers } from "ethers"; import dotenv from "dotenv"; -dotenv.config() +dotenv.config(); let validConfig = true; if (process.env.RPC_URL === undefined) { @@ -14,19 +14,18 @@ if (process.env.DEPLOYER_PRIVATE_KEY === undefined) { } if (!validConfig) process.exit(1); -export const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL) -export const wallet = new ethers.Wallet(process.env.DEPLOYER_PRIVATE_KEY ?? "", provider) +export const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL); +export const wallet = new ethers.Wallet(process.env.DEPLOYER_PRIVATE_KEY ?? "", provider); // Deployment helpers export const dec = (val: number, scale: number) => { const zerosCount = scale; const strVal = val.toString(); - const strZeros = ('0').repeat(zerosCount); + const strZeros = "0".repeat(zerosCount); return strVal.concat(strZeros); - } - +}; export const save = (info: object, path: string) => { const content = JSON.stringify(info, null, 1); @@ -37,7 +36,7 @@ export const save = (info: object, path: string) => { fs.mkdirSync(dir, { recursive: true }); } - return fs.writeFile(path, content, { encoding: "utf-8"}, (err) => { if(err) console.log(err); }) -} - - + return fs.writeFile(path, content, { encoding: "utf-8" }, (err) => { + if (err) console.log(err); + }); +}; diff --git a/contracts/src/governance/SmartWalletChecker.sol b/contracts/src/governance/SmartWalletChecker.sol new file mode 100644 index 000000000..5b9be3925 --- /dev/null +++ b/contracts/src/governance/SmartWalletChecker.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.6; + +//import "@openzeppelin/contracts-4.2.0/access/Ownable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract SmartWalletChecker is Ownable { + bool public isWhitelistEnabled; + mapping(address => bool) public wallets; + address public checker; + address public future_checker; + + event ApproveWallet(address); + event RevokeWallet(address); + event WhitelistEnabled(bool); + + constructor(bool _isWhitelistEnabled) public Ownable() { + // Set state variables + setIsWhitelistEnabled(_isWhitelistEnabled); + } + + function commitSetChecker(address _checker) external onlyOwner { + future_checker = _checker; + } + + function applySetChecker() external onlyOwner { + checker = future_checker; + } + + function approveWallet(address _wallet) external onlyOwner { + wallets[_wallet] = true; + + emit ApproveWallet(_wallet); + } + + function revokeWallet(address _wallet) external onlyOwner { + wallets[_wallet] = false; + + emit RevokeWallet(_wallet); + } + + function setIsWhitelistEnabled(bool _isWhitelistEnabled) public onlyOwner { + isWhitelistEnabled = _isWhitelistEnabled; + + emit WhitelistEnabled(_isWhitelistEnabled); + } + + function check(address _wallet) external view returns (bool) { + if (!isWhitelistEnabled) { + return true; + } + + bool _check = wallets[_wallet]; + if (_check) { + return _check; + } else { + if (checker != address(0)) { + return SmartWalletChecker(checker).check(_wallet); + } + } + return false; + } +} diff --git a/contracts/src/governance/VotingEscrow.vy b/contracts/src/governance/VotingEscrow.vy index bd09ec9b2..c61a69a02 100644 --- a/contracts/src/governance/VotingEscrow.vy +++ b/contracts/src/governance/VotingEscrow.vy @@ -1,4 +1,4 @@ -# @version 0.3.0 +# @version 0.3.1 """ @title Voting Escrow @author Curve Finance @@ -668,4 +668,4 @@ def changeController(_newController: address): @dev Dummy method required for Aragon compatibility """ assert msg.sender == self.controller - self.controller = _newController \ No newline at end of file + self.controller = _newController diff --git a/contracts/src/governance/VotingEscrowDepositor.sol b/contracts/src/governance/VotingEscrowDepositor.sol new file mode 100644 index 000000000..984098592 --- /dev/null +++ b/contracts/src/governance/VotingEscrowDepositor.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC20 { + function transfer(address recipient, uint256 amount) external returns (bool); +} + +interface IVotingEscrow { + function deposit_for(address _addr, uint256 _value) external; +} + +contract VotingEscrowDepositor { + IERC20 public token; + IVotingEscrow public escrowToken; + + constructor(address _tokenAddress, address _escrowTokenAddress) { + token = IERC20(_tokenAddress); + escrowToken = IVotingEscrow(_escrowTokenAddress); + } + + function transfer_and_deposit_for(address addr, uint256 value) external { + require(token.transfer(addr, value), "Token transfer failed"); + escrowToken.deposit_for(addr, value); + } +} diff --git a/contracts/src/sweepstakes/mooney_mumbai.sol b/contracts/src/sweepstakes/mooney_mumbai.sol deleted file mode 100644 index b0773447a..000000000 --- a/contracts/src/sweepstakes/mooney_mumbai.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.6; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; -import "@paulrberg/contracts/token/erc20/Erc20Permit.sol"; - -import "./ITickets.sol"; - -import "@openzeppelin/contracts/access/Ownable.sol"; - -contract MumbaiMooney is ERC20, ERC20Permit, Ownable, ITickets { - constructor(string memory _name, string memory _symbol) - ERC20(_name, _symbol) - ERC20Permit(_name) - { - _mint(msg.sender, 1000000 * 10 ** 18); - } - - function print(address _account, uint256 _amount) - external - override - onlyOwner - { - return _mint(_account, _amount); - } - - function redeem(address _account, uint256 _amount) - external - override - onlyOwner - { - return _burn(_account, _amount); - } -} \ No newline at end of file diff --git a/contracts/src/sweepstakes/tts2_mumbai.sol b/contracts/src/sweepstakes/tts2_mumbai.sol deleted file mode 100644 index bc05bb2e3..000000000 --- a/contracts/src/sweepstakes/tts2_mumbai.sol +++ /dev/null @@ -1,250 +0,0 @@ -/* - NAME: TICKET-TO-SPACE-2 - CHAIN: MAINNET -*/ - -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import "@thirdweb-dev/contracts/eip/ERC721A.sol"; -import "@thirdweb-dev/contracts/extension/Ownable.sol"; -import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol"; -import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; -import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; -import "@openzeppelin/contracts/utils/Base64.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; - -contract TicketToSpace2 is ERC721A, Ownable, VRFConsumerBaseV2 { - VRFCoordinatorV2Interface COORDINATOR; - LinkTokenInterface immutable LINKTOKEN = - LinkTokenInterface(0x326C977E6efc84E512bB9C30f76E30c160eD06FB); //https://vrf.chain.link/mainnet - bytes32 immutable keyHash = - 0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f; //200gwei mainnet - address immutable vrfCoordinator_ = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed; - - struct RequestStatus { - bool paid; // paid? - bool fulfilled; // whether the request has been successfully fulfilled - uint256[] randomWords; - } - - mapping(uint256 => RequestStatus) - public s_requests; /* requestId --> requestStatus */ - - mapping(uint256 => address) - public winners; - - uint256 public winnersCount = 5; - - string private _nftName = "Ticket to Space NFT 2"; - string private _image = "ipfs://Qmba3umb3db7DqCA19iRSSbtzv9nYUmP8Cibo5QMkLpgpP"; - - IERC20 public mooneyToken = IERC20(0x3818f3273D1f46259b737342Ad30e576A7A74f09); - uint256 public mintPrice = 100 * 10 ** 18; - - bytes32 public previousEntrantsMerkleRoot; - - //VRF subscription ID. - uint64 s_subscriptionId; - - uint256[] public requestIds; - uint256 public lastRequestId; - - uint16 immutable requestConfirmations = 6; - uint32 immutable numWords = 1; - uint256 public immutable maxTokens = 2**256 - 1; - uint256 public immutable maxWalletMints = 50; - - bool public allowTransfers = false; - bool public allowMinting = false; - - bool internal locked; //re-entry lock - - //EVENTS - event RequestSent(uint256 requestId, uint32 numWords); - event RequestFulfilled(uint256 requestId, uint256[] randomWords); - event WinnerSelected(uint256 winnerNum, address winner); - - constructor() VRFConsumerBaseV2(0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed) ERC721A("Ticket to Space 2", "TTS2") { - COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator_); - _setupOwner(msg.sender); - } - - //MODIFIERS - modifier reEntrancyGuard(){ - require(!locked, "No re-entrancy"); - locked = true; - _; - locked = false; - } - - //FUNCTIONS - function _canSetOwner() internal view virtual override returns (bool) { - return msg.sender == owner(); - } - - function setImage(string memory image) public onlyOwner { - _image = image; - } - - function setSubscript(uint64 subscriptionId_) external onlyOwner { - s_subscriptionId = subscriptionId_; - } - - function setAllowTransfers(bool allowTransfers_) external onlyOwner { - allowTransfers = allowTransfers_; - } - - function setAllowMinting(bool allowMinting_) external onlyOwner { - allowMinting = allowMinting_; - } - - function setMooneyToken(IERC20 mooneyToken_) external onlyOwner { - mooneyToken = mooneyToken_; - } - - function setMintPrice(uint256 mintPrice_) external onlyOwner { - mintPrice = mintPrice_; - } - - function addMerkleRoot(bytes32 _merkleRoot) external onlyOwner { - previousEntrantsMerkleRoot = _merkleRoot; - } - - function toString(uint256 value) internal pure returns (string memory) { - if (value == 0) { - return "0"; - } - uint256 temp = value; - uint256 digits; - while (temp != 0) { - digits++; - temp /= 10; - } - bytes memory buffer = new bytes(digits); - while (value != 0) { - digits -= 1; - buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); - value /= 10; - } - return string(buffer); - } - - function tokenURI(uint256 tokenId) public view virtual override returns (string memory){ - require(_exists(tokenId), "URI query for nonexistent token"); - string memory json = Base64.encode( - bytes( - string( - abi.encodePacked( - '{"name": "', - _nftName, - " #", - toString(tokenId), - '", "image": "', - _image, - '"}' - ) - ) - ) - ); - return string(abi.encodePacked("data:application/json;base64,", json)); - } - - function getSupply() public view returns (uint256) { - return _currentIndex; - } - - function canClaimFree(bytes32[] calldata merkleProof, address receiver) - public - view - returns (bool) - { - - return - MerkleProof.verify( - merkleProof, - previousEntrantsMerkleRoot, - keccak256(abi.encodePacked(receiver)) - ) && balanceOf(receiver) == 0; - } - - function claimFree(bytes32[] calldata merkleProof) public { - require(allowMinting, "Minting is not currently open"); - require(_currentIndex < maxTokens, "error:10003 NFT mint limit reached"); - require(balanceOf(msg.sender) < maxWalletMints, "Address has already minted the maximum amount of NFTs"); - - address claimAddress = msg.sender; - require(canClaimFree(merkleProof, claimAddress), "Address cannot claim for free, or has already claimed"); - _safeMint(claimAddress, 1); - } - - function mint(uint256 count) public payable { - require(allowMinting, "Minting is not currently open"); - require(_currentIndex + count < maxTokens, "Tickets already minted"); - require(count + balanceOf(msg.sender) <= maxWalletMints, "Mint amount is more than allowed"); - - mooneyToken.transferFrom(msg.sender, address(0x000000000000000000000000000000000000dEaD), mintPrice*count); - _safeMint(msg.sender, count); - } - - function _beforeTokenTransfers(address from, address to, uint256 tokenId, uint256 batchSize) - internal - override - { - super._beforeTokenTransfers(from, to, tokenId, batchSize); - //non-transferable after mint until allowTransfers = true - if(from != address(0x0) && !allowTransfers) revert("Transfers disabled"); - } - - function chooseWinner(uint32 limit) external onlyOwner returns(uint256 requestId) { - uint32 callbackGasLimit = limit; - requestId = COORDINATOR.requestRandomWords( - keyHash, - s_subscriptionId, - requestConfirmations, - callbackGasLimit, - numWords - ); - s_requests[requestId] = RequestStatus({ - randomWords: new uint256[](0), - paid: true, - fulfilled: false - }); - requestIds.push(requestId); - lastRequestId = requestId; - emit RequestSent(requestId, numWords); - return requestId; - } - - function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) - internal - override - { - require(s_requests[_requestId].paid, "request not found"); - s_requests[_requestId].fulfilled = true; - s_requests[_requestId].randomWords = _randomWords; - addWinner(this.ownerOf(_randomWords[0] % _currentIndex)); - emit RequestFulfilled(_requestId, _randomWords); - } - - function getRequestStatus( - uint256 _requestId - ) - external - view - returns (bool paid, bool fulfilled, uint256[] memory randomWords) - { - RequestStatus memory request = s_requests[_requestId]; - return (request.paid, request.fulfilled, request.randomWords); - } - - function addWinner(address winner) internal { - require(winnersCount > 0, "All winners already chosen"); - winners[winnersCount] = winner; - winnersCount -= 1; - emit WinnerSelected(winnersCount, winner); - } -} diff --git a/contracts/src/test/MerkleDistributorV2.t.sol b/contracts/src/test/MerkleDistributorV2.t.sol deleted file mode 100644 index 34cad461c..000000000 --- a/contracts/src/test/MerkleDistributorV2.t.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity =0.8.10; - -import {DSTest} from "./utils/test.sol"; -import {Hevm} from "./utils/Hevm.sol"; -import {Signatures as sig} from "./utils/Signatures.sol"; -import {MockERC20} from "./utils/mocks/MockERC20.sol"; -import {MerkleDistributorV2} from "../distributors/MerkleDistributorV2.sol"; - -contract MerkleDistributorTest is DSTest { - Hevm evm = Hevm(HEVM_ADDRESS); - - MockERC20 token; - MerkleDistributorV2 distributor; - - uint256 public constant tokenSupply = 42069 * 1e21; - uint256 public constant airdropAllowance = 1337 * 1e21; - bytes32 public constant merkleRoot = 0x5cdea970c9f23ca3ad7c3d706650a7d1a1cf0269632e0059c9dcfcb544d3a5c8; - - function setUp() public { - token = new MockERC20("Nation3 Network Token", "NATION", tokenSupply); - distributor = new MerkleDistributorV2(); - distributor.setUp(address(this), token, merkleRoot); - // Set allowance for the airdrop - token.approve(address(distributor), airdropAllowance); - } - - function getValidClaimer() - public - pure - returns ( - uint256 index, - address account, - uint256 amount, - bytes32[] memory proofs - ) - { - index = 19; - account = 0xBC61c73CFc191321DA837def848784c002279a01; - amount = 5; - - proofs = new bytes32[](3); - proofs[0] = 0xebe77a4d8819f67553d4563538abf6fd6417c99b9b85486d47458fb8e42fbdd6; - proofs[1] = 0xc1e42302dfdf0b0d2c52bc4f0fdbb35f8b4d02b1a276fe895d77b4116d639f05; - proofs[2] = 0x5221b4a135c004944c72c1a4aa7cd3f203283a3a0822fabd5dc8efbab6689980; - } - - function testInit() public { - assertEq(distributor.sender(), address(this)); - assertEq(address(distributor.token()), address(token)); - assertEq(distributor.merkleRoot(), merkleRoot); - } - - function testClaim() public { - (uint256 index, address account, uint256 amount, bytes32[] memory proofs) = getValidClaimer(); - - distributor.claim(index, account, amount, proofs); - - // Check that the tokens has been sent from sender and the allowance adjust properly - uint256 diffSenderBalance = tokenSupply - token.balanceOf(address(this)); - uint256 diffDistributorAllowance = airdropAllowance - token.allowance(address(this), address(distributor)); - assertEq(token.balanceOf(account), amount); - assertEq(diffSenderBalance, amount); - assertEq(diffDistributorAllowance, amount); - - // Trying to claim again must revert - evm.expectRevert(sig.selector("AlreadyClaimed()")); - distributor.claim(index, account, amount, proofs); - } - - function testClaimInvalidProofs() public { - (uint256 index, address account, uint256 amount, bytes32[] memory proofs) = getValidClaimer(); - - // Invalid proofs - bytes32[] memory badProofs = new bytes32[](2); - badProofs[0] = proofs[1]; - badProofs[1] = proofs[0]; - - evm.expectRevert(sig.selector("InvalidProof()")); - distributor.claim(index, account, amount, badProofs); - } - - function testClaimInvalidAccount() public { - (uint256 index, , uint256 amount, bytes32[] memory proofs) = getValidClaimer(); - - // Random account - address badAccount = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; - - evm.expectRevert(sig.selector("InvalidProof()")); - distributor.claim(index, badAccount, amount, proofs); - } - - function testClaimInvalidAmount() public { - (uint256 index, address account, , bytes32[] memory proofs) = getValidClaimer(); - - // Random amount - uint256 badAmount = 20; - - evm.expectRevert(sig.selector("InvalidProof()")); - distributor.claim(index, account, badAmount, proofs); - } - - function testDropReset() public { - (uint256 index, address account, uint256 amount, bytes32[] memory proofs) = getValidClaimer(); - - distributor.claim(index, account, amount, proofs); - - distributor.setUp(address(this), token, merkleRoot); - - distributor.claim(index, account, amount, proofs); - } -} diff --git a/contracts/src/test/VotingEscrowDepositor.t.sol b/contracts/src/test/VotingEscrowDepositor.t.sol new file mode 100644 index 000000000..9a9374b1e --- /dev/null +++ b/contracts/src/test/VotingEscrowDepositor.t.sol @@ -0,0 +1,76 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../src/VotingEscrowDepositor.sol"; + +//Using deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +//Deploying MOONEY.. +//Deployed MOONEY to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 +//Minted 0.0000000001 tokens to deployer address +//Deploying vMOONEY.. +//Deployed vMOONEY to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 +//Deployment manifest saved to ./deployments/local.json + +address MOONEY = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512; +address vMOONEY = 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0; +contract MockToken is IERC20 { + mapping(address => uint256) public balanceOf; + + function transfer(address recipient, uint256 amount) external override returns (bool) { + require(balanceOf[msg.sender] >= amount, "Insufficient balance"); + balanceOf[msg.sender] -= amount; + balanceOf[recipient] += amount; + return true; + } + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + } +} + +contract MockVotingEscrow is IVotingEscrow { + mapping(address => uint256) public deposits; + + function deposit_for(address _addr, uint256 _value) external override { + deposits[_addr] += _value; + } +} + +contract VotingEscrowDepositorTest is Test { + VotingEscrowDepositor public depositor; + MockToken public token; + MockVotingEscrow public escrowToken; + + address public user = address(0x123); + uint256 public initialBalance = 1000; + + function setUp() public { + token = new MockToken(); + escrowToken = new MockVotingEscrow(); + depositor = new VotingEscrowDepositor(address(MOONEY), address(vMOONEY)); + + // Mint tokens for the depositor contract to transfer + token.mint(address(depositor), initialBalance); + } + + function testTransferAndDepositFor() public { + uint256 depositAmount = 500; + + // Verify initial balances and deposits + assertEq(token.balanceOf(address(depositor)), initialBalance); + assertEq(token.balanceOf(user), 0); + assertEq(escrowToken.deposits(user), 0); + + // Call transfer_and_deposit_for + depositor.transfer_and_deposit_for(user, depositAmount); + + // Verify token transfer + assertEq(token.balanceOf(address(depositor)), initialBalance - depositAmount); + assertEq(token.balanceOf(user), depositAmount); + + // Verify escrow deposit + assertEq(escrowToken.deposits(user), depositAmount); + } +} diff --git a/ui/components/nance/DePrize.tsx b/ui/components/nance/DePrize.tsx index 5da29c299..4fe415a90 100644 --- a/ui/components/nance/DePrize.tsx +++ b/ui/components/nance/DePrize.tsx @@ -1,12 +1,17 @@ import { Arbitrum, Sepolia } from '@thirdweb-dev/chains' import { useAddress, useContract } from '@thirdweb-dev/react' import ERC20 from 'const/abis/ERC20.json' +import REVDeployer from 'const/abis/REVDeployer.json' import { DEPRIZE_DISTRIBUTION_TABLE_ADDRESSES, SNAPSHOT_RETROACTIVE_REWARDS_ID, PRIZE_TOKEN_ADDRESSES, PRIZE_DECIMALS, + REVNET_ADDRESSES, + PRIZE_REVNET_ID, + BULK_TOKEN_SENDER_ADDRESSES, } from 'const/config' +import { BigNumber } from 'ethers' import _ from 'lodash' import { useState, useEffect } from 'react' import toast from 'react-hot-toast' @@ -14,9 +19,11 @@ import { useCitizens } from '@/lib/citizen/useCitizen' import { useAssets } from '@/lib/dashboard/hooks' import toastStyle from '@/lib/marketplace/marketplace-utils/toastConfig' import { SNAPSHOT_SPACE_NAME } from '@/lib/nance/constants' +import useIsOperator from '@/lib/revnet/hooks/useIsOperator' import { useVotingPowers } from '@/lib/snapshot' import useWindowSize from '@/lib/team/use-window-size' import useTokenBalances from '@/lib/tokens/hooks/useTokenBalances' +import useTokenSupply from '@/lib/tokens/hooks/useTokenSupply' import useWatchTokenBalance from '@/lib/tokens/hooks/useWatchTokenBalance' import { getBudget, getPayouts } from '@/lib/utils/rewards' import { runQuadraticVoting } from '@/lib/utils/voting' @@ -103,6 +110,14 @@ export function DePrize({ PRIZE_TOKEN_ADDRESSES[chain.slug], ERC20.abi ) + const { contract: revnetContract } = useContract( + REVNET_ADDRESSES[chain.slug], + REVDeployer.abi + ) + const { contract: bulkTokenSenderContract } = useContract( + BULK_TOKEN_SENDER_ADDRESSES[chain.slug] + ) + const isOperator = useIsOperator(revnetContract, userAddress, PRIZE_REVNET_ID) const prizeBalance = useWatchTokenBalance(prizeContract, PRIZE_DECIMALS) const tokenBalances = useTokenBalances( prizeContract, @@ -148,7 +163,9 @@ export function DePrize({ year, quarter ) - const prizeBudget = 2_500_000 + const prizeSupply = useTokenSupply(prizeContract, PRIZE_DECIMALS) + const prizeBudget = prizeSupply * 0.1 + const winnerPool = prizeSupply * 0.3 const prizePrice = 1 const competitorIdToPrizePayout = competitors ? Object.fromEntries( @@ -225,6 +242,41 @@ export function DePrize({ }) } } + const handleSend = async () => { + try { + const addresses = competitors.map((c) => c.treasury) + const amounts = competitors.map( + (c) => competitorIdToPrizePayout[c.id] * 10 ** PRIZE_DECIMALS + ) + console.log('amounts') + console.log(amounts) + console.log('addresses') + console.log(addresses) + console.log('PRIZE_TOKEN_ADDRESSES[chain.slug]') + console.log(PRIZE_TOKEN_ADDRESSES[chain.slug]) + // approve bulk token sender + //await prizeContract?.call('approve', [ + //BULK_TOKEN_SENDER_ADDRESSES[chain.slug], + //String(amounts.reduce((a, b) => a + b, 0)), + //]) + await bulkTokenSenderContract?.call('send', [ + PRIZE_TOKEN_ADDRESSES[chain.slug], + addresses.slice(0, 1), + amounts.map(String).slice(0, 1), + ]) + toast.success('Rewards sent successfully!', { + style: toastStyle, + }) + setTimeout(() => { + refreshRewards() + }, 5000) + } catch (error) { + console.error('Error sending rewards:', error) + toast.error('Error sending rewards. Please try again.', { + style: toastStyle, + }) + } + } return (
@@ -262,6 +314,16 @@ export function DePrize({ ).toLocaleString()} />
+ {userAddress && ( +
+

+ Winner Prize +

+ +
+ )} {userAddress && (
Voting Power - +
)} @@ -364,6 +419,14 @@ export function DePrize({ Delete Distribution )} + {isOperator && ( + + Send Rewards + + )} ) : ( diff --git a/ui/const/abis/REVDeployer.json b/ui/const/abis/REVDeployer.json new file mode 100644 index 000000000..b770e2077 --- /dev/null +++ b/ui/const/abis/REVDeployer.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"contract IJBController","name":"controller","type":"address"},{"internalType":"contract IJBSuckerRegistry","name":"suckerRegistry","type":"address"},{"internalType":"uint256","name":"feeRevnetId","type":"uint256"},{"internalType":"contract IJB721TiersHookDeployer","name":"hookDeployer","type":"address"},{"internalType":"contract CTPublisher","name":"publisher","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"x","type":"uint256"},{"internalType":"uint256","name":"y","type":"uint256"},{"internalType":"uint256","name":"denominator","type":"uint256"}],"name":"PRBMath_MulDiv_Overflow","type":"error"},{"inputs":[],"name":"REVDeployer_CashOutDelayNotFinished","type":"error"},{"inputs":[],"name":"REVDeployer_CashOutsCantBeTurnedOffCompletely","type":"error"},{"inputs":[],"name":"REVDeployer_StageNotStarted","type":"error"},{"inputs":[],"name":"REVDeployer_StageTimesMustIncrease","type":"error"},{"inputs":[],"name":"REVDeployer_StagesRequired","type":"error"},{"inputs":[],"name":"REVDeployer_Unauthorized","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"revnetId","type":"uint256"},{"components":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"ticker","type":"string"},{"internalType":"string","name":"uri","type":"string"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"internalType":"struct REVDescription","name":"description","type":"tuple"},{"internalType":"uint32","name":"baseCurrency","type":"uint32"},{"internalType":"address","name":"splitOperator","type":"address"},{"components":[{"internalType":"uint40","name":"startsAtOrAfter","type":"uint40"},{"components":[{"internalType":"uint32","name":"chainId","type":"uint32"},{"internalType":"uint104","name":"count","type":"uint104"},{"internalType":"address","name":"beneficiary","type":"address"}],"internalType":"struct REVAutoMint[]","name":"autoMints","type":"tuple[]"},{"internalType":"uint16","name":"splitPercent","type":"uint16"},{"internalType":"uint112","name":"initialIssuance","type":"uint112"},{"internalType":"uint32","name":"issuanceDecayFrequency","type":"uint32"},{"internalType":"uint32","name":"issuanceDecayPercent","type":"uint32"},{"internalType":"uint16","name":"cashOutTaxRate","type":"uint16"},{"internalType":"uint16","name":"extraMetadata","type":"uint16"}],"internalType":"struct REVStageConfig[]","name":"stageConfigurations","type":"tuple[]"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"contract IJBPayoutTerminal","name":"terminal","type":"address"}],"internalType":"struct REVLoanSource[]","name":"loanSources","type":"tuple[]"},{"internalType":"address","name":"loans","type":"address"},{"internalType":"bool","name":"allowCrosschainSuckerExtension","type":"bool"}],"indexed":false,"internalType":"struct REVConfig","name":"configuration","type":"tuple"},{"components":[{"internalType":"contract IJBTerminal","name":"terminal","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint32","name":"currency","type":"uint32"}],"internalType":"struct JBAccountingContext[]","name":"accountingContextsToAccept","type":"tuple[]"}],"indexed":false,"internalType":"struct JBTerminalConfig[]","name":"terminalConfigurations","type":"tuple[]"},{"components":[{"internalType":"contract IJBBuybackHook","name":"hook","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"uint32","name":"twapWindow","type":"uint32"},{"internalType":"uint32","name":"twapSlippageTolerance","type":"uint32"}],"internalType":"struct REVBuybackPoolConfig[]","name":"poolConfigurations","type":"tuple[]"}],"indexed":false,"internalType":"struct REVBuybackHookConfig","name":"buybackHookConfiguration","type":"tuple"},{"components":[{"components":[{"internalType":"contract IJBSuckerDeployer","name":"deployer","type":"address"},{"components":[{"internalType":"address","name":"localToken","type":"address"},{"internalType":"uint32","name":"minGas","type":"uint32"},{"internalType":"address","name":"remoteToken","type":"address"},{"internalType":"uint256","name":"minBridgeAmount","type":"uint256"}],"internalType":"struct JBTokenMapping[]","name":"mappings","type":"tuple[]"}],"internalType":"struct JBSuckerDeployerConfig[]","name":"deployerConfigurations","type":"tuple[]"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"indexed":false,"internalType":"struct REVSuckerDeploymentConfig","name":"suckerDeploymentConfiguration","type":"tuple"},{"components":[{"internalType":"uint48","name":"mustStartAtOrAfter","type":"uint48"},{"internalType":"uint32","name":"duration","type":"uint32"},{"internalType":"uint112","name":"weight","type":"uint112"},{"internalType":"uint32","name":"decayPercent","type":"uint32"},{"internalType":"contract IJBRulesetApprovalHook","name":"approvalHook","type":"address"},{"components":[{"internalType":"uint16","name":"reservedPercent","type":"uint16"},{"internalType":"uint16","name":"redemptionRate","type":"uint16"},{"internalType":"uint32","name":"baseCurrency","type":"uint32"},{"internalType":"bool","name":"pausePay","type":"bool"},{"internalType":"bool","name":"pauseCreditTransfers","type":"bool"},{"internalType":"bool","name":"allowOwnerMinting","type":"bool"},{"internalType":"bool","name":"allowSetCustomToken","type":"bool"},{"internalType":"bool","name":"allowTerminalMigration","type":"bool"},{"internalType":"bool","name":"allowSetTerminals","type":"bool"},{"internalType":"bool","name":"allowSetController","type":"bool"},{"internalType":"bool","name":"allowAddAccountingContext","type":"bool"},{"internalType":"bool","name":"allowAddPriceFeed","type":"bool"},{"internalType":"bool","name":"allowCrosschainSuckerExtension","type":"bool"},{"internalType":"bool","name":"ownerMustSendPayouts","type":"bool"},{"internalType":"bool","name":"holdFees","type":"bool"},{"internalType":"bool","name":"useTotalSurplusForRedemptions","type":"bool"},{"internalType":"bool","name":"useDataHookForPay","type":"bool"},{"internalType":"bool","name":"useDataHookForRedeem","type":"bool"},{"internalType":"address","name":"dataHook","type":"address"},{"internalType":"uint16","name":"metadata","type":"uint16"}],"internalType":"struct JBRulesetMetadata","name":"metadata","type":"tuple"},{"components":[{"internalType":"uint256","name":"groupId","type":"uint256"},{"components":[{"internalType":"bool","name":"preferAddToBalance","type":"bool"},{"internalType":"uint32","name":"percent","type":"uint32"},{"internalType":"uint56","name":"projectId","type":"uint56"},{"internalType":"address payable","name":"beneficiary","type":"address"},{"internalType":"uint48","name":"lockedUntil","type":"uint48"},{"internalType":"contract IJBSplitHook","name":"hook","type":"address"}],"internalType":"struct JBSplit[]","name":"splits","type":"tuple[]"}],"internalType":"struct JBSplitGroup[]","name":"splitGroups","type":"tuple[]"},{"components":[{"internalType":"address","name":"terminal","type":"address"},{"internalType":"address","name":"token","type":"address"},{"components":[{"internalType":"uint224","name":"amount","type":"uint224"},{"internalType":"uint32","name":"currency","type":"uint32"}],"internalType":"struct JBCurrencyAmount[]","name":"payoutLimits","type":"tuple[]"},{"components":[{"internalType":"uint224","name":"amount","type":"uint224"},{"internalType":"uint32","name":"currency","type":"uint32"}],"internalType":"struct JBCurrencyAmount[]","name":"surplusAllowances","type":"tuple[]"}],"internalType":"struct JBFundAccessLimitGroup[]","name":"fundAccessLimitGroups","type":"tuple[]"}],"indexed":false,"internalType":"struct JBRulesetConfig[]","name":"rulesetConfigurations","type":"tuple[]"},{"indexed":false,"internalType":"bytes","name":"encodedConfiguration","type":"bytes"},{"indexed":false,"internalType":"address","name":"caller","type":"address"}],"name":"DeployRevnet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"revnetId","type":"uint256"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":true,"internalType":"bytes32","name":"salt","type":"bytes32"},{"indexed":false,"internalType":"bytes","name":"encodedConfiguration","type":"bytes"},{"components":[{"components":[{"internalType":"contract IJBSuckerDeployer","name":"deployer","type":"address"},{"components":[{"internalType":"address","name":"localToken","type":"address"},{"internalType":"uint32","name":"minGas","type":"uint32"},{"internalType":"address","name":"remoteToken","type":"address"},{"internalType":"uint256","name":"minBridgeAmount","type":"uint256"}],"internalType":"struct JBTokenMapping[]","name":"mappings","type":"tuple[]"}],"internalType":"struct JBSuckerDeployerConfig[]","name":"deployerConfigurations","type":"tuple[]"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"indexed":false,"internalType":"struct REVSuckerDeploymentConfig","name":"suckerDeploymentConfiguration","type":"tuple"},{"indexed":false,"internalType":"address","name":"caller","type":"address"}],"name":"DeploySuckers","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"revnetId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"stageId","type":"uint256"},{"indexed":true,"internalType":"address","name":"beneficiary","type":"address"},{"indexed":false,"internalType":"uint256","name":"count","type":"uint256"},{"indexed":false,"internalType":"address","name":"caller","type":"address"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"revnetId","type":"uint256"},{"indexed":true,"internalType":"address","name":"newSplitOperator","type":"address"},{"indexed":false,"internalType":"address","name":"caller","type":"address"}],"name":"ReplaceSplitOperator","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"revnetId","type":"uint256"},{"indexed":false,"internalType":"address","name":"additionalOperator","type":"address"},{"indexed":false,"internalType":"uint256[]","name":"permissionIds","type":"uint256[]"},{"indexed":false,"internalType":"address","name":"caller","type":"address"}],"name":"SetAdditionalOperator","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"revnetId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"cashOutDelay","type":"uint256"},{"indexed":false,"internalType":"address","name":"caller","type":"address"}],"name":"SetCashOutDelay","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"revnetId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"stageId","type":"uint256"},{"indexed":true,"internalType":"address","name":"beneficiary","type":"address"},{"indexed":false,"internalType":"uint256","name":"count","type":"uint256"},{"indexed":false,"internalType":"address","name":"caller","type":"address"}],"name":"StoreAutoMintAmount","type":"event"},{"inputs":[],"name":"CASH_OUT_DELAY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"CONTROLLER","outputs":[{"internalType":"contract IJBController","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DIRECTORY","outputs":[{"internalType":"contract IJBDirectory","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FEE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FEE_REVNET_ID","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"HOOK_DEPLOYER","outputs":[{"internalType":"contract IJB721TiersHookDeployer","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PERMISSIONS","outputs":[{"internalType":"contract IJBPermissions","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PROJECTS","outputs":[{"internalType":"contract IJBProjects","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PUBLISHER","outputs":[{"internalType":"contract CTPublisher","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SUCKER_REGISTRY","outputs":[{"internalType":"contract IJBSuckerRegistry","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"holder","type":"address"},{"internalType":"uint256","name":"projectId","type":"uint256"},{"internalType":"uint256","name":"rulesetId","type":"uint256"},{"internalType":"uint256","name":"redeemCount","type":"uint256"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint32","name":"currency","type":"uint32"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"struct JBTokenAmount","name":"reclaimedAmount","type":"tuple"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint32","name":"currency","type":"uint32"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"struct JBTokenAmount","name":"forwardedAmount","type":"tuple"},{"internalType":"uint256","name":"redemptionRate","type":"uint256"},{"internalType":"address payable","name":"beneficiary","type":"address"},{"internalType":"bytes","name":"hookMetadata","type":"bytes"},{"internalType":"bytes","name":"redeemerMetadata","type":"bytes"}],"internalType":"struct JBAfterRedeemRecordedContext","name":"context","type":"tuple"}],"name":"afterRedeemRecordedWith","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"},{"internalType":"uint256","name":"stageId","type":"uint256"},{"internalType":"address","name":"beneficiary","type":"address"}],"name":"amountToAutoMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"},{"internalType":"uint256","name":"stageId","type":"uint256"},{"internalType":"address","name":"beneficiary","type":"address"}],"name":"autoMintFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"terminal","type":"address"},{"internalType":"address","name":"payer","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint32","name":"currency","type":"uint32"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"struct JBTokenAmount","name":"amount","type":"tuple"},{"internalType":"uint256","name":"projectId","type":"uint256"},{"internalType":"uint256","name":"rulesetId","type":"uint256"},{"internalType":"address","name":"beneficiary","type":"address"},{"internalType":"uint256","name":"weight","type":"uint256"},{"internalType":"uint256","name":"reservedPercent","type":"uint256"},{"internalType":"bytes","name":"metadata","type":"bytes"}],"internalType":"struct JBBeforePayRecordedContext","name":"context","type":"tuple"}],"name":"beforePayRecordedWith","outputs":[{"internalType":"uint256","name":"weight","type":"uint256"},{"components":[{"internalType":"contract IJBPayHook","name":"hook","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes","name":"metadata","type":"bytes"}],"internalType":"struct JBPayHookSpecification[]","name":"hookSpecifications","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"terminal","type":"address"},{"internalType":"address","name":"holder","type":"address"},{"internalType":"uint256","name":"projectId","type":"uint256"},{"internalType":"uint256","name":"rulesetId","type":"uint256"},{"internalType":"uint256","name":"redeemCount","type":"uint256"},{"internalType":"uint256","name":"totalSupply","type":"uint256"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint32","name":"currency","type":"uint32"},{"internalType":"uint256","name":"value","type":"uint256"}],"internalType":"struct JBTokenAmount","name":"surplus","type":"tuple"},{"internalType":"bool","name":"useTotalSurplus","type":"bool"},{"internalType":"uint256","name":"redemptionRate","type":"uint256"},{"internalType":"bytes","name":"metadata","type":"bytes"}],"internalType":"struct JBBeforeRedeemRecordedContext","name":"context","type":"tuple"}],"name":"beforeRedeemRecordedWith","outputs":[{"internalType":"uint256","name":"redemptionRate","type":"uint256"},{"internalType":"uint256","name":"redeemCount","type":"uint256"},{"internalType":"uint256","name":"totalSupply","type":"uint256"},{"components":[{"internalType":"contract IJBRedeemHook","name":"hook","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes","name":"metadata","type":"bytes"}],"internalType":"struct JBRedeemHookSpecification[]","name":"hookSpecifications","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"}],"name":"buybackHookOf","outputs":[{"internalType":"contract IJBRulesetDataHook","name":"buybackHook","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"}],"name":"cashOutDelayOf","outputs":[{"internalType":"uint256","name":"cashOutDelay","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"},{"components":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"ticker","type":"string"},{"internalType":"string","name":"uri","type":"string"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"internalType":"struct REVDescription","name":"description","type":"tuple"},{"internalType":"uint32","name":"baseCurrency","type":"uint32"},{"internalType":"address","name":"splitOperator","type":"address"},{"components":[{"internalType":"uint40","name":"startsAtOrAfter","type":"uint40"},{"components":[{"internalType":"uint32","name":"chainId","type":"uint32"},{"internalType":"uint104","name":"count","type":"uint104"},{"internalType":"address","name":"beneficiary","type":"address"}],"internalType":"struct REVAutoMint[]","name":"autoMints","type":"tuple[]"},{"internalType":"uint16","name":"splitPercent","type":"uint16"},{"internalType":"uint112","name":"initialIssuance","type":"uint112"},{"internalType":"uint32","name":"issuanceDecayFrequency","type":"uint32"},{"internalType":"uint32","name":"issuanceDecayPercent","type":"uint32"},{"internalType":"uint16","name":"cashOutTaxRate","type":"uint16"},{"internalType":"uint16","name":"extraMetadata","type":"uint16"}],"internalType":"struct REVStageConfig[]","name":"stageConfigurations","type":"tuple[]"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"contract IJBPayoutTerminal","name":"terminal","type":"address"}],"internalType":"struct REVLoanSource[]","name":"loanSources","type":"tuple[]"},{"internalType":"address","name":"loans","type":"address"},{"internalType":"bool","name":"allowCrosschainSuckerExtension","type":"bool"}],"internalType":"struct REVConfig","name":"configuration","type":"tuple"},{"components":[{"internalType":"contract IJBTerminal","name":"terminal","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint32","name":"currency","type":"uint32"}],"internalType":"struct JBAccountingContext[]","name":"accountingContextsToAccept","type":"tuple[]"}],"internalType":"struct JBTerminalConfig[]","name":"terminalConfigurations","type":"tuple[]"},{"components":[{"internalType":"contract IJBBuybackHook","name":"hook","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"uint32","name":"twapWindow","type":"uint32"},{"internalType":"uint32","name":"twapSlippageTolerance","type":"uint32"}],"internalType":"struct REVBuybackPoolConfig[]","name":"poolConfigurations","type":"tuple[]"}],"internalType":"struct REVBuybackHookConfig","name":"buybackHookConfiguration","type":"tuple"},{"components":[{"components":[{"internalType":"contract IJBSuckerDeployer","name":"deployer","type":"address"},{"components":[{"internalType":"address","name":"localToken","type":"address"},{"internalType":"uint32","name":"minGas","type":"uint32"},{"internalType":"address","name":"remoteToken","type":"address"},{"internalType":"uint256","name":"minBridgeAmount","type":"uint256"}],"internalType":"struct JBTokenMapping[]","name":"mappings","type":"tuple[]"}],"internalType":"struct JBSuckerDeployerConfig[]","name":"deployerConfigurations","type":"tuple[]"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"internalType":"struct REVSuckerDeploymentConfig","name":"suckerDeploymentConfiguration","type":"tuple"}],"name":"deployFor","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"},{"internalType":"bytes","name":"encodedConfiguration","type":"bytes"},{"components":[{"components":[{"internalType":"contract IJBSuckerDeployer","name":"deployer","type":"address"},{"components":[{"internalType":"address","name":"localToken","type":"address"},{"internalType":"uint32","name":"minGas","type":"uint32"},{"internalType":"address","name":"remoteToken","type":"address"},{"internalType":"uint256","name":"minBridgeAmount","type":"uint256"}],"internalType":"struct JBTokenMapping[]","name":"mappings","type":"tuple[]"}],"internalType":"struct JBSuckerDeployerConfig[]","name":"deployerConfigurations","type":"tuple[]"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"internalType":"struct REVSuckerDeploymentConfig","name":"suckerDeploymentConfiguration","type":"tuple"}],"name":"deploySuckersFor","outputs":[{"internalType":"address[]","name":"suckers","type":"address[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"},{"components":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"ticker","type":"string"},{"internalType":"string","name":"uri","type":"string"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"internalType":"struct REVDescription","name":"description","type":"tuple"},{"internalType":"uint32","name":"baseCurrency","type":"uint32"},{"internalType":"address","name":"splitOperator","type":"address"},{"components":[{"internalType":"uint40","name":"startsAtOrAfter","type":"uint40"},{"components":[{"internalType":"uint32","name":"chainId","type":"uint32"},{"internalType":"uint104","name":"count","type":"uint104"},{"internalType":"address","name":"beneficiary","type":"address"}],"internalType":"struct REVAutoMint[]","name":"autoMints","type":"tuple[]"},{"internalType":"uint16","name":"splitPercent","type":"uint16"},{"internalType":"uint112","name":"initialIssuance","type":"uint112"},{"internalType":"uint32","name":"issuanceDecayFrequency","type":"uint32"},{"internalType":"uint32","name":"issuanceDecayPercent","type":"uint32"},{"internalType":"uint16","name":"cashOutTaxRate","type":"uint16"},{"internalType":"uint16","name":"extraMetadata","type":"uint16"}],"internalType":"struct REVStageConfig[]","name":"stageConfigurations","type":"tuple[]"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"contract IJBPayoutTerminal","name":"terminal","type":"address"}],"internalType":"struct REVLoanSource[]","name":"loanSources","type":"tuple[]"},{"internalType":"address","name":"loans","type":"address"},{"internalType":"bool","name":"allowCrosschainSuckerExtension","type":"bool"}],"internalType":"struct REVConfig","name":"configuration","type":"tuple"},{"components":[{"internalType":"contract IJBTerminal","name":"terminal","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint32","name":"currency","type":"uint32"}],"internalType":"struct JBAccountingContext[]","name":"accountingContextsToAccept","type":"tuple[]"}],"internalType":"struct JBTerminalConfig[]","name":"terminalConfigurations","type":"tuple[]"},{"components":[{"internalType":"contract IJBBuybackHook","name":"hook","type":"address"},{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"uint32","name":"twapWindow","type":"uint32"},{"internalType":"uint32","name":"twapSlippageTolerance","type":"uint32"}],"internalType":"struct REVBuybackPoolConfig[]","name":"poolConfigurations","type":"tuple[]"}],"internalType":"struct REVBuybackHookConfig","name":"buybackHookConfiguration","type":"tuple"},{"components":[{"components":[{"internalType":"contract IJBSuckerDeployer","name":"deployer","type":"address"},{"components":[{"internalType":"address","name":"localToken","type":"address"},{"internalType":"uint32","name":"minGas","type":"uint32"},{"internalType":"address","name":"remoteToken","type":"address"},{"internalType":"uint256","name":"minBridgeAmount","type":"uint256"}],"internalType":"struct JBTokenMapping[]","name":"mappings","type":"tuple[]"}],"internalType":"struct JBSuckerDeployerConfig[]","name":"deployerConfigurations","type":"tuple[]"},{"internalType":"bytes32","name":"salt","type":"bytes32"}],"internalType":"struct REVSuckerDeploymentConfig","name":"suckerDeploymentConfiguration","type":"tuple"},{"components":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"symbol","type":"string"},{"internalType":"string","name":"baseUri","type":"string"},{"internalType":"contract IJB721TokenUriResolver","name":"tokenUriResolver","type":"address"},{"internalType":"string","name":"contractUri","type":"string"},{"components":[{"components":[{"internalType":"uint104","name":"price","type":"uint104"},{"internalType":"uint32","name":"initialSupply","type":"uint32"},{"internalType":"uint32","name":"votingUnits","type":"uint32"},{"internalType":"uint16","name":"reserveFrequency","type":"uint16"},{"internalType":"address","name":"reserveBeneficiary","type":"address"},{"internalType":"bytes32","name":"encodedIPFSUri","type":"bytes32"},{"internalType":"uint24","name":"category","type":"uint24"},{"internalType":"uint8","name":"discountPercent","type":"uint8"},{"internalType":"bool","name":"allowOwnerMint","type":"bool"},{"internalType":"bool","name":"useReserveBeneficiaryAsDefault","type":"bool"},{"internalType":"bool","name":"transfersPausable","type":"bool"},{"internalType":"bool","name":"useVotingUnits","type":"bool"},{"internalType":"bool","name":"cannotBeRemoved","type":"bool"},{"internalType":"bool","name":"cannotIncreaseDiscountPercent","type":"bool"}],"internalType":"struct JB721TierConfig[]","name":"tiers","type":"tuple[]"},{"internalType":"uint32","name":"currency","type":"uint32"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"contract IJBPrices","name":"prices","type":"address"}],"internalType":"struct JB721InitTiersConfig","name":"tiersConfig","type":"tuple"},{"internalType":"address","name":"reserveBeneficiary","type":"address"},{"components":[{"internalType":"bool","name":"noNewTiersWithReserves","type":"bool"},{"internalType":"bool","name":"noNewTiersWithVotes","type":"bool"},{"internalType":"bool","name":"noNewTiersWithOwnerMinting","type":"bool"},{"internalType":"bool","name":"preventOverspending","type":"bool"}],"internalType":"struct JB721TiersHookFlags","name":"flags","type":"tuple"}],"internalType":"struct JBDeploy721TiersHookConfig","name":"baseline721HookConfiguration","type":"tuple"},{"internalType":"bool","name":"splitOperatorCanAdjustTiers","type":"bool"},{"internalType":"bool","name":"splitOperatorCanUpdateMetadata","type":"bool"},{"internalType":"bool","name":"splitOperatorCanMint","type":"bool"},{"internalType":"bool","name":"splitOperatorCanIncreaseDiscountPercent","type":"bool"}],"internalType":"struct REVDeploy721TiersHookConfig","name":"tiered721HookConfiguration","type":"tuple"},{"components":[{"internalType":"uint24","name":"category","type":"uint24"},{"internalType":"uint104","name":"minimumPrice","type":"uint104"},{"internalType":"uint32","name":"minimumTotalSupply","type":"uint32"},{"internalType":"uint32","name":"maximumTotalSupply","type":"uint32"},{"internalType":"address[]","name":"allowedAddresses","type":"address[]"}],"internalType":"struct REVCroptopAllowedPost[]","name":"allowedPosts","type":"tuple[]"}],"name":"deployWith721sFor","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"contract IJB721TiersHook","name":"hook","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"},{"internalType":"address","name":"addr","type":"address"}],"name":"hasMintPermissionFor","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"},{"internalType":"address","name":"addr","type":"address"}],"name":"isSplitOperatorOf","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"}],"name":"loansOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"},{"internalType":"address","name":"newSplitOperator","type":"address"}],"name":"setSplitOperatorOf","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"}],"name":"tiered721HookOf","outputs":[{"internalType":"contract IJB721TiersHook","name":"tiered721Hook","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"revnetId","type":"uint256"}],"name":"unrealizedAutoMintAmountOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] diff --git a/ui/const/config.ts b/ui/const/config.ts index 7c7d27256..627cc7b78 100644 --- a/ui/const/config.ts +++ b/ui/const/config.ts @@ -108,6 +108,10 @@ export const DEPRIZE_DISTRIBUTION_TABLE_ADDRESSES: Index = { export const PRIZE_TOKEN_ADDRESSES: Index = { sepolia: '0xf2a29F67fb5e6d7B9682591c0fD100d357dA85A7', } +export const BULK_TOKEN_SENDER_ADDRESSES: Index = { + sepolia: '0xfEcb8E75658d768C9CdB418d81607eF4Dab5d001', +} + export const PRIZE_DECIMALS = 18 export const CITIZEN_WHITELIST_ADDRESSES: Index = { @@ -159,6 +163,12 @@ export const JOBS_TABLE_ADDRESSES: Index = { sepolia: '0x1c588261069604490e73188343B2C930F35ae2EF', } +export const PRIZE_REVNET_ID = 50 + +export const REVNET_ADDRESSES: Index = { + sepolia: '0x25bc5d5a708c2e426ef3a5196cc18de6b2d5a3d1', +} + export const MARKETPLACE_ADDRESS = process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? polygonConfig.Marketplace diff --git a/ui/lib/revnet/hooks/useIsOperator.tsx b/ui/lib/revnet/hooks/useIsOperator.tsx new file mode 100644 index 000000000..e1e529b96 --- /dev/null +++ b/ui/lib/revnet/hooks/useIsOperator.tsx @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react' + +export default function useIsOperator( + revnetContract: any, + operator: string, + revnetId: number +) { + const [isOperator, setIsOperator] = useState(false) + useEffect(() => { + async function checkIsOperator() { + if (!revnetContract) return + const isOperator = await revnetContract.call('isSplitOperatorOf', [ + revnetId, + operator, + ]) + setIsOperator(isOperator) + } + checkIsOperator() + }, [revnetContract]) + return isOperator +} diff --git a/ui/lib/tokens/hooks/useTokenBalances.tsx b/ui/lib/tokens/hooks/useTokenBalances.tsx index 4e8f29a4c..f6216b803 100644 --- a/ui/lib/tokens/hooks/useTokenBalances.tsx +++ b/ui/lib/tokens/hooks/useTokenBalances.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' export default function useTokenBalances( tokenContract: any, diff --git a/ui/lib/tokens/hooks/useTokenSupply.tsx b/ui/lib/tokens/hooks/useTokenSupply.tsx new file mode 100644 index 000000000..2054e1bc6 --- /dev/null +++ b/ui/lib/tokens/hooks/useTokenSupply.tsx @@ -0,0 +1,16 @@ +import { useEffect, useState } from 'react' + +export default function useTokenSupply(tokenContract: any, decimals: number) { + const [tokenSupply, setTokenSupply] = useState(0) + + useEffect(() => { + async function getSupply() { + if (!tokenContract) return + const supply = await tokenContract.call('totalSupply') + setTokenSupply(+supply.toString() / 10 ** decimals) + } + + getSupply() + }, [tokenContract]) + return tokenSupply +} diff --git a/ui/pages/_app.tsx b/ui/pages/_app.tsx index 626e7cc4d..8c54d9bf9 100644 --- a/ui/pages/_app.tsx +++ b/ui/pages/_app.tsx @@ -14,8 +14,6 @@ function App({ Component, pageProps: { session, ...pageProps } }: any) { const [selectedChain, setSelectedChain]: any = useState( process.env.NEXT_PUBLIC_CHAIN === 'mainnet' ? Arbitrum : Sepolia ) - console.log('selectedChain') - console.log(selectedChain) const [lightMode, setLightMode] = useLightMode() diff --git a/ui/public/sitemap-0.xml b/ui/public/sitemap-0.xml index 7420db2d4..2ee6ddebe 100644 --- a/ui/public/sitemap-0.xml +++ b/ui/public/sitemap-0.xml @@ -1,29 +1,34 @@ -amoondao.com2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/about2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/almost-there2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/analytics2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/constitution2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/dude-perfect2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/events2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/get-mooney2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/governance2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/info2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/jobs2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/join2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/join-us2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/lifeship2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/linktree2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/lock2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/marketplace2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/network2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/news2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/propose2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/sweepstakes2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/team2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/thank-you2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/vote2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/zero-gravity2024-07-14T21:19:19.708Zdaily0.7 -moondao.com/4042024-07-14T21:19:19.708Zdaily0.7 +moondao.com2024-10-31T00:28:44.874Zdaily0.7 +moondao.com/about2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/almost-there2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/analytics2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/bridge2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/citizen2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/constitution2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/current-projects2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/deprize2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/dude-perfect2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/events2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/get-mooney2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/governance2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/info2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/jobs2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/join2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/join-us2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/lifeship2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/linktree2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/lock2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/marketplace2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/network2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/news2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/propose2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/rewards2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/sweepstakes2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/team2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/thank-you2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/vote2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/zero-gravity2024-10-31T00:28:44.875Zdaily0.7 +moondao.com/4042024-10-31T00:28:44.875Zdaily0.7 \ No newline at end of file From 5b2f1b0633f9a0bc3872f272baf991d20a75f298 Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Thu, 7 Nov 2024 11:14:32 -0800 Subject: [PATCH 09/78] forge install: forge-std v1.9.4 --- .gitmodules | 3 +++ contracts/lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/forge-std diff --git a/.gitmodules b/.gitmodules index fc0f2d5c4..d29bbd749 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "contracts/lib/solmate"] path = contracts/lib/solmate url = https://github.com/rari-capital/solmate +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 000000000..1eea5bae1 --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 From 5c01d25a020a0f121bb355d1c33463b97a6cbc73 Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Fri, 8 Nov 2024 15:02:41 -0800 Subject: [PATCH 10/78] adds VotingEscrowDepositor --- contracts/src/governance/IVotingEscrow.sol | 12 ++ .../src/governance/SmartWalletChecker.sol | 2 +- contracts/src/governance/VotingEscrow.vy | 1 + .../src/governance/VotingEscrowDepositor.sol | 55 +++++++-- .../src/test/VotingEscrowDepositor.t.sol | 114 +++++++++++------- .../src/test/utils/mocks/MockVotingEscrow.sol | 37 ------ 6 files changed, 129 insertions(+), 92 deletions(-) delete mode 100644 contracts/src/test/utils/mocks/MockVotingEscrow.sol diff --git a/contracts/src/governance/IVotingEscrow.sol b/contracts/src/governance/IVotingEscrow.sol index d5877f211..46e1c7956 100644 --- a/contracts/src/governance/IVotingEscrow.sol +++ b/contracts/src/governance/IVotingEscrow.sol @@ -10,4 +10,16 @@ interface IVotingEscrow { function locked(address) external view returns (LockedBalance memory); function balanceOf(address) external view returns (uint256); + function deploy_for(address _addr, uint256 _value) external; + function commit_smart_wallet_checker(address) external; + function apply_smart_wallet_checker() external; + + function admin() external view returns (address); + function create_lock(uint256 _value, uint256 _unlock_time) external; + function totalSupply() external view returns (uint256); + function checkpoint() external; + function user_point_history(address, uint256) external view returns (uint256, uint256); + function user_point_history__ts(address, uint256) external view returns (uint256); + function get_last_user_slope(address) external view returns (int128); + } diff --git a/contracts/src/governance/SmartWalletChecker.sol b/contracts/src/governance/SmartWalletChecker.sol index 5b9be3925..400e2b169 100644 --- a/contracts/src/governance/SmartWalletChecker.sol +++ b/contracts/src/governance/SmartWalletChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.6; +pragma solidity 0.8.10; //import "@openzeppelin/contracts-4.2.0/access/Ownable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/contracts/src/governance/VotingEscrow.vy b/contracts/src/governance/VotingEscrow.vy index c61a69a02..66fd976fd 100644 --- a/contracts/src/governance/VotingEscrow.vy +++ b/contracts/src/governance/VotingEscrow.vy @@ -94,6 +94,7 @@ epoch: public(uint256) point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch] user_point_epoch: public(HashMap[address, uint256]) + slope_changes: public(HashMap[uint256, int128]) # time -> signed slope change # Aragon's view methods for compatibility diff --git a/contracts/src/governance/VotingEscrowDepositor.sol b/contracts/src/governance/VotingEscrowDepositor.sol index 984098592..1e9b7fe28 100644 --- a/contracts/src/governance/VotingEscrowDepositor.sol +++ b/contracts/src/governance/VotingEscrowDepositor.sol @@ -1,25 +1,60 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -interface IERC20 { +import "@openzeppelin/contracts/access/Ownable.sol"; + +interface IERC20Interface { function transfer(address recipient, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); } -interface IVotingEscrow { +interface IVotingEscrowInterface { function deposit_for(address _addr, uint256 _value) external; } -contract VotingEscrowDepositor { - IERC20 public token; - IVotingEscrow public escrowToken; +contract VotingEscrowDepositor is Ownable{ + IERC20Interface public token; + IVotingEscrowInterface public escrowToken; + // map from address to amount availabe to withdraw + mapping(address => uint256) public availableToWithdraw; + address[] public withdrawAddresses; constructor(address _tokenAddress, address _escrowTokenAddress) { - token = IERC20(_tokenAddress); - escrowToken = IVotingEscrow(_escrowTokenAddress); + token = IERC20Interface(_tokenAddress); + escrowToken = IVotingEscrowInterface(_escrowTokenAddress); + } + + function increaseWithdrawAmounts(address[] memory addresses, uint256[] memory amounts) external onlyOwner{ + require(addresses.length == amounts.length, "Arrays must be of equal length"); + uint256 totalAmount = 0; + for (uint256 i = 0; i < addresses.length; i++) { + if (availableToWithdraw[addresses[i]] == 0) { + withdrawAddresses.push(addresses[i]); + } + availableToWithdraw[addresses[i]] += amounts[i]; + totalAmount += amounts[i]; + } + require(token.transferFrom(msg.sender, address(this), totalAmount), "Token transfer failed"); + } + + function clearWithdrawAmounts() external onlyOwner { + for (uint256 i = 0; i < withdrawAddresses.length; i++) { + availableToWithdraw[withdrawAddresses[i]] = 0; + } + delete withdrawAddresses; + } + + function withdraw() external { + uint256 amount = availableToWithdraw[msg.sender]; + require(amount > 0, "No amount available to withdraw"); + availableToWithdraw[msg.sender] = 0; + require(token.transfer(msg.sender, amount), "Token transfer failed"); + escrowToken.deposit_for(msg.sender, amount); } - function transfer_and_deposit_for(address addr, uint256 value) external { - require(token.transfer(addr, value), "Token transfer failed"); - escrowToken.deposit_for(addr, value); + function returnTokens() external onlyOwner { + uint256 balance = token.balanceOf(address(this)); + require(token.transfer(msg.sender, balance), "Token transfer failed"); } } diff --git a/contracts/src/test/VotingEscrowDepositor.t.sol b/contracts/src/test/VotingEscrowDepositor.t.sol index 9a9374b1e..94c4eddc0 100644 --- a/contracts/src/test/VotingEscrowDepositor.t.sol +++ b/contracts/src/test/VotingEscrowDepositor.t.sol @@ -1,9 +1,11 @@ - // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "forge-std/Test.sol"; -import "../src/VotingEscrowDepositor.sol"; +import "../governance/VotingEscrowDepositor.sol"; +import "../governance/SmartWalletChecker.sol"; +import "../governance/IVotingEscrow.sol"; +import "../tokens/MyToken.sol"; //Using deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 //Deploying MOONEY.. @@ -13,64 +15,88 @@ import "../src/VotingEscrowDepositor.sol"; //Deployed vMOONEY to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 //Deployment manifest saved to ./deployments/local.json -address MOONEY = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512; -address vMOONEY = 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0; -contract MockToken is IERC20 { - mapping(address => uint256) public balanceOf; - - function transfer(address recipient, uint256 amount) external override returns (bool) { - require(balanceOf[msg.sender] >= amount, "Insufficient balance"); - balanceOf[msg.sender] -= amount; - balanceOf[recipient] += amount; - return true; - } - - function mint(address to, uint256 amount) external { - balanceOf[to] += amount; - } -} - -contract MockVotingEscrow is IVotingEscrow { - mapping(address => uint256) public deposits; - - function deposit_for(address _addr, uint256 _value) external override { - deposits[_addr] += _value; - } -} - contract VotingEscrowDepositorTest is Test { VotingEscrowDepositor public depositor; - MockToken public token; - MockVotingEscrow public escrowToken; + MyToken public token; + IVotingEscrow public escrowToken; + SmartWalletChecker public checker; + //0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 address public user = address(0x123); - uint256 public initialBalance = 1000; + //uint256 public initialBalance = 1000; + uint256 public initialBalance = 126144000 * 2; + uint256 depositAmount = 126144000; + + address public MOONEY = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512; + address public vMOONEY = 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0; function setUp() public { - token = new MockToken(); - escrowToken = new MockVotingEscrow(); - depositor = new VotingEscrowDepositor(address(MOONEY), address(vMOONEY)); + token = MyToken(MOONEY); + escrowToken = IVotingEscrow(vMOONEY); + vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + depositor = new VotingEscrowDepositor(address(MOONEY), address(vMOONEY)); + checker = new SmartWalletChecker(true); + checker.approveWallet(address(depositor)); + // TODO we really shouldn't need this? + checker.approveWallet(address(user)); + //escrowToken.admin(); + vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + escrowToken.commit_smart_wallet_checker(address(checker)); + vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + escrowToken.apply_smart_wallet_checker(); + //token.transfer(address(depositor), initialBalance); + address[] memory addresses = new address[](1); + addresses[0] = address(user); + uint256[] memory amounts = new uint256[](1); + amounts[0] = depositAmount; + vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + token.approve(address(depositor), depositAmount); + vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + depositor.increaseWithdrawAmounts(addresses, amounts); + + vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + token.transfer(address(user), initialBalance); + vm.prank(user); + token.approve(address(escrowToken), initialBalance); + vm.prank(user); + escrowToken.create_lock(initialBalance, block.timestamp + 4*60 * 60 * 24 * 365); // Mint tokens for the depositor contract to transfer - token.mint(address(depositor), initialBalance); + //token.mint(address(depositor), initialBalance); } function testTransferAndDepositFor() public { - uint256 depositAmount = 500; + //uint256 depositAmount = 500; + // Verify initial balances and deposits - assertEq(token.balanceOf(address(depositor)), initialBalance); - assertEq(token.balanceOf(user), 0); - assertEq(escrowToken.deposits(user), 0); + //assertEq(token.balanceOf(address(depositor)), initialBalance); + //assertEq(token.balanceOf(user), 0); + //assertEq(escrowToken.balanceOf(user), initialBalance); + escrowToken.checkpoint(); + + //assertEq(escrowToken.admin(), address(0)); // Call transfer_and_deposit_for - depositor.transfer_and_deposit_for(user, depositAmount); + vm.prank(user); + token.approve(address(escrowToken), depositAmount); + vm.prank(user); + //depositor.transfer_and_deposit_for(user, depositAmount); + depositor.withdraw(); // Verify token transfer - assertEq(token.balanceOf(address(depositor)), initialBalance - depositAmount); - assertEq(token.balanceOf(user), depositAmount); - - // Verify escrow deposit - assertEq(escrowToken.deposits(user), depositAmount); + //assertEq(token.balanceOf(address(depositor)), initialBalance - depositAmount); + //assertEq(token.balanceOf(user), 0); + ////assertEq(escrowToken.totalSupply(), initialBalance + depositAmount); + //escrowToken.user_point_history(user, 0); + //escrowToken.user_point_history(user, 1); + //escrowToken.user_point_history(user, 2); + //escrowToken.get_last_user_slope(user); + //escrowToken.user_point_history__ts(user, 0); + //escrowToken.user_point_history__ts(user, 1); + //escrowToken.user_point_history__ts(user, 2); + + //// Verify escrow deposit + assertEq(escrowToken.balanceOf(user), depositAmount + initialBalance); } } diff --git a/contracts/src/test/utils/mocks/MockVotingEscrow.sol b/contracts/src/test/utils/mocks/MockVotingEscrow.sol deleted file mode 100644 index a5293439a..000000000 --- a/contracts/src/test/utils/mocks/MockVotingEscrow.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.10; -import {IVotingEscrow} from "../../../governance/IVotingEscrow.sol"; - -contract MockVotingEscrow is IVotingEscrow { - string name; - string symbol; - - mapping(address => uint256) internal _balance; - - constructor(string memory _name, string memory _symbol) { - name = _name; - symbol = _symbol; - } - - function allowance(address owner, address spender) external view returns (uint256) {} - - function approve(address spender, uint256 amount) external returns (bool) {} - - function totalSupply() external view returns (uint256) {} - - function transfer(address to, uint256 amount) external returns (bool) {} - - function transferFrom(address from, address to, uint256 amount) external returns (bool) {} - - function setBalance(address to, uint256 amount) public { - _balance[to] = amount; - } - - function balanceOf(address account) public view returns (uint256) { - return _balance[account]; - } - - function locked(address) external view returns (LockedBalance memory) {} -} - - From 5208c18e6ef91b28b80cd87c5170c1bdc0f7053f Mon Sep 17 00:00:00 2001 From: jaderiverstokes Date: Fri, 15 Nov 2024 13:26:10 -0800 Subject: [PATCH 11/78] wip --- contracts/foundry.toml | 2 +- contracts/scripts/VotingEscrowDepositor.s.sol | 29 +++++++++++++ .../src/governance/SmartWalletChecker.sol | 2 +- .../src/governance/VotingEscrowDepositor.sol | 7 +++- .../src/test/VotingEscrowDepositor.t.sol | 15 ++++--- ui/components/nance/RetroactiveRewards.tsx | 42 +++++++++++++++++++ ui/const/config.ts | 9 ++++ ui/lib/utils/hooks/useWithdrawAmount.ts | 23 ++++++++++ 8 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 contracts/scripts/VotingEscrowDepositor.s.sol create mode 100644 ui/lib/utils/hooks/useWithdrawAmount.ts diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 2fe11be02..a5f4ed352 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -5,7 +5,7 @@ libs = ["lib"] evm_version = "berlin" #[profile.default.compiler] -#version = "0.8.17" +#version = "0.8.10" # Vyper compiler settings [profile.default.vyper] diff --git a/contracts/scripts/VotingEscrowDepositor.s.sol b/contracts/scripts/VotingEscrowDepositor.s.sol new file mode 100644 index 000000000..b11b2561a --- /dev/null +++ b/contracts/scripts/VotingEscrowDepositor.s.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; +import "../src/governance/VotingEscrowDepositor.sol"; + +contract MyScript is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + uint256 networkChainId = block.chainid; + address MOONEY; + address VMOONEY; + // arbitrum + if (networkChainId == 42161) { + MOONEY = 0x1Fa56414549BdccBB09916f61f0A5827f779a85c; + VMOONEY = 0xB255c74F8576f18357cE6184DA033c6d93C71899; + } else if (networkChainId == 11155111) { + MOONEY = 0x85A3C597F43B0cCE657793Cf31b05DF6969FbD2C; + VMOONEY = 0xA4F6A4B135b9AF7909442A7a3bF7797b61e609b1; + } else { + revert("Unsupported network"); + } + + + VotingEscrowDepositor sender = new VotingEscrowDepositor(MOONEY, VMOONEY); + + vm.stopBroadcast(); + } +} diff --git a/contracts/src/governance/SmartWalletChecker.sol b/contracts/src/governance/SmartWalletChecker.sol index 400e2b169..752ec7a50 100644 --- a/contracts/src/governance/SmartWalletChecker.sol +++ b/contracts/src/governance/SmartWalletChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.10; +pragma solidity ^0.8.0; //import "@openzeppelin/contracts-4.2.0/access/Ownable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/contracts/src/governance/VotingEscrowDepositor.sol b/contracts/src/governance/VotingEscrowDepositor.sol index 1e9b7fe28..049308482 100644 --- a/contracts/src/governance/VotingEscrowDepositor.sol +++ b/contracts/src/governance/VotingEscrowDepositor.sol @@ -20,7 +20,7 @@ contract VotingEscrowDepositor is Ownable{ mapping(address => uint256) public availableToWithdraw; address[] public withdrawAddresses; - constructor(address _tokenAddress, address _escrowTokenAddress) { + constructor(address _tokenAddress, address _escrowTokenAddress){ token = IERC20Interface(_tokenAddress); escrowToken = IVotingEscrowInterface(_escrowTokenAddress); } @@ -57,4 +57,9 @@ contract VotingEscrowDepositor is Ownable{ uint256 balance = token.balanceOf(address(this)); require(token.transfer(msg.sender, balance), "Token transfer failed"); } + + function sendVotingEscrowTokens(address _addr, uint256 _value) external onlyOwner { + require(token.transfer(_addr, _value), "Token transfer failed"); + escrowToken.deposit_for(_addr, _value); + } } diff --git a/contracts/src/test/VotingEscrowDepositor.t.sol b/contracts/src/test/VotingEscrowDepositor.t.sol index 94c4eddc0..8f5934d1a 100644 --- a/contracts/src/test/VotingEscrowDepositor.t.sol +++ b/contracts/src/test/VotingEscrowDepositor.t.sol @@ -27,8 +27,10 @@ contract VotingEscrowDepositorTest is Test { uint256 public initialBalance = 126144000 * 2; uint256 depositAmount = 126144000; - address public MOONEY = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512; - address public vMOONEY = 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0; + address public MOONEY = 0x5FbDB2315678afecb367f032d93F642f64180aa3; + + address public vMOONEY = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512; + function setUp() public { token = MyToken(MOONEY); @@ -54,7 +56,6 @@ contract VotingEscrowDepositorTest is Test { token.approve(address(depositor), depositAmount); vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); depositor.increaseWithdrawAmounts(addresses, amounts); - vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); token.transfer(address(user), initialBalance); vm.prank(user); @@ -63,6 +64,10 @@ contract VotingEscrowDepositorTest is Test { escrowToken.create_lock(initialBalance, block.timestamp + 4*60 * 60 * 24 * 365); // Mint tokens for the depositor contract to transfer //token.mint(address(depositor), initialBalance); + vm.prank(user); + token.approve(address(escrowToken), depositAmount); + vm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); + depositor.sendVotingEscrowTokens(address(user), depositAmount); } function testTransferAndDepositFor() public { @@ -79,10 +84,8 @@ contract VotingEscrowDepositorTest is Test { // Call transfer_and_deposit_for vm.prank(user); - token.approve(address(escrowToken), depositAmount); - vm.prank(user); //depositor.transfer_and_deposit_for(user, depositAmount); - depositor.withdraw(); + //depositor.withdraw(); // Verify token transfer //assertEq(token.balanceOf(address(depositor)), initialBalance - depositAmount); diff --git a/ui/components/nance/RetroactiveRewards.tsx b/ui/components/nance/RetroactiveRewards.tsx index 12de480f8..a8458c7a4 100644 --- a/ui/components/nance/RetroactiveRewards.tsx +++ b/ui/components/nance/RetroactiveRewards.tsx @@ -3,6 +3,7 @@ import { useAddress, useContract } from '@thirdweb-dev/react' import { DISTRIBUTION_TABLE_ADDRESSES, SNAPSHOT_RETROACTIVE_REWARDS_ID, + VOTING_ESCROW_DEPOSITOR_ADDRESSES, } from 'const/config' import _ from 'lodash' import { useState, useEffect } from 'react' @@ -13,6 +14,7 @@ import toastStyle from '@/lib/marketplace/marketplace-utils/toastConfig' import { SNAPSHOT_SPACE_NAME } from '@/lib/nance/constants' import { useVotingPowers } from '@/lib/snapshot' import useWindowSize from '@/lib/team/use-window-size' +import useWithdrawAmount from '@/lib/utils/hooks/useWithdrawAmount' import { getBudget, getPayouts } from '@/lib/utils/rewards' import { computeRewardPercentages } from '@/lib/utils/voting' import Asset from '@/components/dashboard/treasury/balance/Asset' @@ -125,6 +127,21 @@ export function RetroactiveRewards({ const { contract: distributionTableContract } = useContract( DISTRIBUTION_TABLE_ADDRESSES[chain.slug] ) + const { contract: votingEscrowDepositorContract } = useContract( + VOTING_ESCROW_DEPOSITOR_ADDRESSES[chain.slug] + ) + //console.log('votingEscrowDepositorContract') + //console.log(votingEscrowDepositorContract) + const withdrawable = useWithdrawAmount( + votingEscrowDepositorContract, + userAddress + ) + //console.log('withdrawable') + //console.log(withdrawable) + ;(async () => { + console.log(await withdrawable) + })() + const { tokens } = useAssets() const { ethBudget, usdBudget, mooneyBudget, ethPrice } = getBudget( tokens, @@ -205,6 +222,22 @@ export function RetroactiveRewards({ }) } } + const handleWithdraw = async () => { + try { + await votingEscrowDepositorContract?.call('withdraw', [userAddress]) + toast.success('Withdrawal successful!', { + style: toastStyle, + }) + setTimeout(() => { + refreshRewards() + }, 5000) + } catch (error) { + console.error('Error withdrawing:', error) + toast.error('Error withdrawing. Please try again.', { + style: toastStyle, + }) + } + } return (
@@ -394,6 +427,15 @@ export function RetroactiveRewards({ )} + {userAddress && withdrawable > 0 && ( + + Withdraw Rewards + + )} diff --git a/ui/const/config.ts b/ui/const/config.ts index 627cc7b78..317a2b14f 100644 --- a/ui/const/config.ts +++ b/ui/const/config.ts @@ -101,6 +101,15 @@ export const DISTRIBUTION_TABLE_ADDRESSES: Index = { arbitrum: '0xabD8D3693439A72393220d87aee159952261Ad1f', 'arbitrum-sepolia': '0xd1D57F18252D06a6b28DE96B6cbF7F4283A4F205', } +export const VOTING_ESCROW_DEPOSITOR_ADDRESSES: Index = { + arbitrum: '0xBE19a62384014F103686dfE6D9d50B1D3E81B2d0', + sepolia: '0xe77ede9B472E9AE450a1AcD4A90dcd3fb2e50cD0', +} +export const SMART_WALLET_CHECKER_ADDRESSES: Index = { + arbitrum: '0x609BaFab765135091DB407b53D77c4C471Df3e8F', + sepolia: '0x1cF442024fAeE8aAF2d5E26d79718231Bf59d740', +} + export const DEPRIZE_DISTRIBUTION_TABLE_ADDRESSES: Index = { sepolia: '0xA441f20115c868dc66bC1977E1c17D4B9A0189c7', } diff --git a/ui/lib/utils/hooks/useWithdrawAmount.ts b/ui/lib/utils/hooks/useWithdrawAmount.ts new file mode 100644 index 000000000..c077d4c5a --- /dev/null +++ b/ui/lib/utils/hooks/useWithdrawAmount.ts @@ -0,0 +1,23 @@ +import { useState, useEffect } from 'react' + +export default function useWithdrawAmount( + votingEscrowDepositorContract: any, + userAddress: string +) { + const [withdrawAmount, setWithdrawAmount] = useState(0) + + useEffect(() => { + async function fetchWithdrawAmount() { + if (!votingEscrowDepositorContract || !userAddress) return + const theWithdrawAmount = await votingEscrowDepositorContract.call( + 'availableToWithdraw', + [userAddress] + ) + setWithdrawAmount(theWithdrawAmount) + } + + fetchWithdrawAmount() + }, [votingEscrowDepositorContract, userAddress]) + + return withdrawAmount +} From 91ccf9b6e9b7a999b550d32bb71a23db16ece147 Mon Sep 17 00:00:00 2001 From: "name.get" <99998750+namedotget@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:26:41 -0600 Subject: [PATCH 12/78] Job expirations and timed marketplace listings (#251) * Added marketplace listing and job start/end times * Removed log * Refactor * Refactor and added ids * Added testing state and env * Added tests * Adjusted import * Adjusted expiration input * Added citizen savings and original price * Adjusted oldFormResponseId * Adjusted timestamp rounding * Updated test to check savings and original price * Relocated share, edit and delete buttons * Added ids * Updated tests * Adjusted styling and truncated savings --- .github/workflows/ci.yml | 1 + ui/components/jobs/Job.tsx | 195 +++++--- ui/components/onboarding/CreateCitizen.tsx | 5 +- ui/components/onboarding/CreateTeam.tsx | 1 + ui/components/privy/PrivyConnectWallet.tsx | 62 +-- ui/components/privy/PrivyWeb3Button.tsx | 12 +- ui/components/subscription/TeamJobModal.tsx | 74 ++- ui/components/subscription/TeamListing.tsx | 465 +++++++++++------- .../TeamMarketplaceListingModal.tsx | 142 +++++- .../subscription/TeamMetadataModal.tsx | 8 +- ui/const/abis/JobBoardTable.json | 30 +- ui/const/abis/MarketplaceTable.json | 10 + ui/const/config.ts | 8 +- ui/cypress/fixtures/jobs/job.json | 11 + ui/cypress/fixtures/marketplace/listing.json | 2 + .../onboarding/create-citizen.cy.tsx | 4 +- .../integration/onboarding/create-team.cy.tsx | 4 +- .../subscription/team-job-modal.cy.tsx | 101 ++++ .../integration/subscription/team-job.cy.tsx | 112 +++++ .../subscription/team-listing.cy.tsx | 77 ++- .../team-marketplace-listing-modal.cy.tsx | 88 ++++ ui/lib/utils/hooks/useCurrUnixTime.tsx | 15 + ui/lib/utils/timestamp.ts | 20 + ui/pages/jobs.tsx | 11 +- ui/pages/marketplace/index.tsx | 6 +- 25 files changed, 1139 insertions(+), 325 deletions(-) create mode 100644 ui/cypress/fixtures/jobs/job.json create mode 100644 ui/cypress/integration/subscription/team-job-modal.cy.tsx create mode 100644 ui/cypress/integration/subscription/team-job.cy.tsx create mode 100644 ui/cypress/integration/subscription/team-marketplace-listing-modal.cy.tsx create mode 100644 ui/lib/utils/hooks/useCurrUnixTime.tsx create mode 100644 ui/lib/utils/timestamp.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 751e62534..9b082d180 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: runs-on: ubuntu-latest env: NEXT_PUBLIC_ENV: "dev" + NEXT_PUBLIC_TEST_ENV: "true" NEXT_PUBLIC_CHAIN: "testnet" DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }} diff --git a/ui/components/jobs/Job.tsx b/ui/components/jobs/Job.tsx index 2742f3b3e..dcc4fccc9 100644 --- a/ui/components/jobs/Job.tsx +++ b/ui/components/jobs/Job.tsx @@ -1,7 +1,9 @@ import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline' import { useNFT } from '@thirdweb-dev/react' import Link from 'next/link' -import { useState } from 'react' +import { useEffect, useState } from 'react' +import useCurrUnixTime from '@/lib/utils/hooks/useCurrUnixTime' +import { daysSinceTimestamp } from '@/lib/utils/timestamp' import Frame from '../layout/Frame' import { LoadingSpinner } from '../layout/LoadingSpinner' import StandardButton from '../layout/StandardButton' @@ -12,6 +14,10 @@ export type Job = { teamId: number title: string description: string + endTime: number + timestamp: number + tag: string + metadata: string contactInfo: string } @@ -34,84 +40,135 @@ export default function Job({ }: JobProps) { const [enabledEditJobModal, setEnabledEditJobModal] = useState(false) const [isDeleting, setIsDeleting] = useState(false) + const [isActive, setIsActive] = useState(false) + const [isExpired, setIsExpired] = useState(false) const { data: teamNft } = useNFT(teamContract, job.teamId) + const currTime = useCurrUnixTime() + + const daysSincePosting = daysSinceTimestamp(job?.timestamp) + + useEffect(() => { + if (currTime <= job.endTime || job.endTime === 0 || editable) { + setIsActive(true) + } else { + setIsActive(false) + } + + if ( + currTime > job.endTime && + job.endTime !== 0 && + job.endTime !== undefined + ) { + setIsExpired(true) + } else { + setIsExpired(false) + } + }, [currTime, job.endTime, editable]) + return ( -
- -
-
- {showTeam && teamNft && ( - - {teamNft.metadata.name} - - )} -

{job.title}

-
-
-
- {job.contactInfo && ( - { - window.open(job.contactInfo) - }} - > - Apply - - )} - {editable && ( -
- - {isDeleting ? ( - - ) : ( - + Apply + )} - {enabledEditJobModal && ( - + {editable && ( +
+ + {isDeleting ? ( + + ) : ( + + )} + {enabledEditJobModal && ( + + )} +
)}
- )} +
-
+

{job.description}

+ {editable && isExpired && ( +

+ {`*This job post has expired and is no longer available.`} +

+ )} + {!isExpired && job.endTime != 0 && ( +

+ {`This job was posted ${ + daysSincePosting === 0 + ? `today` + : daysSincePosting === 1 + ? `${daysSincePosting} day ago` + : `${daysSincePosting} days ago` + }`} +

+ )} +
-

{job.description}

- - + )} + ) } diff --git a/ui/components/onboarding/CreateCitizen.tsx b/ui/components/onboarding/CreateCitizen.tsx index c0c858a4c..f5620d72f 100644 --- a/ui/components/onboarding/CreateCitizen.tsx +++ b/ui/components/onboarding/CreateCitizen.tsx @@ -276,7 +276,9 @@ export default function CreateCitizen({ )} {inputImage?.name === citizenImage?.name || generateError ? (

- {'Unable to generate an image, please try again later, or proceed with checkout to use the image above.'} + { + 'Unable to generate an image, please try again later, or proceed with checkout to use the image above.' + }

) : ( <> @@ -423,6 +425,7 @@ export default function CreateCitizen({ { diff --git a/ui/components/onboarding/CreateTeam.tsx b/ui/components/onboarding/CreateTeam.tsx index f9211098b..f4c637808 100644 --- a/ui/components/onboarding/CreateTeam.tsx +++ b/ui/components/onboarding/CreateTeam.tsx @@ -332,6 +332,7 @@ export default function CreateTeam({ { diff --git a/ui/components/privy/PrivyConnectWallet.tsx b/ui/components/privy/PrivyConnectWallet.tsx index 2ec0d7f77..30233b07a 100644 --- a/ui/components/privy/PrivyConnectWallet.tsx +++ b/ui/components/privy/PrivyConnectWallet.tsx @@ -1,6 +1,5 @@ import { XMarkIcon } from '@heroicons/react/24/outline' import { useFundWallet, usePrivy, useWallets } from '@privy-io/react-auth' -import { allChains, Chain } from '@thirdweb-dev/chains' import { useAddress, useContract, useSDK } from '@thirdweb-dev/react' import { ethers } from 'ethers' import Image from 'next/image' @@ -8,7 +7,6 @@ import { useContext, useEffect, useMemo, useState } from 'react' import toast from 'react-hot-toast' import PrivyWalletContext from '../../lib/privy/privy-wallet-context' import ChainContext from '../../lib/thirdweb/chain-context' -import { useHandleRead } from '../../lib/thirdweb/hooks' import { useNativeBalance } from '../../lib/thirdweb/hooks/useNativeBalance' import { useENS } from '../../lib/utils/hooks/useENS' import { useImportToken } from '../../lib/utils/import-token' @@ -64,30 +62,34 @@ function SendModal({ let icon if (selectedToken === 'native') { icon = networkIcon - } else if (selectedToken === 'mooney') { - icon = - } else if (selectedToken === 'dai') { - icon = - } else if (selectedToken === 'usdc') { - icon = - } else if (selectedToken === 'usdt') { - icon = + } else { + icon = ( + + ) } return icon }, [selectedToken, networkIcon]) + const tokenContracts: { [key: string]: any } = { + mooney: mooneyContract, + dai: daiContract, + usdc: usdcContract, + usdt: usdtContract, + } + useEffect(() => { if (selectedToken === 'native') { setBalance(nativeBalance) - } else if (selectedToken === 'mooney') { - setBalance(formattedBalances.mooney) - } else if (selectedToken === 'dai') { - setBalance(formattedBalances.dai) - } else if (selectedToken === 'usdc') { - setBalance(formattedBalances.usdc) - } else if (selectedToken === 'usdt') { - setBalance(formattedBalances.usdt) + } else { + setBalance(formattedBalances[selectedToken]) } }, [selectedToken, nativeBalance, formattedBalances]) @@ -119,26 +121,14 @@ function SendModal({ to, value: formattedAmount, }) - } else if (selectedToken === 'mooney') { - if (+amount > formattedBalances.mooney) - return toast.error('Insufficient funds') - - await mooneyContract.call('transfer', [to, formattedAmount]) - } else if (selectedToken === 'dai') { - if (+amount > formattedBalances.dai) + } else { + if (+amount > formattedBalances[selectedToken]) return toast.error('Insufficient funds') - await daiContract.call('transfer', [to, formattedAmount]) - } else if (selectedToken === 'usdc') { - if (+amount > formattedBalances.usdc) - return toast.error('Insufficient funds') - - await usdcContract.call('transfer', [to, formattedAmount]) - } else if (selectedToken === 'usdt') { - if (+amount > formattedBalances.usdt) - return toast.error('Insufficient funds') - - await usdtContract.call('transfer', [to, formattedAmount]) + await tokenContracts[selectedToken].call('transfer', [ + to, + formattedAmount, + ]) } } catch (err) { console.log(err) diff --git a/ui/components/privy/PrivyWeb3Button.tsx b/ui/components/privy/PrivyWeb3Button.tsx index fd87c0950..078c8c9c3 100644 --- a/ui/components/privy/PrivyWeb3Button.tsx +++ b/ui/components/privy/PrivyWeb3Button.tsx @@ -11,6 +11,7 @@ Button States: */ type PrivyWeb3BtnProps = { + id?: string label: any type?: string action: Function @@ -23,6 +24,7 @@ type PrivyWeb3BtnProps = { } function Button({ + id, type = 'button', className, onClick, @@ -31,6 +33,7 @@ function Button({ }: any) { return ( )} {btnState === 1 && (