diff --git a/apps/web/src/components/Icon/assets/info-16.svg b/apps/web/src/components/Icon/assets/info-16.svg
new file mode 100644
index 000000000..743c277a2
--- /dev/null
+++ b/apps/web/src/components/Icon/assets/info-16.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/apps/web/src/components/Icon/icons.ts b/apps/web/src/components/Icon/icons.ts
index f82ce104b..26d8ddb1d 100644
--- a/apps/web/src/components/Icon/icons.ts
+++ b/apps/web/src/components/Icon/icons.ts
@@ -22,6 +22,7 @@ import External from './assets/external-16.svg'
import Github from './assets/github.svg'
import Globe from './assets/globe.svg'
import HandlebarCircle from './assets/handlebar-circle.svg'
+import Info16 from './assets/info-16.svg'
import Move from './assets/move.svg'
import NewWindow from './assets/new-window.svg'
import NounsConnect from './assets/nouns-connect.svg'
@@ -56,6 +57,7 @@ export const icons = {
github: Github,
eth: Eth,
handlebarCircle: HandlebarCircle,
+ 'info-16': Info16,
globe: Globe,
move: Move,
newWindow: NewWindow,
diff --git a/apps/web/src/modules/create-dao/components/AllocationForm/AllocationForm.schema.ts b/apps/web/src/modules/create-dao/components/AllocationForm/AllocationForm.schema.ts
index c688dd457..6f131cef2 100644
--- a/apps/web/src/modules/create-dao/components/AllocationForm/AllocationForm.schema.ts
+++ b/apps/web/src/modules/create-dao/components/AllocationForm/AllocationForm.schema.ts
@@ -17,7 +17,7 @@ const validateAddress = async (
export const deboucedValidateAddress = debounce(validateAddress, 500)
-const allocationSchema = Yup.object().shape({
+export const allocationSchema = Yup.object().shape({
founderAddress: Yup.string()
.test(
'isValidAddress',
@@ -28,12 +28,12 @@ const allocationSchema = Yup.object().shape({
allocationPercentage: Yup.number()
.transform((value) => (isNaN(value) ? undefined : value))
.required('*')
+ .integer('Must be whole number')
+ .max(100, '< 100')
.when('admin', (admin, schema) => {
if (!admin) return schema.min(1, '> 0') // (condition, errorMessage) - allocation represented as % must be greater than or equal to 0
return schema
- })
- .max(100, '< 100')
- .integer('Must be whole number'),
+ }),
endDate: Yup.string()
.required('*')
.test('isDateInFuture', 'Must be in future', (value: string | undefined) => {
diff --git a/apps/web/src/modules/dao/components/About/About.tsx b/apps/web/src/modules/dao/components/About/About.tsx
index 400af4c41..ae29a2c9c 100644
--- a/apps/web/src/modules/dao/components/About/About.tsx
+++ b/apps/web/src/modules/dao/components/About/About.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex, Text } from '@zoralabs/zord'
+import { Box, Flex, Grid, Text } from '@zoralabs/zord'
import HtmlReactParser from 'html-react-parser'
import { getFetchableUrl } from 'ipfs-service'
import Image from 'next/legacy/image'
@@ -11,6 +11,7 @@ import { CHAIN } from 'src/constants/network'
import SWR_KEYS from 'src/constants/swrKeys'
import { metadataAbi, tokenAbi } from 'src/data/contract/abis'
import { sdk } from 'src/data/graphql/client'
+import { useLayoutStore } from 'src/stores'
import { about, daoDescription, daoInfo, daoName } from 'src/styles/About.css'
import { unpackOptionalArray } from 'src/utils/helpers'
import { formatCryptoVal } from 'src/utils/numbers'
@@ -18,12 +19,14 @@ import { formatCryptoVal } from 'src/utils/numbers'
import { useDaoStore } from '../../stores'
import { parseContractURI } from '../../utils'
import { ExternalLinks } from './ExternalLinks'
+import { Founder } from './Founder'
import { Statistic } from './Statistic'
export const About: React.FC = () => {
const {
addresses: { token, treasury, metadata },
} = useDaoStore()
+ const { isMobile } = useLayoutStore()
const tokenContractParams = {
abi: tokenAbi,
@@ -38,16 +41,15 @@ export const About: React.FC = () => {
contracts: [
{ ...tokenContractParams, functionName: 'name' },
{ ...tokenContractParams, functionName: 'totalSupply' },
+ { ...tokenContractParams, functionName: 'getFounders' },
{ ...metadataContractParams, functionName: 'contractImage' },
{ ...metadataContractParams, functionName: 'description' },
{ ...metadataContractParams, functionName: 'contractURI' },
],
})
- const [name, totalSupply, daoImage, description, contractURI] = unpackOptionalArray(
- contractData,
- 5
- )
+ const [name, totalSupply, founders, daoImage, description, contractURI] =
+ unpackOptionalArray(contractData, 6)
const parsedContractURI = parseContractURI(contractURI)
const { data: balance } = useBalance({ address: treasury as Address })
@@ -133,6 +135,19 @@ export const About: React.FC = () => {
>
+
+ {typeof founders !== 'undefined' && founders.length > 0 ? (
+ <>
+
+ Founders
+
+
+ {founders.map((founder) => (
+
+ ))}
+
+ >
+ ) : null}
)
}
diff --git a/apps/web/src/modules/dao/components/About/Founder.tsx b/apps/web/src/modules/dao/components/About/Founder.tsx
new file mode 100644
index 000000000..2f2dc36e0
--- /dev/null
+++ b/apps/web/src/modules/dao/components/About/Founder.tsx
@@ -0,0 +1,69 @@
+import { Box, Flex, PopUp, Text } from '@zoralabs/zord'
+import { useState } from 'react'
+
+import { Avatar } from 'src/components/Avatar'
+import { Icon } from 'src/components/Icon'
+import { ETHERSCAN_BASE_URL } from 'src/constants/etherscan'
+import { useEnsData } from 'src/hooks'
+import { AddressType } from 'src/typings'
+
+interface FounderProps {
+ wallet: AddressType
+ ownershipPct: number
+ vestExpiry: number
+}
+
+export const Founder: React.FC = ({ wallet, ownershipPct, vestExpiry }) => {
+ const [showTooltip, setShowTooltip] = useState(false)
+ const { displayName, ensAvatar } = useEnsData(wallet as string)
+ const vestDate = new Date(vestExpiry * 1000).toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })
+ return (
+
+
+
+
+
+ {displayName}
+
+
+
+
+
+ {ownershipPct}%
+
+ setShowTooltip(true)}
+ onMouseLeave={() => setShowTooltip(false)}
+ >
+
+
+ >}>
+ {`In effect until ${vestDate}`}
+
+
+
+ )
+}
diff --git a/apps/web/src/modules/dao/components/AdminForm/AdminForm.schema.ts b/apps/web/src/modules/dao/components/AdminForm/AdminForm.schema.ts
index a5766ff70..84915c66f 100644
--- a/apps/web/src/modules/dao/components/AdminForm/AdminForm.schema.ts
+++ b/apps/web/src/modules/dao/components/AdminForm/AdminForm.schema.ts
@@ -1,7 +1,8 @@
import { Provider } from '@ethersproject/abstract-provider'
import * as Yup from 'yup'
-import { auctionSettingsValidationSchema } from 'src/modules/create-dao'
+import { TokenAllocation, auctionSettingsValidationSchema } from 'src/modules/create-dao'
+import { allocationSchema } from 'src/modules/create-dao/components/AllocationForm/AllocationForm.schema'
import { Duration } from 'src/typings'
import { isValidAddress } from 'src/utils/ens'
import { durationValidationSchema, urlValidationSchema } from 'src/utils/yup'
@@ -17,6 +18,7 @@ export interface AdminFormValues {
quorumThreshold: number
votingPeriod: Duration
votingDelay: Duration
+ founderAllocation: TokenAllocation[]
vetoPower: boolean
vetoer: string
}
@@ -48,6 +50,16 @@ export const adminValidationSchema = (provider: Provider | undefined) =>
{ value: tenMinutes, description: '10 minutes' },
{ value: twentyFourWeeks, description: '24 weeks' }
),
+ founderAllocation: Yup.array()
+ .of(allocationSchema)
+ .test(
+ 'unique',
+ 'Founder allocation addresses should be unique.',
+ function (values) {
+ const addresses = values?.map((v) => v.founderAddress)
+ return values?.length === new Set(addresses)?.size
+ }
+ ),
vetoPower: Yup.bool().required('*'),
})
)
diff --git a/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx b/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx
index fefa2c15b..3462e86ca 100644
--- a/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx
+++ b/apps/web/src/modules/dao/components/AdminForm/AdminForm.tsx
@@ -1,6 +1,6 @@
-import { Flex, Stack } from '@zoralabs/zord'
-import { Contract, ethers } from 'ethers'
-import { Formik, FormikValues } from 'formik'
+import { Flex, Stack, Text } from '@zoralabs/zord'
+import { BigNumber, Contract, ethers } from 'ethers'
+import { FieldArray, Formik, FormikValues } from 'formik'
import { AnimatePresence, motion } from 'framer-motion'
import isEqual from 'lodash/isEqual'
import { useRouter } from 'next/router'
@@ -15,7 +15,8 @@ import TextArea from 'src/components/Fields/TextArea'
import { NUMBER, TEXT } from 'src/components/Fields/types'
import SingleImageUpload from 'src/components/SingleImageUpload/SingleImageUpload'
import { NULL_ADDRESS } from 'src/constants/addresses'
-import { auctionAbi, governorAbi, metadataAbi } from 'src/data/contract/abis'
+import { auctionAbi, governorAbi, metadataAbi, tokenAbi } from 'src/data/contract/abis'
+import { TokenAllocation } from 'src/modules/create-dao'
import {
BuilderTransaction,
TransactionType,
@@ -24,11 +25,14 @@ import {
import { formValuesToTransactionMap } from 'src/modules/dao/utils/adminFormFieldToTransaction'
import { useLayoutStore } from 'src/stores'
import { sectionWrapperStyle } from 'src/styles/dao.css'
+import { AddressType } from 'src/typings'
import { getEnsAddress } from 'src/utils/ens'
import { compareAndReturn, fromSeconds, unpackOptionalArray } from 'src/utils/helpers'
import { DaoContracts, useDaoStore } from '../../stores'
import { AdminFormValues, adminValidationSchema } from './AdminForm.schema'
+import { AdminFounderAllocationFields } from './AdminFounderAllocationFields'
+import { Section } from './Section'
interface AdminFormProps {
collectionAddress: string
@@ -66,9 +70,15 @@ export const AdminForm: React.FC = ({ collectionAddress }) => {
address: addresses?.metadata as Address,
}
+ const tokenContractParams = {
+ abi: tokenAbi,
+ address: addresses?.token as Address,
+ }
+
const auctionContract = useContract(auctionContractParams)
const governorContract = useContract(governorContractParams)
const metadataContract = useContract(metadataContractParams)
+ const tokenContract = useContract(tokenContractParams)
const { data } = useContractReads({
contracts: [
@@ -83,6 +93,7 @@ export const AdminForm: React.FC = ({ collectionAddress }) => {
{ ...metadataContractParams, functionName: 'projectURI' },
{ ...metadataContractParams, functionName: 'rendererBase' },
{ ...metadataContractParams, functionName: 'description' },
+ { ...tokenContractParams, functionName: 'getFounders' },
],
})
@@ -98,12 +109,14 @@ export const AdminForm: React.FC = ({ collectionAddress }) => {
daoWebsite,
rendererBase,
description,
- ] = unpackOptionalArray(data, 11)
+ founders,
+ ] = unpackOptionalArray(data, 12)
const contracts: DaoContracts = {
auctionContract: auctionContract ?? undefined,
governorContract: governorContract ?? undefined,
metadataContract: metadataContract ?? undefined,
+ tokenContract: tokenContract ?? undefined,
}
const initialValues: AdminFormValues = {
@@ -121,6 +134,12 @@ export const AdminForm: React.FC = ({ collectionAddress }) => {
quorumThreshold: Number(quorumVotesBps) / 100 || 0,
votingPeriod: fromSeconds(votingPeriod && Number(votingPeriod)),
votingDelay: fromSeconds(votingDelay && Number(votingDelay)),
+ founderAllocation:
+ founders?.map((x) => ({
+ founderAddress: x.wallet,
+ allocationPercentage: x.ownershipPct,
+ endDate: new Date(x.vestExpiry * 1000).toISOString(),
+ })) || [],
vetoPower: !!vetoer && vetoer !== NULL_ADDRESS,
vetoer: vetoer || '',
@@ -189,6 +208,19 @@ export const AdminForm: React.FC = ({ collectionAddress }) => {
value = await getEnsAddress(value as string, provider)
}
+ if (field === 'founderAllocation') {
+ // @ts-ignore
+ value = (value as TokenAllocation[]).map(
+ ({ founderAddress, allocationPercentage, endDate }) => ({
+ wallet: founderAddress as AddressType,
+ ownershipPct: allocationPercentage
+ ? BigNumber.from(allocationPercentage)
+ : BigNumber.from(0),
+ vestExpiry: BigNumber.from(Math.floor(new Date(endDate).getTime() / 1000)),
+ })
+ )
+ }
+
const transactionProperties = formValuesToTransactionMap[field]
// @ts-ignore
const calldata = transactionProperties.constructCalldata(contracts, value)
@@ -253,180 +285,222 @@ export const AdminForm: React.FC = ({ collectionAddress }) => {
validateOnMount
>
{(formik) => {
- const changes = compareAndReturn(formik.initialValues, formik.values).length
-
+ const founderChanges = isEqual(
+ formik.initialValues.founderAllocation,
+ formik.values.founderAllocation
+ )
+ ? 0
+ : 1
+ const changes =
+ compareAndReturn(formik.initialValues, formik.values).length +
+ founderChanges
return (
-
-
-
-
- {
- formik.setFieldValue('daoWebsite', target.value)
- }}
- onBlur={formik.handleBlur}
- errorMessage={formik.errors['daoWebsite']}
- placeholder={'https://www.nouns.wtf'}
- />
-
- {
- formik.setFieldValue('rendererBase', target.value)
- }}
- onBlur={formik.handleBlur}
- errorMessage={formik.errors['rendererBase']}
- helperText={
- 'This is the base url of the image stacker used to stack the layers and compose an nft.'
- }
- />
-
-
-
- {
- formik.setFieldValue(
- 'auctionReservePrice',
- parseFloat(target.value)
- )
- }}
- onBlur={formik.handleBlur}
- errorMessage={formik.errors['auctionReservePrice']}
- perma={'ETH'}
- />
-
- {
- formik.setFieldValue('proposalThreshold', parseFloat(target.value))
- }}
- onBlur={formik.handleBlur}
- errorMessage={formik.errors['proposalThreshold']}
- perma={'%'}
- step={0.1}
- helperText={
- 'This is the percentage of all existing tokens that must be owned by someone attempting to create a proposal. We recommend a starting value of 0.5% to encourage participation.'
- }
- />
-
- {
- formik.setFieldValue('quorumThreshold', parseFloat(target.value))
- }}
- onBlur={formik.handleBlur}
- errorMessage={formik.errors['quorumThreshold']}
- perma={'%'}
- step={1}
- helperText={
- 'This is the percentage of all existing tokens that must vote in a proposal in order for it to pass (as long as a majority of votes approve). We recommend a starting value of 10%.'
- }
- />
-
-
-
-
-
-
- {formik.values['vetoPower'] === true && (
-
-
- Admin
+
+ Editing DAO settings will create a proposal.
+
+
+
+
+
+
+
+ {
+ formik.setFieldValue('daoWebsite', target.value)
+ }}
+ onBlur={formik.handleBlur}
+ errorMessage={formik.errors['daoWebsite']}
+ placeholder={'https://www.nouns.wtf'}
+ />
+
+ {
+ formik.setFieldValue('rendererBase', target.value)
+ }}
+ onBlur={formik.handleBlur}
+ errorMessage={formik.errors['rendererBase']}
+ helperText={
+ 'This is the base url of the image stacker used to stack the layers and compose an nft.'
+ }
+ />
+
+
+
+
+
+ {
+ formik.setFieldValue(
+ 'auctionReservePrice',
+ parseFloat(target.value)
+ )
+ }}
+ onBlur={formik.handleBlur}
+ errorMessage={formik.errors['auctionReservePrice']}
+ perma={'ETH'}
+ />
+
+
+
+ {
+ formik.setFieldValue(
+ 'proposalThreshold',
+ parseFloat(target.value)
+ )
+ }}
+ onBlur={formik.handleBlur}
+ errorMessage={formik.errors['proposalThreshold']}
+ perma={'%'}
+ step={0.1}
+ helperText={
+ 'This is the percentage of all existing tokens that must be owned by someone attempting to create a proposal. We recommend a starting value of 0.5% to encourage participation.'
+ }
+ />
+
+ {
+ formik.setFieldValue('quorumThreshold', parseFloat(target.value))
+ }}
+ onBlur={formik.handleBlur}
+ errorMessage={formik.errors['quorumThreshold']}
+ perma={'%'}
+ step={1}
+ helperText={
+ 'This is the percentage of all existing tokens that must vote in a proposal in order for it to pass (as long as a majority of votes approve). We recommend a starting value of 10%.'
+ }
+ />
+
+
+
+
+
+
+
+
+
+ {formik.values['vetoPower'] === true && (
+
+
+
+
+
+ )}
+
+
+
+
+ {({ remove, push }) => (
+
+ push({ founderAddress: '', allocation: '', endDate: '' })
}
/>
-
-
- )}
+ )}
+
+
+ touched: FormikTouched
+ formik: FormikProps
+ removeFounderAddress: (index: number) => void
+ addFounderAddress: () => void
+}
+
+export const AdminFounderAllocationFields = ({
+ values,
+ auctionDuration,
+ errors,
+ touched,
+ formik,
+ removeFounderAddress,
+ addFounderAddress,
+}: AdminFounderAllocationFieldsProps) => {
+ return (
+
+
+ {values.founderAllocation.map((founder, index) => {
+ const error =
+ typeof errors?.founderAllocation === 'object'
+ ? (errors?.founderAllocation?.[index] as FormikErrors)
+ : undefined
+
+ const touchedField = touched?.founderAllocation?.[index]
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {founder?.allocationPercentage && founder?.endDate ? (
+
+ ~{' '}
+ {calculateMaxAllocation(
+ founder?.allocationPercentage,
+ founder?.endDate,
+ auctionDuration
+ )}{' '}
+ Tokens
+
+ ) : null}
+
+
+ )
+ })}
+
+
+
+
+
+
+ {typeof errors?.founderAllocation === 'string' && (
+
+ {errors?.founderAllocation}
+
+ )}
+
+ )
+}
diff --git a/apps/web/src/modules/dao/components/AdminForm/Section.tsx b/apps/web/src/modules/dao/components/AdminForm/Section.tsx
new file mode 100644
index 000000000..1f2f95a34
--- /dev/null
+++ b/apps/web/src/modules/dao/components/AdminForm/Section.tsx
@@ -0,0 +1,28 @@
+import { Box, Text } from '@zoralabs/zord'
+
+interface SectionProps {
+ title: string
+ children: React.ReactNode
+}
+
+export const Section: React.FC = ({ title, children }) => {
+ return (
+ <>
+
+ {title}
+
+
+ {children}
+
+ >
+ )
+}
diff --git a/apps/web/src/modules/dao/utils/adminFormFieldToTransaction.tsx b/apps/web/src/modules/dao/utils/adminFormFieldToTransaction.tsx
index 86614e0d9..5d3b639d7 100644
--- a/apps/web/src/modules/dao/utils/adminFormFieldToTransaction.tsx
+++ b/apps/web/src/modules/dao/utils/adminFormFieldToTransaction.tsx
@@ -106,6 +106,15 @@ export const formValuesToTransactionMap: FormValuesTransactionMap = {
toSeconds(value),
]),
},
+ founderAllocation: {
+ functionSignature: 'updateFounders',
+ getTarget: (addresses) => addresses.token as AddressType,
+ constructCalldata: ({ tokenContract }, value) =>
+ tokenContract?.interface.encodeFunctionData(
+ 'updateFounders((address,uint256,uint256)[])',
+ [value]
+ ),
+ },
vetoPower: {
functionSignature: 'burnVetoer',
getTarget: (addresses) => addresses.governor as AddressType,