diff --git a/package.json b/package.json index 905c00a45..623867b84 100755 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@ethersproject/strings": "^5.6.1", "@ethersproject/units": "^5.6.1", "@ethersproject/wallet": "^5.6.2", - "@snapshot-labs/snapshot.js": "^0.9.2", + "@snapshot-labs/snapshot.js": "^0.9.5", "@spruceid/didkit-wasm-node": "^0.2.1", "@uniswap/sdk-core": "^3.0.1", "@uniswap/v3-sdk": "^3.9.0", diff --git a/src/strategies/clipper-staked-sail/README.md b/src/strategies/clipper-staked-sail/README.md new file mode 100644 index 000000000..caab3c5b5 --- /dev/null +++ b/src/strategies/clipper-staked-sail/README.md @@ -0,0 +1,15 @@ +# AdmiralDao Staked Sail + +This strategy returns the voting power of an address that has staked sail in the vesail staking contract [VeSail](https://etherscan.io/token/0x26fe2f89a1fef1bc90b8a89d8ad18a1891166ff5). +The voting power is calculated as: +- The amount of vesail held in their address. +- The result is then used to interract with the tosail method in the contract to get the sail equivalent. +- Lastly it will take the equivalent sail amount and apply a square root operation. + +```JSON +{ + "strategies": [ + ["clipper-staked-sail"] + ] +} +``` diff --git a/src/strategies/clipper-staked-sail/examples.json b/src/strategies/clipper-staked-sail/examples.json new file mode 100644 index 000000000..16cc02da6 --- /dev/null +++ b/src/strategies/clipper-staked-sail/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example of clipper-staked-sail Strategy", + "strategy": { + "name": "clipper-staked-sail" + }, + "network": "1", + "decimals": 18, + "addresses": [ + "0x3334829670F9e8D309C9D9F318C4E6876755eDe2", + "0x8e70Ca936a2f2d81cBbF1Dc84Aabe4213C87b8E9", + "0x314C0695273Ba259Bb60074f2C92c67AC7ae6D40", + "0x2c2e209465D5312e6dF0cd5F7D1066f1aff9a953", + "0x4d768cFDb6E0077aD0a971678fa84DBcac32CE62", + "0x26f8435Bf2a7B8b4771F0D5317beb09fB1F197C3" + ], + "snapshot": 18558302 + } +] diff --git a/src/strategies/clipper-staked-sail/index.ts b/src/strategies/clipper-staked-sail/index.ts new file mode 100644 index 000000000..83154b61f --- /dev/null +++ b/src/strategies/clipper-staked-sail/index.ts @@ -0,0 +1,90 @@ +import { multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +const vesailTokenAddress = '0x26fE2f89a1FEf1bC90b8a89D8AD18a1891166ff5'; +const decimals = 18; + +export const author = 'cryptotrades20'; +export const version = '0.1.0'; + +//read vesail balance +const vesailBalanceOfABI = [ + 'function balanceOf(address account) view returns (uint256)' +]; + +//vesail to sail conversion +const toSAILABI = [ + 'function toSAIL(uint256 sailAmount) view returns (uint256)' +]; + +/** + * Voting power is calculated as the conversion of their vesail balance to sail + * Then take that sail amount and apply square root operation to it + */ +async function getVesailBalance(network, provider, snapshot, addresses) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + vesailBalanceOfABI, + addresses.map((address) => [vesailTokenAddress, 'balanceOf', [address]]), + { blockTag } + ); + return response.map((result) => result[0]); +} + +//read vesail to sail balance +async function readToSail( + network, + provider, + snapshot, + addresses, + vesailBalances +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + toSAILABI, + addresses.map((address, index) => [ + vesailTokenAddress, + 'toSAIL', + [vesailBalances[index]] + ]), + { blockTag } + ); + return response.map((result) => result[0]); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const vesailBalances = await getVesailBalance( + network, + provider, + snapshot, + addresses + ); + const sailAmounts = await readToSail( + network, + provider, + snapshot, + addresses, + vesailBalances + ); + + return Object.fromEntries( + addresses.map((address, index) => [ + address, + //square root the resulting vesail to sail amount + Math.sqrt( + Number(BigNumber.from(sailAmounts[index].toString())) / 10 ** decimals + ) + ]) + ); +} diff --git a/src/strategies/delegate-registry-v2/examples.json b/src/strategies/delegate-registry-v2/examples.json index 930f4c022..c4f6a3a28 100644 --- a/src/strategies/delegate-registry-v2/examples.json +++ b/src/strategies/delegate-registry-v2/examples.json @@ -5,21 +5,24 @@ "name": "delegate-registry-v2", "params": { "backendUrl": "https://delegate-registry-backend.vercel.app", + "delegationV1VChainIds": [1, 100], "strategies": [ { "name": "erc20-balance-of", "params": { - "address": "0x6b175474e89094c44da98b954eedeac495271d0f", - "symbol": "DAI", + "symbol": "ST", + "address": "0xE666Ad68a6e2897CD06A9ff378ED8b0d71093398", + "network": "5", "decimals": 18 - } + }, + "network": "5" }, { - "name": "gno", + "name": "erc20-balance-of", "params": { - "symbol": "GNO", - "decimals": 18, - "SUBGRAPH_URL": "https://api.thegraph.com/subgraphs/id/QmduKVUHCPjR5tmNEgooXHBMGKqDJWrUPdp6dEMeJM6Kqa" + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 } } ] @@ -27,6 +30,11 @@ }, "network": "1", "addresses": [ + "0x2011c83e8f75c0ceb90ec140d8e8adfc836e3685", + "0xde1e6a7ed0ad3f61d531a8a78e83ccddbd6e0c49", + "0x007de57773b6eb4ebbf6a740dfde1efdd5629630", + "0x6cc5b30cd0a93c1f85c7868f5f2620ab8c458190", + "0xd028d504316fec029cfa36bdc3a8f053f6e5a6e4", "0x000e37ed92d86a7667f520c53b73b01ff5c206eb", "0x000dbf2733da51135c1b21c8ef71a3d474383f0d", "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", @@ -40,6 +48,6 @@ "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad" ], - "snapshot": 17463998 + "snapshot": 18478898 } ] diff --git a/src/strategies/delegate-registry-v2/index.ts b/src/strategies/delegate-registry-v2/index.ts index 6304fee96..e22fb4805 100644 --- a/src/strategies/delegate-registry-v2/index.ts +++ b/src/strategies/delegate-registry-v2/index.ts @@ -5,13 +5,14 @@ import { getScoresDirect } from '../../utils'; import { getAddress } from '@ethersproject/address'; export const author = 'gnosis'; -export const version = '0.0.1'; +export const version = '0.0.2'; const DEFAULT_BACKEND_URL = 'https://delegate-registry-backend.vercel.app'; type Params = { backendUrl: string; strategies: Strategy[]; + delegationV1VChainIds?: number[]; // add this to include v1 delegations }; /* @@ -42,8 +43,11 @@ export async function strategy( 'Content-Type': 'application/json' }, body: JSON.stringify({ - addresses: addresses, - strategies: options.strategies + spaceParams: { + ...options, + mainChainId: Number(network) + }, + addresses }) } ); @@ -75,7 +79,7 @@ export async function strategy( ...addressesNotDelegatingOrDelegatedTo, ...addressesDelegatedTo.map(([address]) => address) ], - snapshot + blockTag ); const delegationObject = addressesDelegatedTo.reduce( diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 2c3606302..139583bb0 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -475,6 +475,8 @@ import * as floki from './floki'; import * as hatsProtocolHatId from './hats-protocol-hat-id'; import * as hatsProtocolHatIds from './hats-protocol-hat-ids'; import * as bubblegumKids from './bubblegum-kids'; +import * as clipperStakedSail from './clipper-staked-sail'; +import * as snote from './snote'; const strategies = { 'cap-voting-power': capVotingPower, @@ -958,7 +960,9 @@ const strategies = { floki, 'hats-protocol-hat-id': hatsProtocolHatId, 'hats-protocol-hat-ids': hatsProtocolHatIds, - 'bubblegum-kids': bubblegumKids + 'bubblegum-kids': bubblegumKids, + 'clipper-staked-sail': clipperStakedSail, + snote }; Object.keys(strategies).forEach(function (strategyName) { diff --git a/src/strategies/snote/examples.json b/src/strategies/snote/examples.json new file mode 100644 index 000000000..1ec38cd4e --- /dev/null +++ b/src/strategies/snote/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "snote", + "params": { "symbol": "sNOTE" } + }, + "network": "1", + "addresses": [ + "0xCece1920D4dBb96BAf88705ce0A6Eb3203ed2eB1", + "0x4E8014fF5bacE498DAB1a9E2B5c3f4240bC059B6", + "0x741AA7CFB2c7bF2A1E7D4dA2e3Df6a56cA4131F3" + ], + "snapshot": 18629000 + } +] diff --git a/src/strategies/snote/index.ts b/src/strategies/snote/index.ts new file mode 100644 index 000000000..1eae9a07a --- /dev/null +++ b/src/strategies/snote/index.ts @@ -0,0 +1,80 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'kaiserpy'; +export const version = '0.1.0'; + +const abi = [ + 'function getVotes(address account) external view returns (uint256)', + 'function votingPowerWithoutDelegation(address account) external view returns (uint256)', + 'function delegates(address account) external view returns (address)' +]; + +const STAKED_NOTE_CONTRACT_ADDRESS = + '0x38de42f4ba8a35056b33a746a6b45be9b1c3b9d2'; + +interface VotingInfo { + [address: string]: BigNumberish; +} + +export async function strategy( + space: any, + network: string, + provider: any, + addresses: string[], + options: any, + snapshot: number | string +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Helper function to fetch data using multicall + async function fetchMulticallData(method: string) { + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + multi.call(address, STAKED_NOTE_CONTRACT_ADDRESS, method, [address]) + ); + return await multi.execute(); + } + + // Fetch delegate votes, non-delegated votes, and delegation information + const delegatedVotes: VotingInfo = await fetchMulticallData('getVotes'); + const nonDelegatedVotes: VotingInfo = await fetchMulticallData( + 'votingPowerWithoutDelegation' + ); + const delegationInfo: Record = await fetchMulticallData( + 'delegates' + ).then((result2: Record) => + Object.fromEntries( + Object.entries(result2).map(([address, delegate]) => [ + address, + delegate.toLowerCase() === '0x0000000000000000000000000000000000000000' + ]) + ) + ); + + // Process and filter the data + const delegateVotingPowers = Object.fromEntries( + Object.entries(delegatedVotes).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, 8)) + ]) + ); + const votingPowers = Object.fromEntries( + Object.entries(nonDelegatedVotes).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, 8)) + ]) + ); + + const filteredBalances: Record = {}; + addresses.forEach((address) => { + if (delegationInfo[address]) { + const delegateVotingPower = delegateVotingPowers[address] || 0; + const votingPower = votingPowers[address] || 0; + filteredBalances[address] = delegateVotingPower + votingPower; + } + }); + + return filteredBalances; +} diff --git a/test/__snapshots__/delegation.test.ts.snap b/test/__snapshots__/delegation.test.ts.snap index 50cfedd58..662b921f5 100644 --- a/test/__snapshots__/delegation.test.ts.snap +++ b/test/__snapshots__/delegation.test.ts.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` getDelegations 1`] = ` -Object { - "in": Array [ +{ + "in": [ "0x073aBA73177668Ba1c6661A4CCddbF77dfC8809a", "0x0D996171E7883A286eF720030935f72D0Bac8219", "0x186E20ae3530520C9F3E6C46F2f5d1062b784761", diff --git a/test/__snapshots__/vp.test.ts.snap b/test/__snapshots__/vp.test.ts.snap index 80a296baa..973d41de0 100644 --- a/test/__snapshots__/vp.test.ts.snap +++ b/test/__snapshots__/vp.test.ts.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` getVp with delegation 1`] = ` -Object { +{ "vp": 10.077130335431244, - "vp_by_strategy": Array [ + "vp_by_strategy": [ 0, 10.028706352441185, 0, @@ -14,9 +14,9 @@ Object { `; exports[` getVp without delegation 1`] = ` -Object { +{ "vp": 21.55404462002206, - "vp_by_strategy": Array [ + "vp_by_strategy": [ 0, 9.998985610441185, 11.506635026590818, diff --git a/yarn.lock b/yarn.lock index f16ace042..f26d1d88b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1363,10 +1363,10 @@ dependencies: "@sinonjs/commons" "^2.0.0" -"@snapshot-labs/snapshot.js@^0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@snapshot-labs/snapshot.js/-/snapshot.js-0.9.2.tgz#1d4ab0046c342c259b7409f030a46aeddf65becd" - integrity sha512-kdZvBZ4nLFwY7ONpM9ad7+VpLvo091ogq/eeDxoUpDQ9F+z2RiH/xNi4wxdNBGtAPT1cn2Pv3StbjoYrdUnvxw== +"@snapshot-labs/snapshot.js@^0.9.5": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@snapshot-labs/snapshot.js/-/snapshot.js-0.9.5.tgz#2fe0f12b0d913d43121a2fb23e99ac160a3cd41f" + integrity sha512-+Vqn+kms+Yw2pa87cSSp0aIKoJim6wiuUVxl0DIZ2e4yWe8YGZGbJuLXMgozZhwMRVtjW3QX9sHE7lyI1cjMpg== dependencies: "@ensdomains/eth-ens-namehash" "^2.0.15" "@ethersproject/abi" "^5.6.4"