diff --git a/src/strategies/bubblegum-kids/README.md b/src/strategies/bubblegum-kids/README.md new file mode 100644 index 000000000..a5571fdb1 --- /dev/null +++ b/src/strategies/bubblegum-kids/README.md @@ -0,0 +1,3 @@ +# bubblegum-kids + +This strategy determines voting power based on an address' holdings of Bubblegum Kids and Bubblegum Puppies NFTs, including staked NFTs. diff --git a/src/strategies/bubblegum-kids/examples.json b/src/strategies/bubblegum-kids/examples.json new file mode 100644 index 000000000..d659782ef --- /dev/null +++ b/src/strategies/bubblegum-kids/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "bubblegum-kids" + }, + "network": "1", + "addresses": [ + "0xfec4465b6aeac9c90e552ae7766bdc0ee0d8dbc9", + "0x146860d31ab0f6dc858106f1a5d7a52bd6c86d1d", + "0x8edc7272a444057d1556df2a1173e6799c3c5ae0", + "0x8af2bd409ea0290029c14aac5d1be77d5c9a114f", + "0x41636ddf4abb1aa78863101626acdc861d4e8a0b", + "0xd341108b067cd308cf8cc7e13fb9982b0cba2f4e", + "0xdf05770b388ee62362917bea9dc96f0dee3b246e", + "0xf98e849c4b0de9750630f1c23c1bea9e79b4edb3", + "0xf68b74b89093315c5bf018348886c9fdd8308d7f", + "0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5" + ], + "snapshot": 18589032 + } +] diff --git a/src/strategies/bubblegum-kids/index.ts b/src/strategies/bubblegum-kids/index.ts new file mode 100644 index 000000000..d9382344b --- /dev/null +++ b/src/strategies/bubblegum-kids/index.ts @@ -0,0 +1,98 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'flaflafla'; +export const version = '0.1.1'; + +// contracts available on Ethereum mainnet and Goerli testnet +const kidsAddressByNetwork = { + 1: '0xa5ae87B40076745895BB7387011ca8DE5fde37E0', + 5: '0x66B04973b83ea796960D6f8ea22856714e01765f' +}; + +const puppiesAddressByNetwork = { + 1: '0x86e9C5ad3D4b5519DA2D2C19F5c71bAa5Ef40933', + 5: '0x053A3213E75b78c0b80b2f88e243cf519e834c02' +}; + +const stakingAddressByNetwork = { + 1: '0xf48415039913DBdF17e337e681de922A9cb04010', + 5: '0xe5f1433b6eCc6bE74E413b54f4c1eA2671b1cA0F' +}; + +const abi = [ + // Kids and Puppies contracts + 'function balanceOf(address owner) external view returns (uint256)', + // staking contract + 'function depositsOf(address account) external view returns (uint256[][2])' +]; + +/** + * Voting power is calculated as the sum of a user's Bubblegum Kids + * and Bubblegum Puppies holdings, with each NFT counting as 1. Kids + * and Puppies deposited to the staking contract also count as 1. + */ +export async function strategy( + _space, + network, + provider, + addresses, + _options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + addresses.forEach((address) => { + // get Kids balance + multi.call(`${address}.kids`, kidsAddressByNetwork[network], 'balanceOf', [ + address + ]); + + // get Puppies balance + multi.call( + `${address}.puppies`, + puppiesAddressByNetwork[network], + 'balanceOf', + [address] + ); + + // get staking deposits (returns and array of two arrays: + // a list of Kids ids and a list of Puppies ids) + multi.call( + `${address}.staking`, + stakingAddressByNetwork[network], + 'depositsOf', + [address] + ); + }); + + const result: { + address: { + kids: BigNumberish; + puppies: BigNumberish; + staking: [Array, Array]; + }; + } = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, { kids, puppies, staking }]) => { + const [stakedKids, stakedPuppies] = staking; + + const kidsCount = parseFloat(formatUnits(kids, 0)); + const stakedKidsCount = stakedKids.length; + + const puppiesCount = parseFloat(formatUnits(puppies, 0)); + const stakedPuppiesCount = stakedPuppies.length; + + // calculate total voting power + const votingPower = + kidsCount + stakedKidsCount + puppiesCount + stakedPuppiesCount; + + return [getAddress(address), votingPower]; + }) + ); +} diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 5c3b93514..d1dec8ae8 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -474,6 +474,7 @@ import * as mangroveStationQVScaledToMGV from './mangrove-station-qv-scaled-to-m 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'; const strategies = { 'cap-voting-power': capVotingPower, @@ -956,7 +957,8 @@ const strategies = { 'mangrove-station-qv-scaled-to-mgv': mangroveStationQVScaledToMGV, floki, 'hats-protocol-hat-id': hatsProtocolHatId, - 'hats-protocol-hat-ids': hatsProtocolHatIds + 'hats-protocol-hat-ids': hatsProtocolHatIds, + 'bubblegum-kids': bubblegumKids, }; Object.keys(strategies).forEach(function (strategyName) {