diff --git a/src/strategies/delegated-ape/README.md b/src/strategies/delegated-ape/README.md new file mode 100644 index 000000000..6e0108fab --- /dev/null +++ b/src/strategies/delegated-ape/README.md @@ -0,0 +1,23 @@ +# Delegated APE + +This strategy calculates voting power based on gas token balance with delegation support. + +If a user has delegated their voting power to someone else, their own balance is not counted toward their voting power. + +## Parameters + +| Parameter | Description | Required | +| --------- | ----------- | -------- | +| `delegationContract` | Address of the delegation contract | Yes | +| `delegationId` | Delegation ID (bytes32) to query | Yes | +| `symbol` | Symbol to display | No | + +## Example Configuration + +```json +{ + "delegationContract": "0xDd6B74123b2aB93aD701320D3F8D1b92B4fA5202", + "delegationId": "0x0000000000000000000000000000000000000000000000000000000000000001", + "symbol": "APE" +} +``` diff --git a/src/strategies/delegated-ape/examples.json b/src/strategies/delegated-ape/examples.json new file mode 100644 index 000000000..4c3b95cc8 --- /dev/null +++ b/src/strategies/delegated-ape/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Delegated APE strategy", + "strategy": { + "name": "delegated-ape", + "params": { + "symbol": "APE", + "delegationContract": "0xDd6B74123b2aB93aD701320D3F8D1b92B4fA5202", + "delegationId": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + }, + "network": "33111", + "addresses": [ + "0x556B14CbdA79A36dC33FcD461a04A5BCb5dC2A70", + "0x5EF29cf961cf3Fc02551B9BdaDAa4418c446c5dd", + "0x537f1896541d28F4c70116EEa602b1B34Da95163", + "0xa40839f84CF98Ee6F4fdB84c1bB1a448e7835EfE", + "0x220bc93D88C0aF11f1159eA89a885d5ADd3A7Cf6", + "0xF6108479b97EB92E2727edD5f2707620C51EB2DF" + ], + "snapshot": 18653634 + } +] diff --git a/src/strategies/delegated-ape/index.ts b/src/strategies/delegated-ape/index.ts new file mode 100644 index 000000000..114b78ed4 --- /dev/null +++ b/src/strategies/delegated-ape/index.ts @@ -0,0 +1,109 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import networks from '@snapshot-labs/snapshot.js/src/networks.json'; +import { Multicaller } from '../../utils'; + +export const author = 'snapshot-labs'; +export const version = '0.1.0'; + +const abi = [ + 'function delegation(address delegator, bytes32 id) view returns (address delegate)', + 'function getDelegators(address delegate, bytes32 id) view returns (address[])', + 'function getEthBalance(address account) public view returns (uint256 balance)' +]; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +export async function strategy( + space: string, + network: string, + provider: any, + addresses: string[], + options: any, + snapshot: string | number +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const delegationMulticall = new Multicaller(network, provider, abi, { + blockTag + }); + + addresses.forEach((address: string) => { + delegationMulticall.call( + `delegation.${address}`, + options.delegationContract, + 'delegation', + [address, options.delegationId] + ); + delegationMulticall.call( + `delegators.${address}`, + options.delegationContract, + 'getDelegators', + [address, options.delegationId] + ); + }); + + const results = await delegationMulticall.execute(); + + const allDelegators = new Set(); + + addresses.forEach((voterAddress: string) => { + const delegation = results.delegation[voterAddress]; + const delegators = results.delegators[voterAddress] || []; + + delegators.forEach((delegator: string) => { + allDelegators.add(delegator.toLowerCase()); + }); + + if (delegation === ZERO_ADDRESS) { + allDelegators.add(voterAddress.toLowerCase()); + } + }); + + const balanceMulticall = new Multicaller( + network, + provider, + [ + 'function getEthBalance(address addr) public view returns (uint256 balance)' + ], + { blockTag } + ); + + Array.from(allDelegators).forEach((delegator: string) => { + balanceMulticall.call( + delegator, + networks[network].multicall, + 'getEthBalance', + [delegator] + ); + }); + + const balanceResults: Record = + await balanceMulticall.execute(); + + const scores: Record = {}; + + addresses.forEach((voterAddress: string) => { + const delegation = results.delegation[voterAddress]; + const delegators = results.delegators[voterAddress] || []; + + let totalVotingPower = BigNumber.from(0); + + delegators.forEach((delegator: string) => { + const balance = balanceResults[delegator.toLowerCase()] || 0; + totalVotingPower = totalVotingPower.add(BigNumber.from(balance)); + }); + + if (delegation === ZERO_ADDRESS) { + const voterBalance = balanceResults[voterAddress.toLowerCase()] || 0; + totalVotingPower = totalVotingPower.add(BigNumber.from(voterBalance)); + } + + scores[getAddress(voterAddress)] = parseFloat( + formatUnits(totalVotingPower, 18) + ); + }); + + return scores; +} diff --git a/src/strategies/delegated-ape/schema.json b/src/strategies/delegated-ape/schema.json new file mode 100644 index 000000000..237f69198 --- /dev/null +++ b/src/strategies/delegated-ape/schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "symbol": { + "type": "string", + "title": "Symbol", + "examples": ["e.g. APE"], + "maxLength": 16 + }, + "delegationContract": { + "type": "string", + "title": "Delegation Contract Address", + "examples": ["e.g. 0xDd6B74123b2aB93aD701320D3F8D1b92B4fA5202"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "delegationId": { + "type": "string", + "title": "Delegation ID", + "examples": [ + "e.g. 0x0000000000000000000000000000000000000000000000000000000000000001" + ], + "pattern": "^0x[a-fA-F0-9]{64}$", + "minLength": 0, + "maxLength": 100 + } + }, + "required": ["delegationContract", "delegationId"], + "additionalProperties": false + } + } +} diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 36bf57a2a..c10c7c5d1 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -84,6 +84,7 @@ import * as stablexswap from './stablexswap'; import * as stakedKeep from './staked-keep'; import * as stakedDaomaker from './staked-daomaker'; import * as typhoon from './typhoon'; +import * as delegatedApe from './delegated-ape'; import * as delegation from './delegation'; import * as delegationWithCap from './delegation-with-cap'; import * as delegationWithOverrides from './delegation-with-overrides'; @@ -596,6 +597,7 @@ const strategies = { 'staked-daomaker': stakedDaomaker, 'balancer-unipool': balancerUnipool, typhoon, + 'delegated-ape': delegatedApe, delegation, 'delegation-with-cap': delegationWithCap, 'delegation-with-overrides': delegationWithOverrides,