diff --git a/src/strategies/index.ts b/src/strategies/index.ts index c28fc3116..b94781317 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'fs'; import path from 'path'; +import * as subgraphSplitDelegation from './subgraph-split-delegation'; import * as polygonSelfStaked from './polygon-self-staked-pol'; import * as delegatexyzErc721BalanceOf from './delegatexyz-erc721-balance-of'; import * as urbitGalaxies from './urbit-galaxies/index'; @@ -831,6 +832,7 @@ const strategies = { spaceid, 'delegate-registry-v2': delegateRegistryV2, 'split-delegation': splitDelegation, + 'subgraph-split-delegation': subgraphSplitDelegation, 'polygon-self-staked-pol': polygonSelfStaked, 'hats-protocol-single-vote-per-org': hatsProtocolSingleVotePerOrg, 'karma-discord-roles': karmaDiscordRoles, diff --git a/src/strategies/subgraph-split-delegation/README.md b/src/strategies/subgraph-split-delegation/README.md new file mode 100644 index 000000000..da884137a --- /dev/null +++ b/src/strategies/subgraph-split-delegation/README.md @@ -0,0 +1,33 @@ +# subgraph-split-delegation + +If you want to delegate your voting power to different addresses, you can use this strategy to calculate the voting power that will be delegated based on the Subgraph data. + +```TEXT +Total VP = incoming delegated VP + own VP - outgoing delegated VP +``` + +The sub strategies defined in params are used to get the votint power that will be delegated based on the Subgraph data. + +| Param Name | Description | +| ----------- | ----------- | +| strategies | list of sub strategies to calculate voting power based on delegation | +| subgraphUrl | The URL of the subgraph to query for the delegation data | + +Here is an example of parameters: + +```json +{ + "subgraphUrl": "https://api.studio.thegraph.com/query/87073/split-delegation/v0.0.5", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0xD01Db8Fb3CE7AeeBfB24317E12a0A854c256E99b", + "symbol": "EPT", + "decimals": 18 + } + } + ] +} + +``` diff --git a/src/strategies/subgraph-split-delegation/examples.json b/src/strategies/subgraph-split-delegation/examples.json new file mode 100644 index 000000000..3592ccd33 --- /dev/null +++ b/src/strategies/subgraph-split-delegation/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Subgraph split delegation", + "strategy": { + "name": "subgraph-split-delegation", + "params": { + "subgraphUrl": "https://api.studio.thegraph.com/query/87073/split-delegation/v0.0.5", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0xD01Db8Fb3CE7AeeBfB24317E12a0A854c256E99b", + "symbol": "EPT", + "decimals": 18 + } + } + ] + } + }, + "network": "11155111", + "addresses": [ + "0xb347106e4a026dd86c4bbe8df7274ba9ee7442cc", + "0x048fee7c3279a24af0790b6b002ded42be021d2b", + "0x139a9032a46c3afe3456eb5f0a35183b5f189cae", + "0xc1d60f584879f024299da0f19cdb47b931e35b53", + "0x376c649111543c46ce15fd3a9386b4f202a6e06c", + "0x0dcbc5d2bda11c4247d088f1ac5209e30f47b595", + "0x35911cc89aabe7af6726046823d5b678b6a1498d", + "0x6cdebe940bc0f26850285caca097c11c33103e47", + "0xa76c5788be9a2e1416de639c50c60b184d0ceccd", + "0xffb026f67da0869eb3abb090cb7f015ce0925cdf" + ], + "snapshot": 6716841 + } +] \ No newline at end of file diff --git a/src/strategies/subgraph-split-delegation/index.ts b/src/strategies/subgraph-split-delegation/index.ts new file mode 100644 index 000000000..2895e16cf --- /dev/null +++ b/src/strategies/subgraph-split-delegation/index.ts @@ -0,0 +1,210 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest, getScoresDirect } from '../../utils'; +import { Strategy } from '@snapshot-labs/snapshot.js/dist/src/voting/types'; + +export const author = 'aragon'; +export const version = '0.1.0'; + +const DEFAULT_BACKEND_URL = + 'https://api.studio.thegraph.com/query/87073/split-delegation/version/latest'; + +type Params = { + subgraphUrl: string; + strategies: Strategy[]; +}; + +type Delegation = { + delegator?: Member; + delegatee?: Member; + ratio: number; +}; + +type Member = { + id?: string; + address: string; + delegators?: Delegation[]; + delegatees?: Delegation[]; +}; + +export async function strategy( + space: string, + network: string, + provider: any, + addresses: string[], + options: Params = { + subgraphUrl: DEFAULT_BACKEND_URL, + strategies: [] + }, + snapshot: string | number +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const block = await provider.getBlock(blockTag); + + const members = await getDelegations( + options.subgraphUrl, + addresses, + space, + block + ); + + addresses = addresses.map(getAddress); + + const allAddresses = new Set(addresses); + + members.forEach((member) => { + allAddresses.add(member.address); + member.delegators?.forEach((delegation) => + delegation.delegator + ? allAddresses.add(delegation.delegator.address) + : null + ); + member.delegatees?.forEach((delegation) => + delegation.delegatee + ? allAddresses.add(delegation.delegatee.address) + : null + ); + }); + + const scores: { [k: string]: unknown }[] = ( + await getScoresDirect( + space, + options.strategies, + network, + provider, + [...allAddresses], + snapshot + ) + ).filter((score) => Object.keys(score).length !== 0); + + return Object.fromEntries( + addresses.map((address) => { + const member = members.find( + (member) => member.address.toLowerCase() === address.toLowerCase() + ); + return [ + getAddress(address), + member ? getVp(member, scores) : getAddressScore(scores, address) + ]; + }) + ); +} + +const getAddressScore = ( + scores: { [k: string]: any }[], + address?: string +): any => { + if (!address) return 0; + return scores.reduce((total, score) => total + (score[address] ?? 0), 0); +}; + +const getVp = (member: Member, scores: { [k: string]: any }[]): any => { + const addressScore = getAddressScore(scores, member.address); + const delegatedVp = + member.delegatees?.reduce((total, delegation) => { + const vp = addressScore; + return total + delegation.ratio * vp; + }, 0) ?? 0; + const receivedVp = + member.delegators?.reduce((total, delegation) => { + const vp = getAddressScore(scores, delegation.delegator?.address); + return total + delegation.ratio * vp; + }, 0) ?? 0; + + return addressScore + receivedVp - delegatedVp; +}; + +async function getDelegations( + subgraphURL: string, + addresses: string[], + space: string, + block: any +): Promise { + const chunkSize = 25; + const pageSize = 20; // chunkSize * pageSize * 2 <= 1000 (max elements per query) + + const chunks: string[][] = []; + for (let i = 0; i < addresses.length; i += chunkSize) { + chunks.push(addresses.slice(i, i + chunkSize)); + } + + const results: Member[] = []; + for (const chunk of chunks) { + let page = 0; + let reqAddresses = chunk.map((address) => address.toLowerCase()); + while (reqAddresses.length) { + const params = { + members: { + __args: { + block: { number: block.number }, + where: { + address_in: reqAddresses + } + }, + id: true, + address: true, + delegators: { + __args: { + where: { + context: space, + expirationTimestamp_gte: block.timestamp + }, + first: pageSize, + skip: page * pageSize + }, + delegator: { + address: true + }, + ratio: true + }, + delegatees: { + __args: { + where: { + context: space, + expirationTimestamp_gte: block.timestamp + }, + first: pageSize, + skip: page * pageSize + }, + delegatee: { + address: true + }, + ratio: true + } + } + }; + const result: { members: Member[] } = await subgraphRequest( + subgraphURL, + params + ); + result.members.forEach((newMember) => { + const existingMemberIndex = results.findIndex( + (member) => + member.address.toLowerCase() === newMember.address.toLowerCase() + ); + if (existingMemberIndex !== -1) { + const existingMember = results[existingMemberIndex]; + existingMember.delegatees = [ + ...(existingMember.delegatees || []), + ...(newMember.delegatees || []) + ]; + existingMember.delegators = [ + ...(existingMember.delegators || []), + ...(newMember.delegators || []) + ]; + } else { + results.push(newMember); + } + }); + reqAddresses = result.members + .filter( + (member) => + member.delegatees?.length === pageSize || + member.delegators?.length === pageSize + ) + .map((member) => member.address); + page++; + } + } + + return results; +} diff --git a/src/strategies/subgraph-split-delegation/schema.json b/src/strategies/subgraph-split-delegation/schema.json new file mode 100644 index 000000000..454e1000a --- /dev/null +++ b/src/strategies/subgraph-split-delegation/schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "subgraphUrl": { + "type": "string", + "title": "Subgraph endpoint" + }, + "strategies": { + "title": "Strategies", + "type": "array", + "items": { + "title": "Strategy", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "params": { + "type": "object" + } + }, + "required": [ + "name", + "params" + ] + } + } + }, + "required": [ + "subgraphUrl", + "strategies" + ], + "additionalProperties": false + } + } +} \ No newline at end of file