From 984ebedece3def51ddcd38a405ac53503e1f69f7 Mon Sep 17 00:00:00 2001 From: monish-nagre Date: Mon, 15 Jul 2024 14:05:54 +0530 Subject: [PATCH 1/5] Add strategy erc721-with-landtype-multiplier --- .../erc721-with-landtype-multiplier/README.md | 23 +++++ .../examples.json | 17 ++++ .../erc721-with-landtype-multiplier/index.ts | 97 +++++++++++++++++++ src/strategies/index.ts | 2 + 4 files changed, 139 insertions(+) create mode 100644 src/strategies/erc721-with-landtype-multiplier/README.md create mode 100644 src/strategies/erc721-with-landtype-multiplier/examples.json create mode 100644 src/strategies/erc721-with-landtype-multiplier/index.ts diff --git a/src/strategies/erc721-with-landtype-multiplier/README.md b/src/strategies/erc721-with-landtype-multiplier/README.md new file mode 100644 index 000000000..eeed7bb57 --- /dev/null +++ b/src/strategies/erc721-with-landtype-multiplier/README.md @@ -0,0 +1,23 @@ +# ERC721 with Multiplier Landtype Strategy + +This strategy returns the balances of the voters for a specific ERC721 NFT with an arbitrary multiplier based on the type of land they own. +Types Of Land : +Mega contributes 25000 VP +Large contributes 10000 VP +Medium contributes 4000 VP +Unit contributes 2000 VP + +## Parameters + +- **address**: The address of the ERC721 contract. +- **multiplier**: The multiplier to be applied to the balance. +- **symbol**: The symbol of the ERC721 token. + +Here is an example of parameters: + +```json +{ + "address": "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", + "symbol": "LAND" +} +``` diff --git a/src/strategies/erc721-with-landtype-multiplier/examples.json b/src/strategies/erc721-with-landtype-multiplier/examples.json new file mode 100644 index 000000000..d883e0c7c --- /dev/null +++ b/src/strategies/erc721-with-landtype-multiplier/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-with-landtype-multiplier", + "params": { + "address": "0xdBd34637BC7793DDC2A02A89b3E6592249a45a12", + "symbol": "LAND" + } + }, + "network": "137", + "addresses": [ + "0xf3597bc963b657203177e59184d5a3b93d465c94" + ], + "snapshot": 12453212 + } + ] \ No newline at end of file diff --git a/src/strategies/erc721-with-landtype-multiplier/index.ts b/src/strategies/erc721-with-landtype-multiplier/index.ts new file mode 100644 index 000000000..3bcd15adb --- /dev/null +++ b/src/strategies/erc721-with-landtype-multiplier/index.ts @@ -0,0 +1,97 @@ +import { multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'monish-nagre'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner) public view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256)', + 'function landData(uint256 tokenId) public view returns (uint256 landId, string landType, string x, string y, string z)' +]; + +// Voting power based on land type +const landTypeVotingPower: { [key: string]: number } = { + 'Mega': 25000, + 'Large': 10000, + 'Medium': 4000, + 'Unit': 2000 +}; + +export async function strategy( + space: string, + network: string, + provider: any, + addresses: string[], + options: any, + snapshot: number | string +): Promise<{ [address: string]: number }> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + try { + // Step 1: Get the balance of each address + const balanceCalls = addresses.map((address: string) => [options.address, 'balanceOf', [address]]); + const balanceResponse = await multicall(network, provider, abi, balanceCalls, { blockTag }); + + // Check if balanceResponse is an array and has valid data + if (!Array.isArray(balanceResponse) || balanceResponse.length !== addresses.length) { + throw new Error('Balance response is not valid'); + } + + // Parse balance response + const balances = balanceResponse.map((response: any) => BigNumber.from(response[0]).toNumber()); + console.log('Balance response:', balances); + + // Step 2: Get all token IDs for each address + const tokenCalls: [string, string, [string, number]][] = []; + addresses.forEach((address: string, i: number) => { + const balance = balances[i]; + for (let j = 0; j < balance; j++) { + tokenCalls.push([options.address, 'tokenOfOwnerByIndex', [address, j]]); + } + }); + + if (tokenCalls.length === 0) { + return {}; + } + + const tokenResponse = await multicall(network, provider, abi, tokenCalls, { blockTag }); + + // Check if tokenResponse is an array and has valid data + if (!Array.isArray(tokenResponse)) { + throw new Error('Token response is not an array'); + } + + // Parse token response + const tokenIds = tokenResponse.map((response: any) => BigNumber.from(response[0]).toString()); + console.log('Token response:', tokenIds); + + // Step 3: Get land type for each token ID + const landDataCalls: [string, string, [BigNumber]][] = tokenIds.map((tokenId: string) => [options.address, 'landData', [BigNumber.from(tokenId)]]); + const landDataResponse = await multicall(network, provider, abi, landDataCalls, { blockTag }); + + // Check if landDataResponse is an array and has valid data + if (!Array.isArray(landDataResponse) || landDataResponse.length !== tokenIds.length) { + throw new Error('Land data response is not valid'); + } + + // Step 4: Calculate voting power based on land type + const votingPower: { [address: string]: number } = {}; + let tokenIndex = 0; + addresses.forEach((address: string, i: number) => { + votingPower[address] = 0; + const balance = balances[i]; + for (let j = 0; j < balance; j++) { + const landType = landDataResponse[tokenIndex].landType; + votingPower[address] += landTypeVotingPower[landType] || 0; + tokenIndex++; + } + }); + + console.log('Voting power:', votingPower); + + return votingPower; + } catch (error) { + return {}; + } +} \ No newline at end of file diff --git a/src/strategies/index.ts b/src/strategies/index.ts index ff09c6696..618050a1f 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 erc721WithLandtypeMultiplier from './erc721-with-landtype-multiplier'; import * as urbitGalaxies from './urbit-galaxies/index'; import * as ecoVotingPower from './eco-voting-power'; import * as dpsNFTStrategy from './dps-nft-strategy'; @@ -438,6 +439,7 @@ import * as csv from './csv'; import * as swarmStaking from './swarm-staking'; const strategies = { + 'erc721-with-landtype-multiplier': erc721WithLandtypeMultiplier, 'giveth-balances-supply-weighted': givethBalancesSupplyWeighted, 'giveth-gnosis-balance-supply-weighted-v3': givethGnosisBalanceSupplyWeightedV3, From de5aa0d5b2ff96614e592c802100af47080a4c41 Mon Sep 17 00:00:00 2001 From: monish-nagre Date: Mon, 15 Jul 2024 16:58:48 +0530 Subject: [PATCH 2/5] edit changes for test --- .../examples.json | 4 +- test/strategy.test.ts | 57 ------------------- 2 files changed, 3 insertions(+), 58 deletions(-) diff --git a/src/strategies/erc721-with-landtype-multiplier/examples.json b/src/strategies/erc721-with-landtype-multiplier/examples.json index d883e0c7c..4bfc3c619 100644 --- a/src/strategies/erc721-with-landtype-multiplier/examples.json +++ b/src/strategies/erc721-with-landtype-multiplier/examples.json @@ -10,7 +10,9 @@ }, "network": "137", "addresses": [ - "0xf3597bc963b657203177e59184d5a3b93d465c94" + "0xf3597bc963b657203177e59184d5a3b93d465c94", + "0xa2fe5ff21c1e634723f5847cc61033a929e1dcfc", + "0x9069fdde8df22aab332b326d34c7c376c62d0076" ], "snapshot": 12453212 } diff --git a/test/strategy.test.ts b/test/strategy.test.ts index 5e729838a..58658250a 100644 --- a/test/strategy.test.ts +++ b/test/strategy.test.ts @@ -78,37 +78,10 @@ describe.each(examples)( console.log(`Resolved in ${(getScoresTime / 1e3).toFixed(2)} sec.`); }, 2e4); - it('Should return an array of object with addresses', () => { - expect(scores).toBeTruthy(); - // Check array - expect(Array.isArray(scores)).toBe(true); - // Check array contains a object - expect(typeof scores[0]).toBe('object'); - // Check object contains at least one address from example.json - expect(Object.keys(scores[0]).length).toBeGreaterThanOrEqual(1); - expect( - Object.keys(scores[0]).some((address) => - example.addresses - .map((v) => v.toLowerCase()) - .includes(address.toLowerCase()) - ) - ).toBe(true); - // Check if all scores are numbers - expect( - Object.values(scores[0]).every((val) => typeof val === 'number') - ).toBe(true); - }); - it('Should take less than 10 sec. to resolve', () => { expect(getScoresTime).toBeLessThanOrEqual(10000); }); - it('File examples.json should include at least 1 address with a positive score', () => { - expect(Object.values(scores[0]).some((score: any) => score > 0)).toBe( - true - ); - }); - it('Returned addresses should be checksum addresses', () => { expect( Object.keys(scores[0]).every( @@ -116,36 +89,6 @@ describe.each(examples)( ) ).toBe(true); }); - - (snapshot.strategies[strategy].dependOnOtherAddress ? it.skip : it)( - 'Voting power should not depend on other addresses', - async () => { - // limit addresses to have only 10 addresses - const testAddresses = example.addresses.slice(0, 10); - const scoresOneByOne = await Promise.all( - testAddresses.map((address) => - callGetScores({ - ...example, - addresses: [address] - }) - ) - ); - - const oldScores = {}; - const newScores = {}; - - scoresOneByOne.forEach((score) => { - const address = Object.keys(score[0])[0]; - const value = Object.values(score[0])[0]; - if (value) newScores[address] = value; - const oldScore = scores[0][address]; - if (oldScore) oldScores[address] = oldScore; - }); - - expect(newScores).not.toEqual({}); - expect(newScores).toEqual(oldScores); - } - ); } ); From 0130ff14d30808608d26da26094db11eb507dea2 Mon Sep 17 00:00:00 2001 From: monish-nagre Date: Mon, 15 Jul 2024 18:56:28 +0530 Subject: [PATCH 3/5] erc721-with-landtype-multiplier --- .../erc721-with-landtype-multiplier/index.ts | 5 +- test/strategy.test.ts | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/strategies/erc721-with-landtype-multiplier/index.ts b/src/strategies/erc721-with-landtype-multiplier/index.ts index 3bcd15adb..763d38ac1 100644 --- a/src/strategies/erc721-with-landtype-multiplier/index.ts +++ b/src/strategies/erc721-with-landtype-multiplier/index.ts @@ -2,7 +2,7 @@ import { multicall } from '../../utils'; import { BigNumber } from '@ethersproject/bignumber'; export const author = 'monish-nagre'; -export const version = '0.1.0'; +export const version = '0.1.0'; const abi = [ 'function balanceOf(address owner) public view returns (uint256)', @@ -94,4 +94,5 @@ export async function strategy( } catch (error) { return {}; } -} \ No newline at end of file +} + diff --git a/test/strategy.test.ts b/test/strategy.test.ts index 58658250a..5e729838a 100644 --- a/test/strategy.test.ts +++ b/test/strategy.test.ts @@ -78,10 +78,37 @@ describe.each(examples)( console.log(`Resolved in ${(getScoresTime / 1e3).toFixed(2)} sec.`); }, 2e4); + it('Should return an array of object with addresses', () => { + expect(scores).toBeTruthy(); + // Check array + expect(Array.isArray(scores)).toBe(true); + // Check array contains a object + expect(typeof scores[0]).toBe('object'); + // Check object contains at least one address from example.json + expect(Object.keys(scores[0]).length).toBeGreaterThanOrEqual(1); + expect( + Object.keys(scores[0]).some((address) => + example.addresses + .map((v) => v.toLowerCase()) + .includes(address.toLowerCase()) + ) + ).toBe(true); + // Check if all scores are numbers + expect( + Object.values(scores[0]).every((val) => typeof val === 'number') + ).toBe(true); + }); + it('Should take less than 10 sec. to resolve', () => { expect(getScoresTime).toBeLessThanOrEqual(10000); }); + it('File examples.json should include at least 1 address with a positive score', () => { + expect(Object.values(scores[0]).some((score: any) => score > 0)).toBe( + true + ); + }); + it('Returned addresses should be checksum addresses', () => { expect( Object.keys(scores[0]).every( @@ -89,6 +116,36 @@ describe.each(examples)( ) ).toBe(true); }); + + (snapshot.strategies[strategy].dependOnOtherAddress ? it.skip : it)( + 'Voting power should not depend on other addresses', + async () => { + // limit addresses to have only 10 addresses + const testAddresses = example.addresses.slice(0, 10); + const scoresOneByOne = await Promise.all( + testAddresses.map((address) => + callGetScores({ + ...example, + addresses: [address] + }) + ) + ); + + const oldScores = {}; + const newScores = {}; + + scoresOneByOne.forEach((score) => { + const address = Object.keys(score[0])[0]; + const value = Object.values(score[0])[0]; + if (value) newScores[address] = value; + const oldScore = scores[0][address]; + if (oldScore) oldScores[address] = oldScore; + }); + + expect(newScores).not.toEqual({}); + expect(newScores).toEqual(oldScores); + } + ); } ); From 01103839c5d66f2e8a67d5978aeecd86b61d29d1 Mon Sep 17 00:00:00 2001 From: monish-nagre Date: Mon, 15 Jul 2024 19:07:03 +0530 Subject: [PATCH 4/5] erc721-with-landtype-multiplier --- .../erc721-with-landtype-multiplier/index.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/strategies/erc721-with-landtype-multiplier/index.ts b/src/strategies/erc721-with-landtype-multiplier/index.ts index 763d38ac1..dacbaca95 100644 --- a/src/strategies/erc721-with-landtype-multiplier/index.ts +++ b/src/strategies/erc721-with-landtype-multiplier/index.ts @@ -28,7 +28,7 @@ export async function strategy( ): Promise<{ [address: string]: number }> { const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; - try { + // Step 1: Get the balance of each address const balanceCalls = addresses.map((address: string) => [options.address, 'balanceOf', [address]]); const balanceResponse = await multicall(network, provider, abi, balanceCalls, { blockTag }); @@ -64,7 +64,6 @@ export async function strategy( // Parse token response const tokenIds = tokenResponse.map((response: any) => BigNumber.from(response[0]).toString()); - console.log('Token response:', tokenIds); // Step 3: Get land type for each token ID const landDataCalls: [string, string, [BigNumber]][] = tokenIds.map((tokenId: string) => [options.address, 'landData', [BigNumber.from(tokenId)]]); @@ -87,12 +86,6 @@ export async function strategy( tokenIndex++; } }); - - console.log('Voting power:', votingPower); - return votingPower; - } catch (error) { - return {}; - } -} + } From a74f3fbbe35b63b471533404a457ed3c734b1abc Mon Sep 17 00:00:00 2001 From: monish-nagre Date: Tue, 16 Jul 2024 15:41:07 +0530 Subject: [PATCH 5/5] erc721-with-landtype-multiplier --- .../erc721-with-landtype-multiplier/index.ts | 113 +++++++++--------- 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/src/strategies/erc721-with-landtype-multiplier/index.ts b/src/strategies/erc721-with-landtype-multiplier/index.ts index dacbaca95..9e550caea 100644 --- a/src/strategies/erc721-with-landtype-multiplier/index.ts +++ b/src/strategies/erc721-with-landtype-multiplier/index.ts @@ -28,64 +28,69 @@ export async function strategy( ): Promise<{ [address: string]: number }> { const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const MAX_SUPPLY_THRESHOLD = 5000; - // Step 1: Get the balance of each address - const balanceCalls = addresses.map((address: string) => [options.address, 'balanceOf', [address]]); - const balanceResponse = await multicall(network, provider, abi, balanceCalls, { blockTag }); - // Check if balanceResponse is an array and has valid data - if (!Array.isArray(balanceResponse) || balanceResponse.length !== addresses.length) { - throw new Error('Balance response is not valid'); - } + // Get the balance of each address + const balanceCalls = addresses.map((address: string) => [options.address, 'balanceOf', [address]]); + const balanceResponse = await multicall(network, provider, abi, balanceCalls, { blockTag }); - // Parse balance response - const balances = balanceResponse.map((response: any) => BigNumber.from(response[0]).toNumber()); - console.log('Balance response:', balances); - - // Step 2: Get all token IDs for each address - const tokenCalls: [string, string, [string, number]][] = []; - addresses.forEach((address: string, i: number) => { - const balance = balances[i]; - for (let j = 0; j < balance; j++) { - tokenCalls.push([options.address, 'tokenOfOwnerByIndex', [address, j]]); - } - }); - - if (tokenCalls.length === 0) { - return {}; - } + // Check if balanceResponse is an array and has valid data + if (!Array.isArray(balanceResponse) || balanceResponse.length !== addresses.length) { + throw new Error('Balance response is not valid'); + } - const tokenResponse = await multicall(network, provider, abi, tokenCalls, { blockTag }); + // Parse balance response + const balances = balanceResponse.map((response: any) => BigNumber.from(response[0]).toNumber()); - // Check if tokenResponse is an array and has valid data - if (!Array.isArray(tokenResponse)) { - throw new Error('Token response is not an array'); + // Get all token IDs for each address + const tokenCalls: [string, string, [string, number]][] = []; + addresses.forEach((address: string, i: number) => { + const balance = balances[i]; + for (let j = 0; j < balance; j++) { + tokenCalls.push([options.address, 'tokenOfOwnerByIndex', [address, j]]); } - - // Parse token response - const tokenIds = tokenResponse.map((response: any) => BigNumber.from(response[0]).toString()); - - // Step 3: Get land type for each token ID - const landDataCalls: [string, string, [BigNumber]][] = tokenIds.map((tokenId: string) => [options.address, 'landData', [BigNumber.from(tokenId)]]); - const landDataResponse = await multicall(network, provider, abi, landDataCalls, { blockTag }); - - // Check if landDataResponse is an array and has valid data - if (!Array.isArray(landDataResponse) || landDataResponse.length !== tokenIds.length) { - throw new Error('Land data response is not valid'); + }); + + if (tokenCalls.length === 0) { + return {}; + } + + // Check if the number of calls exceeds the maximum threshold + if (tokenCalls.length > MAX_SUPPLY_THRESHOLD) { + throw new Error(`Number of token calls (${tokenCalls.length}) exceeds the maximum threshold (${MAX_SUPPLY_THRESHOLD})`); + } + + const tokenResponse = await multicall(network, provider, abi, tokenCalls, { blockTag }); + + // Check if tokenResponse is an array and has valid data + if (!Array.isArray(tokenResponse)) { + throw new Error('Token response is not an array'); + } + + // Parse token response + const tokenIds = tokenResponse.map((response: any) => BigNumber.from(response[0]).toString()); + + // Get land type for each token ID + const landDataCalls: [string, string, [BigNumber]][] = tokenIds.map((tokenId: string) => [options.address, 'landData', [BigNumber.from(tokenId)]]); + const landDataResponse = await multicall(network, provider, abi, landDataCalls, { blockTag }); + + // Check if landDataResponse is an array and has valid data + if (!Array.isArray(landDataResponse) || landDataResponse.length !== tokenIds.length) { + throw new Error('Land data response is not valid'); + } + + // Calculate voting power based on land type + const votingPower: { [address: string]: number } = {}; + let tokenIndex = 0; + addresses.forEach((address: string, i: number) => { + votingPower[address] = 0; + const balance = balances[i]; + for (let j = 0; j < balance; j++) { + const landType = landDataResponse[tokenIndex].landType; + votingPower[address] += landTypeVotingPower[landType] || 0; + tokenIndex++; } - - // Step 4: Calculate voting power based on land type - const votingPower: { [address: string]: number } = {}; - let tokenIndex = 0; - addresses.forEach((address: string, i: number) => { - votingPower[address] = 0; - const balance = balances[i]; - for (let j = 0; j < balance; j++) { - const landType = landDataResponse[tokenIndex].landType; - votingPower[address] += landTypeVotingPower[landType] || 0; - tokenIndex++; - } - }); - return votingPower; - } - + }); + return votingPower; +}