diff --git a/Implementations/API/README.md b/Implementations/API/README.md index 6959a54e..98ebe349 100644 --- a/Implementations/API/README.md +++ b/Implementations/API/README.md @@ -11,12 +11,11 @@ Currently supported DAOs and frameworks: | DAOStack | Yes | Yes | No | Mainnet | | Aave | No | Yes | No | Mainnet | | Safe | Yes | Yes | Yes | Mainnet | - +| Nouns | Yes | | | Mainnet | It can be forked to support other DAO frameworks. - ## Moloch *This reference implementation has not been finalized. It is pending changes to reflect the most recent version of the DAO standard.* diff --git a/Implementations/API/backend/functions/config.ts b/Implementations/API/backend/functions/config.ts index 7716fb91..89daa242 100644 --- a/Implementations/API/backend/functions/config.ts +++ b/Implementations/API/backend/functions/config.ts @@ -24,4 +24,8 @@ export const gnosisApiConfig: { [key: string]: any } = { export const snapshotApiConfig: { [key: string]: any } = { '1': 'https://hub.snapshot.org/graphql' +} + +export const nonusApiConfig: { [key: string]: any } = { + '1': 'https://api.thegraph.com/subgraphs/name/nounsdao/nouns-subgraph' } \ No newline at end of file diff --git a/Implementations/API/backend/functions/nouns/getMembers.ts b/Implementations/API/backend/functions/nouns/getMembers.ts new file mode 100644 index 00000000..b181073f --- /dev/null +++ b/Implementations/API/backend/functions/nouns/getMembers.ts @@ -0,0 +1,171 @@ +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; +import { ConstructorFragment } from "ethers/lib/utils"; +import { nonusApiConfig } from "functions/config"; +import fetch, { RequestInit } from 'node-fetch' + +function apiRequest(path: string, method: string, data: any) { + return fetch(path, { + headers: { + 'Content-Type': 'application/json', + }, + method, + redirect: 'follow', + body: JSON.stringify(data), + }).then((res) => res.json()) +} + +async function getRes(query: string, path: string) { + const data = { + query + } + + const res = (await apiRequest(path, 'POST', data)) as any + return res; +} + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const network = event?.pathParameters?.network + if (!network) return { statusCode: 400, message: 'Missing network' } + + console.log({ graphConfig: nonusApiConfig }) + const path = nonusApiConfig[network] + if (!path) return { statusCode: 400, message: 'Missing config for network' } + + const eventId = event?.pathParameters?.id + if (!eventId) return { statusCode: 400, message: 'Missing id' } + + const template = { + '@context': { + '@vocab': 'http://daostar.org/', + }, + type: 'DAO', + name: eventId, + } + + let res = await getRes(`query { + accounts( where: {tokenBalance_not: "0"},first:1000) { + tokenBalance + id + nouns { + seed { + id + } + owner { + delegate { + id + } + } + } + } + delegates(first: 1000) { + id + delegatedVotes + nounsRepresented { + id + owner { + id + } + } + } + }`, path); + + let res_1 = await getRes(`query { + delegates(skip: 1000, first: 1000) { + id + delegatedVotes + nounsRepresented { + id + owner { + id + } + } + } + }`, path); + + + let members: any = []; + let delegates = res.data.delegates.concat(res_1.data.delegates); + // accounts + for (let index = 0; index < res.data.accounts.length; index++) { + const n = res.data.accounts[index]; + let member: any = {} + //let tokenBalance = n.tokenBalance; + member['memberId'] = { + "@value": n.id, + "@type": "EthereumAddress", + } + let votingShares = Number(n.tokenBalance); + let nonvotingShares = 0; + let delegatee_ethereum_address: any = []; + let delegate = null; + //let delegatedVotes = 0; + for (let index = 0; index < n.nouns.length; index++) { + const m = n.nouns[index]; + if (m.owner.delegate.id != n.id) { + votingShares = votingShares - 1; + nonvotingShares = nonvotingShares + 1; + } + } + delegate = delegates.find((d: any) => { + return d.id == n.id + }); + delegates = delegates.filter((d: any) => { + return d.id != n.id + }); + + member['votingShares'] = votingShares; + member['nonVotingShares'] = nonvotingShares; + member['delegatedShares'] = delegate.delegatedVotes; + if (delegate.delegatedVotes > 0) { + delegate.nounsRepresented.forEach((m: any) => { + delegatee_ethereum_address.push({ + "@value": m.owner.id, + "@type": "EthereumAddress", + }) + }); + } + + member['delegatee-ethereum-address'] = delegatee_ethereum_address; + members.push(member); + + } + + for (let index = 0; index < delegates.length; index++) { + const n = delegates[index]; + if (n.delegatedVotes > 0) { + let member: any = {} + //let tokenBalance = n.tokenBalance; + member['memberId'] = { + "@value": n.id, + "@type": "EthereumAddress", + } + + let delegatee_ethereum_address: any = []; + + //let delegatedVotes = 0; + + member['votingShares'] = 0; + member['nonVotingShares'] = 0; + member['delegatedShares'] = n.delegatedVotes; + n.nounsRepresented.forEach((m: any) => { + delegatee_ethereum_address.push({ + "@value": m.owner.id, + "@type": "EthereumAddress", + }) + }); + member['delegatee-ethereum-address'] = delegatee_ethereum_address; + members.push(member); + } + } + const transformed = { members: members, ...template }; + return transformed + ? { + statusCode: 200, + body: JSON.stringify(transformed) + } + : { + statusCode: 404, + body: JSON.stringify({ error: true }) + } + +} diff --git a/Implementations/API/stacks/MyStack.ts b/Implementations/API/stacks/MyStack.ts index 38926fdd..bbbcdb4a 100644 --- a/Implementations/API/stacks/MyStack.ts +++ b/Implementations/API/stacks/MyStack.ts @@ -20,6 +20,7 @@ export function MyStack({ stack }: StackContext) { 'GET /daodao/proposals/{network}/{id}': 'functions/daodao/getProposals.handler', 'GET /snapshot/members/{id}': 'functions/snapshot/getMembers.handler', 'GET /snapshot/proposals/{id}': 'functions/snapshot/getProposals.handler', + 'GET /nouns/members/{network}/{id}': 'functions/nouns/getMembers.handler', }, customDomain: { domainName: 'services.daostar.org', diff --git a/Implementations/Nouns/README.md b/Implementations/Nouns/README.md new file mode 100644 index 00000000..15162b5c --- /dev/null +++ b/Implementations/Nouns/README.md @@ -0,0 +1,108 @@ +# Nouns + +Nouns DAO uses a fork of Compound Governance. + +*This reference implementation has not been finalized. It is pending changes to reflect the most recent version of the DAO standard.* + +### Members URI + +In this implementation of membersURI, we define a member of the DAO as any address with a voting share OR non-voting shares greater than or equal to zero. votingShares represent the amount of token each address has and is not delegated into another address. nonVotingShares represent the number of tokens one delegated to another address. Also, delegatedShares indicates the total number of tokens delegated to the member’s address. + +We add a checkpoint property because the Nouns governance contract keeps track of the voting weights based on the block time by using checkpoints. Nouns governance token contract has a structure named Checkpoint which has two elements; fromBlock and votes. Each time a member transfers tokens to another party delegates the tokens to another party or cancels the delegation this structure for the member will be updated. fromBlock will be changed into the block number in which the change happen and votes will be changed in a proper way based on the transfer of voting right. + +Also, it should be mentioned that Nouns governance calculates the voting weight of each member, by calculating the voting power each member had at the starting time of each proposal. So, when a member decides to vote on a specific proposal, the smart contract is using checkpoints of the member to find the amount of voting power the member had in the start time of the proposal (the block time in which that specific proposal is submitted). + +So, to have the members and their voting power on each proposal we need to keep track of the checkpoints and votingShares, nonVotingShares and delegatedShares related to each of the checkpoints of the members. + +(For optimization we can just calculate the start time of the oldest ongoing proposal and remove the objects with checkpoints lesser than that from the membersURI JSON) + +We add "delegatee-ethereum-address" field to specify to which address the member delegated their voting right. + +```json +{ + + "@context": { + + "members": "", + + "member": "", + + "checkpoint" : "", + + "memberId": "", + + "votingShares": "", + + "nonVotingShares": "", + + "ethereum-address": "", + + "delegatee-ethereum-address": "", + + }, + + "members": [{ + + "memberId": { + + "@value": "0xabc123", + + "@type": "EthereumAddress" + + "checkpoint" : "12000000" + + }, + + "votingShares": "0", + + "nonVotingShares": "100", + + "delegatedShares" : "0", + + "delegatee-ethereum-address" : { + + "@value": "0xabc444", + + "@type": "EthereumAddress" + + } + + }, + + "memberId": { + + "@value": "0xdef987", + + "@type": "EthereumAddress" + + "checkpoint" : "12500000" + + }, + + "votingShares": "150", + + "nonvotingShares": "0", + + "delegatedShares" : "200", + + "delegatee-ethereum-address" : { + + "@value": "0xasfh85", + + "@type": "EthereumAddress" + + } + + } + + ] + +} +``` + +### Proposals URI + + + +## Compound +