Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[subgraph-split-delegation] New strategy: Subgraph split delegation #1583

Merged
merged 9 commits into from
Sep 26, 2024
2 changes: 2 additions & 0 deletions src/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions src/strategies/subgraph-split-delegation/README.md
Original file line number Diff line number Diff line change
@@ -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
}
}
]
}

```
35 changes: 35 additions & 0 deletions src/strategies/subgraph-split-delegation/examples.json
Original file line number Diff line number Diff line change
@@ -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
}
]
210 changes: 210 additions & 0 deletions src/strategies/subgraph-split-delegation/index.ts
Original file line number Diff line number Diff line change
@@ -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';
ChaituVR marked this conversation as resolved.
Show resolved Hide resolved

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<Member[]> {
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;
}
44 changes: 44 additions & 0 deletions src/strategies/subgraph-split-delegation/schema.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading