From e4eeea5d32a173f4a57bdd24f123150ef0cb9420 Mon Sep 17 00:00:00 2001 From: Isaac Patka Date: Tue, 8 Aug 2023 22:22:48 -0400 Subject: [PATCH] ens wip --- .../API/backend/functions/config.ts | 13 +- .../functions/snapshot/getDelegations.ts | 91 + .../API/backend/utils/snapshot/index.ts | 10 + .../aave-governance-power/README.md | 11 + .../aave-governance-power/examples.json | 17 + .../strategies/aave-governance-power/index.ts | 75 + .../aavegotchi-agip-17/examples.json | 14 + .../strategies/aavegotchi-agip-17/index.ts | 104 + .../README.md | 22 + .../examples.json | 38 + .../index.ts | 211 + .../aavegotchi-agip-37-wap-ghst/README.md | 9 + .../aavegotchi-agip-37-wap-ghst/examples.json | 39 + .../aavegotchi-agip-37-wap-ghst/index.ts | 165 + .../strategies/aavegotchi-agip/examples.json | 19 + .../strategies/aavegotchi-agip/index.ts | 482 ++ .../aavegotchi-wagmi-guild/examples.json | 15 + .../aavegotchi-wagmi-guild/index.ts | 77 + .../strategies/aavegotchi/examples.json | 21 + .../snapshot/strategies/aavegotchi/index.ts | 151 + .../strategies/across-staked-acx/README.md | 31 + .../across-staked-acx/examples.json | 26 + .../strategies/across-staked-acx/index.ts | 146 + .../strategies/aelin-council/README.md | 15 + .../strategies/aelin-council/examples.json | 24 + .../strategies/aelin-council/index.ts | 187 + .../utils/snapshot/strategies/agave/README.md | 10 + .../snapshot/strategies/agave/examples.json | 20 + .../utils/snapshot/strategies/agave/index.ts | 60 + .../utils/snapshot/strategies/aks/README.md | 9 + .../snapshot/strategies/aks/examples.json | 17 + .../utils/snapshot/strategies/aks/index.ts | 101 + .../snapshot/strategies/anti-whale/README.md | 83 + .../strategies/anti-whale/examples.json | 32 + .../snapshot/strategies/anti-whale/index.ts | 85 + .../strategies/apescape/examples.json | 14 + .../snapshot/strategies/apescape/index.ts | 137 + .../snapshot/strategies/apeswap/README.md | 33 + .../snapshot/strategies/apeswap/examples.json | 24 + .../snapshot/strategies/apeswap/index.ts | 58 + .../snapshot/strategies/api-post/README.md | 54 + .../strategies/api-post/examples.json | 23 + .../snapshot/strategies/api-post/index.ts | 39 + .../snapshot/strategies/api-v2/README.md | 50 + .../snapshot/strategies/api-v2/examples.json | 23 + .../utils/snapshot/strategies/api-v2/index.ts | 75 + .../utils/snapshot/strategies/api/README.md | 98 + .../snapshot/strategies/api/examples.json | 22 + .../utils/snapshot/strategies/api/index.ts | 62 + .../ari10-staking-locked/examples.json | 20 + .../strategies/ari10-staking-locked/index.ts | 36 + .../ari10-staking-locked/schema.json | 32 + .../strategies/arrakis-finance/README.md | 21 + .../strategies/arrakis-finance/examples.json | 21 + .../strategies/arrakis-finance/index.ts | 72 + .../strategies/arrakis-finance/schema.json | 41 + .../strategies/arrow-vesting/README.md | 24 + .../strategies/arrow-vesting/examples.json | 23 + .../strategies/arrow-vesting/index.ts | 122 + .../aura-balance-of-vlaura-vebal/README.md | 30 + .../examples.json | 20 + .../aura-balance-of-vlaura-vebal/index.ts | 58 + .../aura-balance-of-vlaura-vebal/schema.json | 38 + .../README.md | 30 + .../examples.json | 21 + .../index.ts | 74 + .../README.md | 30 + .../examples.json | 22 + .../aura-vlaura-vebal-with-overrides/index.ts | 76 + .../schema.json | 38 + .../strategies/aura-vlaura-vebal/README.md | 28 + .../aura-vlaura-vebal/examples.json | 20 + .../strategies/aura-vlaura-vebal/index.ts | 59 + .../strategies/aura-vlaura-vebal/schema.json | 38 + .../avn-balance-of-staked/examples.json | 19 + .../strategies/avn-balance-of-staked/index.ts | 162 + .../strategies/babywealthyclub/examples.json | 23 + .../strategies/babywealthyclub/index.ts | 48 + .../snapshot/strategies/badgeth/examples.json | 27 + .../snapshot/strategies/badgeth/index.ts | 47 + .../balance-in-vdfyn-vault/README.md | 55 + .../balance-in-vdfyn-vault/examples.json | 54 + .../balance-in-vdfyn-vault/index.ts | 53 + .../strategies/balance-of-subgraph/README.md | 24 + .../balance-of-subgraph/examples.json | 17 + .../strategies/balance-of-subgraph/index.ts | 55 + .../examples.json | 24 + .../index.ts | 136 + .../balance-of-with-min/examples.json | 22 + .../strategies/balance-of-with-min/index.ts | 29 + .../balance-of-with-thresholds/README.md | 19 + .../balance-of-with-thresholds/examples.json | 27 + .../balance-of-with-thresholds/index.ts | 44 + .../balancer-delegation/examples.json | 33 + .../strategies/balancer-delegation/index.ts | 29 + .../README.md | 14 + .../examples.json | 37 + .../index.ts | 37 + .../strategies/balancer-poolid/README.md | 16 + .../strategies/balancer-poolid/examples.json | 21 + .../strategies/balancer-poolid/index.ts | 107 + .../balancer-smart-pool/examples.json | 22 + .../strategies/balancer-smart-pool/index.ts | 58 + .../strategies/balancer-unipool/examples.json | 22 + .../strategies/balancer-unipool/index.ts | 110 + .../strategies/balancer/examples.json | 39 + .../snapshot/strategies/balancer/index.ts | 87 + .../examples.json | 21 + .../index.ts | 59 + .../examples.json | 26 + .../index.ts | 69 + .../snapshot/strategies/banksy-dao/README.md | 46 + .../strategies/banksy-dao/examples.json | 19 + .../snapshot/strategies/banksy-dao/index.ts | 91 + .../battlefly-vgfly-and-staked-gfly/README.md | 26 + .../examples.json | 28 + .../battlefly-vgfly-and-staked-gfly/index.ts | 225 + .../snapshot/strategies/biswap/README.md | 41 + .../snapshot/strategies/biswap/examples.json | 29 + .../utils/snapshot/strategies/biswap/index.ts | 136 + .../blockzerolabs-cryptonauts/README.md | 16 + .../blockzerolabs-cryptonauts/examples.json | 22 + .../blockzerolabs-cryptonauts/index.ts | 88 + .../snapshot/strategies/botto-dao/README.md | 33 + .../strategies/botto-dao/examples.json | 25 + .../snapshot/strategies/botto-dao/index.ts | 73 + .../snapshot/strategies/brightid/README.md | 44 + .../strategies/brightid/examples.json | 19 + .../snapshot/strategies/brightid/index.ts | 63 + .../snapshot/strategies/bsc-mvb/examples.json | 27 + .../snapshot/strategies/bsc-mvb/index.ts | 182 + .../utils/snapshot/strategies/cake/README.md | 9 + .../snapshot/strategies/cake/examples.json | 35 + .../utils/snapshot/strategies/cake/index.ts | 297 + .../strategies/cap-voting-power/README.md | 62 + .../strategies/cap-voting-power/examples.json | 66 + .../strategies/cap-voting-power/index.ts | 278 + .../strategies/capitaldao-staking/README.md | 15 + .../capitaldao-staking/examples.json | 21 + .../strategies/capitaldao-staking/index.ts | 49 + .../strategies/celer-sgn-delegation/README.md | 12 + .../celer-sgn-delegation/examples.json | 43 + .../strategies/celer-sgn-delegation/index.ts | 142 + .../strategies/chubbykaijudao/README.md | 33 + .../strategies/chubbykaijudao/examples.json | 20 + .../strategies/chubbykaijudao/index.ts | 49 + .../strategies/citydao-square-root/README.md | 29 + .../citydao-square-root/examples.json | 41 + .../strategies/citydao-square-root/index.ts | 64 + .../clqdr-balance-with-lp/examples.json | 35 + .../strategies/clqdr-balance-with-lp/index.ts | 148 + .../snapshot/strategies/coinswap/README.md | 47 + .../strategies/coinswap/examples.json | 36 + .../snapshot/strategies/coinswap/index.ts | 164 + .../strategies/colony-reputation/README.md | 13 + .../colony-reputation/examples.json | 36 + .../strategies/colony-reputation/index.ts | 82 + .../comp-like-votes-inclusive/README.md | 3 + .../comp-like-votes-inclusive/examples.json | 19 + .../comp-like-votes-inclusive/index.ts | 40 + .../strategies/comp-like-votes/examples.json | 18 + .../strategies/comp-like-votes/index.ts | 57 + .../strategies/contract-call/README.md | 81 + .../strategies/contract-call/examples.json | 35 + .../strategies/contract-call/index.ts | 45 + .../strategies/conv-finance/examples.json | 64 + .../snapshot/strategies/conv-finance/index.ts | 350 + .../snapshot/strategies/coordinape/README.md | 11 + .../strategies/coordinape/examples.json | 19 + .../snapshot/strategies/coordinape/index.ts | 34 + .../snapshot/strategies/cream/examples.json | 58 + .../utils/snapshot/strategies/cream/index.ts | 301 + .../strategies/credit-lp/examples.json | 26 + .../snapshot/strategies/credit-lp/index.ts | 56 + .../snapshot/strategies/cronaswap/README.md | 45 + .../strategies/cronaswap/examples.json | 35 + .../snapshot/strategies/cronaswap/index.ts | 120 + .../crucible-erc20-balance-of/README.md | 13 + .../crucible-erc20-balance-of/examples.json | 28 + .../crucible-erc20-balance-of/index.ts | 107 + .../README.md | 22 + .../examples.json | 28 + .../index.ts | 185 + .../schema.json | 44 + .../snapshot/strategies/ctoken/README.md | 27 + .../snapshot/strategies/ctoken/examples.json | 26 + .../utils/snapshot/strategies/ctoken/index.ts | 112 + .../strategies/ctsi-staking-pool/README.md | 7 + .../ctsi-staking-pool/examples.json | 24 + .../strategies/ctsi-staking-pool/index.ts | 125 + .../strategies/ctsi-staking-pool/schema.json | 23 + .../strategies/ctsi-staking/README.md | 23 + .../strategies/ctsi-staking/examples.json | 26 + .../snapshot/strategies/ctsi-staking/index.ts | 172 + .../strategies/ctsi-staking/schema.json | 23 + .../curio-cards-erc20-weighted/README.md | 5 + .../curio-cards-erc20-weighted/examples.json | 17 + .../curio-cards-erc20-weighted/index.ts | 171 + .../strategies/cyberkongz-v2/examples.json | 27 + .../strategies/cyberkongz-v2/index.ts | 77 + .../strategies/cyberkongz-v3/examples.json | 58 + .../strategies/cyberkongz-v3/index.ts | 97 + .../strategies/cyberkongz/examples.json | 27 + .../snapshot/strategies/cyberkongz/index.ts | 52 + .../strategies/darkforest-score/README.md | 8 + .../strategies/darkforest-score/examples.json | 29 + .../strategies/darkforest-score/index.ts | 46 + .../decentraland-estate-size/README.md | 14 + .../decentraland-estate-size/examples.json | 19 + .../decentraland-estate-size/index.ts | 87 + .../decentraland-rental-lessors/README.md | 30 + .../decentraland-rental-lessors/examples.json | 30 + .../decentraland-rental-lessors/index.ts | 223 + .../decentraland-rental-lessors/types.ts | 15 + .../decentraland-wearable-rarity/README.md | 47 + .../examples.json | 35 + .../decentraland-wearable-rarity/index.ts | 112 + .../strategies/defidollar/examples.json | 19 + .../snapshot/strategies/defidollar/index.ts | 72 + .../snapshot/strategies/defiplaza/README.md | 20 + .../strategies/defiplaza/examples.json | 36 + .../snapshot/strategies/defiplaza/index.ts | 85 + .../snapshot/strategies/defiplaza/schema.json | 35 + .../README.md | 36 + .../examples.json | 19 + .../degenzoo-erc721-animals-weighted/index.ts | 90 + .../delegation-with-overrides/README.md | 50 + .../delegation-with-overrides/examples.json | 35 + .../delegation-with-overrides/index.ts | 75 + .../snapshot/strategies/delegation/README.md | 37 + .../strategies/delegation/examples.json | 27 + .../snapshot/strategies/delegation/index.ts | 59 + .../deposit-in-sablier-stream/README.md | 12 + .../deposit-in-sablier-stream/examples.json | 35 + .../deposit-in-sablier-stream/index.ts | 69 + .../deposit-in-sablier-stream/schema.json | 35 + .../dextf-staked-in-vaults/README.md | 42 + .../dextf-staked-in-vaults/examples.json | 36 + .../dextf-staked-in-vaults/index.ts | 61 + .../strategies/dfyn-staked-in-farms/README.md | 67 + .../dfyn-staked-in-farms/examples.json | 63 + .../strategies/dfyn-staked-in-farms/index.ts | 158 + .../dfyn-staked-in-vaults/README.md | 43 + .../dfyn-staked-in-vaults/examples.json | 42 + .../strategies/dfyn-staked-in-vaults/index.ts | 61 + .../snapshot/strategies/dgenesis/README.md | 3 + .../strategies/dgenesis/examples.json | 20 + .../snapshot/strategies/dgenesis/index.ts | 52 + .../digitalax-deco-to-mona/README.md | 14 + .../digitalax-deco-to-mona/examples.json | 21 + .../digitalax-deco-to-mona/index.ts | 55 + .../digitalax-genesis-contribution/README.md | 13 + .../examples.json | 25 + .../digitalax-genesis-contribution/index.ts | 154 + .../digitalax-lp-stakers-matic/examples.json | 19 + .../digitalax-lp-stakers-matic/index.ts | 68 + .../digitalax-lp-stakers/examples.json | 21 + .../strategies/digitalax-lp-stakers/index.ts | 68 + .../digitalax-mona-quickswap/README.md | 21 + .../digitalax-mona-quickswap/examples.json | 21 + .../digitalax-mona-quickswap/index.ts | 56 + .../examples.json | 21 + .../digitalax-mona-stakers-matic/index.ts | 46 + .../snapshot/strategies/dittomoney/README.md | 35 + .../strategies/dittomoney/examples.json | 29 + .../snapshot/strategies/dittomoney/index.ts | 139 + .../strategies/dogs-unchained/README.md | 5 + .../strategies/dogs-unchained/examples.json | 33 + .../strategies/dogs-unchained/index.ts | 141 + .../snapshot/strategies/dopamine/README.md | 14 + .../strategies/dopamine/examples.json | 36 + .../snapshot/strategies/dopamine/index.ts | 86 + .../snapshot/strategies/dopamine/schema.json | 40 + .../dps-nft-strategy-nova/README.md | 15 + .../dps-nft-strategy-nova/examples.json | 20 + .../strategies/dps-nft-strategy-nova/index.ts | 88 + .../dps-nft-strategy-nova/schema.json | 17 + .../strategies/dps-nft-strategy/README.md | 15 + .../strategies/dps-nft-strategy/examples.json | 20 + .../strategies/dps-nft-strategy/index.ts | 84 + .../strategies/dps-nft-strategy/schema.json | 17 + .../README.md | 15 + .../examples.json | 37 + .../index.ts | 79 + .../schema.json | 51 + .../README.md | 12 + .../examples.json | 35 + .../index.ts | 38 + .../schema.json | 30 + .../echelon-cached-erc1155-decay/README.md | 24 + .../examples.json | 28 + .../echelon-cached-erc1155-decay/index.ts | 51 + .../README.md | 36 + .../examples.json | 37 + .../index.ts | 99 + .../README.md | 28 + .../examples.json | 30 + .../index.ts | 77 + .../strategies/eco-voting-power/README.md | 11 + .../strategies/eco-voting-power/examples.json | 34 + .../strategies/eco-voting-power/index.ts | 192 + .../strategies/eco-voting-power/schema.json | 22 + .../snapshot/strategies/egl-vote/README.md | 15 + .../strategies/egl-vote/examples.json | 22 + .../snapshot/strategies/egl-vote/index.ts | 54 + .../strategies/ens-10k-club/README.md | 16 + .../strategies/ens-10k-club/examples.json | 21 + .../snapshot/strategies/ens-10k-club/index.ts | 83 + .../strategies/ens-all-club-digits/README.md | 16 + .../ens-all-club-digits/examples.json | 20 + .../strategies/ens-all-club-digits/index.ts | 87 + .../strategies/ens-domains-owned/README.md | 39 + .../ens-domains-owned/examples.json | 18 + .../strategies/ens-domains-owned/index.ts | 71 + .../strategies/ens-reverse-record/README.md | 35 + .../ens-reverse-record/examples.json | 17 + .../strategies/ens-reverse-record/index.ts | 64 + .../erc1155-all-balances-of/README.md | 6 + .../erc1155-all-balances-of/examples.json | 33 + .../erc1155-all-balances-of/index.ts | 87 + .../erc1155-balance-of-cv/examples.json | 21 + .../strategies/erc1155-balance-of-cv/index.ts | 25 + .../examples.json | 17 + .../erc1155-balance-of-ids-weighted/index.ts | 29 + .../erc1155-balance-of-ids/examples.json | 16 + .../erc1155-balance-of-ids/index.ts | 38 + .../strategies/erc1155-balance-of/README.md | 14 + .../erc1155-balance-of/examples.json | 21 + .../strategies/erc1155-balance-of/index.ts | 37 + .../erc1155-weighted-by-id/README.md | 11 + .../erc1155-weighted-by-id/examples.json | 40 + .../erc1155-weighted-by-id/index.ts | 48 + .../erc1155-with-multiplier/examples.json | 21 + .../erc1155-with-multiplier/index.ts | 38 + .../strategies/erc20-balance-of-at/README.md | 14 + .../erc20-balance-of-at/examples.json | 28 + .../strategies/erc20-balance-of-at/index.ts | 34 + .../erc20-balance-of-at/schema.json | 38 + .../erc20-balance-of-coeff/examples.json | 26 + .../erc20-balance-of-coeff/index.ts | 28 + .../README.md | 29 + .../examples.json | 34 + .../index.ts | 33 + .../erc20-balance-of-cv/examples.json | 25 + .../strategies/erc20-balance-of-cv/index.ts | 25 + .../erc20-balance-of-delegation/examples.json | 21 + .../erc20-balance-of-delegation/index.ts | 44 + .../examples.json | 26 + .../erc20-balance-of-fixed-total/index.ts | 29 + .../erc20-balance-of-indexed/README.md | 23 + .../erc20-balance-of-indexed/examples.json | 24 + .../erc20-balance-of-indexed/index.ts | 37 + .../README.md | 19 + .../examples.json | 21 + .../index.ts | 40 + .../erc20-balance-of-weighted/README.md | 14 + .../erc20-balance-of-weighted/examples.json | 36 + .../erc20-balance-of-weighted/index.ts | 25 + .../README.md | 13 + .../examples.json | 21 + .../erc20-balance-of-with-delegation/index.ts | 33 + .../strategies/erc20-balance-of/README.md | 13 + .../strategies/erc20-balance-of/examples.json | 35 + .../strategies/erc20-balance-of/index.ts | 34 + .../strategies/erc20-balance-of/schema.json | 33 + .../snapshot/strategies/erc20-price/README.md | 20 + .../strategies/erc20-price/examples.json | 35 + .../snapshot/strategies/erc20-price/index.ts | 63 + .../strategies/erc20-rebase-wrapper/README.md | 13 + .../erc20-rebase-wrapper/examples.json | 16 + .../strategies/erc20-rebase-wrapper/index.ts | 45 + .../strategies/erc20-received/README.md | 81 + .../strategies/erc20-received/examples.json | 52 + .../strategies/erc20-received/index.ts | 143 + .../erc20-token-and-lp-weighted/README.md | 19 + .../erc20-token-and-lp-weighted/examples.json | 27 + .../erc20-token-and-lp-weighted/index.ts | 92 + .../erc20-token-and-lp-weighted/schema.json | 36 + .../strategies/erc20-tokens-per-uni/README.md | 7 + .../erc20-tokens-per-uni/examples.json | 18 + .../strategies/erc20-tokens-per-uni/index.ts | 56 + .../erc20-votes-with-override/README.md | 137 + .../erc20-votes-with-override/examples.json | 21 + .../erc20-votes-with-override/index.ts | 271 + .../strategies/erc20-votes/examples.json | 18 + .../snapshot/strategies/erc20-votes/index.ts | 35 + .../strategies/erc20-with-balance/README.md | 25 + .../erc20-with-balance/examples.json | 49 + .../strategies/erc20-with-balance/index.ts | 28 + .../erc3525-flexible-voucher/examples.json | 16 + .../erc3525-flexible-voucher/index.ts | 106 + .../erc3525-flexible-voucher/utils.ts | 40 + .../erc3525-vesting-voucher/examples.json | 16 + .../erc3525-vesting-voucher/index.ts | 102 + .../erc3525-vesting-voucher/utils.ts | 36 + .../erc721-collateral-held/README.md | 12 + .../erc721-collateral-held/examples.json | 20 + .../erc721-collateral-held/index.ts | 81 + .../erc721-collateral-held/schema.json | 28 + .../erc721-enumerable/examples.json | 18 + .../strategies/erc721-enumerable/index.ts | 23 + .../erc721-multi-registry-weighted/README.md | 18 + .../examples.json | 30 + .../erc721-multi-registry-weighted/index.ts | 41 + .../erc721-multi-registry/README.md | 15 + .../erc721-multi-registry/examples.json | 26 + .../strategies/erc721-multi-registry/index.ts | 38 + .../strategies/erc721-pair-weights/README.md | 38 + .../erc721-pair-weights/examples.json | 23 + .../strategies/erc721-pair-weights/index.ts | 51 + .../erc721-with-metadata-by-ownerof/README.md | 55 + .../examples.json | 20 + .../erc721-with-metadata-by-ownerof/index.ts | 128 + .../strategies/erc721-with-metadata/README.md | 53 + .../erc721-with-metadata/examples.json | 20 + .../strategies/erc721-with-metadata/index.ts | 74 + .../erc721-with-multiplier/README.md | 13 + .../erc721-with-multiplier/examples.json | 19 + .../erc721-with-multiplier/index.ts | 34 + .../README.md | 17 + .../examples.json | 67 + .../index.ts | 144 + .../types.d.ts | 5 + .../README.md | 17 + .../examples.json | 24 + .../index.ts | 75 + .../erc721-with-tokenid-weighted/README.md | 15 + .../examples.json | 20 + .../erc721-with-tokenid-weighted/index.ts | 35 + .../strategies/erc721-with-tokenid/README.md | 13 + .../erc721-with-tokenid/examples.json | 21 + .../strategies/erc721-with-tokenid/index.ts | 37 + .../snapshot/strategies/erc721/README.md | 12 + .../snapshot/strategies/erc721/examples.json | 21 + .../utils/snapshot/strategies/erc721/index.ts | 33 + .../snapshot/strategies/erc721/schema.json | 28 + .../strategies/esd-delegation/examples.json | 22 + .../strategies/esd-delegation/index.ts | 37 + .../utils/snapshot/strategies/esd/README.md | 29 + .../snapshot/strategies/esd/examples.json | 27 + .../utils/snapshot/strategies/esd/index.ts | 97 + .../strategies/eth-balance/examples.json | 33 + .../snapshot/strategies/eth-balance/index.ts | 40 + .../strategies/eth-philanthropy/README.md | 105 + .../strategies/eth-philanthropy/examples.json | 24 + .../strategies/eth-philanthropy/index.ts | 38 + .../eth-received/ElasticSearchTxResult.ts | 81 + .../strategies/eth-received/README.md | 90 + .../strategies/eth-received/examples.json | 70 + .../snapshot/strategies/eth-received/index.ts | 83 + .../strategies/eth-wallet-age/README.md | 3 + .../strategies/eth-wallet-age/examples.json | 34 + .../strategies/eth-wallet-age/index.ts | 71 + .../strategies/eth-with-balance/README.md | 9 + .../strategies/eth-with-balance/examples.json | 34 + .../strategies/eth-with-balance/index.ts | 28 + .../strategies/ethalend-balance-of/README.md | 16 + .../ethalend-balance-of/examples.json | 32 + .../strategies/ethalend-balance-of/index.ts | 56 + .../ethalend-balance-of/schema.json | 41 + .../ethercats-founder-series/examples.json | 14 + .../ethercats-founder-series/index.ts | 60 + .../ethercats-founders-series/examples.json | 18 + .../ethercats-founders-series/index.ts | 70 + .../strategies/ethermon-erc721/README.md | 12 + .../strategies/ethermon-erc721/examples.json | 17 + .../strategies/ethermon-erc721/index.ts | 101 + .../etherorcs-combo-balanceof/README.md | 1 + .../etherorcs-combo-balanceof/examples.json | 30 + .../etherorcs-combo-balanceof/index.ts | 47 + .../strategies/faraland-staking/examples.json | 15 + .../strategies/faraland-staking/index.ts | 51 + .../snapshot/strategies/fight-club/README.md | 57 + .../strategies/fight-club/examples.json | 27 + .../snapshot/strategies/fight-club/index.ts | 117 + .../strategies/flashstake/examples.json | 22 + .../snapshot/strategies/flashstake/index.ts | 105 + .../flexa-capacity-staking/examples.json | 22 + .../flexa-capacity-staking/index.ts | 40 + .../strategies/forta-shares/README.md | 3 + .../strategies/forta-shares/examples.json | 11 + .../snapshot/strategies/forta-shares/index.ts | 89 + .../strategies/frax-finance/examples.json | 24 + .../snapshot/strategies/frax-finance/index.ts | 229 + .../galaxy-nft-with-score/examples.json | 48 + .../strategies/galaxy-nft-with-score/index.ts | 201 + .../strategies/galxe-loyalty-points/README.md | 27 + .../galxe-loyalty-points/examples.json | 21 + .../strategies/galxe-loyalty-points/index.ts | 60 + .../strategies/gamium-voting/README.md | 16 + .../strategies/gamium-voting/examples.json | 24 + .../strategies/gamium-voting/index.ts | 121 + .../strategies/gatenet-total-staked/README.md | 19 + .../gatenet-total-staked/examples.json | 33 + .../strategies/gatenet-total-staked/index.ts | 241 + .../gatenet-total-staked/schema.json | 43 + .../snapshot/strategies/genart/examples.json | 20 + .../utils/snapshot/strategies/genart/index.ts | 47 + .../snapshot/strategies/genomesdao/README.md | 16 + .../strategies/genomesdao/examples.json | 41 + .../snapshot/strategies/genomesdao/index.ts | 69 + .../strategies/genomesdao/schema.json | 45 + .../genzees-from-subgraph/README.md | 21 + .../genzees-from-subgraph/examples.json | 21 + .../strategies/genzees-from-subgraph/index.ts | 59 + .../strategies/gin-finance/examples.json | 15 + .../snapshot/strategies/gin-finance/index.ts | 88 + .../giveth-balancer-balance/README.md | 12 + .../giveth-balancer-balance/examples.json | 19 + .../giveth-balancer-balance/index.ts | 101 + .../giveth-gnosis-balance-v2/README.md | 12 + .../giveth-gnosis-balance-v2/examples.json | 19 + .../giveth-gnosis-balance-v2/index.ts | 87 + .../strategies/giveth-xdai-balance/README.md | 12 + .../giveth-xdai-balance/examples.json | 19 + .../strategies/giveth-xdai-balance/index.ts | 141 + .../snapshot/strategies/glide/examples.json | 26 + .../utils/snapshot/strategies/glide/index.ts | 135 + .../snapshot/strategies/gno/examples.json | 24 + .../utils/snapshot/strategies/gno/index.ts | 40 + .../goldfinch-membership/examples.json | 16 + .../strategies/goldfinch-membership/index.ts | 54 + .../goldfinch-voting-power/examples.json | 18 + .../goldfinch-voting-power/index.ts | 101 + .../gooddollar-multichain/README.md | 73 + .../gooddollar-multichain/examples.json | 75 + .../strategies/gooddollar-multichain/index.ts | 57 + .../strategies/governor-alpha/examples.json | 20 + .../strategies/governor-alpha/index.ts | 37 + .../strategies/governor-delegator/README.md | 14 + .../governor-delegator/examples.json | 23 + .../strategies/governor-delegator/index.ts | 47 + .../gysr-lp-staking-balance/README.md | 14 + .../gysr-lp-staking-balance/examples.json | 22 + .../gysr-lp-staking-balance/index.ts | 72 + .../gysr-lp-staking-balance/schema.json | 41 + .../strategies/gysr-pending-rewards/README.md | 16 + .../gysr-pending-rewards/examples.json | 21 + .../strategies/gysr-pending-rewards/index.ts | 85 + .../gysr-pending-rewards/schema.json | 36 + .../strategies/gysr-staking-balance/README.md | 13 + .../gysr-staking-balance/examples.json | 68 + .../strategies/gysr-staking-balance/index.ts | 34 + .../utils/snapshot/strategies/h2o/README.md | 17 + .../snapshot/strategies/h2o/examples.json | 23 + .../utils/snapshot/strategies/h2o/index.ts | 50 + .../utils/snapshot/strategies/h2o/schema.json | 26 + .../strategies/harmony-staking/examples.json | 34 + .../strategies/harmony-staking/index.ts | 45 + .../snapshot/strategies/has-rock/README.md | 3 + .../strategies/has-rock/examples.json | 15 + .../snapshot/strategies/has-rock/index.ts | 40 + .../strategies/hashes-voting/examples.json | 18 + .../strategies/hashes-voting/index.ts | 37 + .../hashflow-governance-power/README.md | 19 + .../hashflow-governance-power/examples.json | 68 + .../hashflow-governance-power/index.ts | 62 + .../hashflow-governance-power/schema.json | 38 + .../strategies/hashflow-vehft/README.md | 23 + .../strategies/hashflow-vehft/examples.json | 22 + .../strategies/hashflow-vehft/index.ts | 65 + .../strategies/hashflow-vehft/schema.json | 30 + .../strategies/hedgey-delegate/README.md | 17 + .../strategies/hedgey-delegate/examples.json | 27 + .../strategies/hedgey-delegate/index.ts | 40 + .../strategies/hedgey-delegate/schema.json | 49 + .../strategies/hedgey-multi/README.md | 16 + .../strategies/hedgey-multi/examples.json | 27 + .../snapshot/strategies/hedgey-multi/index.ts | 217 + .../snapshot/strategies/hedgey/README.md | 19 + .../snapshot/strategies/hedgey/examples.json | 16 + .../utils/snapshot/strategies/hedgey/index.ts | 78 + .../utils/snapshot/strategies/helix/README.md | 41 + .../snapshot/strategies/helix/examples.json | 32 + .../utils/snapshot/strategies/helix/index.ts | 138 + .../strategies/holds-tokens/README.md | 30 + .../strategies/holds-tokens/examples.json | 34 + .../snapshot/strategies/holds-tokens/index.ts | 41 + .../snapshot/strategies/honeyswap/README.md | 13 + .../strategies/honeyswap/examples.json | 24 + .../snapshot/strategies/honeyswap/index.ts | 224 + .../hopr-bridged-balance/examples.json | 65 + .../strategies/hopr-bridged-balance/index.ts | 180 + .../hopr-stake-and-balance-qv/README.md | 31 + .../hopr-stake-and-balance-qv/examples.json | 48 + .../hopr-stake-and-balance-qv/index.ts | 396 ++ .../hopr-staking-by-season/examples.json | 34 + .../hopr-staking-by-season/index.ts | 124 + .../strategies/hopr-staking-s2/examples.json | 32 + .../strategies/hopr-staking-s2/index.ts | 107 + .../strategies/hopr-staking/examples.json | 61 + .../snapshot/strategies/hopr-staking/index.ts | 104 + .../strategies/hopr-uni-lp-farm/examples.json | 38 + .../strategies/hopr-uni-lp-farm/index.ts | 199 + .../utils/snapshot/strategies/ilv/README.md | 15 + .../utils/snapshot/strategies/ilv/index.ts | 25 + .../strategies/immutable-x/examples.json | 48 + .../snapshot/strategies/immutable-x/index.ts | 158 + .../strategies/impossible-finance/README.md | 5 + .../impossible-finance/examples.json | 49 + .../strategies/impossible-finance/index.ts | 111 + .../utils/snapshot/strategies/index.ts | 938 +++ .../README.md | 26 + .../examples.json | 21 + .../infinityprotocol-liquidity-pools/index.ts | 87 + .../strategies/inverse-xinv/README.md | 11 + .../strategies/inverse-xinv/examples.json | 18 + .../snapshot/strategies/inverse-xinv/index.ts | 51 + .../strategies/iotex-balance/examples.json | 19 + .../strategies/iotex-balance/index.ts | 56 + .../iotex-staked-balance/examples.json | 19 + .../strategies/iotex-staked-balance/index.ts | 42 + .../snapshot/strategies/izumi-veizi/README.md | 13 + .../strategies/izumi-veizi/examples.json | 31 + .../snapshot/strategies/izumi-veizi/index.ts | 83 + .../strategies/izumi-veizi/schema.json | 33 + .../snapshot/strategies/jade-smrt/README.md | 7 + .../strategies/jade-smrt/examples.json | 73 + .../snapshot/strategies/jade-smrt/index.ts | 170 + .../strategies/jpegd-locked-jpeg-of/README.md | 3 + .../jpegd-locked-jpeg-of/examples.json | 16 + .../strategies/jpegd-locked-jpeg-of/index.ts | 70 + .../strategies/juicebox/examples.json | 59 + .../snapshot/strategies/juicebox/index.ts | 44 + .../strategies/l2-deversifi/examples.json | 20 + .../snapshot/strategies/l2-deversifi/index.ts | 51 + .../strategies/landdao-token-tiers/README.md | 13 + .../landdao-token-tiers/examples.json | 24 + .../strategies/landdao-token-tiers/index.ts | 83 + .../strategies/liftkitchen/examples.json | 17 + .../snapshot/strategies/liftkitchen/index.ts | 135 + .../linear-vesting-power/examples.json | 22 + .../strategies/linear-vesting-power/index.ts | 139 + .../liquidity-token-provide/README.md | 22 + .../liquidity-token-provide/examples.json | 18 + .../liquidity-token-provide/index.ts | 77 + .../strategies/lit-dao-governance/README.md | 5 + .../lit-dao-governance/examples.json | 18 + .../strategies/lit-dao-governance/index.ts | 102 + .../strategies/lit-dao-governance/schema.json | 20 + .../strategies/lodestar-staked-lp/README.md | 18 + .../lodestar-staked-lp/examples.json | 22 + .../strategies/lodestar-staked-lp/index.ts | 90 + .../strategies/lodestar-vesting/README.md | 19 + .../strategies/lodestar-vesting/examples.json | 30 + .../strategies/lodestar-vesting/index.ts | 46 + .../loot-character-guilds/examples.json | 145 + .../strategies/loot-character-guilds/index.ts | 82 + .../strategies/lqty-proxy-stakers/README.md | 4 + .../lqty-proxy-stakers/examples.json | 19 + .../strategies/lqty-proxy-stakers/index.ts | 50 + .../lrc-l2-nft-balance-of/README.md | 21 + .../lrc-l2-nft-balance-of/examples.json | 29 + .../strategies/lrc-l2-nft-balance-of/index.ts | 102 + .../lrc-l2-subgraph-balance-of/README.md | 14 + .../lrc-l2-subgraph-balance-of/examples.json | 35 + .../lrc-l2-subgraph-balance-of/index.ts | 93 + .../lrc-lp-subgraph-balance-of/README.md | 19 + .../lrc-lp-subgraph-balance-of/examples.json | 42 + .../lrc-lp-subgraph-balance-of/index.ts | 214 + .../strategies/lrc-nft-dao-search/README.md | 28 + .../lrc-nft-dao-search/examples.json | 21 + .../strategies/lrc-nft-dao-search/index.ts | 126 + .../strategies/lrc-nft-search-mult/README.md | 29 + .../lrc-nft-search-mult/examples.json | 26 + .../strategies/lrc-nft-search-mult/index.ts | 128 + .../strategies/lydia-gov-vault/README.md | 3 + .../strategies/lydia-gov-vault/examples.json | 20 + .../strategies/lydia-gov-vault/index.ts | 42 + .../strategies/maker-ds-chief/examples.json | 25 + .../strategies/maker-ds-chief/index.ts | 59 + .../strategies/marsecosystem/README.md | 69 + .../strategies/marsecosystem/examples.json | 92 + .../strategies/marsecosystem/index.ts | 148 + .../examples.json | 41 + .../index.ts | 96 + .../masterchef-pool-balance-price/README.md | 369 ++ .../examples.json | 253 + .../masterchef-pool-balance-price/index.ts | 518 ++ .../masterchef-pool-balance/examples.json | 46 + .../masterchef-pool-balance/index.ts | 162 + .../strategies/masterchef/examples.json | 16 + .../snapshot/strategies/masterchef/index.ts | 147 + .../utils/snapshot/strategies/math/README.md | 89 + .../snapshot/strategies/math/examples.json | 230 + .../utils/snapshot/strategies/math/index.ts | 190 + .../utils/snapshot/strategies/math/options.ts | 152 + .../mcb-balance-from-graph/README.md | 24 + .../mcb-balance-from-graph/examples.json | 20 + .../mcb-balance-from-graph/index.ts | 65 + .../snapshot/strategies/mcn-farm/README.md | 15 + .../strategies/mcn-farm/examples.json | 27 + .../snapshot/strategies/mcn-farm/index.ts | 98 + .../meebitsdao-delegation/README.md | 32 + .../meebitsdao-delegation/examples.json | 58 + .../strategies/meebitsdao-delegation/index.ts | 135 + .../snapshot/strategies/meebitsdao/README.md | 15 + .../strategies/meebitsdao/examples.json | 22 + .../snapshot/strategies/meebitsdao/index.ts | 59 + .../snapshot/strategies/membership/README.md | 29 + .../strategies/membership/examples.json | 33 + .../snapshot/strategies/membership/index.ts | 100 + .../strategies/metropolis-pod/README.md | 20 + .../strategies/metropolis-pod/examples.json | 20 + .../strategies/metropolis-pod/index.ts | 28 + .../strategies/metropolis-pod/schema.json | 30 + .../strategies/minmax-mcn-farm/README.md | 15 + .../strategies/minmax-mcn-farm/examples.json | 24 + .../strategies/minmax-mcn-farm/index.ts | 90 + .../strategies/minotaur-money/examples.json | 17 + .../strategies/minotaur-money/index.ts | 138 + .../strategies/minto-balance-of-all/README.md | 15 + .../minto-balance-of-all/examples.json | 36 + .../strategies/minto-balance-of-all/index.ts | 63 + .../strategies/mithcash/examples.json | 23 + .../snapshot/strategies/mithcash/index.ts | 116 + .../strategies/modefi-staking/examples.json | 21 + .../strategies/modefi-staking/index.ts | 83 + .../snapshot/strategies/modefi/examples.json | 19 + .../utils/snapshot/strategies/modefi/index.ts | 93 + .../strategies/moloch-all/examples.json | 24 + .../snapshot/strategies/moloch-all/index.ts | 59 + .../strategies/moloch-loot/examples.json | 22 + .../snapshot/strategies/moloch-loot/index.ts | 126 + .../snapshot/strategies/moloch/examples.json | 25 + .../utils/snapshot/strategies/moloch/index.ts | 126 + .../moonbeam-free-balance/README.md | 24 + .../moonbeam-free-balance/examples.json | 18 + .../strategies/moonbeam-free-balance/index.ts | 119 + .../snapshot/strategies/mstable/README.md | 19 + .../snapshot/strategies/mstable/examples.json | 19 + .../snapshot/strategies/mstable/index.ts | 61 + .../strategies/multichain-serie/examples.json | 36 + .../strategies/multichain-serie/index.ts | 53 + .../snapshot/strategies/multichain/README.md | 52 + .../strategies/multichain/examples.json | 36 + .../snapshot/strategies/multichain/index.ts | 54 + .../strategies/multisig-owners/README.md | 6 + .../strategies/multisig-owners/examples.json | 21 + .../strategies/multisig-owners/index.ts | 49 + .../strategies/mushrooms/examples.json | 19 + .../snapshot/strategies/mushrooms/index.ts | 186 + .../mutant-cats-stakers-and-holders/README.md | 32 + .../examples.json | 20 + .../mutant-cats-stakers-and-holders/index.ts | 49 + .../nation3-votes-with-delegations/README.md | 13 + .../examples.json | 22 + .../nation3-votes-with-delegations/index.ts | 187 + .../schema.json | 30 + .../strategies/nexon-army-nft/examples.json | 16 + .../strategies/nexon-army-nft/index.ts | 70 + .../ninechronicles-staked-and-dcc/README.md | 73 + .../examples.json | 36 + .../ninechronicles-staked-and-dcc/index.ts | 159 + .../strategies/niu-staked/examples.json | 22 + .../snapshot/strategies/niu-staked/index.ts | 51 + .../strategies/nouns-rfp-power/examples.json | 20 + .../strategies/nouns-rfp-power/index.ts | 58 + .../strategies/occ-stake-of/README.md | 15 + .../strategies/occ-stake-of/examples.json | 27 + .../snapshot/strategies/occ-stake-of/index.ts | 38 + .../strategies/ocean-dao-brightid/README.md | 94 + .../ocean-dao-brightid/examples.json | 141 + .../strategies/ocean-dao-brightid/index.ts | 133 + .../strategies/ocean-marketplace-v4/README.md | 51 + .../ocean-marketplace-v4/examples.json | 94 + .../strategies/ocean-marketplace-v4/index.ts | 171 + .../ocean-marketplace-v4/oceanUtils.ts | 31 + .../strategies/ocean-marketplace/README.md | 67 + .../ocean-marketplace/examples.json | 41 + .../strategies/ocean-marketplace/index.ts | 144 + .../ocean-marketplace/oceanUtils.ts | 21 + .../strategies/offchain-delegation/README.md | 34 + .../offchain-delegation/examples.json | 32 + .../strategies/offchain-delegation/index.ts | 74 + .../utils/snapshot/strategies/ogn/README.md | 17 + .../snapshot/strategies/ogn/examples.json | 36 + .../utils/snapshot/strategies/ogn/index.ts | 65 + .../strategies/oolongswap/examples.json | 15 + .../snapshot/strategies/oolongswap/index.ts | 83 + .../snapshot/strategies/opium/examples.json | 23 + .../utils/snapshot/strategies/opium/index.ts | 139 + .../orange-reputation-based-voting/README.md | 12 + .../examples.json | 34 + .../orange-reputation-based-voting/index.ts | 43 + .../README.md | 34 + .../examples.json | 34 + .../index.ts | 72 + .../orbs-network-delegation/README.md | 17 + .../orbs-network-delegation/examples.json | 49 + .../orbs-network-delegation/index.ts | 55 + .../snapshot/strategies/orca-pod/README.md | 20 + .../strategies/orca-pod/examples.json | 16 + .../snapshot/strategies/orca-pod/index.ts | 28 + .../snapshot/strategies/orca-pod/schema.json | 30 + .../strategies/otterspace-badges/README.md | 33 + .../otterspace-badges/examples.json | 98 + .../strategies/otterspace-badges/index.ts | 131 + .../strategies/pagination/examples.json | 40 + .../snapshot/strategies/pagination/index.ts | 33 + .../snapshot/strategies/pancake/README.md | 27 + .../snapshot/strategies/pancake/examples.json | 25 + .../snapshot/strategies/pancake/index.ts | 129 + .../path-balance-staked-and-locked/README.md | 46 + .../examples.json | 47 + .../path-balance-staked-and-locked/index.ts | 72 + .../pdn-balances-and-vests/README.md | 20 + .../pdn-balances-and-vests/examples.json | 20 + .../pdn-balances-and-vests/index.ts | 84 + .../pdn-balances-and-vests/schema.json | 33 + .../snapshot/strategies/pepemon/examples.json | 23 + .../snapshot/strategies/pepemon/index.ts | 164 + .../snapshot/strategies/piedao/examples.json | 23 + .../utils/snapshot/strategies/piedao/index.ts | 136 + .../planet-finance-v2/examples.json | 20 + .../strategies/planet-finance-v2/index.ts | 150 + .../strategies/planet-finance/examples.json | 16 + .../strategies/planet-finance/index.ts | 221 + .../strategies/poap-with-weight-v2/README.md | 17 + .../poap-with-weight-v2/examples.json | 22 + .../strategies/poap-with-weight-v2/index.ts | 94 + .../strategies/poap-with-weight/README.md | 18 + .../strategies/poap-with-weight/examples.json | 23 + .../strategies/poap-with-weight/index.ts | 70 + .../utils/snapshot/strategies/poap/README.md | 14 + .../snapshot/strategies/poap/examples.json | 19 + .../utils/snapshot/strategies/poap/index.ts | 105 + .../strategies/pob-hash/examples.json | 20 + .../snapshot/strategies/pob-hash/index.ts | 57 + .../snapshot/strategies/pod-leader/README.md | 36 + .../strategies/pod-leader/examples.json | 63 + .../snapshot/strategies/pod-leader/index.ts | 343 + .../strategies/polis-balance/examples.json | 22 + .../strategies/polis-balance/index.ts | 150 + .../strategies/posichain-staking/README.md | 3 + .../posichain-staking/examples.json | 18 + .../strategies/posichain-staking/index.ts | 41 + .../posichain-total-balance/README.md | 3 + .../posichain-total-balance/examples.json | 18 + .../posichain-total-balance/index.ts | 72 + .../position-governance-power/README.md | 15 + .../position-governance-power/examples.json | 22 + .../position-governance-power/index.ts | 74 + .../position-governance-power/schema.json | 54 + .../snapshot/strategies/potion/README.md | 3 + .../snapshot/strategies/potion/examples.json | 24 + .../utils/snapshot/strategies/potion/index.ts | 86 + .../strategies/prepo-vesting/README.md | 48 + .../strategies/prepo-vesting/examples.json | 36 + .../strategies/prepo-vesting/index.ts | 58 + .../strategies/prepo-vesting/schema.json | 33 + .../printer-financial/examples.json | 22 + .../strategies/printer-financial/index.ts | 79 + .../strategies/printer-financial/schema.json | 65 + .../strategies/proof-of-humanity/README.md | 11 + .../proof-of-humanity/examples.json | 17 + .../strategies/proof-of-humanity/index.ts | 32 + .../strategies/proof-of-humanity/schema.json | 22 + .../protofi-erc721-tier-weighted/README.md | 15 + .../examples.json | 23 + .../protofi-erc721-tier-weighted/index.ts | 109 + .../README.md | 5 + .../examples.json | 20 + .../proxyprotocol-erc1155-balance-of/index.ts | 60 + .../proxyprotocol-erc20-balance-of/README.md | 5 + .../examples.json | 19 + .../proxyprotocol-erc20-balance-of/index.ts | 60 + .../schema.json | 33 + .../proxyprotocol-erc721-balance-of/README.md | 5 + .../examples.json | 18 + .../proxyprotocol-erc721-balance-of/index.ts | 60 + .../schema.json | 28 + .../psp-in-sepsp2-balance/README.md | 66 + .../psp-in-sepsp2-balance/examples.json | 47 + .../strategies/psp-in-sepsp2-balance/index.ts | 147 + .../strategies/push-voting-power/README.md | 24 + .../push-voting-power/examples.json | 31 + .../strategies/push-voting-power/index.ts | 159 + .../radicle-community-tokens/README.md | 13 + .../radicle-community-tokens/examples.json | 19 + .../radicle-community-tokens/index.ts | 86 + .../snapshot/strategies/rari-fuse/README.md | 22 + .../strategies/rari-fuse/examples.json | 22 + .../snapshot/strategies/rari-fuse/index.ts | 62 + .../snapshot/strategies/rari-fuse/schema.json | 36 + .../razor-network-voting/examples.json | 22 + .../strategies/razor-network-voting/index.ts | 135 + .../strategies/rdnt-capital-voting/README.md | 11 + .../rdnt-capital-voting/examples.json | 40 + .../strategies/rdnt-capital-voting/index.ts | 139 + .../snapshot/strategies/rebased/examples.json | 24 + .../snapshot/strategies/rebased/index.ts | 103 + .../strategies/recusal-list/README.md | 22 + .../strategies/recusal-list/examples.json | 29 + .../snapshot/strategies/recusal-list/index.ts | 44 + .../snapshot/strategies/reliquary/README.md | 40 + .../strategies/reliquary/examples.json | 25 + .../snapshot/strategies/reliquary/index.ts | 164 + .../strategies/ren-nodes/examples.json | 40 + .../snapshot/strategies/ren-nodes/index.ts | 113 + .../snapshot/strategies/rep3-badges/README.md | 52 + .../strategies/rep3-badges/examples.json | 105 + .../snapshot/strategies/rep3-badges/index.ts | 222 + .../reverse-voting-escrow/README.md | 49 + .../reverse-voting-escrow/examples.json | 22 + .../strategies/reverse-voting-escrow/index.ts | 183 + .../snapshot/strategies/revest/examples.json | 34 + .../utils/snapshot/strategies/revest/index.ts | 163 + .../riskharbor-underwriter/README.md | 10 + .../riskharbor-underwriter/examples.json | 22 + .../riskharbor-underwriter/index.ts | 79 + .../riskharbor-underwriter/schema.json | 29 + .../strategies/rnbw-balance/examples.json | 16 + .../snapshot/strategies/rnbw-balance/index.ts | 72 + .../rocketpool-node-operator/README.md | 13 + .../rocketpool-node-operator/examples.json | 33 + .../rocketpool-node-operator/index.ts | 48 + .../snapshot/strategies/rowdy-roos/README.md | 39 + .../strategies/rowdy-roos/examples.json | 17 + .../snapshot/strategies/rowdy-roos/index.ts | 71 + .../strategies/ruler-staked-lp/examples.json | 18 + .../strategies/ruler-staked-lp/index.ts | 125 + .../strategies/ruler-staked-token/README.md | 14 + .../ruler-staked-token/examples.json | 22 + .../strategies/ruler-staked-token/index.ts | 61 + .../strategies/saddle-finance-v2/README.md | 3 + .../saddle-finance-v2/examples.json | 20 + .../strategies/saddle-finance-v2/index.ts | 153 + .../saddle-finance-v2/vestingContractAddrs.ts | 74 + .../strategies/saddle-finance/README.md | 3 + .../strategies/saddle-finance/examples.json | 20 + .../strategies/saddle-finance/index.ts | 153 + .../saddle-finance/vestingContractAddrs.ts | 74 + .../snapshot/strategies/safe-vested/README.md | 33 + .../strategies/safe-vested/examples.json | 24 + .../snapshot/strategies/safe-vested/index.ts | 110 + .../safety-module-bpt-power/README.md | 18 + .../safety-module-bpt-power/examples.json | 37 + .../safety-module-bpt-power/index.ts | 227 + .../safety-module-bpt-power/schema.json | 66 + .../strategies/saffron-finance-v2/README.md | 94 + .../saffron-finance-v2/examples.json | 44 + .../strategies/saffron-finance-v2/index.ts | 179 + .../strategies/saffron-finance/examples.json | 217 + .../strategies/saffron-finance/index.ts | 340 + .../samurailegends-generals-balance/README.md | 21 + .../examples.json | 17 + .../samurailegends-generals-balance/index.ts | 44 + .../snapshot/strategies/sandman-dao/README.md | 62 + .../strategies/sandman-dao/examples.json | 18 + .../snapshot/strategies/sandman-dao/index.ts | 91 + .../strategies/sd-boost-twavp/README.md | 21 + .../strategies/sd-boost-twavp/examples.json | 25 + .../strategies/sd-boost-twavp/index.ts | 188 + .../snapshot/strategies/sd-boost/README.md | 15 + .../strategies/sd-boost/examples.json | 22 + .../snapshot/strategies/sd-boost/index.ts | 139 + .../strategies/sd-vote-boost-twavp/README.md | 26 + .../sd-vote-boost-twavp/examples.json | 27 + .../strategies/sd-vote-boost-twavp/index.ts | 105 + .../strategies/sd-vote-boost/README.md | 18 + .../strategies/sd-vote-boost/examples.json | 24 + .../strategies/sd-vote-boost/index.ts | 62 + .../snapshot/strategies/selfswap/README.md | 9 + .../strategies/selfswap/examples.json | 19 + .../snapshot/strategies/selfswap/index.ts | 224 + .../README.md | 12 + .../examples.json | 20 + .../index.ts | 48 + .../single-staking-pools-balanceof/README.md | 14 + .../examples.json | 16 + .../single-staking-pools-balanceof/index.ts | 39 + .../single-staking-vault-balanceof/README.md | 13 + .../examples.json | 20 + .../single-staking-vault-balanceof/index.ts | 39 + .../skale-delegation-weighted/README.md | 16 + .../skale-delegation-weighted/examples.json | 31 + .../skale-delegation-weighted/index.ts | 72 + .../strategies/snet-farmers/README.md | 15 + .../strategies/snet-farmers/examples.json | 21 + .../snapshot/strategies/snet-farmers/index.ts | 56 + .../snet-liquidity-providers/README.md | 14 + .../snet-liquidity-providers/examples.json | 24 + .../snet-liquidity-providers/index.ts | 88 + .../strategies/snet-stakers/README.md | 15 + .../strategies/snet-stakers/examples.json | 28 + .../snapshot/strategies/snet-stakers/index.ts | 67 + .../snapshot/strategies/snowswap/README.md | 12 + .../strategies/snowswap/examples.json | 20 + .../snapshot/strategies/snowswap/index.ts | 51 + .../solv-voucher-claimable/README.md | 14 + .../solv-voucher-claimable/examples.json | 42 + .../solv-voucher-claimable/index.ts | 78 + .../solv-voucher-claimable/schema.json | 28 + .../snapshot/strategies/spaceid/README.md | 1 + .../snapshot/strategies/spaceid/examples.json | 16 + .../snapshot/strategies/spaceid/index.ts | 95 + .../snapshot/strategies/spaceid/schema.json | 13 + .../snapshot/strategies/spacey2025/README.md | 15 + .../strategies/spacey2025/examples.json | 19 + .../snapshot/strategies/spacey2025/index.ts | 82 + .../strategies/spookyswap/examples.json | 34 + .../snapshot/strategies/spookyswap/index.ts | 152 + .../snapshot/strategies/spreadsheet/README.md | 22 + .../strategies/spreadsheet/examples.json | 20 + .../snapshot/strategies/spreadsheet/index.ts | 55 + .../strategies/spreadsheet/schema.json | 30 + .../strategies/squadz-power/README.md | 15 + .../strategies/squadz-power/examples.json | 36 + .../snapshot/strategies/squadz-power/index.ts | 50 + .../snapshot/strategies/squid-dao/README.md | 10 + .../strategies/squid-dao/examples.json | 19 + .../snapshot/strategies/squid-dao/index.ts | 118 + .../snapshot/strategies/squid-dao/schema.json | 25 + .../snapshot/strategies/stablexswap/README.md | 11 + .../strategies/stablexswap/examples.json | 42 + .../snapshot/strategies/stablexswap/index.ts | 131 + .../strategies/staked-balancer/examples.json | 23 + .../strategies/staked-balancer/index.ts | 115 + .../strategies/staked-daomaker/README.md | 15 + .../strategies/staked-daomaker/examples.json | 35 + .../strategies/staked-daomaker/index.ts | 92 + .../ABI/openStakingABI.json | 26 + .../ABI/standardStakingABI.json | 21 + .../strategies/staked-defi-balance/README.md | 40 + .../staked-defi-balance/examples.json | 30 + .../strategies/staked-defi-balance/index.ts | 94 + .../staked-defi-balance/schema.json | 52 + .../strategies/staked-defi-balance/types.ts | 17 + .../snapshot/strategies/staked-keep/README.md | 11 + .../strategies/staked-keep/examples.json | 20 + .../snapshot/strategies/staked-keep/index.ts | 45 + .../strategies/staked-psp-balance/README.md | 21 + .../staked-psp-balance/examples.json | 42 + .../strategies/staked-psp-balance/index.ts | 60 + .../staked-uniswap-modifiable/README.md | 57 + .../staked-uniswap-modifiable/examples.json | 37 + .../staked-uniswap-modifiable/index.ts | 52 + .../strategies/staked-uniswap/examples.json | 18 + .../strategies/staked-uniswap/index.ts | 84 + .../stakedao-governance-update/README.md | 18 + .../stakedao-governance-update/examples.json | 26 + .../stakedao-governance-update/index.ts | 135 + .../strategies/stakedotlink-vesting/README.md | 19 + .../stakedotlink-vesting/examples.json | 21 + .../strategies/stakedotlink-vesting/index.ts | 42 + .../stakedotlink-vesting/schema.json | 33 + .../strategies/stakers-and-holders/README.md | 34 + .../stakers-and-holders/examples.json | 24 + .../strategies/stakers-and-holders/index.ts | 49 + .../staking-claimed-unclaimed/README.md | 39 + .../staking-claimed-unclaimed/examples.json | 22 + .../staking-claimed-unclaimed/index.ts | 69 + .../starcatchers-top-window/README.md | 13 + .../starcatchers-top-window/examples.json | 21 + .../starcatchers-top-window/index.ts | 67 + .../starlay-ve-balance-of-locker-id/README.md | 13 + .../examples.json | 35 + .../starlay-ve-balance-of-locker-id/index.ts | 40 + .../schema.json | 33 + .../snapshot/strategies/starsharks/README.md | 3 + .../strategies/starsharks/examples.json | 18 + .../snapshot/strategies/starsharks/index.ts | 82 + .../strategies/sumami-holders/README.md | 16 + .../strategies/sumami-holders/examples.json | 37 + .../strategies/sumami-holders/index.ts | 70 + .../snapshot/strategies/sunder/examples.json | 20 + .../utils/snapshot/strategies/sunder/index.ts | 37 + .../sunrisegaming-staking/README.md | 15 + .../sunrisegaming-staking/examples.json | 27 + .../strategies/sunrisegaming-staking/index.ts | 49 + .../sunrisegaming-univ2-lp/README.md | 16 + .../sunrisegaming-univ2-lp/examples.json | 27 + .../sunrisegaming-univ2-lp/index.ts | 82 + .../snapshot/strategies/sushiswap/README.md | 17 + .../strategies/sushiswap/examples.json | 20 + .../snapshot/strategies/sushiswap/index.ts | 279 + .../strategies/svs-staking/examples.json | 23 + .../snapshot/strategies/svs-staking/index.ts | 55 + .../utils/snapshot/strategies/swapr/README.md | 13 + .../snapshot/strategies/swapr/commons.ts | 15 + .../snapshot/strategies/swapr/examples.json | 15 + .../utils/snapshot/strategies/swapr/index.ts | 55 + .../snapshot/strategies/swapr/swapr-lps.ts | 211 + .../strategies/sybil-protection/README.md | 22 + .../strategies/sybil-protection/examples.json | 46 + .../strategies/sybil-protection/index.ts | 66 + .../synthetic-nouns-with-claimer/README.md | 14 + .../examples.json | 20 + .../synthetic-nouns-with-claimer/index.ts | 75 + .../synthetix-non-quadratic/README.md | 15 + .../synthetix-non-quadratic/examples.json | 24 + .../synthetix-non-quadratic/index.ts | 184 + .../synthetix-non-quadratic_1/README.md | 15 + .../synthetix-non-quadratic_1/examples.json | 21 + .../synthetix-non-quadratic_1/index.ts | 153 + .../synthetix-non-quadratic_2/README.md | 15 + .../synthetix-non-quadratic_2/examples.json | 27 + .../synthetix-non-quadratic_2/index.ts | 153 + .../strategies/synthetix-quadratic/README.md | 15 + .../synthetix-quadratic/examples.json | 24 + .../strategies/synthetix-quadratic/index.ts | 184 + .../synthetix-quadratic_1/README.md | 15 + .../synthetix-quadratic_1/examples.json | 21 + .../strategies/synthetix-quadratic_1/index.ts | 152 + .../synthetix-quadratic_2/README.md | 15 + .../synthetix-quadratic_2/examples.json | 27 + .../strategies/synthetix-quadratic_2/index.ts | 151 + .../snapshot/strategies/synthetix/README.md | 15 + .../strategies/synthetix/examples.json | 24 + .../snapshot/strategies/synthetix/helper.ts | 99 + .../snapshot/strategies/synthetix/index.ts | 187 + .../snapshot/strategies/synthetix_1/README.md | 15 + .../strategies/synthetix_1/examples.json | 24 + .../snapshot/strategies/synthetix_1/index.ts | 123 + .../tech-quadratic-ranked-choice/README.md | 13 + .../examples.json | 26 + .../tech-quadratic-ranked-choice/index.ts | 48 + .../snapshot/strategies/thales/README.md | 15 + .../snapshot/strategies/thales/examples.json | 26 + .../utils/snapshot/strategies/thales/index.ts | 90 + .../strategies/the-graph-balance/README.md | 11 + .../strategies/the-graph-balance/balances.ts | 188 + .../the-graph-balance/examples.json | 102 + .../strategies/the-graph-balance/index.ts | 24 + .../strategies/the-graph-delegation/README.md | 11 + .../the-graph-delegation/delegators.ts | 119 + .../the-graph-delegation/examples.json | 54 + .../strategies/the-graph-delegation/index.ts | 24 + .../strategies/the-graph-indexing/README.md | 11 + .../the-graph-indexing/examples.json | 54 + .../strategies/the-graph-indexing/index.ts | 24 + .../strategies/the-graph-indexing/indexers.ts | 100 + .../snapshot/strategies/the-graph/README.md | 7 + .../strategies/the-graph/baseStrategy.ts | 138 + .../strategies/the-graph/graphUtils.ts | 103 + .../strategies/the-graph/tokenLockWallets.ts | 66 + .../snapshot/strategies/thresholds/README.md | 28 + .../strategies/thresholds/examples.json | 32 + .../snapshot/strategies/thresholds/index.ts | 37 + .../strategies/ticket-validity/README.md | 22 + .../strategies/ticket-validity/examples.json | 23 + .../strategies/ticket-validity/index.ts | 28 + .../snapshot/strategies/ticket/README.md | 10 + .../snapshot/strategies/ticket/examples.json | 18 + .../utils/snapshot/strategies/ticket/index.ts | 8 + .../snapshot/strategies/ticket/schema.json | 25 + .../strategies/tokenlon/examples.json | 34 + .../snapshot/strategies/tokenlon/index.ts | 344 + .../strategies/tomb-finance/examples.json | 24 + .../snapshot/strategies/tomb-finance/index.ts | 140 + .../snapshot/strategies/tomyumswap/README.md | 9 + .../strategies/tomyumswap/examples.json | 17 + .../snapshot/strategies/tomyumswap/index.ts | 132 + .../total-axion-shares/examples.json | 19 + .../strategies/total-axion-shares/index.ts | 60 + .../strategies/tpro-staking/examples.json | 32 + .../snapshot/strategies/tpro-staking/index.ts | 80 + .../tranche-staking-lp/examples.json | 24 + .../strategies/tranche-staking-lp/index.ts | 106 + .../tranche-staking-slice/examples.json | 21 + .../strategies/tranche-staking-slice/index.ts | 133 + .../strategies/tranche-staking/examples.json | 21 + .../strategies/tranche-staking/index.ts | 62 + .../strategies/tutellus-protocol/README.md | 3 + .../tutellus-protocol/examples.json | 19 + .../strategies/tutellus-protocol/index.ts | 107 + .../strategies/tutellus-protocol/schema.json | 13 + .../snapshot/strategies/typhoon/examples.json | 23 + .../snapshot/strategies/typhoon/index.ts | 96 + .../snapshot/strategies/uma-voting/README.md | 46 + .../strategies/uma-voting/examples.json | 56 + .../snapshot/strategies/uma-voting/index.ts | 64 + .../strategies/umami-voting/README.md | 14 + .../strategies/umami-voting/examples.json | 22 + .../snapshot/strategies/umami-voting/index.ts | 83 + .../snapshot/strategies/uni/examples.json | 20 + .../utils/snapshot/strategies/uni/index.ts | 61 + .../examples.json | 95 + .../unipilot-vault-pilot-balance/index.ts | 87 + .../unipool-same-token/examples.json | 16 + .../strategies/unipool-same-token/index.ts | 67 + .../strategies/unipool-univ2-lp/examples.json | 25 + .../strategies/unipool-univ2-lp/index.ts | 74 + .../strategies/unipool-xsushi/examples.json | 25 + .../strategies/unipool-xsushi/index.ts | 74 + .../strategies/uniswap-v3-staking/README.md | 21 + .../uniswap-v3-staking/examples.json | 20 + .../strategies/uniswap-v3-staking/helper.ts | 136 + .../strategies/uniswap-v3-staking/index.ts | 102 + .../strategies/uniswap-v3/examples.json | 19 + .../snapshot/strategies/uniswap-v3/helper.ts | 76 + .../snapshot/strategies/uniswap-v3/index.ts | 95 + .../snapshot/strategies/uniswap/examples.json | 15 + .../snapshot/strategies/uniswap/index.ts | 80 + .../README.md | 32 + .../examples.json | 18 + .../index.ts | 58 + .../snapshot/strategies/validation/README.md | 32 + .../strategies/validation/examples.json | 45 + .../snapshot/strategies/validation/index.ts | 79 + .../vault-token-lp-balance/README.md | 17 + .../vault-token-lp-balance/examples.json | 24 + .../vault-token-lp-balance/index.ts | 62 + .../strategies/ve-balance-of-at-nft/README.md | 13 + .../ve-balance-of-at-nft/examples.json | 20 + .../strategies/ve-balance-of-at-nft/index.ts | 71 + .../ve-balance-of-at-nft/schema.json | 33 + .../strategies/ve-balance-of-at/README.md | 13 + .../strategies/ve-balance-of-at/examples.json | 35 + .../strategies/ve-balance-of-at/index.ts | 35 + .../strategies/ve-balance-of-at/schema.json | 30 + .../ve-ribbon-voting-power/README.md | 13 + .../ve-ribbon-voting-power/examples.json | 35 + .../ve-ribbon-voting-power/index.ts | 34 + .../ve-ribbon-voting-power/schema.json | 33 + .../snapshot/strategies/ve-ribbon/README.md | 13 + .../strategies/ve-ribbon/examples.json | 29 + .../snapshot/strategies/ve-ribbon/index.ts | 46 + .../snapshot/strategies/ve-ribbon/schema.json | 33 + .../snapshot/strategies/vesper/examples.json | 24 + .../utils/snapshot/strategies/vesper/index.ts | 74 + .../strategies/vested-deversifi/examples.json | 102 + .../strategies/vested-deversifi/index.ts | 65 + .../strategies/volt-voting-power/README.md | 19 + .../volt-voting-power/examples.json | 24 + .../strategies/volt-voting-power/index.ts | 164 + .../strategies/vote-power-and-share/README.md | 11 + .../vote-power-and-share/examples.json | 21 + .../strategies/vote-power-and-share/index.ts | 70 + .../strategies/vsta-pool-staking/README.md | 27 + .../vsta-pool-staking/examples.json | 24 + .../strategies/vsta-pool-staking/index.ts | 55 + .../strategies/vsta-pool-staking/schema.json | 73 + .../strategies/wagdie-subgraph/README.md | 28 + .../strategies/wagdie-subgraph/examples.json | 21 + .../strategies/wagdie-subgraph/index.ts | 93 + .../wanakafarm-land-ingame/examples.json | 15 + .../wanakafarm-land-ingame/index.ts | 51 + .../wanakafarm-staking/examples.json | 15 + .../strategies/wanakafarm-staking/index.ts | 51 + .../strategies/whitelist-weighted/README.md | 3 + .../whitelist-weighted/examples.json | 22 + .../strategies/whitelist-weighted/index.ts | 14 + .../strategies/whitelist/examples.json | 22 + .../snapshot/strategies/whitelist/index.ts | 12 + .../strategies/winr-staking/README.md | 20 + .../strategies/winr-staking/examples.json | 23 + .../snapshot/strategies/winr-staking/index.ts | 88 + .../strategies/winr-staking/schema.json | 22 + .../strategies/with-delegation/README.md | 45 + .../strategies/with-delegation/examples.json | 28 + .../strategies/with-delegation/index.ts | 76 + .../snapshot/strategies/work/examples.json | 39 + .../utils/snapshot/strategies/work/index.ts | 52 + .../snapshot/strategies/xcover/examples.json | 17 + .../utils/snapshot/strategies/xcover/index.ts | 56 + .../xdai-easy-staking/examples.json | 25 + .../strategies/xdai-easy-staking/index.ts | 148 + .../strategies/xdai-easy-staking/utils.ts | 58 + .../xdai-posdao-staking/examples.json | 23 + .../strategies/xdai-posdao-staking/index.ts | 22 + .../xdai-stake-delegation/examples.json | 24 + .../strategies/xdai-stake-delegation/index.ts | 77 + .../xdai-stake-holders/examples.json | 23 + .../strategies/xdai-stake-holders/index.ts | 22 + .../xdai-stakers-and-holders/examples.json | 16 + .../xdai-stakers-and-holders/index.ts | 82 + .../snapshot/strategies/xkawa-farm/README.md | 3 + .../strategies/xkawa-farm/examples.json | 21 + .../snapshot/strategies/xkawa-farm/index.ts | 53 + .../strategies/xrc20-balance-of/examples.json | 20 + .../strategies/xrc20-balance-of/index.ts | 58 + .../examples.json | 37 + .../index.ts | 58 + .../snapshot/strategies/xseen/examples.json | 17 + .../utils/snapshot/strategies/xseen/index.ts | 92 + .../strategies/yearn-vault/examples.json | 20 + .../snapshot/strategies/yearn-vault/index.ts | 60 + .../utils/snapshot/strategies/zorro/README.md | 33 + .../snapshot/strategies/zorro/examples.json | 31 + .../utils/snapshot/strategies/zorro/index.ts | 36 + .../snapshot/strategies/zorro/schema.json | 30 + .../strategies/zrx-voting-power/README.md | 5 + .../strategies/zrx-voting-power/examples.json | 22 + .../strategies/zrx-voting-power/index.ts | 78 + .../README.md | 3 + .../examples.json | 36 + .../index.ts | 154 + .../schema.json | 70 + .../API/backend/utils/snapshot/typings.d.ts | 4 + .../API/backend/utils/snapshot/utils.ts | 97 + .../utils/snapshot/utils/delegation.ts | 95 + .../API/backend/utils/snapshot/utils/vp.ts | 220 + .../snapshot/validations/arbitrum/README.md | 19 + .../validations/arbitrum/examples.json | 26 + .../snapshot/validations/arbitrum/index.ts | 61 + .../snapshot/validations/basic/examples.json | 18 + .../utils/snapshot/validations/basic/index.ts | 35 + .../snapshot/validations/basic/schema.json | 46 + .../utils/snapshot/validations/index.ts | 67 + .../validations/passport-gated/README.md | 29 + .../validations/passport-gated/examples.json | 13 + .../validations/passport-gated/index.ts | 72 + .../validations/passport-gated/schema.json | 271 + .../validations/passport-weighted/README.md | 31 + .../passport-weighted/examples.json | 20 + .../validations/passport-weighted/index.ts | 52 + .../validations/passport-weighted/schema.json | 278 + .../utils/snapshot/validations/validation.ts | 31 + Implementations/API/package-lock.json | 5875 ++++++++++++++++- Implementations/API/package.json | 1 + Implementations/API/stacks/MyStack.ts | 1 + 1313 files changed, 75392 insertions(+), 32 deletions(-) create mode 100644 Implementations/API/backend/functions/snapshot/getDelegations.ts create mode 100644 Implementations/API/backend/utils/snapshot/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-17/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-17/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-wagmi-guild/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi-wagmi-guild/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aavegotchi/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aelin-council/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aelin-council/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aelin-council/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/agave/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/agave/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/agave/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aks/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aks/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aks/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/anti-whale/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/anti-whale/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/anti-whale/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/apescape/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/apescape/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/apeswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/apeswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/apeswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api-post/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api-post/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api-post/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api-v2/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api-v2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api-v2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/api/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/avn-balance-of-staked/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/avn-balance-of-staked/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/babywealthyclub/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/babywealthyclub/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/badgeth/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/badgeth/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-with-linear-vesting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-with-linear-vesting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-with-min/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-with-min/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-smart-pool/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-smart-pool/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-unipool/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer-unipool/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/balancer/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/bancor-pool-token-underlying-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/bancor-pool-token-underlying-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/bancor-standard-rewards-underlying-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/bancor-standard-rewards-underlying-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/banksy-dao/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/banksy-dao/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/banksy-dao/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/biswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/biswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/biswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/botto-dao/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/botto-dao/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/botto-dao/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/brightid/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/brightid/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/brightid/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/bsc-mvb/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/bsc-mvb/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cake/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cake/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cake/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/clqdr-balance-with-lp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/clqdr-balance-with-lp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/coinswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/coinswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/coinswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/colony-reputation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/colony-reputation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/colony-reputation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/comp-like-votes/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/comp-like-votes/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/contract-call/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/contract-call/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/contract-call/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/conv-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/conv-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/coordinape/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/coordinape/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/coordinape/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cream/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cream/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/credit-lp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/credit-lp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cronaswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cronaswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cronaswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctoken/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctoken/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctoken/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v3/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v3/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cyberkongz/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/cyberkongz/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/darkforest-score/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/darkforest-score/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/darkforest-score/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/types.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/defidollar/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/defidollar/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/defiplaza/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/defiplaza/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/defiplaza/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/defiplaza/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dgenesis/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dgenesis/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dgenesis/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers-matic/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers-matic/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-stakers-matic/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-stakers-matic/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dittomoney/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dittomoney/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dittomoney/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dopamine/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dopamine/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dopamine/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dopamine/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/egl-vote/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/egl-vote/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/egl-vote/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-cv/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-cv/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-with-multiplier/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc1155-with-multiplier/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-coeff/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-coeff/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-cv/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-cv/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-fixed-total/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-fixed-total/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-price/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-price/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-price/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-received/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-received/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-received/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-votes/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-votes/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/utils.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/utils.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-enumerable/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-enumerable/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/types.d.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/erc721/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/esd-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/esd-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/esd/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/esd/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/esd/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-received/ElasticSearchTxResult.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-received/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-received/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-received/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethercats-founder-series/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethercats-founder-series/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethercats-founders-series/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethercats-founders-series/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/faraland-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/faraland-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/fight-club/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/fight-club/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/fight-club/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/flashstake/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/flashstake/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/flexa-capacity-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/flexa-capacity-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/forta-shares/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/forta-shares/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/forta-shares/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/frax-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/frax-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/galaxy-nft-with-score/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/galaxy-nft-with-score/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gamium-voting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gamium-voting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gamium-voting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genart/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genart/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genomesdao/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genomesdao/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genomesdao/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genomesdao/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gin-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gin-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/glide/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/glide/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gno/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gno/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/goldfinch-membership/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/goldfinch-membership/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/goldfinch-voting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/goldfinch-voting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/governor-alpha/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/governor-alpha/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/governor-delegator/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/governor-delegator/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/governor-delegator/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/h2o/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/h2o/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/h2o/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/h2o/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/harmony-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/harmony-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/has-rock/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/has-rock/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/has-rock/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashes-voting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashes-voting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hedgey/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/helix/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/helix/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/helix/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/holds-tokens/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/holds-tokens/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/holds-tokens/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/honeyswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/honeyswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/honeyswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-bridged-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-bridged-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-staking-by-season/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-staking-by-season/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-staking-s2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-staking-s2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-uni-lp-farm/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/hopr-uni-lp-farm/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ilv/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ilv/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/immutable-x/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/immutable-x/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/impossible-finance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/impossible-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/impossible-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/iotex-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/iotex-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/iotex-staked-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/iotex-staked-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/jade-smrt/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/jade-smrt/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/jade-smrt/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/juicebox/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/juicebox/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/l2-deversifi/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/l2-deversifi/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/liftkitchen/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/liftkitchen/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/linear-vesting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/linear-vesting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/loot-character-guilds/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/loot-character-guilds/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/maker-ds-chief/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/maker-ds-chief/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/marsecosystem/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/marsecosystem/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/marsecosystem/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-no-rewarddebt/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-no-rewarddebt/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/masterchef/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/math/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/math/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/math/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/math/options.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mcn-farm/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mcn-farm/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mcn-farm/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/meebitsdao/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/meebitsdao/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/meebitsdao/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/membership/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/membership/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/membership/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/minotaur-money/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/minotaur-money/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mithcash/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mithcash/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/modefi-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/modefi-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/modefi/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/modefi/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moloch-all/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moloch-all/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moloch-loot/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moloch-loot/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moloch/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moloch/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mstable/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mstable/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mstable/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/multichain-serie/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/multichain-serie/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/multichain/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/multichain/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/multichain/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/multisig-owners/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/multisig-owners/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/multisig-owners/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mushrooms/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mushrooms/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/nexon-army-nft/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/nexon-army-nft/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/niu-staked/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/niu-staked/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/nouns-rfp-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/nouns-rfp-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/oceanUtils.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/oceanUtils.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ogn/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ogn/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ogn/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/oolongswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/oolongswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/opium/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/opium/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orca-pod/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orca-pod/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orca-pod/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/orca-pod/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pagination/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pagination/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pancake/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pancake/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pancake/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pepemon/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pepemon/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/piedao/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/piedao/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/planet-finance-v2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/planet-finance-v2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/planet-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/planet-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/poap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pob-hash/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pob-hash/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pod-leader/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pod-leader/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/pod-leader/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/polis-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/polis-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/posichain-staking/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/posichain-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/posichain-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/position-governance-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/position-governance-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/position-governance-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/position-governance-power/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/potion/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/potion/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/potion/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/printer-financial/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/printer-financial/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/printer-financial/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/push-voting-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/push-voting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/push-voting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rari-fuse/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rari-fuse/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rari-fuse/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rari-fuse/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/razor-network-voting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/razor-network-voting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rebased/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rebased/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/recusal-list/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/recusal-list/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/recusal-list/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/reliquary/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/reliquary/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/reliquary/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ren-nodes/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ren-nodes/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rep3-badges/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rep3-badges/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rep3-badges/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/revest/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/revest/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rnbw-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rnbw-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ruler-staked-lp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ruler-staked-lp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/vestingContractAddrs.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saddle-finance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saddle-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saddle-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saddle-finance/vestingContractAddrs.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/safe-vested/README.md create mode 100755 Implementations/API/backend/utils/snapshot/strategies/safe-vested/examples.json create mode 100755 Implementations/API/backend/utils/snapshot/strategies/safe-vested/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saffron-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/saffron-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sandman-dao/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sandman-dao/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sandman-dao/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-boost/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-boost/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-boost/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/selfswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/selfswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/selfswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-farmers/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-farmers/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-farmers/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-stakers/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-stakers/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snet-stakers/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snowswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snowswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/snowswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spaceid/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spaceid/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spaceid/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spaceid/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spacey2025/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spacey2025/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spacey2025/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spookyswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spookyswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spreadsheet/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spreadsheet/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spreadsheet/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/spreadsheet/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/squadz-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/squadz-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/squadz-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/squid-dao/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/squid-dao/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/squid-dao/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/squid-dao/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stablexswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stablexswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stablexswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-balancer/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-balancer/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/ABI/openStakingABI.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/ABI/standardStakingABI.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/types.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-keep/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-keep/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-keep/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-uniswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staked-uniswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starsharks/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starsharks/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/starsharks/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sumami-holders/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sumami-holders/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sumami-holders/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sunder/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sunder/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sushiswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sushiswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sushiswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/svs-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/svs-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/swapr/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/swapr/commons.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/swapr/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/swapr/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/swapr/swapr-lps.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sybil-protection/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sybil-protection/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/sybil-protection/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix/helper.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix_1/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix_1/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/synthetix_1/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/thales/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/thales/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/thales/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/balances.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/delegators.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/indexers.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph/baseStrategy.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph/graphUtils.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/the-graph/tokenLockWallets.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/thresholds/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/thresholds/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/thresholds/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ticket-validity/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ticket-validity/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ticket-validity/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ticket/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ticket/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ticket/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ticket/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tokenlon/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tokenlon/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tomb-finance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tomb-finance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tomyumswap/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tomyumswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tomyumswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/total-axion-shares/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/total-axion-shares/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tpro-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tpro-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tranche-staking-lp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tranche-staking-lp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tranche-staking-slice/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tranche-staking-slice/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tranche-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tranche-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/typhoon/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/typhoon/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uma-voting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uma-voting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uma-voting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/umami-voting/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/umami-voting/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/umami-voting/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uni/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uni/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unipilot-vault-pilot-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unipilot-vault-pilot-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unipool-same-token/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unipool-same-token/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unipool-univ2-lp/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unipool-univ2-lp/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unipool-xsushi/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unipool-xsushi/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/helper.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/helper.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/uniswap/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/validation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/validation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/validation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vesper/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vesper/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vested-deversifi/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vested-deversifi/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/wanakafarm-land-ingame/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/wanakafarm-land-ingame/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/wanakafarm-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/wanakafarm-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/whitelist/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/whitelist/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/winr-staking/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/winr-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/winr-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/winr-staking/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/with-delegation/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/with-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/with-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/work/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/work/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xcover/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xcover/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/utils.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-posdao-staking/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-posdao-staking/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-stake-delegation/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-stake-delegation/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-stake-holders/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-stake-holders/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-stakers-and-holders/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xdai-stakers-and-holders/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xrc20-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xrc20-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xrook-balance-of-underlying-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xrook-balance-of-underlying-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xseen/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/xseen/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/yearn-vault/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/yearn-vault/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zorro/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zorro/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zorro/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zorro/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/README.md create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/typings.d.ts create mode 100644 Implementations/API/backend/utils/snapshot/utils.ts create mode 100644 Implementations/API/backend/utils/snapshot/utils/delegation.ts create mode 100644 Implementations/API/backend/utils/snapshot/utils/vp.ts create mode 100644 Implementations/API/backend/utils/snapshot/validations/arbitrum/README.md create mode 100644 Implementations/API/backend/utils/snapshot/validations/arbitrum/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/validations/arbitrum/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/validations/basic/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/validations/basic/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/validations/basic/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/validations/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/validations/passport-gated/README.md create mode 100644 Implementations/API/backend/utils/snapshot/validations/passport-gated/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/validations/passport-gated/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/validations/passport-gated/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/validations/passport-weighted/README.md create mode 100644 Implementations/API/backend/utils/snapshot/validations/passport-weighted/examples.json create mode 100644 Implementations/API/backend/utils/snapshot/validations/passport-weighted/index.ts create mode 100644 Implementations/API/backend/utils/snapshot/validations/passport-weighted/schema.json create mode 100644 Implementations/API/backend/utils/snapshot/validations/validation.ts diff --git a/Implementations/API/backend/functions/config.ts b/Implementations/API/backend/functions/config.ts index 89daa242..f7e4d3be 100644 --- a/Implementations/API/backend/functions/config.ts +++ b/Implementations/API/backend/functions/config.ts @@ -28,4 +28,15 @@ export const snapshotApiConfig: { [key: string]: any } = { export const nonusApiConfig: { [key: string]: any } = { '1': 'https://api.thegraph.com/subgraphs/name/nounsdao/nouns-subgraph' -} \ No newline at end of file +} + +export const delegationsSubgraphs: { [key: string]: string } = { + "1": "https://subgrapher.snapshot.org/gateway.thegraph.com/api/0f15b42bdeff7a063a4e1757d7e2f99e/deployments/id/QmXZiV6S13ha6QXq4dmaM3TB4CHcDxBMvGexSNu9Kc28EH", + "5": "https://subgrapher.snapshot.org/api.thegraph.com/subgraphs/name/snapshot-labs/snapshot-goerli", + "10": "https://subgrapher.snapshot.org/api.thegraph.com/subgraphs/name/snapshot-labs/snapshot-optimism", + "56": "https://subgrapher.snapshot.org/api.thegraph.com/subgraphs/name/snapshot-labs/snapshot-binance-smart-chain", + "100": "https://subgrapher.snapshot.org/api.thegraph.com/subgraphs/name/snapshot-labs/snapshot-gnosis-chain", + "137": "https://subgrapher.snapshot.org/api.thegraph.com/subgraphs/name/snapshot-labs/snapshot-polygon", + "250": "https://subgrapher.snapshot.org/api.thegraph.com/subgraphs/name/snapshot-labs/snapshot-fantom", + "42161": "https://subgrapher.snapshot.org/api.thegraph.com/subgraphs/name/snapshot-labs/snapshot-arbitrum" +} diff --git a/Implementations/API/backend/functions/snapshot/getDelegations.ts b/Implementations/API/backend/functions/snapshot/getDelegations.ts new file mode 100644 index 00000000..015aa092 --- /dev/null +++ b/Implementations/API/backend/functions/snapshot/getDelegations.ts @@ -0,0 +1,91 @@ +import { APIGatewayProxyHandlerV2 } from 'aws-lambda' +import { delegationsSubgraphs } from 'functions/config' +import snapshot from '@snapshot-labs/snapshot.js'; +import { getAddress } from '@ethersproject/address'; + + +const { + subgraphRequest, +} = snapshot.utils; + +async function getDelegationsOutAndIn( + network: string, + space: string +): Promise { + if (!delegationsSubgraphs[network]) return []; + + const max = 1000; + let result: any = []; + let page = 0; + + const members: string[] = [] + + const query = { + delegations: { + __args: { + first: max, + skip: 0, + where: { + space_in: [space], + } + }, + delegator: true, + delegate: true, + space: true + } + }; + while (true) { + query.delegations.__args.skip = page * max; + const pageResult = await subgraphRequest(delegationsSubgraphs[network], query); + const pageDelegations = pageResult.delegations || []; + result = result.concat(pageDelegations); + page++; + if (pageDelegations.length < max) break; + } + + result.forEach((delegation: any) => { + const delegator = getAddress(delegation.delegator); + const delegate = getAddress(delegation.delegate); + if (delegation.space === space) { + members.push(delegator) + members.push(delegate) + + } + }); + + return members +} + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + + const space = event?.pathParameters?.id + if (!space) return { statusCode: 400, message: 'Missing id' } + + const template = { + '@context': { + '@vocab': 'http://daostar.org/', + }, + type: 'DAO', + name: space, + } + + const members = await getDelegationsOutAndIn('1', space) + + console.log({ members }) + + const membersFormatted = members.map((m: string) => { + return { id: m, type: 'EthereumAddress' } + }) + + const transformed = { members: membersFormatted, ...template } + + return transformed + ? { + statusCode: 200, + body: JSON.stringify(transformed), + } + : { + statusCode: 404, + body: JSON.stringify({ error: true }), + } +} diff --git a/Implementations/API/backend/utils/snapshot/index.ts b/Implementations/API/backend/utils/snapshot/index.ts new file mode 100644 index 00000000..e1e0f7e6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/index.ts @@ -0,0 +1,10 @@ +import 'dotenv/config'; +import strategies from './strategies'; +import validations from './validations'; +import utils from './utils'; + +export default { + strategies, + validations, + utils +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/README.md new file mode 100644 index 00000000..f091e23b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/README.md @@ -0,0 +1,11 @@ +# Contract call strategy + +Allows to get Voting power or Proposition power from an Aave GovernanceStrategy contract. + +## Strategy Parameters + +| Param | Type | Description | | | +| ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------- | --- | --- | +| governanceStrategy | string | The Ethereum address of the GovernanceStrategy contract to measure voting or proposition power from an address at a block. | | | +| powerType | string | Use `vote` for Voting Power or `proposition` for Proposition Power | | | +| | | | | | diff --git a/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/examples.json new file mode 100644 index 00000000..76b6e2b5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example of Aave Governance Power strategy", + "strategy": { + "name": "aave-governance-power", + "params": { + "governanceStrategy": "0xb7e383ef9b1e9189fc0f71fb30af8aa14377429e", + "powerType": "vote", + "symbol": "Voting Power", + "decimals": 18 + } + }, + "network": "1", + "addresses": ["0x5BC928BF0DAb1e4A2ddd9e347b0F22e88026D76c"], + "snapshot": 12657715 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/index.ts new file mode 100644 index 00000000..0e0542f9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aave-governance-power/index.ts @@ -0,0 +1,75 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'kartojal'; +export const version = '0.1.0'; + +/** + * Aave Governance strategy to measure voting or + */ + +const abi = [ + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'uint256', name: 'blockNumber', type: 'uint256' } + ], + name: 'getPropositionPowerAt', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'uint256', name: 'blockNumber', type: 'uint256' } + ], + name: 'getVotingPowerAt', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +]; + +const powerTypesToMethod = { + vote: 'getVotingPowerAt', + proposition: 'getPropositionPowerAt' +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = + typeof snapshot === 'number' + ? snapshot + : await provider.getBlockNumber(snapshot); + + // Early return 0 voting power if governanceStrategy or powerType is not correctly set + if (!options.governanceStrategy || !powerTypesToMethod[options.powerType]) { + return Object.fromEntries(addresses.map((address) => [address, '0'])); + } + + const response: BigNumber[] = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.governanceStrategy, + powerTypesToMethod[options.powerType], + [address.toLowerCase(), blockTag] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-17/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-17/examples.json new file mode 100644 index 00000000..360eac9d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-17/examples.json @@ -0,0 +1,14 @@ +[ + { + "name": "AGIP 17: Voting power for GHST value of parcels", + "strategy": { + "name": "aavegotchi-agip-17", + "params": { + "symbol": "REALM" + } + }, + "network": "137", + "addresses": ["0x027Ffd3c119567e85998f4E6B9c3d83D5702660c"], + "snapshot": 22089223 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-17/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-17/index.ts new file mode 100644 index 00000000..cccb7bda --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-17/index.ts @@ -0,0 +1,104 @@ +import { subgraphRequest } from '../../utils'; + +export const author = 'candoizo'; +export const version = '0.1.2'; + +const AAVEGOTCHI_SUBGRAPH_URL = { + 137: 'https://subgraph.satsuma-prod.com/tWYl5n5y04oz/aavegotchi/aavegotchi-core-matic/api' +}; + +const maxResponsePerQuery = 1000; + +// agip 17: Voting power of 0.5 GHST/pixel +const realmSizeVotePower = { + 0: 32, // humble + 1: 128, // reasonable + 2: 1028, // spacious + 3: 1028, // spacious + 4: 2048 // partner +}; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const args: { + where: { id_in: string[] }; + first: number; + block?: { number: number }; + } = { + where: { + id_in: addresses.map((addr: string) => addr.toLowerCase()) + }, + first: addresses.length + }; + if (blockTag !== 'latest') args.block = { number: blockTag }; + + const batchQuery = (i) => { + return { + ['parcelsOwned_' + i]: { + __aliasFor: 'parcelsOwned', + __args: { + first: maxResponsePerQuery, + skip: maxResponsePerQuery * i + }, + size: true + } + }; + }; + let parcelsOwnedQueryParams = { + users: { + __args: args, + id: true + } + }; + for (let i = 0; i < 6; i++) { + parcelsOwnedQueryParams = { + ...parcelsOwnedQueryParams, + users: { + ...parcelsOwnedQueryParams.users, + ...batchQuery(i) + } + }; + } + + const result = await subgraphRequest( + AAVEGOTCHI_SUBGRAPH_URL[network], + parcelsOwnedQueryParams + ); + + const userToInfo = Object.fromEntries( + result.users.map((user) => { + return [user.id, user]; + }) + ); + + return Object.fromEntries( + addresses.map((addr: string) => { + let realmVotingPowerValue = 0; + const res = userToInfo[addr.toLowerCase()]; + if (res) { + const parcelsOwned = Object.entries(res) + .map(([key, val]) => { + if (key.startsWith('parcelsOwned')) return val; + else return []; + }) + .flat(1) as unknown as { size: number }[]; + if (parcelsOwned.length > 0) { + parcelsOwned.map((r: { size: number }) => { + let votePower = realmSizeVotePower[r.size]; + if (isNaN(votePower)) votePower = 0; + realmVotingPowerValue += votePower; + }); + } + } + return [addr, realmVotingPowerValue]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/README.md b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/README.md new file mode 100644 index 00000000..81369b71 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/README.md @@ -0,0 +1,22 @@ +# Aavegotchi AGIP 37 GLTR Staked LP Strategy + +## Description + +This snapshot strategy enables voting power for the following assets staked in GLTR staking pools + +- GHST-FUD LP +- GHST-FOMO LP +- GHST-ALPHA LP +- GHST-KEK LP +- GHST-GLTR LP +- GHST-USDC LP +- GHST-WMATIC LP + +Please note this excludes voting power from: +- Staked wapGHST and unstaked wapGHST held in a wallet (see aavegotchi-agip-37-wap-ghst) +- amGHST (see erc20-balance-of) +- Unstaked GHST-FUD, GHST-FOMO, GHST-ALPHA, GHST-KEK, GHST-GLTR LP tokens (see erc20-tokens-per-uni) + +## References + +Aavegotchi AGIP 37: https://snapshot.org/#/aavegotchi.eth/proposal/0x9923aab6825158ec2503d88e3ee2f9c5fbb12000581d06343ac9829aa59b66a6 \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/examples.json new file mode 100644 index 00000000..8b8669b6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/examples.json @@ -0,0 +1,38 @@ +[ + { + "name": "AGIP 37: Voting Power for GHST-FUD, GHST-FOMO, GHST-ALPHA, GHST-KEK, GHST-GLTR, GHST-USDC, and GHST-WMATIC staked in the GLTR farming contract. Note: Excludes wapGHST.", + "strategy": { + "name": "aavegotchi-agip-37-gltr-staked-lp", + "params": { + "ghstAddress": "0x385Eeac5cB85A38A9a07A70c73e0a3271CfB54A7", + "gltrStakingAddress": "0x1fE64677Ab1397e20A1211AFae2758570fEa1B8c", + "amGhstAddress": "0x080b5BF8f360F624628E0fb961F4e67c9e3c7CF1", + "wapGhstAddress": "0x73958d46B7aA2bc94926d8a215Fa560A5CdCA3eA", + "wapGhstPoolId": 0, + "ghstFudAddress": "0xfec232cc6f0f3aeb2f81b2787a9bc9f6fc72ea5c", + "ghstFudPoolId": 1, + "ghstFomoAddress": "0x641ca8d96b01db1e14a5fba16bc1e5e508a45f2b", + "ghstFomoPoolId": 2, + "ghstAlphaAddress": "0xc765eca0ad3fd27779d36d18e32552bd7e26fd7b", + "ghstAlphaPoolId": 3, + "ghstKekAddress": "0xbfad162775ebfb9988db3f24ef28ca6bc2fb92f0", + "ghstKekPoolId": 4, + "ghstUsdcAddress": "0x096c5ccb33cfc5732bcd1f3195c13dbefc4c82f4", + "ghstUsdcPoolId": 5, + "ghstWmaticAddress": "0xf69e93771F11AECd8E554aA165C3Fe7fd811530c", + "ghstWmaticPoolId": 6, + "ghstGltrAddress": "0xb0E35478a389dD20050D66a67FB761678af99678", + "ghstGltrPoolId": 7, + "symbol": "GHST", + "decimals": 18 + } + }, + "network": "137", + "addresses": [ + "0x26cf02F892B04aF4Cf350539CE2C77FCF79Ec172", + "0x027Ffd3c119567e85998f4E6B9c3d83D5702660c", + "0xc4cb6cb969e8b4e309ab98e4da51b77887afad96" + ], + "snapshot": 33423178 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/index.ts new file mode 100644 index 00000000..24d60c73 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-gltr-staked-lp/index.ts @@ -0,0 +1,211 @@ +import { multicall } from '../../utils'; + +export const author = 'programmablewealth'; +export const version = '0.0.1'; + +const tokenAbi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function totalSupply() view returns (uint256)', + 'function allUserInfo(address _user) view returns (tuple(address lpToken, uint256 allocPoint, uint256 pending, uint256 userBalance, uint256 poolBalance)[] _info)', + 'function convertToAssets(uint256 shares) view returns (uint)' +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + options.ghstAddress = + options.ghstAddress || '0x385Eeac5cB85A38A9a07A70c73e0a3271CfB54A7'; + + options.gltrStakingAddress = + options.gltrStakingAddress || '0x1fE64677Ab1397e20A1211AFae2758570fEa1B8c'; + + options.amGhstAddress = + options.amGhstAddress || '0x080b5BF8f360F624628E0fb961F4e67c9e3c7CF1'; + + options.wapGhstAddress = + options.wapGhstAddress || '0x73958d46B7aA2bc94926d8a215Fa560A5CdCA3eA'; + options.wapGhstPoolId = options.wapGhstPoolId || 0; + + options.ghstFudAddress = + options.ghstFudAddress || '0xfec232cc6f0f3aeb2f81b2787a9bc9f6fc72ea5c'; + options.ghstFudPoolId = options.ghstFudPoolId || 1; + + options.ghstFomoAddress = + options.ghstFomoAddress || '0x641ca8d96b01db1e14a5fba16bc1e5e508a45f2b'; + options.ghstFomoPoolId = options.ghstFomoPoolId || 2; + + options.ghstAlphaAddress = + options.ghstAlphaAddress || '0xc765eca0ad3fd27779d36d18e32552bd7e26fd7b'; + options.ghstAlphaPoolId = options.ghstAlphaPoolId || 3; + + options.ghstKekAddress = + options.ghstKekAddress || '0xbfad162775ebfb9988db3f24ef28ca6bc2fb92f0'; + options.ghstKekPoolId = options.ghstKekPoolId || 4; + + options.ghstUsdcAddress = + options.ghstUsdcAddress || '0x096c5ccb33cfc5732bcd1f3195c13dbefc4c82f4'; + options.ghstUsdcPoolId = options.ghstUsdcPoolId || 5; + + options.ghstWmaticAddress = + options.ghstWmaticAddress || '0xf69e93771F11AECd8E554aA165C3Fe7fd811530c'; + options.ghstWmaticPoolId = options.ghstWmaticPoolId || 6; + + options.ghstGltrAddress = + options.ghstGltrAddress || '0xb0E35478a389dD20050D66a67FB761678af99678'; + options.ghstGltrPoolId = options.ghstGltrPoolId || 7; + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakeQuery = addresses.map((address: string) => [ + options.gltrStakingAddress, + 'allUserInfo', + [address] + ]); + + let slicedStakedQueries: any = [stakeQuery]; + if (stakeQuery.length > 1) { + const middle = stakeQuery.length / 2; + slicedStakedQueries = [ + stakeQuery.slice(0, middle), + stakeQuery.slice(middle, stakeQuery.length) + ]; + } + + let res = await multicall( + network, + provider, + tokenAbi, + [ + [options.ghstFudAddress, 'totalSupply', []], + [options.ghstAddress, 'balanceOf', [options.ghstFudAddress]], + [options.ghstFomoAddress, 'totalSupply', []], + [options.ghstAddress, 'balanceOf', [options.ghstFomoAddress]], + [options.ghstAlphaAddress, 'totalSupply', []], + [options.ghstAddress, 'balanceOf', [options.ghstAlphaAddress]], + [options.ghstKekAddress, 'totalSupply', []], + [options.ghstAddress, 'balanceOf', [options.ghstKekAddress]], + [options.ghstGltrAddress, 'totalSupply', []], + [options.ghstAddress, 'balanceOf', [options.ghstGltrAddress]], + [options.ghstUsdcAddress, 'totalSupply', []], + [options.ghstAddress, 'balanceOf', [options.ghstUsdcAddress]], + [options.ghstWmaticAddress, 'totalSupply', []], + [options.ghstAddress, 'balanceOf', [options.ghstWmaticAddress]], + ...slicedStakedQueries[0] + ], + { blockTag } + ); + + if (slicedStakedQueries.length > 1) { + const res2 = await multicall( + network, + provider, + tokenAbi, + [...slicedStakedQueries[1]], + { blockTag } + ); + + res = [...res, ...res2]; + } + + const tokensPerUni = (balanceInUni: number, totalSupply: number) => { + return balanceInUni / 1e18 / (totalSupply / 1e18); + }; + + const lpTokensStartIndex = 0; + const lpTokensPerUni = { + ghstFudLp: tokensPerUni( + res[lpTokensStartIndex + 1], + res[lpTokensStartIndex] + ), + ghstFomoLp: tokensPerUni( + res[lpTokensStartIndex + 3], + res[lpTokensStartIndex + 2] + ), + ghstAlphaLp: tokensPerUni( + res[lpTokensStartIndex + 5], + res[lpTokensStartIndex + 4] + ), + ghstKekLp: tokensPerUni( + res[lpTokensStartIndex + 7], + res[lpTokensStartIndex + 6] + ), + ghstGltrLp: tokensPerUni( + res[lpTokensStartIndex + 9], + res[lpTokensStartIndex + 8] + ), + ghstUsdcLp: tokensPerUni( + res[lpTokensStartIndex + 11], + res[lpTokensStartIndex + 10] + ), + ghstWmaticLp: tokensPerUni( + res[lpTokensStartIndex + 13], + res[lpTokensStartIndex + 12] + ) + }; + + const entries = {}; + for (let addressIndex = 0; addressIndex < addresses.length; addressIndex++) { + const i = addressIndex + 14; + const tokens = { + staked: { + ghstFudLp: + Number(res[i]._info[options.ghstFudPoolId].userBalance.toString()) / + 1e18, + ghstFomoLp: + Number(res[i]._info[options.ghstFomoPoolId].userBalance.toString()) / + 1e18, + ghstAlphaLp: + Number(res[i]._info[options.ghstAlphaPoolId].userBalance.toString()) / + 1e18, + ghstKekLp: + Number(res[i]._info[options.ghstKekPoolId].userBalance.toString()) / + 1e18, + ghstGltrLp: + Number(res[i]._info[options.ghstGltrPoolId].userBalance.toString()) / + 1e18, + ghstUsdcLp: + Number(res[i]._info[options.ghstUsdcPoolId].userBalance.toString()) / + 1e18, + ghstWmaticLp: + Number( + res[i]._info[options.ghstWmaticPoolId].userBalance.toString() + ) / 1e18 + } + }; + + const votingPower = { + staked: { + ghstFudLp: tokens.staked.ghstFudLp * lpTokensPerUni.ghstFudLp, + ghstFomoLp: tokens.staked.ghstFomoLp * lpTokensPerUni.ghstFomoLp, + ghstAlphaLp: tokens.staked.ghstAlphaLp * lpTokensPerUni.ghstAlphaLp, + ghstKekLp: tokens.staked.ghstKekLp * lpTokensPerUni.ghstKekLp, + ghstGltrLp: tokens.staked.ghstGltrLp * lpTokensPerUni.ghstGltrLp, + ghstUsdcLp: tokens.staked.ghstUsdcLp * lpTokensPerUni.ghstUsdcLp, + ghstWmaticLp: tokens.staked.ghstWmaticLp * lpTokensPerUni.ghstWmaticLp + } + }; + + let totalVotingPower = 0; + for (let k = 0; k < Object.keys(votingPower.staked).length; k++) { + const key = Object.keys(votingPower.staked)[k]; + totalVotingPower += votingPower.staked[key]; + } + + const address = addresses[addressIndex]; + + // let loggedString = "TOKENS SUMMARY FOR " + address; + // loggedString += "\nSTAKED TOKENS\n" + JSON.stringify(tokens.staked); + // loggedString += "\nSTAKED VOTING POWER\n" + JSON.stringify(votingPower.staked); + // loggedString += "\nTOTAL VOTING POWER\n" + totalVotingPower; + // console.log(loggedString); + + entries[address] = totalVotingPower; + } + + return entries; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/README.md b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/README.md new file mode 100644 index 00000000..45c8a29a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/README.md @@ -0,0 +1,9 @@ +# Aavegotchi AGIP 37 Strategy WAP GHST + +## Description + +This snapshot strategy enables voting power for staked and unstaked wapGHST. + +## References + +Aavegotchi AGIP 37: https://snapshot.org/#/aavegotchi.eth/proposal/0x9923aab6825158ec2503d88e3ee2f9c5fbb12000581d06343ac9829aa59b66a6 \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/examples.json new file mode 100644 index 00000000..b26817fd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/examples.json @@ -0,0 +1,39 @@ +[ + { + "name": "AGIP-37: This snapshot strategy enables voting power for staked and unstaked wapGHST", + "strategy": { + "name": "aavegotchi-agip-37-wap-ghst", + "params": { + "ghstAddress": "0x385Eeac5cB85A38A9a07A70c73e0a3271CfB54A7", + "gltrStakingAddress": "0x1fE64677Ab1397e20A1211AFae2758570fEa1B8c", + "amGhstAddress": "0x080b5BF8f360F624628E0fb961F4e67c9e3c7CF1", + "wapGhstAddress": "0x73958d46B7aA2bc94926d8a215Fa560A5CdCA3eA", + "wapGhstPoolId": 0, + "ghstFudAddress": "0xfec232cc6f0f3aeb2f81b2787a9bc9f6fc72ea5c", + "ghstFudPoolId": 1, + "ghstFomoAddress": "0x641ca8d96b01db1e14a5fba16bc1e5e508a45f2b", + "ghstFomoPoolId": 2, + "ghstAlphaAddress": "0xc765eca0ad3fd27779d36d18e32552bd7e26fd7b", + "ghstAlphaPoolId": 3, + "ghstKekAddress": "0xbfad162775ebfb9988db3f24ef28ca6bc2fb92f0", + "ghstKekPoolId": 4, + "ghstUsdcAddress": "0x096c5ccb33cfc5732bcd1f3195c13dbefc4c82f4", + "ghstUsdcPoolId": 5, + "ghstWmaticAddress": "0xf69e93771F11AECd8E554aA165C3Fe7fd811530c", + "ghstWmaticPoolId": 6, + "ghstGltrAddress": "0xb0E35478a389dD20050D66a67FB761678af99678", + "ghstGltrPoolId": 7, + "symbol": "GHST", + "decimals": 18 + } + }, + "network": "137", + "addresses": [ + "0x26cf02F892B04aF4Cf350539CE2C77FCF79Ec172", + "0x027Ffd3c119567e85998f4E6B9c3d83D5702660c", + "0xDEA88c9FE09106b58cA7c026c82383c56eE1E041", + "0x3a564B24EffA1Cb7Dc836AD094BaD28e69FCa371" + ], + "snapshot": 34185374 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/index.ts new file mode 100644 index 00000000..accd3321 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip-37-wap-ghst/index.ts @@ -0,0 +1,165 @@ +import { multicall } from '../../utils'; + +export const author = 'programmablewealth'; +export const version = '0.0.1'; + +const tokenAbi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function totalSupply() view returns (uint256)', + 'function allUserInfo(address _user) view returns (tuple(address lpToken, uint256 allocPoint, uint256 pending, uint256 userBalance, uint256 poolBalance)[] _info)', + 'function convertToAssets(uint256 shares) view returns (uint)' +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + options.ghstAddress = + options.ghstAddress || '0x385Eeac5cB85A38A9a07A70c73e0a3271CfB54A7'; + + options.gltrStakingAddress = + options.gltrStakingAddress || '0x1fE64677Ab1397e20A1211AFae2758570fEa1B8c'; + + options.amGhstAddress = + options.amGhstAddress || '0x080b5BF8f360F624628E0fb961F4e67c9e3c7CF1'; + + options.wapGhstAddress = + options.wapGhstAddress || '0x73958d46B7aA2bc94926d8a215Fa560A5CdCA3eA'; + options.wapGhstPoolId = options.wapGhstPoolId || 0; + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const walletQuery = addresses.map((address: string) => [ + options.wapGhstAddress, + 'balanceOf', + [address] + ]); + + const stakeQuery = addresses.map((address: string) => [ + options.gltrStakingAddress, + 'allUserInfo', + [address] + ]); + + let slicedWalletQueries: any = [walletQuery]; + if (walletQuery.length > 1) { + const middle = walletQuery.length / 2; + slicedWalletQueries = [ + walletQuery.slice(0, middle), + walletQuery.slice(middle, walletQuery.length) + ]; + } + + let res = await multicall( + network, + provider, + tokenAbi, + [...slicedWalletQueries[0]], + { blockTag } + ); + + if (slicedWalletQueries.length > 1) { + const res2 = await multicall( + network, + provider, + tokenAbi, + [...slicedWalletQueries[1]], + { blockTag } + ); + + res = [...res, ...res2]; + } + + let slicedStakeQueries: any = [stakeQuery]; + if (stakeQuery.length > 1) { + const middle = stakeQuery.length / 2; + slicedStakeQueries = [ + stakeQuery.slice(0, middle), + stakeQuery.slice(middle, stakeQuery.length) + ]; + } + + const res3 = await multicall( + network, + provider, + tokenAbi, + [...slicedStakeQueries[0]], + { blockTag } + ); + res = [...res, ...res3]; + + if (slicedStakeQueries.length > 1) { + const res4 = await multicall( + network, + provider, + tokenAbi, + [...slicedStakeQueries[1]], + { blockTag } + ); + + res = [...res, ...res4]; + } + + const unitWapGHST_res = await multicall( + network, + provider, + tokenAbi, + [[options.wapGhstAddress, 'convertToAssets', ['1000000000000000000']]], + { blockTag } + ); + const wapGHST_ghstMulitiplier = Number(unitWapGHST_res[0].toString()) / 1e18; + + const entries = {}; + for (let addressIndex = 0; addressIndex < addresses.length; addressIndex++) { + const tokens = { + staked: { + wapGhst: + Number( + res[addressIndex + addresses.length]._info[ + options.wapGhstPoolId + ].userBalance.toString() + ) / 1e18 + }, + unstaked: { + wapGhst: Number(res[addressIndex].toString()) / 1e18 + } + }; + + const votingPower = { + staked: { + wapGhst: tokens.staked.wapGhst * wapGHST_ghstMulitiplier + }, + unstaked: { + wapGhst: tokens.unstaked.wapGhst * wapGHST_ghstMulitiplier + } + }; + + let totalVotingPower = 0; + for (let k = 0; k < Object.keys(votingPower.unstaked).length; k++) { + const key = Object.keys(votingPower.unstaked)[k]; + totalVotingPower += votingPower.unstaked[key]; + } + for (let k = 0; k < Object.keys(votingPower.staked).length; k++) { + const key = Object.keys(votingPower.staked)[k]; + totalVotingPower += votingPower.staked[key]; + } + + const address = addresses[addressIndex]; + + // let loggedString = "TOKENS SUMMARY FOR " + address; + // loggedString += "\nSTAKED TOKENS\n" + JSON.stringify(tokens.staked); + // loggedString += "\nUNSTAKED TOKENS\n" + JSON.stringify(tokens.unstaked); + // loggedString += "\nSTAKED VOTING POWER\n" + JSON.stringify(votingPower.staked); + // loggedString += "\nUNSTAKED VOTING POWER\n" + JSON.stringify(votingPower.unstaked); + // loggedString += "\nTOTAL VOTING POWER\n" + totalVotingPower; + // console.log(loggedString); + + entries[address] = totalVotingPower; + } + + return entries; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip/examples.json new file mode 100644 index 00000000..72a671a7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "AGIP 8+9: Voting power for GHST value of items, their aavegotchis equippedWearables + baseRarityScore", + "strategy": { + "name": "aavegotchi-agip", + "params": { + "tokenAddress": "0x86935f11c86623dec8a25696e1c19a8659cbf95d", + "symbol": "GOTCHI" + } + }, + "network": "137", + "addresses": [ + "0x51195e21BDaE8722B29919db56d95Ef51FaecA6C", + "0xDd564df884Fd4e217c9ee6F65B4BA6e5641eAC63", + "0xBfe09443556773958bae1699b786d8E9680B5571" + ], + "snapshot": 39986904 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip/index.ts new file mode 100644 index 00000000..d3c27821 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-agip/index.ts @@ -0,0 +1,482 @@ +import { Multicaller } from '../../utils'; +import { subgraphRequest } from '../../utils'; +export const author = 'candoizo'; +export const version = '0.2.5'; + +interface Prices { + [id: string]: 0 | 5 | 10 | 20 | 50 | 100 | 300 | 2000 | 3000 | 10000; +} + +const AAVEGOTCHI_SUBGRAPH_URL = { + 137: 'https://subgraph.satsuma-prod.com/tWYl5n5y04oz/aavegotchi/aavegotchi-core-matic/api' +}; + +const prices: Prices = { + '0': 0, + '1': 5, + '2': 5, + '3': 5, + '4': 10, + '5': 10, + '6': 10, + '7': 100, + '8': 100, + '9': 100, + '10': 300, + '11': 300, + '12': 300, + '13': 2000, + '14': 2000, + '15': 2000, + '16': 10000, + '17': 10000, + '18': 5, + '19': 5, + '20': 5, + '21': 10, + '22': 10, + '23': 10, + '24': 100, + '25': 100, + '26': 100, + '27': 300, + '28': 300, + '29': 300, + '30': 2000, + '31': 2000, + '32': 2000, + '33': 10000, + '34': 10000, + '35': 10000, + '36': 5, + '37': 5, + '38': 5, + '39': 10, + '40': 10, + '41': 10, + '42': 100, + '43': 100, + '44': 100, + '45': 300, + '46': 300, + '47': 300, + '48': 2000, + '49': 2000, + '50': 2000, + '51': 2000, + '52': 10000, + '53': 10000, + '54': 10000, + '55': 100, + '56': 100, + '57': 100, + '58': 100, + '59': 100, + '60': 5, + '61': 300, + '62': 2000, + '63': 10000, + '64': 5, + '65': 300, + '66': 5, + '67': 5, + '68': 5, + '69': 5, + '70': 2000, + '71': 100, + '72': 2000, + '73': 2000, + '74': 2000, + '75': 2000, + '76': 5, + '77': 10, + '78': 10, + '79': 100, + '80': 100, + '81': 100, + '82': 300, + '83': 100, + '84': 300, + '85': 300, + '86': 2000, + '87': 10, + '88': 10, + '89': 100, + '90': 5, + '91': 10, + '92': 100, + '93': 300, + '94': 10, + '95': 10, + '96': 10, + '97': 300, + '98': 300, + '99': 2000, + '100': 300, + '101': 300, + '102': 300, + '103': 2000, + '104': 300, + '105': 300, + '106': 300, + '107': 10000, + '108': 10, + '109': 10, + '110': 100, + '111': 300, + '112': 300, + '113': 10000, + '114': 2000, + '115': 300, + '116': 100, + '117': 5, + '118': 300, + '119': 300, + '120': 300, + '121': 100, + '122': 2000, + '123': 10, + '124': 2000, + '125': 300, + '126': 5, + '127': 20, + '128': 20, + '129': 50, + '130': 5, + '131': 10, + '132': 100, + '133': 300, + '134': 5, + '135': 10, + '136': 100, + '137': 5, + '138': 10, + '139': 100, + '140': 5, + '141': 10, + '142': 100, + '143': 300, + '144': 2000, + '145': 10000, + '146': 5, + '147': 10, + '148': 100, + '149': 300, + '150': 2000, + '151': 5, + '152': 10, + '153': 100, + '154': 300, + '155': 2000, + '156': 10000, + '157': 10, + '158': 100, + '159': 300, + '160': 2000, + '161': 10000, + '162': 5, + '163': 0, + '164': 0, + '165': 0, + '166': 0, + '167': 0, + '168': 0, + '169': 0, + '170': 0, + '171': 0, + '172': 0, + '173': 0, + '174': 0, + '175': 0, + '176': 0, + '177': 0, + '178': 0, + '179': 0, + '180': 0, + '181': 0, + '182': 0, + '183': 0, + '184': 0, + '185': 0, + '186': 0, + '187': 0, + '188': 0, + '189': 0, + '190': 0, + '191': 0, + '192': 0, + '193': 0, + '194': 0, + '195': 0, + '196': 0, + '197': 0, + '198': 0, + '199': 100, + '200': 10, + '201': 300, + '202': 2000, + '203': 100, + '204': 10, + '205': 5, + '206': 100, + '207': 10, + '208': 10, + '209': 300, + '210': 5, + '211': 5, + '212': 3000, + '213': 300, + '214': 10000, + '215': 300, + '216': 3000, + '217': 3000, + '218': 10, + '219': 100, + '220': 300, + '221': 5, + '222': 10, + '223': 10, + '224': 100, + '225': 5, + '226': 100, + '227': 100, + '228': 5, + '229': 10, + '230': 5, + '231': 10, + '232': 5, + '233': 10, + '234': 3000, + '235': 300, + '236': 100, + '237': 3000, + '238': 10000, + '239': 10, + '240': 10, + '241': 100, + '242': 300, + '243': 100, + '244': 100, + '245': 100, + '246': 10, + '247': 10, + '248': 10, + '249': 100, + '250': 100, + '251': 100, + '252': 5, + '253': 5, + '254': 5, + '255': 300, + '256': 300, + '257': 300, + '258': 10000, + '259': 10000, + '260': 10000, + '261': 2000, + '262': 2000, + '263': 2000, + '264': 0, + '265': 0, + '266': 0, + '267': 0, + '268': 0, + '269': 0, + '270': 0, + '271': 0, + '272': 0, + '273': 0, + '274': 0, + '275': 0, + '276': 0, + '277': 0, + '278': 0, + '279': 0, + '280': 0, + '281': 0, + '282': 0, + '283': 0, + '284': 0, + '285': 0, + '286': 0, + '287': 0, + '288': 0, + '289': 0, + '290': 0, + '291': 0, + '292': 5, + '293': 5, + '294': 5, + '295': 5, + '296': 10, + '297': 10, + '298': 5, + '299': 10, + '300': 10, + '301': 100, + '302': 100, + '303': 100, + '304': 100, + '305': 300, + '306': 300, + '307': 300, + '308': 300, + '309': 2000, + '310': 2000, + '311': 2000, + '312': 2000, + '313': 10000, + '314': 10000, + '315': 10000, + + //new + '350': 5, + '351': 5, + '352': 5, + '353': 5, + '354': 10, + '355': 100, + '356': 10, + '357': 100, + '358': 300, + '359': 300, + '360': 300, + '361': 300, + '362': 2000, + '363': 2000, + '364': 2000, + '365': 2000, + '366': 10000, + '367': 10000, + '368': 10000, + '369': 10000 +}; + +const tokenAbi = [ + 'function balanceOf(address account) view returns (uint256)', + { + inputs: [{ internalType: 'address', name: '_account', type: 'address' }], + name: 'itemBalances', + outputs: [ + { + components: [ + { internalType: 'uint256', name: 'itemId', type: 'uint256' }, + { internalType: 'uint256', name: 'balance', type: 'uint256' } + ], + internalType: 'struct ItemsFacet.ItemIdIO[]', + name: 'bals_', + type: 'tuple[]' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +const maxResultsPerQuery = 1000; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const args: { + block?: { number: number }; + } = {}; + if (blockTag !== 'latest') args.block = { number: blockTag }; + + const multi = new Multicaller(network, provider, tokenAbi, { blockTag }); + addresses.map((addr: string) => { + multi.call( + `${options.tokenAddress}.${addr.toLowerCase()}.itemBalances`, + options.tokenAddress, + 'itemBalances', + [addr] + ); + }); + const multiRes = await multi.execute(); + + const query = { + users: { + __args: { + ...args, + first: addresses.length, + where: { + id_in: addresses.map((addr) => addr.toLowerCase()) + } + }, + id: true + } + }; + + for (let i = 0; i <= 5; i++) { + query.users['gotchisOriginalOwned' + i] = { + __aliasFor: 'gotchisOriginalOwned', + __args: { + first: maxResultsPerQuery, + skip: i * maxResultsPerQuery, + orderBy: 'gotchiId' + }, + baseRarityScore: true, + equippedWearables: true + }; + } + + const subgraphRaw = await subgraphRequest( + AAVEGOTCHI_SUBGRAPH_URL[137], + query + ); + + const result = Object.fromEntries( + subgraphRaw.users.map((item) => { + const ownedEntries = Object.entries(item) + .map(([key, value]) => { + if (key.startsWith('gotchis')) return value; + else return []; + }) + .flat(); + return [item.id, ownedEntries]; + }) + ); + + return Object.fromEntries( + addresses.map((address: string) => { + const lowercaseAddr = address.toLowerCase(); + + let gotchisBrsEquipValue = 0; + const allGotchiInfo = result[lowercaseAddr]; + if (allGotchiInfo?.length > 0) { + gotchisBrsEquipValue = allGotchiInfo.reduce( + (total, { baseRarityScore, equippedWearables }) => + total + + Number(baseRarityScore) + + equippedWearables.reduce( + (currentValue, nextIter) => currentValue + prices[nextIter], + 0 + ), + 0 + ); + } + + let ownerItemValue = 0; + const ownerItemInfo = + multiRes[options.tokenAddress][lowercaseAddr]['itemBalances']; + if (ownerItemInfo?.length > 0) { + ownerItemValue = ownerItemInfo.reduce((total, { balance, itemId }) => { + const amountOwned = Number(balance.toString()); + const id = Number(itemId.toString()); + const pricetag = prices[id]; + let cost = pricetag * amountOwned; + if (isNaN(cost)) cost = 0; + return total + cost; + }, 0); + } + + return [address, ownerItemValue + gotchisBrsEquipValue]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-wagmi-guild/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-wagmi-guild/examples.json new file mode 100644 index 00000000..a9729c72 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-wagmi-guild/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Voting power based on aavegotchis equipped with wagie wearables for the WAGMI Warriors Guild", + "strategy": { + "name": "aavegotchi-wagmi-guild", + "params": { + "tokenAddress": "0x86935f11c86623dec8a25696e1c19a8659cbf95d", + "symbol": "GOTCHI" + } + }, + "network": "137", + "addresses": ["0x26cf02F892B04aF4Cf350539CE2C77FCF79Ec172"], + "snapshot": 20122217 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-wagmi-guild/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-wagmi-guild/index.ts new file mode 100644 index 00000000..289a4d4a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi-wagmi-guild/index.ts @@ -0,0 +1,77 @@ +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; + +export const author = 'programmablewealth'; +export const version = '0.1.0'; + +const AAVEGOTCHI_SUBGRAPH_URL = { + 137: 'https://subgraph.satsuma-prod.com/tWYl5n5y04oz/aavegotchi/aavegotchi-core-matic/api' +}; + +const itemPriceParams = { + itemTypes: { + __args: { + first: 1000 + }, + svgId: true, + ghstPrice: true + } +}; + +export async function strategy(_space, network, provider, addresses) { + const walletQueryParams = { + users: { + __args: { + where: { + id_in: addresses.map((addr: string) => addr.toLowerCase()) + }, + first: 1000 + }, + id: true, + gotchisOwned: { + baseRarityScore: true, + equippedWearables: true + } + } + }; + const result = await subgraphRequest(AAVEGOTCHI_SUBGRAPH_URL[network], { + ...itemPriceParams, + ...walletQueryParams + }); + const prices = {}; + result.itemTypes.map((itemInfo) => { + const itemValue = parseFloat(formatUnits(itemInfo.ghstPrice, 18)); + if (itemValue > 0) prices[parseInt(itemInfo.svgId)] = itemValue; + }); + + const itemVotingPower = { '239': 100, '240': 100, '241': 100 }; + + const walletScores = {}; + result.users.map((addrInfo) => { + let gotchiWagieValue = 0; + + const { id, gotchisOwned } = addrInfo; + + if (gotchisOwned.length > 0) + gotchisOwned.map((gotchi) => { + gotchi.equippedWearables + .filter( + (itemId: number) => itemId == 239 || itemId == 240 || itemId == 241 + ) + .map((itemId) => { + const votes = itemVotingPower[itemId.toString()]; + gotchiWagieValue += votes; + }); + }); + + const addr = addresses.find( + (addrOption: string) => addrOption.toLowerCase() === id + ); + walletScores[addr] = gotchiWagieValue; + }); + addresses.map((addr) => { + if (!walletScores[addr]) walletScores[addr] = 0; + }); + + return walletScores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi/examples.json new file mode 100644 index 00000000..4e9e847a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "GHST, USDC-GHST, QUICK-GHST, WETH-GHST, WMATIC-GHST tokens staked in diamond", + "strategy": { + "name": "aavegotchi", + "params": { + "tokenAddress": "0x385Eeac5cB85A38A9a07A70c73e0a3271CfB54A7", + "ghstQuickAddress": "0x8b1fd78ad67c7da09b682c5392b65ca7caa101b9", + "ghstUsdcAddress": "0x096c5ccb33cfc5732bcd1f3195c13dbefc4c82f4", + "ghstWethAddress": "0xccb9d2100037f1253e6c1682adf7dc9944498aff", + "ghstWmaticAddress": "0xf69e93771F11AECd8E554aA165C3Fe7fd811530c", + "stakingAddress": "0xA02d547512Bb90002807499F05495Fe9C4C3943f", + "symbol": "GHST", + "decimals": 18 + } + }, + "network": "137", + "addresses": ["0x027Ffd3c119567e85998f4E6B9c3d83D5702660c"], + "snapshot": 12089223 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aavegotchi/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi/index.ts new file mode 100644 index 00000000..906a449b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aavegotchi/index.ts @@ -0,0 +1,151 @@ +import { multicall } from '../../utils'; + +export const author = 'candoizo'; +export const version = '0.1.1'; + +const tokenAbi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function totalSupply() view returns (uint256)', + 'function stakedInCurrentEpoch(address _account) view returns (tuple(address poolAddress, string poolName, string poolUrl, uint256 rate, uint256 amount)[] _staked)', + 'function staked(address _account) view returns (uint256 ghst_, uint256 poolTokens_, uint256 ghstUsdcPoolToken_)' +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + options.ghstQuickAddress = + options.ghstQuickAddress || '0x8b1fd78ad67c7da09b682c5392b65ca7caa101b9'; + + options.ghstUsdcAddress = + options.ghstUsdcAddress || '0x096c5ccb33cfc5732bcd1f3195c13dbefc4c82f4'; + + options.ghstWethAddress = + options.ghstWethAddress || '0xccb9d2100037f1253e6c1682adf7dc9944498aff'; + + options.ghstWmaticAddress = + options.ghstWmaticAddress || '0xf69e93771F11AECd8E554aA165C3Fe7fd811530c'; + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const blockAfterStakingUpgrade = 22007789; + const afterStakingUpgrade = + blockTag === 'latest' || blockAfterStakingUpgrade < blockTag; + const stakeFunctionName = afterStakingUpgrade + ? 'stakedInCurrentEpoch' + : 'staked'; + const stakeQuery = addresses.map((address: string) => [ + options.stakingAddress, + stakeFunctionName, + [address] + ]); + + const res = await multicall( + network, + provider, + tokenAbi, + [ + [options.ghstQuickAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.ghstQuickAddress]], + [options.ghstUsdcAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.ghstUsdcAddress]], + [options.ghstWethAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.ghstWethAddress]], + [options.ghstWmaticAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.ghstWmaticAddress]], + ...stakeQuery + ], + { blockTag } + ); + + const tokensPerUni = (balanceInUni: number, totalSupply: number) => { + return balanceInUni / 1e18 / (totalSupply / 1e18); + }; + + const ghstQuickTotalSupply = res[0]; + const ghstQuickTokenBalanceInUni = res[1]; + const ghstQuickTokensPerUni = tokensPerUni( + ghstQuickTokenBalanceInUni, + ghstQuickTotalSupply + ); + + const ghstUsdcTotalSupply = res[2]; + const ghstUsdcTokenBalanceInUni = res[3]; + const ghstUsdcTokensPerUni = tokensPerUni( + ghstUsdcTokenBalanceInUni, + ghstUsdcTotalSupply + ); + + const response = res.slice(8); + let entries; + if (afterStakingUpgrade) { + const ghstWethTotalSupply = res[4]; + const ghstWethTokenBalanceInUni = res[5]; + const ghstWethTokensPerUni = tokensPerUni( + ghstWethTokenBalanceInUni, + ghstWethTotalSupply + ); + + const ghstWmaticTotalSupply = res[6]; + const ghstWmaticTokenBalanceInUni = res[7]; + const ghstWmaticTokensPerUni = tokensPerUni( + ghstWmaticTokenBalanceInUni, + ghstWmaticTotalSupply + ); + + entries = response.map((userStakeInfo, i: number) => { + const votePowerAmounts = userStakeInfo._staked.map((info) => { + if ( + info.poolAddress.toLowerCase() === options.tokenAddress.toLowerCase() + ) { + return Number(info.amount.toString()) / 1e18; + } + if ( + info.poolAddress.toLowerCase() === + options.ghstQuickAddress.toLowerCase() + ) { + return ( + (Number(info.amount.toString()) / 1e18) * ghstQuickTokensPerUni + ); + } + if ( + info.poolAddress.toLowerCase() === + options.ghstUsdcAddress.toLowerCase() + ) { + return (Number(info.amount.toString()) / 1e18) * ghstUsdcTokensPerUni; + } + if ( + info.poolAddress.toLowerCase() === + options.ghstWethAddress.toLowerCase() + ) { + return (Number(info.amount.toString()) / 1e18) * ghstWethTokensPerUni; + } + if ( + info.poolAddress.toLowerCase() === + options.ghstWmaticAddress.toLowerCase() + ) { + return ( + (Number(info.amount.toString()) / 1e18) * ghstWmaticTokensPerUni + ); + } + return 0; + }); + + return [addresses[i], votePowerAmounts.reduce((a, b) => a + b, 0)]; + }); + } else { + // before staking upgrade old response + entries = response.map((values, i) => [ + addresses[i], + values[0] / 1e18 + //ghst_ + (values[1] / 10 ** options.decimals) * ghstQuickTokensPerUni + //poolTokens_ + (values[2] / 10 ** options.decimals) * ghstUsdcTokensPerUni //ghstUsdcPoolToken_ + ]); + } + + return Object.fromEntries(entries); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/README.md b/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/README.md new file mode 100644 index 00000000..bc0d0d54 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/README.md @@ -0,0 +1,31 @@ +# across-staked-acx + +This strategy returns the voting power of an address that has staked any LP tokens in the [AcceleratingDistributor](https://etherscan.io/address/0x9040e41eF5E8b281535a96D9a48aCb8cfaBD9a48) +contract. The voting power is calculated as the amount of staked ACX-LP tokens multiplied by the current exchange +rate of ACX-LP to ACX. Finally, outstanding rewards for all staked LP positions denominated in ACX are then added to voting power. + +## Params + +- `acceleratingDistributorAddress` - (**Required**, `string`) Address of contract that emits ACX rewards for staked LP tokens. +- `acxLpTokenAddress` - (**Required**, `string`) Address of ACX-LP token. +- `wethLpTokenAddress` - (**Required**, `string`) Address of WETH-LP token. +- `usdcLpTokenAddress` - (**Required**, `string`) Address of USDC-LP token. +- `wbtcLpTokenAddress` - (**Required**, `string`) Address of WBTC-LP token. +- `daiLpTokenAddress` - (**Required**, `string`) Address of DAI-LP token. +- `hubPoolAddress` - (**Required**, `string`) Address of contract users can deposit tokens to receive LP tokens. +- `acxTokenAddress` - (**Required**, `string`) Address of token that is emitted to staked LP users in the AcceleratingDistributor. + +Here is an example of parameters that work for `"network": "1"` + +```json +{ + "acceleratingDistributorAddress": "0x9040e41eF5E8b281535a96D9a48aCb8cfaBD9a48", + "acxLpTokenAddress": "0xb0C8fEf534223B891D4A430e49537143829c4817", + "wethLpTokenAddress": "0x28F77208728B0A45cAb24c4868334581Fe86F95B", + "usdcLpTokenAddress": "0xC9b09405959f63F72725828b5d449488b02be1cA", + "wbtcLpTokenAddress": "0x59C1427c658E97a7d568541DaC780b2E5c8affb4", + "daiLpTokenAddress": "0x4fabacac8c41466117d6a38f46d08ddd4948a0cb", + "hubPoolAddress": "0xc186fa914353c44b2e33ebe05f21846f1048beda", + "acxTokenAddress": "0x44108f0223A3C3028F5Fe7AEC7f9bb2E66beF82F" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/examples.json b/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/examples.json new file mode 100644 index 00000000..a00b5224 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example of Across Staked-ACX Strategy", + "strategy": { + "name": "across-staked-acx", + "params": { + "acceleratingDistributorAddress": "0x9040e41eF5E8b281535a96D9a48aCb8cfaBD9a48", + "acxLpTokenAddress": "0xb0C8fEf534223B891D4A430e49537143829c4817", + "wethLpTokenAddress": "0x28F77208728B0A45cAb24c4868334581Fe86F95B", + "usdcLpTokenAddress": "0xC9b09405959f63F72725828b5d449488b02be1cA", + "wbtcLpTokenAddress": "0x59C1427c658E97a7d568541DaC780b2E5c8affb4", + "daiLpTokenAddress": "0x4fabacac8c41466117d6a38f46d08ddd4948a0cb", + "hubPoolAddress": "0xc186fa914353c44b2e33ebe05f21846f1048beda", + "acxTokenAddress": "0x44108f0223A3C3028F5Fe7AEC7f9bb2E66beF82F" + } + }, + "network": "1", + "addresses": [ + "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + "0x718648C8c531F91b528A7757dD2bE813c3940608", + "0x996267d7d1B7f5046543feDe2c2Db473Ed4f65e9", + "0xbfb496ACb99299e9eCE84B3FD1B3fDd0f6CDDf49" + ], + "snapshot": 16171362 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/index.ts b/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/index.ts new file mode 100644 index 00000000..43210089 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/across-staked-acx/index.ts @@ -0,0 +1,146 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'nicholaspai'; +export const version = '0.1.0'; + +/** + * @notice This strategy returns the voting power of an address that has staked ACX-LP tokens in the + * AcceleratingDistributor contract. The voting power is calculated as the amount of staked ACX-LP + * tokens multiplied by the current exchange rate of ACX-LP to ACX. Outstanding rewards denominated + * in ACX are also added to voting power. + */ +const abi = [ + 'function getUserStake(address stakedToken, address account) view returns (tuple(uint256 cumulativeBalance, uint256 averageDepositTime, uint256 rewardsAccumulatedPerToken, uint256 rewardsOutstanding))', + 'function getOutstandingRewards(address stakedToken, address account) view returns (uint256)', + 'function pooledTokens(address) view returns (address lpToken, bool isEnabled, uint32 lastLpFeeUpdate, int256 utilizedReserves, uint256 liquidReserves, uint256 undistributedLpFees)', + 'function totalSupply() view returns (uint256)' +]; + +interface PooledToken { + lpToken: string; + isEnabled: boolean; + lastLpFeeUpdate: BigNumber; + utilizedReserves: BigNumber; + liquidReserves: BigNumber; + undistributedLpFees: BigNumber; +} + +const acxLpTokensDecimals = 18; +const oneUnitAcxLp = parseUnits('1', acxLpTokensDecimals); + +export async function strategy( + _space, + network, + provider, + addresses: string[], + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const response = await multicall( + network, + provider, + abi, + [ + [options.hubPoolAddress, 'pooledTokens', [options.acxTokenAddress]], + [options.acxLpTokenAddress, 'totalSupply', []], + ...addresses.map((address) => [ + options.acceleratingDistributorAddress, + 'getUserStake', + [options.acxLpTokenAddress, address] + ]), + ...addresses.map((address) => [ + options.acceleratingDistributorAddress, + 'getOutstandingRewards', + [options.acxLpTokenAddress, address] + ]), + ...addresses.map((address) => [ + options.acceleratingDistributorAddress, + 'getOutstandingRewards', + [options.wethLpTokenAddress, address] + ]), + ...addresses.map((address) => [ + options.acceleratingDistributorAddress, + 'getOutstandingRewards', + [options.usdcLpTokenAddress, address] + ]), + ...addresses.map((address) => [ + options.acceleratingDistributorAddress, + 'getOutstandingRewards', + [options.wbtcLpTokenAddress, address] + ]), + ...addresses.map((address) => [ + options.acceleratingDistributorAddress, + 'getOutstandingRewards', + [options.daiLpTokenAddress, address] + ]) + ], + { blockTag } + ); + + const pooledTokens: PooledToken = response[0]; + const acxLpTokenSupply: BigNumber = response[1][0]; + const userStake: BigNumber[][] = response.slice(2, 2 + addresses.length); + const outstandingAcxLpRewards = response.slice( + 2 + addresses.length, + 2 + addresses.length * 2 + ); + const outstandingWethLpRewards = response.slice( + 2 + addresses.length * 2, + 2 + addresses.length * 3 + ); + const outstandingUsdcLpRewards = response.slice( + 2 + addresses.length * 3, + 2 + addresses.length * 4 + ); + const outstandingWbtcLpRewards = response.slice( + 2 + addresses.length * 4, + 2 + addresses.length * 5 + ); + const outstandingDaiLpRewards = response.slice( + 2 + addresses.length * 5, + 2 + addresses.length * 6 + ); + + // This is the latest exchange rate as of the last HubPool._sync call. + // This computation is based off of this math: + // - https://github.com/across-protocol/contracts-v2/blob/e911cf59ad3469e19f04f5de1c92d6406c336042/contracts/HubPool.sol#L944 + // (liquidReserves + utilizedReserves - undistributedLpFees) / acxLpTokenSupply + const lastExchangeRate = pooledTokens.liquidReserves + .add(pooledTokens.utilizedReserves) + .sub(pooledTokens.undistributedLpFees) + .mul(oneUnitAcxLp) + .div(acxLpTokenSupply); + + // Convert staked ACX-LP balances to underlying by multiplying by ACX-LP:ACX token exchange rate. + // Note: Each UserStake object is stored on-chain as a tuple with the staked balance being the first element. + const stakedLpBalances = userStake.map((value) => + value[0][0].mul(lastExchangeRate).div(oneUnitAcxLp) + ); + // !!Note: This will break if the reward currency doesn't use same decimals as the staked LP token. + + // Add outstanding ACX rewards to staked balance values. This assumes that the reward + // currency ACX is the same unit as the staked ACX-LP's underlying. + const stakedBalancesPlusRewards = Object.fromEntries( + addresses.map((address, i) => [ + address, + parseFloat( + formatUnits( + stakedLpBalances[i] + .add(outstandingAcxLpRewards[i][0]) + .add(outstandingWethLpRewards[i][0]) + .add(outstandingUsdcLpRewards[i][0]) + .add(outstandingWbtcLpRewards[i][0]) + .add(outstandingDaiLpRewards[i][0]) + .toString(), + acxLpTokensDecimals + ) + ) + ]) + ); + + return stakedBalancesPlusRewards; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aelin-council/README.md b/Implementations/API/backend/utils/snapshot/strategies/aelin-council/README.md new file mode 100644 index 00000000..c2ef783d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aelin-council/README.md @@ -0,0 +1,15 @@ +# Quadratic Debt Percentage Strategy + +Calculates the weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["aelin-council"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/aelin-council/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aelin-council/examples.json new file mode 100644 index 00000000..76ed36d8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aelin-council/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "aelin-council", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD", + "blockL1": 14445666, + "blockL2": 4765677 + } + }, + "network": "1", + "addresses": [ + "0x02be99141f66c065e165caf2dbf6a987c8d107a5", + "0x93b220bc7c36ea8e4c64192301b680273a184ec3", + "0x30f1d1ffad34b24bb8310ad9dd237b854b4daea7", + "0x961c18a23306fe44c4323adcb3bc343b0d193670", + "0x5298f608567297f8644fcf097e0e7fe52f8aeac3", + "0x10e19ba32927b28eb5424f7b6a3e2eaa5a607f47" + ], + "snapshot": 14445666 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aelin-council/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aelin-council/index.ts new file mode 100644 index 00000000..5778b801 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aelin-council/index.ts @@ -0,0 +1,187 @@ +import { getAddress } from '@ethersproject/address'; +import { Contract } from '@ethersproject/contracts'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { subgraphRequest } from '../../utils'; +import fetch from 'cross-fetch'; + +export const author = '0xcdb'; +export const version = '1.0.0'; + +const GRAPH_API_URL = { + uniswap: { + mainnet: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2' + }, + aelin: { + mainnet: 'https://api.thegraph.com/subgraphs/name/0xcdb/aelin-governance', + optimism: + 'https://api.thegraph.com/subgraphs/name/0xcdb/aelin-governance-optimism' + } +}; + +const GELATO_POOL_ADDRESS = '0x665d8D87ac09Bdbc1222B8B9E72Ddcb82f76B54A'; + +const gelatoPoolAbi = [ + 'function getUnderlyingBalances() external view returns (uint256 amount0Current, uint256 amount1Current)', + 'function totalSupply() external view returns (uint256)' +]; + +function returnGraphParams(snapshot: number | string, addresses: string[]) { + return { + aelinStakers: { + __args: { + where: { + id_in: addresses.map((address: string) => address.toLowerCase()) + }, + first: 1000, + block: { + number: snapshot + } + }, + id: true, + balancePool1: true, + balancePool2: true + } + }; +} + +const getTokenRates = async () => { + const results = await fetch( + 'https://api.coingecko.com/api/v3/simple/price?ids=aelin%2Cethereum&vs_currencies=usd' + ); + const rates = await results.json(); + const { + aelin: { usd: aelinRate }, + ethereum: { usd: ethRate } + } = rates; + return { aelinRate, ethRate }; +}; + +const getGUniRate = async (contract, aelinRate, ethRate, snapshot) => { + const [balances, gUNITotalSupply] = await Promise.all([ + contract.getUnderlyingBalances({ blockTag: snapshot }), + contract.totalSupply({ blockTag: snapshot }) + ]); + + const { amount0Current, amount1Current } = balances; + const totalValueInPool = + (amount0Current / 1e18) * ethRate + (amount1Current / 1e18) * aelinRate; + return totalValueInPool / (gUNITotalSupply / 1e18); +}; + +const getUniV2Rate = async (aelinRate, ethRate, snapshot) => { + const results = await subgraphRequest(GRAPH_API_URL.uniswap.mainnet, { + pairs: { + __args: { + where: { + id: '0x974d51fafc9013e42cbbb9465ea03fe097824bcc' + }, + first: 1000, + block: { + number: snapshot + } + }, + token0Price: true, + token1Price: true, + reserve0: true, + reserve1: true, + totalSupply: true + } + }); + const { + reserve0: amount0, + reserve1: amount1, + totalSupply + } = results.pairs[0]; + const totalValueInPool = + Number(amount0) * aelinRate + Number(amount1) * ethRate; + return totalValueInPool / Number(totalSupply); +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _options +) { + const L1_SNAPSHOT = _options.blockL1; + const L2_SNAPSHOT = _options.blockL2; + const score = {}; + const squaredScore = {}; + + const [l1Stakers, l2Stakers] = await Promise.all([ + subgraphRequest( + GRAPH_API_URL.aelin.mainnet, + returnGraphParams(L1_SNAPSHOT, _addresses) + ), + subgraphRequest( + GRAPH_API_URL.aelin.optimism, + returnGraphParams(L2_SNAPSHOT, _addresses) + ) + ]); + + // We start by mapping all the Pool 1 balances + (l1Stakers?.aelinStakers ?? []).forEach(({ balancePool1, id }) => { + score[getAddress(id)] = balancePool1 / 1e18; + }); + + (l2Stakers?.aelinStakers ?? []).forEach(({ balancePool1, id }) => { + const key = getAddress(id); + const balance = balancePool1 / 1e18; + if (!!score[key]) { + score[key] += balance; + } else { + score[key] = balance; + } + }); + + // For Pool 2, we need to calculate the price of each staked token (g-uni for OP and uni-v2 for Mainnet) + const optimismProvider = new JsonRpcProvider( + 'https://mainnet.optimism.io', + 'optimism' + ); + const gelatoPoolContract = new Contract( + GELATO_POOL_ADDRESS, + gelatoPoolAbi, + optimismProvider + ); + + const { aelinRate, ethRate } = await getTokenRates(); + const gUNIRate = await getGUniRate( + gelatoPoolContract, + aelinRate, + ethRate, + L2_SNAPSHOT + ); + const uniV2Rate = await getUniV2Rate(aelinRate, ethRate, L1_SNAPSHOT); + + (l2Stakers?.aelinStakers ?? []).forEach(async ({ balancePool2, id }) => { + const gUNIValue = gUNIRate * (balancePool2 / 1e18); + // We divide by 2 because it's a 50-50 pool. Only half the value is in Aelin. + const aelinAmount = gUNIValue / (2 * aelinRate); + const key = getAddress(id); + if (!!score[key]) { + score[key] += aelinAmount; + } else { + score[key] = aelinAmount; + } + }); + + (l1Stakers?.aelinStakers ?? []).forEach(async ({ balancePool2, id }) => { + const uniV2Value = uniV2Rate * (balancePool2 / 1e18); + // We divide by 2 because it's a 50-50 pool. Only half the value is in Aelin. + const aelinAmount = uniV2Value / (2 * aelinRate); + const key = getAddress(id); + if (!!score[key]) { + score[key] += aelinAmount; + } else { + score[key] = aelinAmount; + } + }); + + Object.keys(score).forEach( + (address) => (squaredScore[address] = Math.sqrt(score[address])) + ); + + return squaredScore; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/agave/README.md b/Implementations/API/backend/utils/snapshot/strategies/agave/README.md new file mode 100644 index 00000000..df1c0f9d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/agave/README.md @@ -0,0 +1,10 @@ +# Agave strategy + +Here is an example of parameters: + +```json +{ + "lpToken": "0xf7a28097fDf8c323Da826A9D98617a266A73c0Ef", + "underlyingToken": "0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/agave/examples.json b/Implementations/API/backend/utils/snapshot/strategies/agave/examples.json new file mode 100644 index 00000000..ff542a2f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/agave/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "agave", + "params": { + "lpToken": "0xf7a28097fDf8c323Da826A9D98617a266A73c0Ef", + "underlyingToken": "0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e", + "symbol": "STAKE" + } + }, + "network": "100", + "addresses": [ + "0xF82ce7B9692a32C27FE5477b6C85F163f2920eEA", + "0x79EB604CCe423dD1CbA290EC946646649F4e70ec", + "0x65e714b00f2a8f660bBAc9Ad866EeD7b153cB09D" + ], + "snapshot": 19235916 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/agave/index.ts b/Implementations/API/backend/utils/snapshot/strategies/agave/index.ts new file mode 100644 index 00000000..614d96f4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/agave/index.ts @@ -0,0 +1,60 @@ +import { formatUnits } from '@ethersproject/units'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { Multicaller } from '../../utils'; + +export const author = 'maxaleks'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)', + 'function decimals() external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + multi.call('availableLiquidity', options.underlyingToken, 'balanceOf', [ + options.lpToken + ]); + multi.call('lpTokenTotalSupply', options.lpToken, 'totalSupply'); + multi.call('lpTokenDecimals', options.lpToken, 'decimals'); + multi.call('underlyingTokenDecimals', options.underlyingToken, 'decimals'); + const { + availableLiquidity, + lpTokenTotalSupply, + lpTokenDecimals, + underlyingTokenDecimals + } = await multi.execute(); + + const rate = + parseFloat(formatUnits(availableLiquidity, underlyingTokenDecimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals)); + + const scores = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + { + address: options.lpToken, + decimals: lpTokenDecimals + }, + snapshot + ); + + return Object.fromEntries( + Object.entries(scores).map(([address, balance]) => [ + address, + balance * rate + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aks/README.md b/Implementations/API/backend/utils/snapshot/strategies/aks/README.md new file mode 100644 index 00000000..18050198 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aks/README.md @@ -0,0 +1,9 @@ +# Contract Call Strategy + +Fetches [AKS](https://bscscan.com/address/0xbf6357daa2d199ff2b4d992471e93a264da6abe3) balance from the following sources: + +- Wallet +- AKS-BNB LP Farm +- AKS Pool +- AKS Vault +- Pools that were active at the time of the snapshot diff --git a/Implementations/API/backend/utils/snapshot/strategies/aks/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aks/examples.json new file mode 100644 index 00000000..392987a5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aks/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "aks", + "params": { + "symbol": "AKS" + } + }, + "network": "56", + "addresses": [ + "0xF677b8EF72C34f63c43f47C30612B1A3Ec1b622F", + "0xd7eAd7DD37EFf97531beA958a58282Fa6D3a31A5" + ], + "snapshot": 8599985 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aks/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aks/index.ts new file mode 100644 index 00000000..dc37f348 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aks/index.ts @@ -0,0 +1,101 @@ +import fetch from 'cross-fetch'; +import { subgraphRequest } from '../../utils'; + +export const author = 'akshaysoam8'; +export const version = '0.0.1'; + +type VotingResponse = { + verificationHash: string; + block: number; + aksBalance: string; + aksVaultBalance: string; + aksPoolBalance: string; + aksBnbLpBalance: string; + poolsBalance: string; + total: string; +}; + +const MINIUM_VOTING_POWER = 0.01; +const SMART_CHEF_URL = + 'https://api.thegraph.com/subgraphs/name/akshaysoam8/smartchef'; +const VOTING_API_URL = 'https://voting-api.pancakeswap.info/api/power'; + +/** + * Fetches voting power of one address + */ +const fetchVotingPower = async ( + address: string, + block: number, + poolAddresses: string[] +): Promise => { + const response = await fetch(VOTING_API_URL, { + method: 'POST', + body: JSON.stringify({ + block, + address, + poolAddresses + }) + }); + + const payload = await response.json(); + return payload.data; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = + typeof snapshot === 'number' ? snapshot : await provider.getBlockNumber(); + + const params = { + smartChefs: { + __args: { + where: { + startBlock_lte: blockTag, + endBlock_gte: blockTag + }, + first: 1000, + orderBy: 'block', + orderDirection: 'desc' + }, + id: true, + startBlock: true, + endBlock: true + } + }; + + const results = await subgraphRequest(SMART_CHEF_URL, params); + + if (!results) { + return; + } + + try { + const poolAddresses = results.smartChefs.map((pool) => pool.id); + const promises = addresses.map((address) => { + return fetchVotingPower(address, blockTag, poolAddresses); + }) as ReturnType[]; + const votingPowerResults = await Promise.all(promises); + + const calculatedPower = votingPowerResults.reduce( + (accum, response, index) => { + const address = addresses[index]; + const total = parseFloat(response.total); + + return { + ...accum, + [address]: total <= MINIUM_VOTING_POWER ? MINIUM_VOTING_POWER : total + }; + }, + {} + ); + return calculatedPower; + } catch { + return []; + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/anti-whale/README.md b/Implementations/API/backend/utils/snapshot/strategies/anti-whale/README.md new file mode 100644 index 00000000..28e1a77c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/anti-whale/README.md @@ -0,0 +1,83 @@ +# Anti-Whale strategy + +## Description + +This strategy executes a configured strategy and applies an anti-whale measure to its results to reduce the impact of big wallets in the resulting value, reducing the effect on the voting power as the token amount increases. + +It will apply the following to the result: + + ```none + If result > antiWhale.threshold + result = antiWhale.inflectionPoint * ( result / antiWhale.inflectionPoint ) ^ antiWhale.exponent + + If result <= antiWhale.threshold { + thresholdMultiplier = ( antiWhale.inflectionPoint * ( antiWhale.threshold / antiWhale.inflectionPoint )^antiWhale.exponent ) / antiWhale.threshold + + result = result * thresholdMultiplier + } + ``` + +## Accepted options + +- **strategy.name:** name of the strategy to run + +- **strategy.params:** the strategy specific params to execute it. + +- **log:** Boolean flag to enable or disable logging to the console (used for debugging purposes during development) + +- **thresholdMultiplier:** The multiplier at which all results below threshold are multiplied. This is ratio of antiWhale/result at the threshold point. + +- **antiWhale.threshold:** Point at which antiWhale effect no longer applies. Results less than this will be treated with a static multiplier. This is to reduce infinite incentive for multiple wallet exploits. + - default: 1625. + + - lower cap: > 0 - set to default if <= 0. + +- **antiWhale.inflectionPoint:** Point at which output matches result. Results less than this increase output. Results greater than this decrease output. + - default: 6500. + + - lower cap: > 0 - set to default if <= 0. + + - must be >= antiWhale.threshold. Otherwise will be same as antiWhale.threshold. + +- **antiWhale.exponent:** The exponent is responsible for the antiWhale effect. Must be less than one, or else it will have a pro-whale effect. Must be greater than zero, or else it will cause total voting power to trend to zero. + - default: 0.5. + + - upper cap: 1. + + - lower cap: > 0 - set to default if <= 0. + +## Examples + +```json +[ + { + "name": "anti-whale", + "strategy": { + "name": "anti-whale", + "params": { + "strategy": { + "name": "erc20-balance-of", + "network": "1", + "params": { + "address": "0x579cea1889991f68acc35ff5c3dd0621ff29b0c9", + "decimals": 18 + } + }, + "antiWhale": { + "inflectionPoint": 1000, + "threshold": 250, + "exponent": 0.5 + }, + "log": true + } + }, + "network": "1", + "addresses": [ + "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5", + "0x9feab70f3c4a944b97b7565bac4991df5b7a69ff", + "0xaca39b187352d9805deced6e73a3d72abf86e7a0" + ], + "snapshot": 12419836 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/anti-whale/examples.json b/Implementations/API/backend/utils/snapshot/strategies/anti-whale/examples.json new file mode 100644 index 00000000..b9d7da8a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/anti-whale/examples.json @@ -0,0 +1,32 @@ +[ + { + "name": "anti-whale", + "strategy": { + "name": "anti-whale", + "params": { + "symbol": "ANTI", + "strategy": { + "name": "erc20-balance-of", + "params": { + "address": "0x579cea1889991f68acc35ff5c3dd0621ff29b0c9", + "symbol": "IQ", + "decimals": 18 + } + }, + "antiWhale": { + "inflectionPoint": 1000, + "threshold": 250, + "exponent": 0.5 + }, + "log": true + } + }, + "network": "1", + "addresses": [ + "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5", + "0x9feab70f3c4a944b97b7565bac4991df5b7a69ff", + "0xaca39b187352d9805deced6e73a3d72abf86e7a0" + ], + "snapshot": 12419836 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/anti-whale/index.ts b/Implementations/API/backend/utils/snapshot/strategies/anti-whale/index.ts new file mode 100644 index 00000000..1013808a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/anti-whale/index.ts @@ -0,0 +1,85 @@ +import strategies from '..'; + +export const author = 'joaomajesus'; +export const version = '1.0.0'; + +let log: string[] = []; +let _options; + +function printLog() { + if (_options.log || false) { + console.debug(log); + log = []; + } +} + +function applyAntiWhaleMeasures(result) { + const threshold = + _options.antiWhale.threshold == null || _options.antiWhale.threshold <= 0 + ? 1625 + : _options.antiWhale.threshold; + + let inflectionPoint = + _options.antiWhale.inflectionPoint == null || + _options.antiWhale.inflectionPoint <= 0 + ? 6500 + : _options.antiWhale.inflectionPoint; + + inflectionPoint = inflectionPoint < threshold ? threshold : inflectionPoint; + + const exponent = + _options.antiWhale.exponent == null || _options.antiWhale.exponent <= 0 + ? 0.5 + : _options.antiWhale.exponent > 1 + ? 1 + : _options.antiWhale.exponent; + + log.push(`inflectionPoint = ${inflectionPoint}`); + log.push(`exponent = ${exponent}`); + log.push(`threshold = ${threshold}`); + printLog(); + + if (result > threshold) { + result = inflectionPoint * (result / inflectionPoint) ** exponent; + } else { + const thresholdMultiplier = + (inflectionPoint * (threshold / inflectionPoint) ** exponent) / threshold; + + log.push(`thresholdMultiplier = ${thresholdMultiplier}`); + + result = result * thresholdMultiplier; + } + + log.push(`result = ${result}`); + printLog(); + + return result; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + _options = options; + + const result = await strategies[options.strategy.name].strategy( + space, + network, + provider, + addresses, + options.strategy.params, + snapshot + ); + + const entries = new Map(); + + for (const [address, value] of Object.entries(result)) { + entries.set(address, applyAntiWhaleMeasures(value)); + } + + return Object.fromEntries(entries); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/apescape/examples.json b/Implementations/API/backend/utils/snapshot/strategies/apescape/examples.json new file mode 100644 index 00000000..0cad4fc2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/apescape/examples.json @@ -0,0 +1,14 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "apescape", + "params": { + "symbol": "ROKT" + } + }, + "network": "56", + "addresses": ["0x9Fa02A6DedD0383cF339aeCB9353bfaE1E319Aa6"], + "snapshot": 7218577 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/apescape/index.ts b/Implementations/API/backend/utils/snapshot/strategies/apescape/index.ts new file mode 100644 index 00000000..ee9b4643 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/apescape/index.ts @@ -0,0 +1,137 @@ +import { formatUnits } from '@ethersproject/units'; +import { call, multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'apescape'; +export const version = '0.1.0'; + +const chefAbi = [ + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + }, + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; +const lpPairAbi = [ + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'getReserves', + outputs: [ + { + internalType: 'uint112', + name: '_reserve0', + type: 'uint112' + }, + { + internalType: 'uint112', + name: '_reserve1', + type: 'uint112' + }, + { + internalType: 'uint32', + name: '_blockTimestampLast', + type: 'uint32' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +const chef1Address = '0xCA74b3db871c679e928E70917Ae804DC7BFd8781'; +const chef2Address = '0x062D9b9a97B4eFC67D286e99618dA87C614B166F'; +const lpPairAddress = '0x52307F4C5CeBB1f157c3947D335B999091bAa3F7'; +const decimals = 18; +const precision = BigNumber.from(10).pow(18); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const userInfoPool1 = await multicall( + network, + provider, + chefAbi, + addresses.map((address: any) => [chef1Address, 'userInfo', [1, address]]), + { blockTag } + ); + + const userInfoPool2 = await multicall( + network, + provider, + chefAbi, + addresses.map((address: any) => [chef2Address, 'userInfo', [0, address]]), + { blockTag } + ); + + const lpSupply = await call(provider, lpPairAbi, [ + lpPairAddress, + 'totalSupply', + [] + ]); + const lpReserves = await call(provider, lpPairAbi, [ + lpPairAddress, + 'getReserves', + [] + ]); + + return Object.fromEntries( + userInfoPool1.map((info1, i) => { + const balance1 = info1.amount; + const balance2 = userInfoPool2[i].amount; + + const tokensPerLp = lpReserves._reserve1.mul(precision).div(lpSupply); + const balance2Normalized = balance2.mul(tokensPerLp).div(precision); + const sum = balance1.add(balance2Normalized); + + return [addresses[i], parseFloat(formatUnits(sum.toString(), decimals))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/apeswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/apeswap/README.md new file mode 100644 index 00000000..90c90eef --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/apeswap/README.md @@ -0,0 +1,33 @@ +# Apeswap + +This is the most common strategy, it returns the balances of the voters for a balances GNANA token +in Apeswap project(pools, token). + +Here is an example of parameters: +```json +[ + { + "name": "Example query Apeswap", + "strategy": { + "name": "apeswap", + "params": { + "address": "0xddb3bd8645775f59496c821e4f55a7ea6a6dc299", + "symbol": "GNANA", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0x56E17565Ce37c0Dbb2BDDec0eC607b874785c376", + "0x0326824dB556Ab5525608851713e269Ce583B629", + "0xc47dec7ffde829043b91e904fdaf1e048bdd482c", + "0x356f74457a8002c680a0c8fa628083d619267c88", + "0x607f62572ea0c00da5048eb39d89d32110151681", + "0x75768Ea1A1C3c84121063f7A281ee3081dB1D8Ef" + ], + "snapshot": 18979451 + } +] + + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/apeswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/apeswap/examples.json new file mode 100644 index 00000000..07d7dfa3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/apeswap/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query Apeswap", + "strategy": { + "name": "apeswap", + "params": { + "address": "0xddb3bd8645775f59496c821e4f55a7ea6a6dc299", + "symbol": "GNANA", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0xe5EdeA54596B385C3c8dFd5010C3Cf892d547Acb", + "0x56E17565Ce37c0Dbb2BDDec0eC607b874785c376", + "0x0326824dB556Ab5525608851713e269Ce583B629", + "0xc47dec7ffde829043b91e904fdaf1e048bdd482c", + "0x356f74457a8002c680a0c8fa628083d619267c88", + "0x607f62572ea0c00da5048eb39d89d32110151681", + "0x75768Ea1A1C3c84121063f7A281ee3081dB1D8Ef" + ], + "snapshot": 18979451 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/apeswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/apeswap/index.ts new file mode 100644 index 00000000..4fab8dae --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/apeswap/index.ts @@ -0,0 +1,58 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'ApeSwapFinance'; +export const version = '0.0.1'; + +const GNANA_POOL = '0x8F97B2E6559084CFaBA140e2AB4Da9aAF23FE7F8'; +const abi = [ + 'function balanceOf(address _owner) view returns (uint256 balance)', + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +const bn = (num: any): BigNumber => { + return BigNumber.from(num.toString()); +}; + +const addUserBalance = (userBalances, user: string, balance) => { + if (userBalances[user]) { + return (userBalances[user] = userBalances[user].add(balance)); + } else { + return (userBalances[user] = balance); + } +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const multicall = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address: any) => { + multicall.call(`token.${address}`, options.address, 'balanceOf', [address]); + multicall.call(`pool.${address}`, GNANA_POOL, 'userInfo', [address]); + }); + const result = await multicall.execute(); + + const userBalances: any = []; + for (let i = 0; i < addresses.length - 1; i++) { + userBalances[addresses[i]] = bn(0); + } + + addresses.forEach((address: any) => { + addUserBalance(userBalances, address, result.token[address] ?? 0); + addUserBalance(userBalances, address, result.pool[address][0] ?? 0); + }); + + return Object.fromEntries( + Object.entries(userBalances).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/api-post/README.md b/Implementations/API/backend/utils/snapshot/strategies/api-post/README.md new file mode 100644 index 00000000..e56e5da9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api-post/README.md @@ -0,0 +1,54 @@ +# API POST strategy + +## Description + +This strategy can be used if you want to call a custom HTTP-Endpoint to request the voting power/score for the +participating addresses. + +You can configure the endpoint via the options object when selecting the strategy in your settings. + +The options object is passed to the API as well.
+You may add an API-Key here for example + +##Attention +Make sure your API is secured!
+The request may contain values that can harm your system.
+It's most unlikely to happen but it's good to keep this in mind! + +### Example Request that is sent to your custom endpoint +```json +POST your-project.tld/path/to/your/endpoint +{ + "options": { your strategy options object}, + "network": "1", + "addresses": [ + "0xEA2E9cEcDFF8bbfF107a349aDB9Ad0bd7b08a7B7", + "0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D", + "0xd649bACfF66f1C85618c5376ee4F38e43eE53b63", + "0x726022a9fe1322fA9590FB244b8164936bB00489", + "0xc6665eb39d2106fb1DBE54bf19190F82FD535c19", + "0x6ef2376fa6e12dabb3a3ed0fb44e4ff29847af68" + ], + "snapshot": 11437846 +} +``` + +### Example Response +The response that is sent by your endpoint should look like this +```json +{ + "score": [ + { + "score": 123, + "address": "0xEA2E9cEcDFF8bbfF107a349aDB9Ad0bd7b08a7B7" + }, + { + "score": 456, + "address": "0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D" + }, + { + and so on + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/api-post/examples.json b/Implementations/API/backend/utils/snapshot/strategies/api-post/examples.json new file mode 100644 index 00000000..2a97b3fb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api-post/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "api-post", + "params": { + "api": "https://dao-staging.polychainmonsters.com/v1/votingPowerTestMock", + "symbol": "Points", + "decimals": 0 + } + }, + "network": "1", + "addresses": [ + "0xEA2E9cEcDFF8bbfF107a349aDB9Ad0bd7b08a7B7", + "0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D", + "0xd649bACfF66f1C85618c5376ee4F38e43eE53b63", + "0x726022a9fe1322fA9590FB244b8164936bB00489", + "0xc6665eb39d2106fb1DBE54bf19190F82FD535c19", + "0x6ef2376fa6e12dabb3a3ed0fb44e4ff29847af68" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/api-post/index.ts b/Implementations/API/backend/utils/snapshot/strategies/api-post/index.ts new file mode 100644 index 00000000..115a4a0c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api-post/index.ts @@ -0,0 +1,39 @@ +import { getAddress } from '@ethersproject/address'; +import fetch from 'cross-fetch'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'miertschink'; +export const version = '0.1.1'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const requestBody = { + options, + network, + snapshot, + addresses + }; + + const response = await fetch(options.api, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + const data = await response.json(); + return Object.fromEntries( + data.score.map((value) => [ + getAddress(value.address), + parseFloat(formatUnits(value.score.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/api-v2/README.md b/Implementations/API/backend/utils/snapshot/strategies/api-v2/README.md new file mode 100644 index 00000000..baf43e59 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api-v2/README.md @@ -0,0 +1,50 @@ +# API V2 strategy + +Voting strategy using a REST API endpoint. Number of votes depends on the return of the API endpoint. +(Unlike the `api` strategy, this strategy does not depend on voting power of other addresses) + +> Note: Better to use this strategy only if you are not using any override strategies (example: delegation strategy). + +## Parameters + +| Name | Type | Description | Default | +| --- | --- | --- | --- | +| `url` | `string` | URL of the API endpoint | `undefined` | +| `type` | `string` | Type of the API endpoint ( `api-get` or `api-post` or `ipfs` or `json` ) | `api-get` | +| `additionalParams` | `string` | Additional parameters for the API endpoint (optional) | `` | + +If you are passing a IPFS url use following format: + +```JSON +{ + "url": "ipfs://...", + "type": "ipfs" +} +``` + +If you are passing a JSON url use following format: + +```JSON +{ + "url": "https://...", + "type": "json" +} +``` + +If you are passing a API url use following format: (all voter addresses will be passed in the query string) + +```JSON +{ + "url": "https://...", + "type": "api-get" +} +``` + +If you are passing a API url with POST method use following format: + +```JSON +{ + "url": "https://...", + "type": "api-post" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/api-v2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/api-v2/examples.json new file mode 100644 index 00000000..e71fd883 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api-v2/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "api-v2", + "params": { + "url": "ipfs://QmbmhTivxYuLE5uhNEALoBmvP7Yg9acA2Lkw9V9PqaEmw6", + "type": "ipfs" + } + }, + "network": "1", + "addresses": [ + "0xeD2bcC3104Da5F5f8fA988D6e9fAFd74Ae62f319", + "0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D", + "0xd649bACfF66f1C85618c5376ee4F38e43eE53b63", + "0x726022a9fe1322fA9590FB244b8164936bB00489", + "0xc6665eb39d2106fb1DBE54bf19190F82FD535c19", + "0x6ef2376fa6e12dabb3a3ed0fb44e4ff29847af68", + "0x89446aF03652c5257dB5C8E4E85495EB754196c5" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/api-v2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/api-v2/index.ts new file mode 100644 index 00000000..a7903c60 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api-v2/index.ts @@ -0,0 +1,75 @@ +import { getAddress } from '@ethersproject/address'; +import fetch from 'cross-fetch'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'snapshot-labs'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + let url: string = options.url; + const additionalParameters: string = options.additionalParameters || ''; + const type: string = options.type || 'api-get'; + const method: string = type === 'api-post' ? 'POST' : 'GET'; + let body: string | null = null; + + if (!url) throw new Error('Invalid url'); + + if (type === 'ipfs') { + url = url.replace('ipfs://', 'https://gateway.pinata.cloud/ipfs/'); + } else if (type === 'api-get') { + url += '?network=' + network; + url += '&snapshot=' + snapshot; + url += '&addresses=' + addresses.join(','); + if (additionalParameters) url += '&' + additionalParameters; + } else if (type === 'api-post') { + const requestBody = { + options, + network, + snapshot, + addresses + }; + body = JSON.stringify(requestBody); + } + + const response = await fetch(url, { + method, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body + }); + let responseData: any = await response.text(); + try { + responseData = JSON.parse(responseData); + } catch (e) { + throw new Error( + `[api-v2] Errors found in API: URL: ${url}, Status: ${ + response.status + }, Response: ${responseData.substring(0, 512)}` + ); + } + + if (!responseData?.score) throw new Error('Invalid response from API'); + + return Object.fromEntries( + addresses.map((address) => [ + getAddress(address), + parseFloat( + formatUnits( + responseData.score + .find((s) => s.address === address) + ?.score?.toString() || '0', + options.hasOwnProperty('decimals') ? options.decimals : 0 + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/api/README.md b/Implementations/API/backend/utils/snapshot/strategies/api/README.md new file mode 100644 index 00000000..dfebc906 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api/README.md @@ -0,0 +1,98 @@ +# API strategy + +Voting strategy using a REST API endpoint. Number of votes depends on the return of the API endpoint. + +## Cosntructing the API URL +This strategy will create an `api_url` based on the supplied parameters and the Proposal&Space settings. + +`api_url` is constructed as such: + +### For IPFS endpoints +IPFS endpoint is defined as a url starting with any of the following: + - https://gateway.pinata.cloud/ipfs/ + - https://ipfs.io/ipfs/ + - https://cloudflare-ipfs.com/ipfs/ + +1. `param.api`: The first part of the URL (e.g. https://gateway.pinata.cloud/ipfs/) + +2. `param.strategy`: The IPFS hash + +3. `param.additionalParameters` (optional): Any additional parameters you want to include + +The final URL is expected to look something like: `https://gateway.pinata.cloud/ipfs/QmbmhTivxYuLE5uhNEALoBmvP7Yg9acA2Lkw9V9PqaEmw6` + +## For static endpoints +If your endpoint is not IPFS, but API is returning static data: +You can use an API URL that ends with `.json` for example: `https://www.myapi.com/vote_count.json` +Or You can use the `staticFile` param so not all addresses are passed to the API. This is useful for APIs that have a limit on the number of addresses that can be passed in a single request. + +1. `param.api`: The first part of the URL (e.g. https://www.myapi.com/) + +2. `param.strategy`: The resource name (e.g. get_vote_count) + +3. `param.staticFile`: Set to `true` + +### For non-IPFS endpoints: + +1. `param.api`: The first part of the URL (e.g. https://www.myapi.com/) + +2. `param.strategy`: The resource name (e.g. get_vote_count) + +3. `network`: Set by the Snapshot space settings (e.g. Ethereum = 1) + +4. `snapshot`: Set by blockheight of the proposal (e.g. 11437846) + +5. `addresses`: A comma separated list of addresses to be queried + +The final URL is expected to look something like: `https://www.myapi.com/get_vote_count?network=1&snapshot=11437846&addresses=0xeD2bcC3104Da5F5f8fA988D6e9fAFd74Ae62f319,0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D` + +### List of params: +| Param | Description | Required | Default | +| --- | --- | --- | --- | +| `api` | The first part of the URL (e.g. https://www.myapi.com/) | Yes | | +| `strategy` (optional) | The resource name (e.g. get_vote_count) | Yes | '' | +| `staticFile` (optional) | Set to `true` if you want to use the `static` endpoint | No | `false` | +| `additionalParameters` (optional) | Any additional parameters you want to include | No | | +| `decimals` (optional) | The number of decimals to use when processing the scores from the API response | No | `0` | + +## Expected return of API +The API should return an object with the following structure: +``` +{ + "score": [ + { + "address": "0xeD2bcC3104Da5F5f8fA988D6e9fAFd74Ae62f319", + "score": "184000000000000000000" + }, + { + "address": "0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D", + "score": "7469258545106344000000000" + }, + { + "address": "0xd649bACfF66f1C85618c5376ee4F38e43eE53b63", + "score": "2509787861801245" + }, + { + "address": "0x726022a9fe1322fA9590FB244b8164936bB00489", + "score": "2179896139461561200000" + }, + { + "address": "0xc6665eb39d2106fb1DBE54bf19190F82FD535c19", + "score": "0" + }, + { + "address": "0x6ef2376fa6e12dabb3a3ed0fb44e4ff29847af68", + "score": "100420" + } + ] +} +``` + +Note that for the example above, `element.score` is a string representation in wei. Your response can return any value as long as: + 1. The return can be stringified `.toString()` + 2. The stringified version of your response can be passed into the second argument of `formatUnits`: https://docs.ethers.org/v3/api-utils.html?highlight=formatunits#ether-strings-and-wei + +## Testing +You can test this strategy by updating the `examples.json` file and running `npm run test --strategy=api` + +To test local changes, change to this directory and run: `npm run build & npm run test --strategy=api` diff --git a/Implementations/API/backend/utils/snapshot/strategies/api/examples.json b/Implementations/API/backend/utils/snapshot/strategies/api/examples.json new file mode 100644 index 00000000..dd6fc001 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "api", + "params": { + "api": "https://gateway.pinata.cloud/ipfs", + "strategy": "QmbmhTivxYuLE5uhNEALoBmvP7Yg9acA2Lkw9V9PqaEmw6" + } + }, + "network": "1", + "addresses": [ + "0xeD2bcC3104Da5F5f8fA988D6e9fAFd74Ae62f319", + "0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D", + "0xd649bACfF66f1C85618c5376ee4F38e43eE53b63", + "0x726022a9fe1322fA9590FB244b8164936bB00489", + "0xc6665eb39d2106fb1DBE54bf19190F82FD535c19", + "0x6ef2376fa6e12dabb3a3ed0fb44e4ff29847af68" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/api/index.ts b/Implementations/API/backend/utils/snapshot/strategies/api/index.ts new file mode 100644 index 00000000..d48e05d4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/api/index.ts @@ -0,0 +1,62 @@ +import { getAddress } from '@ethersproject/address'; +import fetch from 'cross-fetch'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'ganzai-san'; +export const version = '0.1.2'; + +const isIPFS = (apiURL) => { + return ( + apiURL.startsWith('https://gateway.pinata.cloud/ipfs/') || + apiURL.startsWith('https://ipfs.io/ipfs/') || + apiURL.startsWith('https://cloudflare-ipfs.com/ipfs/') + ); +}; + +const isStaticAPI = (apiURL: string): boolean => { + return apiURL.endsWith('.json'); +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const api: string = options.api; + const strategy: string = options.strategy || ''; + const additionalParameters: string = options.additionalParameters || ''; + const staticFile: boolean = options.staticFile || false; + + let api_url = api + '/' + strategy; + if (!isIPFS(api_url) && !isStaticAPI(api_url) && !staticFile) { + api_url += '?network=' + network; + api_url += '&snapshot=' + snapshot; + api_url += '&addresses=' + addresses.join(','); + } + if (additionalParameters) api_url += '&' + additionalParameters; + + const response = await fetch(api_url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + return Object.fromEntries( + data.score.map((value) => [ + getAddress(value.address), + parseFloat( + formatUnits( + value.score.toString(), + options.hasOwnProperty('decimals') ? options.decimals : 0 + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/examples.json new file mode 100644 index 00000000..32820871 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ari10-staking-locked", + "params": { + "address": "0xBCC1b6477B52212fb62734D21bC0ecAE684B4970", + "input": "lockedAmount", + "weight": 1 + } + }, + "network": "56", + "addresses": [ + "0x6cb10a891e5713d34076bbecd16d898f26b84b33", + "0xab59182e57fd3e4a459c36b6473b82b0d91d840f", + "0xf58951274cf026db8277f898204f954e8d3e56a3" + ], + "snapshot": 22879530 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/index.ts new file mode 100644 index 00000000..3a865b4f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/index.ts @@ -0,0 +1,36 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'oritwoen'; +export const version = '0.1.0'; + +const abi = [ + 'function userInfo(uint256, address) external view returns (uint256 amount, uint256 lockedAmount)' +]; + +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) => + multi.call(address, options.address, 'userInfo', [0, address]) + ); + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, info]) => [ + address, + parseFloat(formatUnits(info[options.input], 18)) * options.weight + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/schema.json new file mode 100644 index 00000000..ed5c641e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ari10-staking-locked/schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0xBCC1b6477B52212fb62734D21bC0ecAE684B4970"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "input": { + "type": "string", + "title": "Input", + "examples": ["e.g. lockedAmount"] + }, + "weight": { + "type": "number", + "title": "Weight", + "examples": ["e.g. 3"] + } + }, + "required": ["address", "input", "weight"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/README.md b/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/README.md new file mode 100644 index 00000000..796ea738 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/README.md @@ -0,0 +1,21 @@ +# arrakis-finance + +This strategy returns voters underlying token balance for a given Arrakis Finance pool + +## Params + +- `symbol` - (**Optional**, `string`) Symbol of ERC20 token +- `decimals` - (**Required**, `number`) Decimal precision for ERC20 token +- `tokenAddress` - (**Required**, `string`) Address of ERC20 token contract +- `poolAddress` - (**Required**, `string`) Address of Arrakis Finance pool (aka. vault) contract + +Here is an example of parameters: + +```json +{ + "symbol": "BANK", + "decimals": 18, + "tokenAddress": "0x2d94AA3e47d9D5024503Ca8491fcE9A2fB4DA198", + "poolAddress": "0x472D0B0DDFE0BC02C27928b8BcbD67E65D07d48a" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/examples.json new file mode 100644 index 00000000..fdb450e1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "arrakis-finance", + "params": { + "symbol": "BANK", + "decimals": 18, + "tokenAddress": "0x2d94AA3e47d9D5024503Ca8491fcE9A2fB4DA198", + "poolAddress": "0x472D0B0DDFE0BC02C27928b8BcbD67E65D07d48a" + } + }, + "network": "1", + "addresses": [ + "0xb6ac0341Fcf3FB507A8208D34a97f13779e1393D", + "0x3839acf1ee7699d1f46b1be840d8ad8317fdf757", + "0xD38cCf0988b375a9F4be1c8eEaeF5ECCA4bB8e87" + ], + "snapshot": 14765214 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/index.ts new file mode 100644 index 00000000..4efe4b14 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/index.ts @@ -0,0 +1,72 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'MantisClone'; +export const version = '0.1.0'; + +const abi = [ + 'function token0() external view returns (address)', + 'function token1() external view returns (address)', + 'function getUnderlyingBalances() external view returns (uint256 amount0Current, uint256 amount1Current)', + 'function totalSupply() public view returns (uint256)', + , + 'function balanceOf(address account) public view returns (uint256)' +]; + +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 }); + multi.call('token0', options.poolAddress, 'token0', []); + multi.call('token1', options.poolAddress, 'token1', []); + multi.call( + 'underlyingBalances', + options.poolAddress, + 'getUnderlyingBalances', + [] + ); + multi.call('lpTokenTotalSupply', options.poolAddress, 'totalSupply', []); + addresses.forEach((address) => + multi.call(`lpTokenBalances.${address}`, options.poolAddress, 'balanceOf', [ + address + ]) + ); + const result = await multi.execute(); + + const token0: string = result.token0; + const token1: string = result.token1; + const underlyingBalances: [BigNumber, BigNumber] = result.underlyingBalances; + const lpTotalSupply: BigNumber = result.lpTokenTotalSupply; + const lpBalances: Record = result.lpTokenBalances; + + let underlyingBalance: BigNumber; + if (options.tokenAddress === token0) { + underlyingBalance = underlyingBalances[0]; + } else if (options.tokenAddress === token1) { + underlyingBalance = underlyingBalances[1]; + } else { + throw new Error( + `token not in pool. poolAddress=${options.poolAddress}, tokenAddress=${options.tokenAddress}` + ); + } + + return Object.fromEntries( + Object.entries(lpBalances).map(([address, lpBalance]) => [ + address, + parseFloat( + formatUnits( + underlyingBalance.mul(lpBalance).div(lpTotalSupply), + options.decimals + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/schema.json b/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/schema.json new file mode 100644 index 00000000..252df425 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/arrakis-finance/schema.json @@ -0,0 +1,41 @@ +{ + "$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. BANK"], + "maxLength": 16 + }, + "decimals": { + "type": "number", + "title": "decimals", + "examples": ["e.g. 18"] + }, + "tokenAddress": { + "type": "string", + "title": "tokenAddress", + "examples": ["e.g. 0x2d94AA3e47d9D5024503Ca8491fcE9A2fB4DA198"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "poolAddress": { + "type": "string", + "title": "poolAddress", + "examples": ["e.g. 0x472D0B0DDFE0BC02C27928b8BcbD67E65D07d48a"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["decimals", "tokenAddress", "poolAddress"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/README.md b/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/README.md new file mode 100644 index 00000000..34585531 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/README.md @@ -0,0 +1,24 @@ +# arrow-vesting + +This strategy returns voters underlying token balance for a given Arrow vesting contract factory. + +Token balance for is address is equal to the sum of the locked balance in all vesting contracts holding it +as a beneficiary. + +## Params + +- `address` - (**Required**, `string`) Address of ERC20 token contract +- `symbol` - (**Optional**, `string`) Symbol of ERC20 token +- `decimals` - (**Required**, `number`) Decimal precision for ERC20 token +- `vestingFactory` - (**Required**, `string`) Address of Vesting Escrow Factory that creates vesting contracts holding ERC20 tokens + +Here is an example of parameters: + +```json +{ + "address": "0x78b3C724A2F663D11373C4a1978689271895256f", + "symbol": "ARROW", + "decimals": 18, + "vestingFactory": "0xB93427b83573C8F27a08A909045c3e809610411a" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/examples.json new file mode 100644 index 00000000..c862e4d7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "arrow-vesting", + "params": { + "address": "0x78b3C724A2F663D11373C4a1978689271895256f", + "symbol": "ARROW", + "decimals": 18, + "vestingFactory": "0xB93427b83573C8F27a08A909045c3e809610411a" + } + }, + "network": "10", + "addresses": [ + "0xaDc17e5f0e9F755C717B2beE43B590260034b852", + "0xB66f08DBd7A59B32e98033b9A1da08B5793DAb79", + "0x5b8eD2A2CfFCD474B2E688fdeA21CB5c4350E575", + "0x03b5Dc2CE78a7bEe9F66DD619b291595a2E166BB", + "0x06A61f56de8c6a2735D1Dea68340D201ddEd7348" + ], + "snapshot": 18879362 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/index.ts new file mode 100644 index 00000000..a8569d21 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/arrow-vesting/index.ts @@ -0,0 +1,122 @@ +import { Contract } from '@ethersproject/contracts'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'BrassLion'; +export const version = '0.1.1'; + +const vestingFactoryAbi = [ + 'function escrows_length() public view returns (uint256)', + 'function escrows(uint256 index) public view returns (address)' +]; + +const vestingContractAbi = [ + 'function recipient() public view returns (address)', + 'function total_locked() public view returns (uint256)', + 'function start_time() public view returns (uint256)', + 'function end_time() public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Get all vesting contract addresses. + const vestingFactory = new Contract( + options.vestingFactory, + vestingFactoryAbi, + provider + ); + + const vestingContractCount = await vestingFactory.escrows_length({ + blockTag: blockTag + }); + + const vestingFactoryMulti = new Multicaller( + network, + provider, + vestingFactoryAbi, + { blockTag } + ); + + [...Array(vestingContractCount.toNumber()).keys()].forEach((contractIdx) => { + vestingFactoryMulti.call(contractIdx, options.vestingFactory, 'escrows', [ + contractIdx + ]); + }); + + const vestingContracts: Record = + await vestingFactoryMulti.execute(); + + // Get all vesting contract parameters. + const vestingContractMulti = new Multicaller( + network, + provider, + vestingContractAbi, + { blockTag } + ); + + Object.values(vestingContracts).forEach((vestingContractAddress) => { + vestingContractMulti.call( + `${vestingContractAddress}.recipient`, + vestingContractAddress, + 'recipient', + [] + ); + vestingContractMulti.call( + `${vestingContractAddress}.total_locked`, + vestingContractAddress, + 'total_locked', + [] + ); + vestingContractMulti.call( + `${vestingContractAddress}.start_time`, + vestingContractAddress, + 'start_time', + [] + ); + vestingContractMulti.call( + `${vestingContractAddress}.end_time`, + vestingContractAddress, + 'end_time', + [] + ); + }); + + const vestingContractParameters: Record = + await vestingContractMulti.execute(); + + // Sum all vesting contract balances by recipient over requested addresses. + const block = await provider.getBlock(blockTag); + const time = block.timestamp; + const addressBalances: Record = {}; + + addresses.forEach((address) => { + addressBalances[address] = 0; + }); + + Object.entries(vestingContractParameters).forEach(([, params]) => { + const recipient = params['recipient']; + const start = params['start_time']; + + if (recipient in addressBalances && time > start) { + const locked = parseFloat( + formatUnits(params['total_locked'], options.decimals) + ); + const end = params['end_time']; + + addressBalances[recipient] += Math.min( + (locked * (time - start)) / (end - start), + locked + ); + } + }); + + return addressBalances; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/README.md b/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/README.md new file mode 100644 index 00000000..aa1838dd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/README.md @@ -0,0 +1,30 @@ +# aura-balance-of-vlaura-vebal + +This strategy returns proportional voting power for vlAURA holders based on system owned veBAL. + +The voting power is based on the raw balance, rather than delegated voting power. + +For example: +- there are 10000 vlAURA total supply +- a user has 2000 vlAURA (raw balance) +- Aura's voterProxy owns 100k veBAL + +In this example, the user has 20k veBAL balance as they own 20% of the vlAURA voting power. + +_Note: When depositing to the auraLocker, a user does not receive vlAURA until the next epoch has begun (Thursday at 00:00 UTC)_ + +## Params + +- `auraLocker` - (**Required**, `string`) Address of AuraLocker (vlAURA) contract +- `auraVoterProxy` - (**Required**, `string`) Address of Aura VoterProxy contract +- `votingEscrow` - (**Required**, `string`) Address of Balancer VotingEscrow contract + +Here is an example of parameters: + +```json +{ + "auraLocker": "0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC", + "auraVoterProxy": "0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2", + "votingEscrow": "0xC128a9954e6c874eA3d62ce62B468bA073093F25" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/examples.json new file mode 100644 index 00000000..648742d9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "aura-balance-of-vlaura-vebal", + "params": { + "auraLocker": "0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC", + "auraVoterProxy": "0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2", + "votingEscrow": "0xC128a9954e6c874eA3d62ce62B468bA073093F25" + } + }, + "network": "1", + "addresses": [ + "0x512fce9B07Ce64590849115EE6B32fd40eC0f5F3", + "0x808af82545A721C06D1FcCEbea915a6F5128BeF9", + "0x0CAd1d5ea8b4EeE26959cC00B4A3677f7A11e40F" + ], + "snapshot": 15276577 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/index.ts new file mode 100644 index 00000000..d294c271 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/index.ts @@ -0,0 +1,58 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = '0xButterfield'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) public view returns (uint256)', + 'function totalSupply() public view returns (uint256)' +]; + +interface Params { + auraLocker: string; + auraVoterProxy: string; + votingEscrow: string; +} + +interface Response { + vlAuraTotalSupply: BigNumber; + vlAuraBalance: Record; + veBalOwnedByAura: BigNumber; +} + +export async function strategy( + space, + network, + provider, + addresses, + options: Params, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + multi.call('vlAuraTotalSupply', options.auraLocker, 'totalSupply', []); + addresses.forEach((address) => + multi.call(`vlAuraBalance.${address}`, options.auraLocker, 'balanceOf', [ + address + ]) + ); + multi.call('veBalOwnedByAura', options.votingEscrow, 'balanceOf', [ + options.auraVoterProxy + ]); + const res: Response = await multi.execute(); + + return Object.fromEntries( + Object.entries(res.vlAuraBalance).map(([address, balance]) => [ + address, + parseFloat( + formatUnits( + res.veBalOwnedByAura.mul(balance).div(res.vlAuraTotalSupply), + 18 + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/schema.json b/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/schema.json new file mode 100644 index 00000000..85b8c6d2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-balance-of-vlaura-vebal/schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "auraLocker": { + "type": "string", + "title": "auraLocker", + "examples": ["e.g. 0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "auraVoterProxy": { + "type": "string", + "title": "auraVoterProxy", + "examples": ["e.g. 0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "votingEscrow": { + "type": "string", + "title": "votingEscrow", + "examples": ["e.g. 0xC128a9954e6c874eA3d62ce62B468bA073093F25"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["auraLocker", "auraVoterProxy", "votingEscrow"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/README.md b/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/README.md new file mode 100644 index 00000000..7b1cfe56 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/README.md @@ -0,0 +1,30 @@ +# aura-vault-balance-of-single-asset + +This strategy returns the balance of an underlying asset in a BPT LP pair staked on Aura + +For example: +- aura50WETH-50AURA-vault total supply: 137,880 (staked BPT) +- 50WETH-50AURA total supply: 145,710 (unstaked BPT) +- token of interest (either WETH or AURA): AURA +- Alice aura50WETH-50AURA-vault balance: 50,000 +- 50WETH-50AURA total Aura balance: 1,878,388 + +_Note: aura50WETH-50AURA-vault and 50WETH-50AURA minted 1:1_ + +Alice AURA balance: 1,878,388 AURA * 50,000 / 145,710 = 644,563 AURA + +## Params + +- `auraVaultDeposit` - (**Required**, `string`) Address of aura vault deposit token (ex: aura50WETH-50AURA-vault address) +- `tokenIndex` - (**Required**, `string`) Index of token in 50/50 pair on Balancer Vault +- `decimals` - (**Required**, `string`) Decimals of underlying asset + +Here is an example of parameters: + +```json +{ + "auraLocker": "0x712cc5bed99aa06fc4d5fb50aea3750fa5161d0f", + "auraVoterProxy": "1", + "votingEscrow": "18" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/examples.json new file mode 100644 index 00000000..bcb14ae8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "aura-vault-balance-of-single-asset", + "params": { + "auraVaultDeposit": "0x712cc5bed99aa06fc4d5fb50aea3750fa5161d0f", + "tokenIndex": "1", + "decimals": "18" + } + }, + "network": "1", + "addresses": [ + "0x905c1cA2ac32eE0799E4Aa31927f1166A93F3b17", + "0x4F76fF660dc5e37b098De28E6ec32978E4b5bEb6", + "0xDA6b2a5e0c56542984d84A710F90EefD94CA1991", + "0xa669070D106E96CC2390523E9745B425a1b3a0E0" + ], + "snapshot": 16897224 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/index.ts new file mode 100644 index 00000000..19001ea0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vault-balance-of-single-asset/index.ts @@ -0,0 +1,74 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'chuddster'; +export const version = '0.1.0'; + +const BALANCER_VAULT = '0xBA12222222228d8Ba445958a75a0704d566BF2C8'; + +const abi = [ + 'function asset() external view returns (address)', + 'function getPoolId() external view returns (bytes32)', + 'function totalSupply() external view returns (uint256)', + 'function balanceOf(address account) external view returns (uint256)', + 'function getPoolTokens(bytes32 poolId) external view returns (address[] tokens, uint256[] balances, uint256 lastChangeBlock)' +]; + +interface Params { + auraVaultDeposit: string; + tokenIndex: string; + decimals: string; +} + +export async function strategy( + space, + network, + provider, + addresses, + options: Params, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + multi.call('bptAsset', options.auraVaultDeposit, 'asset', []); + + const bptAssetResult: Record = await multi.execute(); + + multi.call('poolId', bptAssetResult.bptAsset, 'getPoolId', []); + + const poolIdResult: Record = await multi.execute(); + + multi.call('underlyingBalance', BALANCER_VAULT, 'getPoolTokens', [ + poolIdResult.poolId + ]); + + const underlyingBalanceResult = await multi.execute(); + const underlyingBalance = + underlyingBalanceResult.underlyingBalance.balances[ + parseInt(options.tokenIndex) + ]; + + multi.call('bptTotalSupply', bptAssetResult.bptAsset, 'totalSupply', []); + + const bptTotalSupply: Record = await multi.execute(); + + addresses.forEach((address) => + multi.call(address, options.auraVaultDeposit, 'balanceOf', [address]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat( + formatUnits( + balance.mul(underlyingBalance).div(bptTotalSupply.bptTotalSupply), + parseInt(options.decimals) + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/README.md b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/README.md new file mode 100644 index 00000000..8f832bb4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/README.md @@ -0,0 +1,30 @@ +# aura-vlaura-vebal-with-overrides + +This strategy returns proportional voting power for vlAURA holders based on system owned veBAL. + +For example: +- there are 10000 vlAURA total supply +- a user has 2000 vlAURA (voting power, delegated to them) +- Aura's voterProxy owns 100k veBAL + +In this example, the user has 20k veBAL voting power as they own 20% of the vlAURA voting power. + +Voters can optionally override their delegated voting power. + +_Note: When depositing to the auraLocker, a user does not receive vlAURA until the next epoch has begun (Thursday at 00:00 UTC)_ + +## Params + +- `auraLocker` - (**Required**, `string`) Address of AuraLocker (vlAURA) contract +- `auraVoterProxy` - (**Required**, `string`) Address of Aura VoterProxy contract +- `votingEscrow` - (**Required**, `string`) Address of Balancer VotingEscrow contract + +Here is an example of parameters: + +```json +{ + "auraLocker": "0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC", + "auraVoterProxy": "0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2", + "votingEscrow": "0xC128a9954e6c874eA3d62ce62B468bA073093F25" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/examples.json new file mode 100644 index 00000000..1442c116 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query Aura with overrides", + "strategy": { + "name": "aura-vlaura-vebal-with-overrides", + "params": { + "auraLocker": "0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC", + "auraVoterProxy": "0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2", + "votingEscrow": "0xC128a9954e6c874eA3d62ce62B468bA073093F25" + } + }, + "network": "1", + "addresses": [ + "0xB1f881f47baB744E7283851bC090bAA626df931d", + "0x808af82545A721C06D1FcCEbea915a6F5128BeF9", + "0x0CAd1d5ea8b4EeE26959cC00B4A3677f7A11e40F", + "0x3cde8fa1c73afe0828f672d197e082463c2ac8e2", + "0xca86d57519dbfe34a25eef0923b259ab07986b71" + ], + "snapshot": 15184638 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/index.ts new file mode 100644 index 00000000..69372c5b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/index.ts @@ -0,0 +1,76 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { strategy as erc20VotesWithOverrideStrategy } from '../erc20-votes-with-override'; + +export const author = '0xMaharishi'; +export const version = '0.1.0'; + +const abi = [ + 'function delegates(address account) external view returns (address)', + 'function getVotes(address account) external view returns (uint256)', + 'function totalSupply() public view returns (uint256)', + 'function balanceOf(address account) public view returns (uint256)' +]; + +interface Options { + auraLocker: string; + auraVoterProxy: string; + votingEscrow: string; + includeSnapshotDelegations?: boolean; + delegationSpace?: string; +} + +interface Response { + vlAuraTotalSupply: BigNumber; + veBalOwnedByAura: BigNumber; +} + +/* + Based on the `erc20-votes-with-override` strategy, with global vote scaling + to represent the share of Aura's veBAL. +*/ +export async function strategy( + space, + network, + provider, + addresses, + options: Options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + multi.call('vlAuraTotalSupply', options.auraLocker, 'totalSupply', []); + multi.call('veBalOwnedByAura', options.votingEscrow, 'balanceOf', [ + options.auraVoterProxy + ]); + const res: Response = await multi.execute(); + + const scores: Record = await erc20VotesWithOverrideStrategy( + space, + network, + provider, + addresses, + { + address: options.auraLocker, + delegatesName: 'delegates', + balanceOfName: 'balanceOf', + getVotesName: 'getVotes', + decimals: 18, + includeSnapshotDelegations: options.includeSnapshotDelegations, + delegationSpace: options.delegationSpace + }, + snapshot + ); + + const veBalOwnedByAura = parseFloat(formatUnits(res.veBalOwnedByAura)); + const vlAuraTotalSupply = parseFloat(formatUnits(res.vlAuraTotalSupply)); + + return Object.fromEntries( + Object.entries(scores).map(([address, score]) => [ + address, + (veBalOwnedByAura * score) / vlAuraTotalSupply + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/schema.json b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/schema.json new file mode 100644 index 00000000..85b8c6d2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal-with-overrides/schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "auraLocker": { + "type": "string", + "title": "auraLocker", + "examples": ["e.g. 0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "auraVoterProxy": { + "type": "string", + "title": "auraVoterProxy", + "examples": ["e.g. 0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "votingEscrow": { + "type": "string", + "title": "votingEscrow", + "examples": ["e.g. 0xC128a9954e6c874eA3d62ce62B468bA073093F25"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["auraLocker", "auraVoterProxy", "votingEscrow"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/README.md b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/README.md new file mode 100644 index 00000000..2ccf3397 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/README.md @@ -0,0 +1,28 @@ +# aura-vlaura-vebal + +This strategy returns proportional voting power for vlAURA holders based on system owned veBAL. + +For example: +- there are 10000 vlAURA total supply +- a user has 2000 vlAURA (voting power, delegated to them) +- Aura's voterProxy owns 100k veBAL + +In this example, the user has 20k veBAL voting power as they own 20% of the vlAURA voting power. + +_Note: When depositing to the auraLocker, a user does not receive vlAURA until the next epoch has begun (Thursday at 00:00 UTC)_ + +## Params + +- `auraLocker` - (**Required**, `string`) Address of AuraLocker (vlAURA) contract +- `auraVoterProxy` - (**Required**, `string`) Address of Aura VoterProxy contract +- `votingEscrow` - (**Required**, `string`) Address of Balancer VotingEscrow contract + +Here is an example of parameters: + +```json +{ + "auraLocker": "0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC", + "auraVoterProxy": "0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2", + "votingEscrow": "0xC128a9954e6c874eA3d62ce62B468bA073093F25" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/examples.json b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/examples.json new file mode 100644 index 00000000..6d4f34d9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "aura-vlaura-vebal", + "params": { + "auraLocker": "0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC", + "auraVoterProxy": "0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2", + "votingEscrow": "0xC128a9954e6c874eA3d62ce62B468bA073093F25" + } + }, + "network": "1", + "addresses": [ + "0xB1f881f47baB744E7283851bC090bAA626df931d", + "0x808af82545A721C06D1FcCEbea915a6F5128BeF9", + "0x0CAd1d5ea8b4EeE26959cC00B4A3677f7A11e40F" + ], + "snapshot": 14974420 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/index.ts b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/index.ts new file mode 100644 index 00000000..8b4e6886 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/index.ts @@ -0,0 +1,59 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = '0xMaharishi'; +export const version = '0.1.0'; + +const abi = [ + 'function getVotes(address account) external view returns (uint256)', + 'function totalSupply() public view returns (uint256)', + 'function balanceOf(address account) public view returns (uint256)' +]; + +interface Params { + auraLocker: string; + auraVoterProxy: string; + votingEscrow: string; +} + +interface Response { + vlAuraTotalSupply: BigNumber; + vlAuraVotes: Record; + veBalOwnedByAura: BigNumber; +} + +export async function strategy( + space, + network, + provider, + addresses, + options: Params, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + multi.call('vlAuraTotalSupply', options.auraLocker, 'totalSupply', []); + addresses.forEach((address) => + multi.call(`vlAuraVotes.${address}`, options.auraLocker, 'getVotes', [ + address + ]) + ); + multi.call('veBalOwnedByAura', options.votingEscrow, 'balanceOf', [ + options.auraVoterProxy + ]); + const res: Response = await multi.execute(); + + return Object.fromEntries( + Object.entries(res.vlAuraVotes).map(([address, votes]) => [ + address, + parseFloat( + formatUnits( + res.veBalOwnedByAura.mul(votes).div(res.vlAuraTotalSupply), + 18 + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/schema.json b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/schema.json new file mode 100644 index 00000000..85b8c6d2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/aura-vlaura-vebal/schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "auraLocker": { + "type": "string", + "title": "auraLocker", + "examples": ["e.g. 0x3Fa73f1E5d8A792C80F426fc8F84FBF7Ce9bBCAC"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "auraVoterProxy": { + "type": "string", + "title": "auraVoterProxy", + "examples": ["e.g. 0xaF52695E1bB01A16D33D7194C28C42b10e0Dbec2"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "votingEscrow": { + "type": "string", + "title": "votingEscrow", + "examples": ["e.g. 0xC128a9954e6c874eA3d62ce62B468bA073093F25"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["auraLocker", "auraVoterProxy", "votingEscrow"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/avn-balance-of-staked/examples.json b/Implementations/API/backend/utils/snapshot/strategies/avn-balance-of-staked/examples.json new file mode 100644 index 00000000..eaf1f114 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/avn-balance-of-staked/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "avn-balance-of-staked", + "params": { + "vrAddress": "0x8D9D42AF6D048B296cDE8B28693b276dF5e8bfB1", + "vr2Address": "0xE30dE0b5d61B297Da4CA00cFF583CaEdEDaa0C42", + "tokenAddress": "0x0d88ed6e74bbfd96b831231638b66c05571e824f", + "symbol": "AVT", + "decimals": 18, + "start": 11750517 + } + }, + "network": "1", + "addresses": ["0xeEfbADa5539f9ae2c87C1151Cdb2057398383342"], + "snapshot": 12273608 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/avn-balance-of-staked/index.ts b/Implementations/API/backend/utils/snapshot/strategies/avn-balance-of-staked/index.ts new file mode 100644 index 00000000..519e6de8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/avn-balance-of-staked/index.ts @@ -0,0 +1,162 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'andrew-frank'; +export const version = '0.1.1'; + +const AVT_ABI = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +const VR_ABI = [ + { + inputs: [ + { internalType: 'uint8', name: 'node', type: 'uint8' }, + { internalType: 'address', name: 'staker', type: 'address' } + ], + name: 'getStakerBalance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +]; + +const NUM_NODES = 10; +// [0, 1, ... , 9] for convinience +const NODES_INDICES = Array.from(Array(NUM_NODES).keys()); +const STAKES_MULTIPLIER = 1; + +class EthCall { + constructor( + public readonly contract: string, + public readonly method: string, + public readonly args: Array + ) {} + get ethCall(): any[] { + return [this.contract, this.method, this.args]; + } +} + +/** creates flat array of eth calls for each user's stake in each VR contract in each node */ +function serializeVrMultiCalls( + vr1Address: string, + vr2Address: string, + userAddresses: string[] +) { + // [ [0, 1, ... , 19], [0, 1, ... , 19] , ..., [0, 1, ... 19], ... ] + const userCalls: EthCall[][] = userAddresses.map((user: string) => { + const method = 'getStakerBalance'; + // map to objects to prevent flatting eth call arrays + const vr1Calls = NODES_INDICES.map( + (node: number) => new EthCall(vr1Address, method, [node, user]) + ); + const vr2Calls = NODES_INDICES.map( + (node: number) => new EthCall(vr2Address, method, [node, user]) + ); + // * [0-9] calls for each node in VR1 + // * [10-19] calls for each node in VR2 + return vr1Calls.concat(vr2Calls); + }); + // flat it and map to a list of the call primitives + const objCalls = userCalls.flat(); + return objCalls.map((obj) => obj.ethCall); +} + +/** splits array into chunks */ +function chunkArray(arr: T[], length: number): T[][] { + const chunks: T[][] = []; + let i = 0; + const n = arr.length; + + while (i < n) { + chunks.push(arr.slice(i, (i += length))); + } + + return chunks; +} + +/** sums big numbers in array */ +function sumNumbers(arr: BigNumber[]): BigNumber { + return arr.reduce((previus, current) => { + return previus.add(current[0]); + }, BigNumber.from(0)); +} + +/** + * Parses multicall response + * @param response multicall response + * @returns summed AVT staked for each user in every node in every VR contract + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function parseVrResponse(response: BigNumber[], users: string[]): BigNumber[] { + return chunkArray(response, 2 * NUM_NODES).map(sumNumbers); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // users AVTs + const avtResponses: Array<[BigNumber]> = await multicall( + network, + provider, + AVT_ABI, + addresses.map((address: any) => [ + options.tokenAddress, + 'balanceOf', + [address] + ]), + { blockTag } + ); + const avtValues = avtResponses.map((value) => value[0]); + + // users AVT staked in VR contracts + const vrMultiResponse = await multicall( + network, + provider, + VR_ABI, + serializeVrMultiCalls(options.vrAddress, options.vr2Address, addresses), + { blockTag } + ); + const stakes = parseVrResponse(vrMultiResponse, addresses); + + // calculate the scores + const vrVotes = stakes.map((v) => v.mul(STAKES_MULTIPLIER)); + const scores = avtValues.map((value, i) => { + return value.add(vrVotes[i]); + }); + + return Object.fromEntries( + scores.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/babywealthyclub/examples.json b/Implementations/API/backend/utils/snapshot/strategies/babywealthyclub/examples.json new file mode 100644 index 00000000..d77fe1f8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/babywealthyclub/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "babywealthyclub", + "params": { + "address": "0x1f9655c660B915b4344b71e2A6d3155C57AD1bB6", + "symbol": "BRC", + "decimals": "9", + "weighted": 20000000000 + } + }, + "network": "56", + "addresses": [ + "0x7F57612b01689a0E4f133730AE20cC19E3dF0208", + "0x4A7998DF2Cd16815271bb6b7d3aE7EB30f50a73a", + "0x08D816526BdC9d077DD685Bd9FA49F58A5Ab8e48", + "0x21fF20E7e1B820020415707298b92299CF0951fE", + "0xa7A01B93B889ff0639d5ec02914A77529924a46F" + ], + "snapshot": 23976608 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/babywealthyclub/index.ts b/Implementations/API/backend/utils/snapshot/strategies/babywealthyclub/index.ts new file mode 100644 index 00000000..aea541d0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/babywealthyclub/index.ts @@ -0,0 +1,48 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'gp6284'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; +const BWC_ADDRESS = '0xb7F7c7D91Ede27b019e265F8ba04c63333991e02'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const erc20Balance = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [BWC_ADDRESS, 'balanceOf', [address]]), + { blockTag } + ); + + return Object.fromEntries( + addresses.map((address, i) => [ + address, + parseFloat(formatUnits(response[i].toString(), 0)) > 0 + ? Math.floor( + erc20Balance[addresses[i]] / (options.weighted || 10000000000) + ) + : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/badgeth/examples.json b/Implementations/API/backend/utils/snapshot/strategies/badgeth/examples.json new file mode 100644 index 00000000..7e78410a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/badgeth/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Badgeth", + "strategy": { + "name": "badgeth", + "params": { + "symbol": "BADGETH" + } + }, + "network": "4", + "snapshot": 8876318, + "addresses": [ + "0xBcE0b2edcA1fBE32891Ac6096b8ea7408dd099c2", + "0x8cABEC8fe71A3604E21e6E2BB55463EC6e26fBf8", + "0x85924aA0B2cb5a0BbeC583Dd090bF7CEdBa5D2Ea", + "0x9149B2b87159c4CC9e2f10C2711357720Da4DA08", + "0xa0710d3b4BA0f848f7edf9CC827aF70A183EAd26", + "0xAE1220f6bFEb414Ed0A95fbb5A6Ecc303b10aa46", + "0xebe986802F7858E1919451C6Ff893e294F31CE54", + "0x2d7cAA8462023af022A5004dA7b781b8ccF81Da7", + "0xE26217836Dd71f49c58a68aD70DabFA1E6d0B75b", + "0x7C572bE1751DdCFeE930286836bF21E6d87c10e6", + "0x6fDcfFDBa2699543a926f0C092F769f3302D3519", + "0x0f6feb3ba20c56e94cfbcd98339e99bce629d912" + ] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/badgeth/index.ts b/Implementations/API/backend/utils/snapshot/strategies/badgeth/index.ts new file mode 100644 index 00000000..5135caf3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/badgeth/index.ts @@ -0,0 +1,47 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'Badgeth'; +export const version = '0.1.0'; + +const BADGETH_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/hardforksoverknives/badgeth-dev'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const params = { + voters: { + __args: { + where: { + id_in: addresses, + votingPower_gt: 0 + }, + first: 1000, + orderBy: 'votingPower', + orderDirection: 'desc' + }, + id: true, + votingPower: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.voters.__args.block = { number: snapshot }; + } + + const score = {}; + const result = await subgraphRequest(BADGETH_SUBGRAPH_URL, params); + if (result && result.voters) { + result.voters.forEach((voter) => { + score[getAddress(voter.id)] = parseInt(voter.votingPower); + }); + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/README.md b/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/README.md new file mode 100644 index 00000000..d5a80e38 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/README.md @@ -0,0 +1,55 @@ +# vDfyn contract call strategy + +This strategy allows users to calculate the total amount of Dfyn staked by the user in the vDfyn vault. + +## Example + +The space config will look like this: + +```JSON +{ + "strategies": [ + ["balance-in-vdfyn-vault", { + // vDfyn vault contract + "contractAddress": "0x75455c3DE45dD32CBE9a5aD5E518D3D50823c976", + // scoreMultiplier can be used to increase users' scores by a certain magnitude + "scoreMultiplier": 2, + // ABI for balanceOf method + "methodABI": [ + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ratio", + "outputs": [ + { + "internalType": "uint256", + "name": "dfynAmount_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + }], + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/examples.json new file mode 100644 index 00000000..8c04c906 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/examples.json @@ -0,0 +1,54 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "balance-in-vdfyn-vault", + "params": { + "name": "vDfyn Vault", + "contractAddress": "0x75455c3DE45dD32CBE9a5aD5E518D3D50823c976", + "symbol": "VDFYN", + "scoreMultiplier": 1, + "methodABI": [ + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ratio", + "outputs": [ + { + "internalType": "uint256", + "name": "dfynAmount_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + } + }, + "network": "137", + "addresses": [ + "0x41CdE29bF9aea095fDD204D150451678b2c6A736", + "0xD7B26f34775fa41D6dCE182851642573aBF9B532" + ], + "snapshot": 19310208 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/index.ts new file mode 100644 index 00000000..8f1fd78e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-in-vdfyn-vault/index.ts @@ -0,0 +1,53 @@ +import { formatUnits } from '@ethersproject/units'; +import { call, multicall } from '../../utils'; + +export const author = 'vatsalgupta13'; +export const version = '0.1.0'; + +function chunk(array, chunkSize) { + const tempArray: any[] = []; + for (let i = 0, len = array.length; i < len; i += chunkSize) + tempArray.push(array.slice(i, i + chunkSize)); + return tempArray; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + let callData: [any, string, [any]][] = []; + const ratio = await call(provider, options.methodABI, [ + options.contractAddress, + 'ratio', + [] + ]); + // console.log('ratio: ', ratio.toString()); + addresses.map((userAddress: any) => { + callData.push([options.contractAddress, 'balanceOf', [userAddress]]); + }); + callData = [...chunk(callData, 2000)]; // chunking the callData into multiple arrays of 2000 requests + const response: any[] = []; + for (let i = 0; i < callData.length; i++) { + const tempArray = await multicall( + network, + provider, + options.methodABI, + callData[i], + { blockTag } + ); + response.push(...tempArray); + } + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + options.scoreMultiplier * + ratio * + parseFloat(formatUnits(value.toString(), 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/README.md b/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/README.md new file mode 100644 index 00000000..86275dbc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/README.md @@ -0,0 +1,24 @@ +# Balance of subgraph + +To calculate the token balance including user's EOA and smart wallet, we developed this strategy. Developers can create their own subparagraphs using the below scheme, and the score will be calculated as a result. + +``` +users{ + id + amount +} +``` + + +## Example + +The space config will look like this: + +```JSON +{ + // subgraphURL for the request + "subGraphURL": "https://api.thegraph.com/subgraphs/name/dinngodev/furucombo-tokenomics-mainnet", + // scoreMultiplier can be used to increase users' scores by a certain magnitude + "scoreMultiplier": 1, +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/examples.json new file mode 100644 index 00000000..c97beb9c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "balance-of-subgraph", + "params": { + "subGraphURL": "https://api.thegraph.com/subgraphs/name/dinngodev/furucombo-tokenomics-mainnet" + } + }, + "network": "1", + "addresses": [ + "0xe4ef29545db14e252AeC1c660A004e2408Dc62d2", + "0xa3c1c91403f0026b9dd086882adbc8cdbc3b3cfb" + ], + "snapshot": 14716396 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/index.ts new file mode 100644 index 00000000..0761ec0d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-subgraph/index.ts @@ -0,0 +1,55 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/dinngodev/furucombo-tokenomics-mainnet', + '137': + 'https://api.thegraph.com/subgraphs/name/dinngodev/furucombo-tokenomics-polygon' +}; + +export const author = 'weizard'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()), + amount_gt: 0 + } + }, + id: true, + amount: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.__args.block = { number: snapshot }; + } + const result = await subgraphRequest( + options.subGraphURL ? options.subGraphURL : SUBGRAPH_URL[network], + params + ); + const score = {}; + if (result && result.users) { + result.users.forEach((user) => { + const userAddress = getAddress(user.id); + let userScore = Number(user.amount); + + if (options.scoreMultiplier) { + userScore = userScore * options.scoreMultiplier; + } + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScore; + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-linear-vesting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-linear-vesting-power/examples.json new file mode 100644 index 00000000..35c0eb32 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-linear-vesting-power/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "balance-of-with-linear-vesting-power", + "params": { + "ERC20Address": "0x9994E35Db50125E0DF82e4c2dde62496CE330999", + "DSSVestAddress": "0x6017dd61f4d0C8123f160F99058Adc5671dF6447", + "decimals": 18, + "vestingNetwork": 1, + "vestingDuration": 94608000, + "startVesting": 1656078603 + } + }, + "network": "1", + "addresses": [ + "0xd9286ab8Ac06a428Bbb45Bfd785f9A22FD0ef0Bd", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 15076584 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-linear-vesting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-linear-vesting-power/index.ts new file mode 100644 index 00000000..504eb900 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-linear-vesting-power/index.ts @@ -0,0 +1,136 @@ +import { call, Multicaller } from '../../utils'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'morpho-labs'; +export const version = '0.1.0'; + +const DSSVestAbi = [ + 'function usr(uint256 _id) external view returns (address)', + 'function tot(uint256 _id) external view returns (uint256)', + 'function accrued(uint256 _id) external view returns (uint256)', + 'function ids() external view returns (uint256)' +]; +const ERC20abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +const vestedAmountPower = ( + totalVestedNotClaimed: BigNumberish, + startDate: BigNumberish, + period: BigNumberish, + now: BigNumberish +) => { + now = BigNumber.from(now); + const amount = BigNumber.from(totalVestedNotClaimed); + + if (now.lte(startDate)) return BigNumber.from(0); + if (now.gt(BigNumber.from(startDate).add(period))) + return totalVestedNotClaimed; + return now.sub(startDate).mul(amount).div(period); +}; + +const idsArray = (maxId: number) => + Array.from({ length: maxId }, (_, i) => i + 1); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const block = await provider.getBlock(blockTag); + const now = block.timestamp; + // fetch the number of vesting accounts + const maxId: BigNumber = await call( + provider, + DSSVestAbi, + [options.DSSVestAddress, 'ids', []], + { + blockTag + } + ); + + // create an array of each vesting ID: [1:maxId] + const ids = idsArray(maxId.toNumber()); + + // We first fetch the balance of ERC20 for each addresses + const multiBalanceOf = new Multicaller(network, provider, ERC20abi, { + blockTag + }); + addresses.forEach((address) => + multiBalanceOf.call(address, options.ERC20Address, 'balanceOf', [address]) + ); + const addressesBalanceOf: Record = + await multiBalanceOf.execute(); + + // And then, we fetch the vesting data for each vesting ID + // 1. vester address + const multiVest = new Multicaller( + options.vestingNetwork, + provider, + DSSVestAbi, + { + blockTag + } + ); + + ids.forEach((id) => multiVest.call(id, options.DSSVestAddress, 'usr', [id])); + const vestedAddresses: Record = await multiVest.execute(); + + // 2. total vested + const multiVestTotCaller = new Multicaller( + options.vestingNetwork, + provider, + DSSVestAbi, + { blockTag } + ); + ids.forEach((id) => + multiVestTotCaller.call(id, options.DSSVestAddress, 'tot', [id]) + ); + const multiVestTot: Record = + await multiVestTotCaller.execute(); + + // 3. total claimed + const multiVestAccruedCaller = new Multicaller( + options.vestingNetwork, + provider, + DSSVestAbi, + { blockTag } + ); + ids.forEach((id) => + multiVestAccruedCaller.call(id, options.DSSVestAddress, 'accrued', [id]) + ); + const multiVestAccrued: Record = + await multiVestAccruedCaller.execute(); + return Object.fromEntries( + Object.entries(addressesBalanceOf).map(([address, balance]) => { + const initialVotingPower = [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]; + // fetch vested users data + const [id] = + Object.entries(vestedAddresses).find( + ([, _address]) => _address === address + ) ?? []; + if (id === undefined) return initialVotingPower; + const totalVested = multiVestTot[id]; + const totalAccrued = multiVestAccrued[id]; + if (!(id && totalAccrued && totalVested)) return initialVotingPower; + const votingPower = BigNumber.from(balance).add( + vestedAmountPower( + BigNumber.from(totalVested).sub(totalAccrued), + options.startVesting, + options.vestingDuration, + now + ) + ); + return [address, parseFloat(formatUnits(votingPower, options.decimals))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-min/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-min/examples.json new file mode 100644 index 00000000..b2185112 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-min/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "balance-of-with-min", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18, + "minBalance": 20 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-min/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-min/index.ts new file mode 100644 index 00000000..c5e27e87 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-min/index.ts @@ -0,0 +1,29 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'samuveth'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + Object.keys(score).forEach((key) => { + if (score[key] >= (options.minBalance || 0)) score[key] = score[key]; + else score[key] = 0; + }); + + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/README.md b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/README.md new file mode 100644 index 00000000..490754f4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/README.md @@ -0,0 +1,19 @@ +# balance-of-with-thresholds + +This strategy return the balances of the voters for a specific ERC20 or ERC721 and maps them to the number of votes that voter gets based on a table of minimum thresholds. + +Here is an example of parameters: + +```json +{ + "address": "0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d", + "symbol": "LAND", + "decimals": 0, + "thresholds": [ + { "threshold": 1, "votes": 1 }, + { "threshold": 4, "votes": 2 }, + { "threshold": 11, "votes": 3 }, + { "threshold": 25, "votes": 4 } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/examples.json new file mode 100644 index 00000000..62ad8ee8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "balance-of-with-thresholds", + "params": { + "address": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", + "symbol": "BAYC", + "decimals": 0, + "thresholds": [ + { "threshold": 1, "votes": 1 }, + { "threshold": 4, "votes": 2 }, + { "threshold": 11, "votes": 3 }, + { "threshold": 25, "votes": 4 } + ] + } + }, + "network": "1", + "addresses": [ + "0xed47015bb8080b9399f9d0ddfc427b9cee2caab1", + "0xf56345338cb4cddaf915ebef3bfde63e70fe3053", + "0x7b15e6c439b27a553b65a9904ce571da6691a0fb", + "0x8d2f3a76a76f055d62a931678ab16b042e7badeb" + ], + "snapshot": 12453212 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/index.ts new file mode 100644 index 00000000..3c32c204 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balance-of-with-thresholds/index.ts @@ -0,0 +1,44 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'lucid-eleven'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const thresholds = options.thresholds || [{ threshold: 1, votes: 1 }]; + if (thresholds.length == 0) thresholds.push({ threshold: 1, votes: 1 }); + + const calculateVotes = (balance) => + thresholds + .sort((a, b) => b.threshold - a.threshold) + .find((t) => t.threshold <= balance)?.votes ?? 0; + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [options.address, 'balanceOf', [address]]), + { blockTag } + ); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + calculateVotes( + parseFloat(formatUnits(value.toString(), options.decimals)) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balancer-delegation/examples.json new file mode 100644 index 00000000..b6a21bf6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-delegation/examples.json @@ -0,0 +1,33 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "balancer-delegation", + "params": { + "address": "0xba100000625a3754423978a60c9317c58a424e3D", + "symbol": "BAL BPT V1+V2 (delegated)", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x808A023B72260170c95d831F589A1ae0DCa1e43E", + "0xC0810E5f17915dFe97b57947A3d84094572689ac", + "0x7dd508a1e4Da1243789B799a480f8B45e58b1B5b", + "0x512fce9B07Ce64590849115EE6B32fd40eC0f5F3", + "0x055b441729041DC9F25a296f48604bf6a36B12F2", + "0x3839AcF1ee7699D1F46b1BE840D8aD8317FDf757", + "0x2FAf55a544c5F73666438BC185aeCC9D685E6E3C", + "0xa586Cbf75fB4b8987bAb7d24BE4545fcaa0e757C", + "0x78993bfE37DA5a8DF2D5a8d7213A41c26B20B49D", + "0xB0331b22161cA290A15f825A29C008dCB5e1ff68", + "0x8b6545E4Fd7D5Cc858BE7Ad449F88eF1b2f7a577", + "0xdB19c47E87Ed3Ff37425a99B9Dee1f4920F755b9", + "0x562821C81BBbFFa42443064917Ee4D90036Fba7c", + "0x2fcDa75f0038D54BD0f0d88D5Db2a62EA298AF36", + "0x39D787fdf7384597C7208644dBb6FDa1CcA4eBdf", + "0xD142c9cfE2F0BEF4c14594358fB65aeE1B726d2C" + ], + "snapshot": 11706500 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balancer-delegation/index.ts new file mode 100644 index 00000000..5151cb48 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-delegation/index.ts @@ -0,0 +1,29 @@ +import { strategy as delegation } from '../delegation'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + return await delegation( + space, + network, + provider, + addresses, + { + strategies: [ + { + name: 'balancer', + params: options + } + ] + }, + snapshot + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/README.md new file mode 100644 index 00000000..78369ef8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/README.md @@ -0,0 +1,14 @@ +# balancer-erc20-internal-balance-of + +This returns the internal balances of the voters for a specific ERC20 token in the Balancer V2 Vault. + +Here is an example of parameters: + +```json +{ + "vault": "0xba12222222228d8ba445958a75a0704d566bf2c8", + "token": "0xba100000625a3754423978a60c9317c58a424e3D", + "symbol": "BAL", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/examples.json new file mode 100644 index 00000000..b5fa0811 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/examples.json @@ -0,0 +1,37 @@ +[ + { + "name": "Internal Balance", + "strategy": { + "name": "balancer-erc20-internal-balance-of", + "params": { + "vault": "0xba12222222228d8ba445958a75a0704d566bf2c8", + "token": "0xba100000625a3754423978a60c9317c58a424e3D", + "symbol": "BAL", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268", + "0x794846f3291E55E00662d37Ef048Aa716dF9ECbf" + ], + "snapshot": 13043171 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/index.ts new file mode 100644 index 00000000..b3a808a2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-erc20-internal-balance-of/index.ts @@ -0,0 +1,37 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'gerrrg'; +export const version = '0.0.1'; + +const abi = [ + 'function getInternalBalance(address user, address[] tokens) external view returns (uint256[] balances)' +]; + +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) => + multi.call(address, options.vault, 'getInternalBalance', [ + address, + [options.token] + ]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance[0], options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/README.md b/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/README.md new file mode 100644 index 00000000..0d687cc6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/README.md @@ -0,0 +1,16 @@ +# Balancer-poolId + +This strategy returns balances of the underlying token in Balancer LP pools. +The use of poolId and pagination makes it better suited to balancer pools with lots of shares (more than 1000) + +Here is an example of parameters: + +```json +{ + "token": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "poolId": "0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a", +} +``` + +- *token* - the underlying token +- *poolId* - the id of the Balancer pool diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/examples.json new file mode 100644 index 00000000..32b58f54 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "balancer-poolid", + "params": { + "poolId": "0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a", + "token": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "symbol": "PSP" + } + }, + "network": "1", + "addresses": [ + "0x0951ff0835302929d6c0162b3d2495a85e38ec3a", + "0x5d577c1cdaf838b264c7d977449c776ef664d654", + "0xefa5121ddac8d083a5a4c3d42e2bfaab2f0390dc", + "0x0ddc793680ff4f5793849c8c6992be1695cbe72a" + ], + "snapshot": 13946324 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/index.ts new file mode 100644 index 00000000..58a6ee93 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-poolid/index.ts @@ -0,0 +1,107 @@ +// Optimized Balancer strategy using poolId to narrow the poolShares returned by the graphql endpoint + using pagination for over 1000 poolshares + +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'jo-chemla'; +export const version = '0.1.0'; + +const PAGE_SIZE = 1000; +const BALANCER_SUBGRAPH_URL_ROOT = + 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer'; + +const NETWORK_KEY = { + '1': '', + '42': '-kovan', + '137': '-polygon', + '42161': '-arbitrum' +}; + +function buildBalancerSubgraphUrl(chainId, version) { + const networkString = NETWORK_KEY[chainId]; + const versionString = version == 2 ? '-v2' : ''; + return `${BALANCER_SUBGRAPH_URL_ROOT}${networkString}${versionString}`; +} + +const params = { + pool: { + __args: { + id: '' + }, + totalShares: true, + tokens: { + id: true, + balance: true + }, + shares: { + __args: { + where: { + userAddress_in: [], + balance_gt: 0 + }, + first: PAGE_SIZE + }, + userAddress: { + id: true + }, + balance: true + } + } +}; +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + // @ts-ignore + params.pool.__args.id = options.poolId; + params.pool.shares.__args.where.userAddress_in = addresses.map((address) => + address.toLowerCase() + ); + + if (snapshot !== 'latest') { + // @ts-ignore + params.pool.__args.block = { number: snapshot }; + } + + // iterate through Balancer V1 & V2 Subgraphs + const score = {}; + for (let version = 1; version <= 2; version++) { + // Skip attempt to query subgraph on networks where V1 isn't deployed + if (network != 1 && network != 42 && version === 1) continue; + + let page = 0; + while (page !== -1) { + // @ts-ignore + params.pool.shares.__args.skip = page * PAGE_SIZE; + + const result = await subgraphRequest( + buildBalancerSubgraphUrl(network, version), + params + ); + if (result && result.pool) { + const pool = result.pool; + pool.shares.forEach((poolShare) => { + pool.tokens.map((poolToken) => { + const [, tokenAddress] = poolToken.id.split('-'); + if (tokenAddress === options.token.toLowerCase()) { + const userAddress = getAddress(poolShare.userAddress.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = + score[userAddress] + + (poolShare.balance / pool.totalShares) * poolToken.balance; + } + }); + }); + // If more shares, use pagination + page = pool.shares.length < PAGE_SIZE ? -1 : page + 1; + } else { + page = -1; + } + } + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-smart-pool/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balancer-smart-pool/examples.json new file mode 100644 index 00000000..cf970d27 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-smart-pool/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Balancer Smart Pool", + "strategy": { + "name": "balancer-smart-pool", + "params": { + "address": "0x06745bee20ad9cc7dfb6f40b66504397235f547c", + "symbol": "GEAR", + "decimals": 18, + "pool": "0x3c035c3f8e271e12df1bed648024e60249f507c3", + "governanceToken": "0xfb5453340c03db5ade474b27e68b6a9c6b2823eb" + } + }, + "network": "1", + "addresses": [ + "0x52d81d4a8b18D6BA0b2c01dd2f9927eAf755dd0a", + "0x2150Cb38ee362bceAC3d4A2704A82eeeD02E93EC", + "0xdf2ff5946df21f818d5f67e62d22486ca470798e" + ], + "snapshot": 12815130 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-smart-pool/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balancer-smart-pool/index.ts new file mode 100644 index 00000000..299d871b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-smart-pool/index.ts @@ -0,0 +1,58 @@ +import { formatUnits } from '@ethersproject/units'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { multicall } from '../../utils'; + +export const author = 'kibagateaux'; +export const version = '0.1.0'; + +const abi = ['function totalSupply() public returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const poolShares = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + const poolGovTokens = ( + await erc20BalanceOfStrategy( + space, + network, + provider, + [options.pool], + { ...options, address: options.governanceToken }, + snapshot + ) + )[options.pool]; + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const totalPoolShares = await multicall( + network, + provider, + abi, + [[options.address, 'totalSupply']], + { blockTag } + ); + + if (!totalPoolShares || !poolGovTokens || !Object.keys(poolShares).length) + return {}; + const totalShares = parseFloat( + formatUnits(totalPoolShares.toString(), options.decimals) + ); + + return Object.fromEntries( + Object.entries(poolShares).map((account) => [ + account[0], + (account[1] / totalShares) * poolGovTokens + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-unipool/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balancer-unipool/examples.json new file mode 100644 index 00000000..1e7e55dc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-unipool/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Balancer LP staked in Unipool rewards contract", + "strategy": { + "name": "balancer-unipool", + "params": { + "symbol": "PERP", + "decimals": 18, + "tokenAddress": "0xbC396689893D065F41bc2C6EcbeE5e0085233447", + "unipoolAddress": "0xb9840A4A8a671f79DE3Df3b812fEEb38047CE552", + "bpoolAddress": "0xf54025af2dc86809be1153c1f20d77adb7e8ecf4" + } + }, + "network": "1", + "addresses": [ + "0x09212c58107c8da21b0f67a63e2144bb68bee4eb", + "0x8506d946CC63d1F1F3a303d68B0Da64597Cd64F3", + "0xc5df4d5ed23f645687a867d8f83a41836fcf8811" + ], + "snapshot": "11635000" + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer-unipool/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balancer-unipool/index.ts new file mode 100644 index 00000000..86819dde --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer-unipool/index.ts @@ -0,0 +1,110 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'tempofeng'; +export const version = '0.1.0'; + +// Merged ABIs from below contracts: +// * BPool from Balancer-labs: https://github.com/balancer-labs/balancer-core/blob/master/contracts/BPool.sol +// * Unipool contract from @k06a: https://github.com/k06a/Unipool/blob/master/contracts/Unipool.sol +const contractAbi = [ + { + constant: true, + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'earned', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'getBalance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + let res = await multicall( + network, + provider, + contractAbi, + [ + [options.bpoolAddress, 'totalSupply', []], + [options.bpoolAddress, 'getBalance', [options.tokenAddress]], + ...addresses.map((address: any) => [ + options.unipoolAddress, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.unipoolAddress, + 'earned', + [address] + ]) + ], + { blockTag } + ); + + const totalBPTsInBPool = bn(res[0]); // decimal: 18 + const totalTokensInBPool = bn(res[1]); // decimal: options.decimal + const tokensPerBPT = totalTokensInBPool.div(totalBPTsInBPool); // decimal: options.decimal / 18 + res = res.slice(2); + + // List how much tokens user have from their BPT tokens + const userTokensFromBPTList = res.slice(0, addresses.length).map((num) => { + const userBPTs = bn(num); // decimal: 18 + return userBPTs.mul(tokensPerBPT); // decimal: options.decimal + }); + + // List how much rewarded tokens user have in the unipool contract + const userEarnedTokensList = res.slice(addresses.length).map((num) => { + return bn(num); // decimal: options.decimal + }); + + const sumList = userTokensFromBPTList.map((userTokensFromBPT, i) => { + return userTokensFromBPT.add(userEarnedTokensList[i]); + }); + + return Object.fromEntries( + sumList.map((sum, i) => { + const parsedSum = parseFloat(formatUnits(sum, options.decimal)); + return [addresses[i], parsedSum]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer/examples.json b/Implementations/API/backend/utils/snapshot/strategies/balancer/examples.json new file mode 100644 index 00000000..6d7c78f0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer/examples.json @@ -0,0 +1,39 @@ +[ + { + "name": "Balancer Pool Tokens", + "strategy": { + "name": "balancer", + "params": { + "address": "0xba100000625a3754423978a60c9317c58a424e3D", + "symbol": "BAL (in Balancer)", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xff052381092420b7f24cc97fded9c0c17b2cbbb9", + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x74c3646adad7e196102d1fe35267adfd401a568b", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268", + "0xd109b2a304587569c84308c55465cd9ff0317bfb", + "0x1d2c4cd9bee9dfe088430b95d274e765151c32db" + ], + "snapshot": 12424932 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/balancer/index.ts b/Implementations/API/backend/utils/snapshot/strategies/balancer/index.ts new file mode 100644 index 00000000..348d0453 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/balancer/index.ts @@ -0,0 +1,87 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.2.0'; + +const BALANCER_SUBGRAPH_URL_ROOT = + 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer'; + +const NETWORK_KEY = { + '1': '', + '42': '-kovan', + '137': '-polygon', + '42161': '-arbitrum' +}; + +function buildBalancerSubgraphUrl(chainId, version) { + const networkString = NETWORK_KEY[chainId]; + const versionString = version == 2 ? '-v2' : ''; + return `${BALANCER_SUBGRAPH_URL_ROOT}${networkString}${versionString}`; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const params = { + poolShares: { + __args: { + where: { + userAddress_in: addresses.map((address) => address.toLowerCase()), + balance_gt: 0 + }, + first: 1000, + orderBy: 'balance', + orderDirection: 'desc' + }, + userAddress: { + id: true + }, + balance: true, + poolId: { + totalShares: true, + tokens: { + id: true, + balance: true + } + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.poolShares.__args.block = { number: snapshot }; + } + + // iterate through Balancer V1 & V2 Subgraphs + const score = {}; + for (let version = 1; version <= 2; version++) { + // Skip attempt to query subgraph on networks where V1 isn't deployed + if (network != 1 && network != 42 && version === 1) continue; + + const result = await subgraphRequest( + buildBalancerSubgraphUrl(network, version), + params + ); + if (result && result.poolShares) { + result.poolShares.forEach((poolShare) => + poolShare.poolId.tokens.map((poolToken) => { + const [, tokenAddress] = poolToken.id.split('-'); + if (tokenAddress === options.address.toLowerCase()) { + const userAddress = getAddress(poolShare.userAddress.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = + score[userAddress] + + (poolToken.balance / poolShare.poolId.totalShares) * + poolShare.balance; + } + }) + ); + } + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/bancor-pool-token-underlying-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/bancor-pool-token-underlying-balance/examples.json new file mode 100644 index 00000000..50f07ae4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/bancor-pool-token-underlying-balance/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "bancor-pool-token-underlying-balance", + "params": { + "symbol": "bnROOK", + "decimals": 18, + "underlyingTokenAddress": "0xfA5047c9c78B8877af97BDcb85Db743fD7313d4a", + "bancorNetworkInfoAddress": "0x8E303D296851B320e6a697bAcB979d13c9D6E760" + } + }, + "network": "1", + "addresses": [ + "0x66e81feea36764103b755144b161c70e75906535", + "0x857eb0eb2572f7092c417cd386ba82e45eba9b8a", + "0x98625957c6b96eeecd040e5bd26bd42100d124b2" + ], + "snapshot": 15191222 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/bancor-pool-token-underlying-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/bancor-pool-token-underlying-balance/index.ts new file mode 100644 index 00000000..97d3cfa9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/bancor-pool-token-underlying-balance/index.ts @@ -0,0 +1,59 @@ +import { call } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'TudorSante'; +export const version = '1.0.0'; + +const erc20ABI = [ + 'function poolToken(address pool) external view returns (address)', + 'function poolTokenToUnderlying(address pool, uint256 poolTokenAmount) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const liquidityPoolTokenAddress = await call( + provider, + erc20ABI, + [ + options.bancorNetworkInfoAddress, + 'poolToken', + [options.underlyingTokenAddress] + ], + { blockTag } + ); + + options.address = liquidityPoolTokenAddress; + const scores = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const poolTokenDecimalScaling = (10 ** options.decimals).toString(); + const underlyingValue = await call( + provider, + erc20ABI, + [ + options.bancorNetworkInfoAddress, + 'poolTokenToUnderlying', + [options.underlyingTokenAddress, poolTokenDecimalScaling] + ], + { blockTag } + ).then((res) => parseFloat(formatUnits(res, options.decimals))); + + return Object.fromEntries( + Object.entries(scores).map((res: any) => [res[0], res[1] * underlyingValue]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/bancor-standard-rewards-underlying-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/bancor-standard-rewards-underlying-balance/examples.json new file mode 100644 index 00000000..14e07792 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/bancor-standard-rewards-underlying-balance/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "bancor-standard-rewards-underlying-balance", + "params": { + "symbol": "bnCROWN", + "decimals": 18, + "underlyingTokenAddress": "0x444d6088B0F625f8C20192623B3C43001135E0fa", + "bancorNetworkInfoAddress": "0x8E303D296851B320e6a697bAcB979d13c9D6E760", + "bancorStandardRewardsAddress": "0xb0B958398ABB0b5DB4ce4d7598Fb868f5A00f372" + } + }, + "network": "1", + "addresses": [ + "0x671e4d58F407BE00fCC383732C020A7Ac1AFde73", + "0xA2C12dBe82f19eF06850d4693F2b2D5724b8eA3E", + "0x57577a7981c74f01cd919776228DBC416A683999", + "0xc19417805208ff8cf0293a5795f3c3d8bd68d9d4", + "0xe67da97f7a14a38ad6d3e7e5784fecbf7160b643", + "0x307b361717d720834d27002e4b08d3a4307877ee", + "0xeb54669ac46b44d6a29eedda0daf7d9a8c4f7386" + ], + "snapshot": 15718324 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/bancor-standard-rewards-underlying-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/bancor-standard-rewards-underlying-balance/index.ts new file mode 100644 index 00000000..8af1793a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/bancor-standard-rewards-underlying-balance/index.ts @@ -0,0 +1,69 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { call } from '../../utils'; +import { Multicaller } from '../../utils'; + +export const author = 'tiagofilipenunes'; +export const version = '0.1.0'; + +const bancorNetworkInfoABI = [ + 'function poolTokenToUnderlying(address pool, uint256 poolTokenAmount) external view returns (uint256)' +]; + +const standardRewardsABI = [ + 'function providerStake(address provider, uint256 id) external view returns (uint256)', + 'function latestProgramId(address pool) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Get last provider Program ID + const latestProgramId = await call( + provider, + standardRewardsABI, + [ + options.bancorStandardRewardsAddress, + 'latestProgramId', + [options.underlyingTokenAddress] + ], + { blockTag } + ); + + // Get each provider's stake in the standard rewards contract + const multi = new Multicaller(network, provider, standardRewardsABI, { + blockTag + }); + addresses.forEach((address) => + multi.call(address, options.bancorStandardRewardsAddress, 'providerStake', [ + address, + latestProgramId.toString() + ]) + ); + const scores: Record = await multi.execute(); + + // Get the Underlying Value of the Pool Token * 10**(decimals) to convert from wei + const poolTokenDecimalScaling = (10 ** options.decimals).toString(); + const underlyingValue = await call( + provider, + bancorNetworkInfoABI, + [ + options.bancorNetworkInfoAddress, + 'poolTokenToUnderlying', + [options.underlyingTokenAddress, poolTokenDecimalScaling] + ], + { blockTag } + ).then((res) => parseFloat(formatUnits(res, 2 * options.decimals))); + + // Update the providers' stakes in the standard rewards contract to their converted underlying value + return Object.fromEntries( + Object.entries(scores).map((res: any) => [res[0], res[1] * underlyingValue]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/README.md b/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/README.md new file mode 100644 index 00000000..8b92a1ca --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/README.md @@ -0,0 +1,46 @@ +# BanksyDAO + + In Banksy Farm, NFTs are the backbone of the platform ownership. And the way to participate in this DAO. + +Our NFTs are composed by various skills, as shown [here](https://docs.banksy.farm/nfts/nft-composition). +One of the skill is "Experience", that starts at 0. And get increased as each community member uses this card. + +This BanksyDAO strategy consists in reading the "Experience" skill of each NFT that the wallet owner has, and the following logic: +1. If the NFTs has 0 "Experience", then it's vote value is 1. +2. IF the NFTs has more than 0 "Experience", then the vote value equals the 1 + and extra value of the experience. +3. IF the wallet has multiple NFTs. then the vote value is the sum of all NFTs. + + +## Strategy Parameters + + + +Example strategy params: + + + +```json +[ + { + "name": "dao banksy farm", + "strategy": { + "name": "banksy-dao", + "params": { + "address": "0x942d791ab07e33Fe4B780Fc0b3874a24Ac3da433", + "symbol": "BanksyNFT" + } + }, + "network": "43114", + "addresses": [ + "0x503d2E56055c7078905369D7fA43c6f20C70a43a", + "0x2730bd7b669e2B5fC6abcDBdf048f0D8e5b0fBE6", + "0xb0dD83eDeB1e615F8E638F4824BA86C8053dF366" + ], + "snapshot": 10085683 + } +] + +``` + +##### DAO BANKSY.FINANCE ENHANCED BY NFT + diff --git a/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/examples.json b/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/examples.json new file mode 100644 index 00000000..70d86883 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "dao banksy farm", + "strategy": { + "name": "banksy-dao", + "params": { + "address": "0x942d791ab07e33Fe4B780Fc0b3874a24Ac3da433", + "symbol": "BanksyNFT" + } + }, + "network": "43114", + "addresses": [ + "0x503d2E56055c7078905369D7fA43c6f20C70a43a", + "0x2730bd7b669e2B5fC6abcDBdf048f0D8e5b0fBE6", + "0xb0dD83eDeB1e615F8E638F4824BA86C8053dF366" + ], + "snapshot": 10085683 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/index.ts b/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/index.ts new file mode 100644 index 00000000..37b5d2bb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/banksy-dao/index.ts @@ -0,0 +1,91 @@ +import { Multicaller } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'BanksyFarm'; +export const version = '0.0.1'; + +const factoryNftABI = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function getArtWorkOverView(uint256 tokenId) external view returns (uint256, uint256, uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // First, get the balance of nft + const callWalletToBalanceOf = new Multicaller( + network, + provider, + factoryNftABI, + { + blockTag + } + ); + for (const walletAddress of addresses) { + callWalletToBalanceOf.call(walletAddress, options.address, 'balanceOf', [ + walletAddress + ]); + } + const walletToBalanceOf: Record = + await callWalletToBalanceOf.execute(); + + // Second, get the tokenId's for each nft + const callWalletToAddresses = new Multicaller( + network, + provider, + factoryNftABI, + { + blockTag + } + ); + for (const [walletAddress, count] of Object.entries(walletToBalanceOf)) { + for (let index = 0; index < count.toNumber(); index++) { + callWalletToAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToAddresses: Record = + await callWalletToAddresses.execute(); + + // Third, get skil's for each tokenId + const callTokenToSkill = new Multicaller(network, provider, factoryNftABI, { + blockTag + }); + + for (const [walletID, tokenId] of Object.entries(walletIDToAddresses)) { + callTokenToSkill.call(walletID, options.address, 'getArtWorkOverView', [ + tokenId + ]); + } + const walletIDToSkills: Record = + await callTokenToSkill.execute(); + + const results = {} as Record; + for (const [walletID, values] of Object.entries(walletIDToSkills)) { + const address = walletID.split('-')[0]; + const currentExperience = values[1] / 1e18; + + let extraBoosted = 1; + if (currentExperience > 0) { + extraBoosted = currentExperience / 100; + } + + results[address] = (results[address] || 0) + extraBoosted; + } + + return Object.fromEntries( + Object.entries(results).map(([address, weight]) => [address, weight]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/README.md b/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/README.md new file mode 100644 index 00000000..9e6ac679 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/README.md @@ -0,0 +1,26 @@ +# battlefly-vgfly-and-staked-gfly + +This strategy calculates the voting power for addresses with one or more of the following requirements: + +* An amount of unvested tokens +* An amount of staked tokens +* An amount of staked LP tokens +* An amount of staked founder tokens + +As input a graphUrl is required which will return those amounts for each queried address. +Additionally, the gFLY, magic vgFLY and LP token addresses are required for some on-chain calls that are not possible with Subgraph. + +```json +{ + "graphUrl": "https://api.thegraph.com/subgraphs/name/battlefly-game/gfly-main", + "gFLYAddress": "0x872bAD41CFc8BA731f811fEa8B2d0b9fd6369585", + "magicAddress": "0x539bdE0d7Dbd336b79148AA742883198BBF60342", + "lpAddress": "0x088F2Bd3667F385427d9289C28725D43d4b74AB4", + "v1FoundersVault": "0x2F67932136D84De27D99Ed89a423b907A1b10930", + "v2FoundersVault": "0x6c9CC5a6d5484CB8eAB1438632BBf667A5E25eD9", + "foundersToken": "0xc43104775BD9f6076808B5F8dF6CbdBeac96d7dE", + "vgFLYAddress": "0x86d643b7f4a2a6772A4B1bFBee5EcE46A1DE3dfD", + "symbol": "gFLY", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/examples.json b/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/examples.json new file mode 100644 index 00000000..40c0cf4c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/examples.json @@ -0,0 +1,28 @@ +[ + { + "name": "Battlefly vgFLY holders, gFLY stakers and founder token stakers", + "strategy": { + "name": "battlefly-vgfly-and-staked-gfly", + "params": { + "graphUrl": "https://api.thegraph.com/subgraphs/name/battlefly-game/gfly-main", + "gFLYAddress": "0x872bAD41CFc8BA731f811fEa8B2d0b9fd6369585", + "magicAddress": "0x539bdE0d7Dbd336b79148AA742883198BBF60342", + "lpAddress": "0x088F2Bd3667F385427d9289C28725D43d4b74AB4", + "v1FoundersVault": "0x2F67932136D84De27D99Ed89a423b907A1b10930", + "v2FoundersVault": "0x6c9CC5a6d5484CB8eAB1438632BBf667A5E25eD9", + "foundersToken": "0xc43104775BD9f6076808B5F8dF6CbdBeac96d7dE", + "vgFLYAddress": "0x86d643b7f4a2a6772A4B1bFBee5EcE46A1DE3dfD", + "symbol": "gFLY", + "decimals": 18 + } + }, + "network": "42161", + "addresses": [ + "0x0eB468F89E5bcFA4c933c8982D8d19554e101cfe", + "0x78063Ed58Edea4Ae4981946D6b4Cc63d8928CCBC", + "0x4026b3Da349C2952255Ca9db4F055ea57F4e037C", + "0x06C244a9BaFBC96E609A8DF32A07178552C7295a" + ], + "snapshot": 47081042 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/index.ts b/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/index.ts new file mode 100644 index 00000000..17a706f6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/battlefly-vgfly-and-staked-gfly/index.ts @@ -0,0 +1,225 @@ +import { multicall, Multicaller, subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'Archethect'; +export const version = '0.0.2'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function decimals() external view returns (uint8)', + 'function token0() external view returns (address)', + 'function token1() external view returns (address)', + 'function totalSupply() external view returns (uint256)', + 'function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)', + 'function totalClaimableOf(address account) external view returns (uint256 total)' +]; + +const calcVotingPower = ( + stakedAsString: string, + stakedLPAsString: string, + vestingAsString: string, + founderStakes: { type: string }[], + claimableOf: BigNumberish, + decimals: number, + tokenWeight: number, + v1VaultGFlyStaked: BigNumber, + v2VaultGFlyStaked: BigNumber, + totalV1FoundersStaked: BigNumber, + totalV2FoundersStaked: BigNumber +) => { + const staked = parseFloat( + formatUnits(BigNumber.from(stakedAsString), decimals) + ); + const stakedLP = parseFloat( + formatUnits(BigNumber.from(stakedLPAsString), decimals) + ); + const vesting = parseFloat( + formatUnits(BigNumber.from(vestingAsString).sub(claimableOf), decimals) + ); + + let v1 = 0; + let v2 = 0; + + founderStakes.map((element) => { + if (element.type === 'v1') { + v1 += 1; + } else if (element.type === 'v2') { + v2 += 1; + } + }); + + const founders = parseFloat( + formatUnits( + BigNumber.from(v1) + .mul(v1VaultGFlyStaked.div(totalV1FoundersStaked)) + .add( + BigNumber.from(v2).mul(v2VaultGFlyStaked.div(totalV2FoundersStaked)) + ), + decimals + ) + ); + + return staked + vesting + stakedLP * tokenWeight + founders; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const DF_SUBGRAPH_URL = options.graphUrl; + + // Setup subgraph query to fetch vgFLY and staked gFLY amounts (both normal and LP) per address + const params = { + _meta: { + block: { + number: true + } + }, + accounts: { + __args: { + where: { + id_in: addresses.map((addr: string) => addr.toLowerCase()) + }, + first: 1000 + }, + founderStakes: { + __args: { + first: 1000 + }, + type: true + }, + id: true, + staked: true, + stakedLP: true, + vesting: true + } + }; + + // Setup subgraph query to fetch amount of gFLY staked by V1 and V2 vaults + const paramsV1AndV2 = { + _meta: { + block: { + number: true + } + }, + accounts: { + __args: { + where: { + id_in: [ + options.v1FoundersVault.toLowerCase(), + options.v2FoundersVault.toLowerCase() + ] + } + }, + id: true, + staked: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.accounts.__args.block = { number: snapshot }; + // @ts-ignore + paramsV1AndV2.accounts.__args.block = params.accounts.__args.block; + } + + const result = await subgraphRequest(DF_SUBGRAPH_URL, { + ...params + }); + + const resultV1andV2 = await subgraphRequest(DF_SUBGRAPH_URL, { + ...paramsV1AndV2 + }); + + // Take the same block number than from subgraph for consistency + const blockTag = + typeof snapshot === 'number' ? snapshot : result._meta.block.number; + + // fetch all token and lp contract data + + const fetchContractData = await multicall( + network, + provider, + abi, + [ + [options.lpAddress, 'token0', []], + [options.lpAddress, 'token1', []], + [options.lpAddress, 'getReserves', []], + [options.lpAddress, 'totalSupply', []], + [options.lpAddress, 'decimals', []], + [options.foundersToken, 'balanceOf', [options.v1FoundersVault]], + [options.foundersToken, 'balanceOf', [options.v2FoundersVault]] + ], + { blockTag } + ); + + // assign multicall data to variables + + const token0Address = fetchContractData[0][0]; + const token1Address = fetchContractData[1][0]; + const lpTokenReserves = fetchContractData[2]; + const lpTokenTotalSupply = fetchContractData[3][0]; + const lpTokenDecimals = fetchContractData[4][0]; + const totalV1FoundersStaked = fetchContractData[5][0]; + const totalV2FoundersStaked = fetchContractData[6][0]; + + // calculate single lp token weight + + let tokenWeight; + + //For token weight we only take the gFLY part into account + if (token0Address === options.gFLYAddress) { + tokenWeight = + parseFloat(formatUnits(lpTokenReserves._reserve0, options.decimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals)); + } else if (token1Address === options.gFLYAddress) { + tokenWeight = + parseFloat(formatUnits(lpTokenReserves._reserve1, options.decimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals)); + } else { + tokenWeight = 0; + } + + // Make sure vested vgFLY which is claimable is not counted + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + multi.call(address, options.vgFLYAddress, 'totalClaimableOf', [address]) + ); + const claimableOfResult: Record = await multi.execute(); + + let v1VaultGFlyStaked; + let v2VaultGFlyStaked; + + resultV1andV2.accounts.map((element) => { + if (element.id === options.v1FoundersVault.toLowerCase()) { + v1VaultGFlyStaked = BigNumber.from(element.staked); + } else if (element.id === options.v2FoundersVault.toLowerCase()) { + v2VaultGFlyStaked = BigNumber.from(element.staked); + } + }); + + return Object.fromEntries( + result.accounts.map((a) => [ + getAddress(a.id), + calcVotingPower( + a.staked, + a.stakedLP, + a.vesting, + a.founderStakes, + claimableOfResult[getAddress(a.id)], + options.decimals, + tokenWeight, + v1VaultGFlyStaked, + v2VaultGFlyStaked, + BigNumber.from(totalV1FoundersStaked), + BigNumber.from(totalV2FoundersStaked) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/biswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/biswap/README.md new file mode 100644 index 00000000..6fe547cf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/biswap/README.md @@ -0,0 +1,41 @@ +# Biswap + +This is the most common strategy, it returns the balances of the voters for a balances BSW token +in Biswap project(pools, farms, Liquidity, token). + +Here is an example of parameters: +```json +[ + { + "name": "Example query Biswap", + "strategy": { + "name": "biswap", + "params": { + "address": "0x965F527D9159dCe6288a2219DB51fc6Eef120dD1", + "masterChef": "0xDbc1A13490deeF9c3C12b44FE77b503c1B061739", + "autoBsw": "0x97a16ff6fd63a46bf973671762a39f3780cda73d", + "autoBswSecond": "0xa4b20183039b2F9881621C3A03732fBF0bfdff10", + "bswLPs": [ + { + "address": "0x2b30c317cedfb554ec525f85e79538d59970beb0", + "pid": 9 + }, + { + "address": "0x46492b26639df0cda9b2769429845cb991591e0a", + "pid": 10 + } + ], + "symbol": "BSW", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0xdBE55A0daDc80EF88e884f15CE41c26c0Af933a0" + ], + "snapshot": 15078771 + } +] + + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/biswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/biswap/examples.json new file mode 100644 index 00000000..c91219d5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/biswap/examples.json @@ -0,0 +1,29 @@ +[ + { + "name": "Example query Biswap", + "strategy": { + "name": "biswap", + "params": { + "address": "0x965F527D9159dCe6288a2219DB51fc6Eef120dD1", + "masterChef": "0xDbc1A13490deeF9c3C12b44FE77b503c1B061739", + "autoBsw": "0x97a16ff6fd63a46bf973671762a39f3780cda73d", + "autoBswSecond": "0xa4b20183039b2F9881621C3A03732fBF0bfdff10", + "bswLPs": [ + { + "address": "0x2b30c317cedfb554ec525f85e79538d59970beb0", + "pid": 9 + }, + { + "address": "0x46492b26639df0cda9b2769429845cb991591e0a", + "pid": 10 + } + ], + "symbol": "BSW", + "decimals": 18 + } + }, + "network": "56", + "addresses": ["0xdBE55A0daDc80EF88e884f15CE41c26c0Af933a0"], + "snapshot": 15078771 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/biswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/biswap/index.ts new file mode 100644 index 00000000..97acc3d7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/biswap/index.ts @@ -0,0 +1,136 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import examplesFile from './examples.json'; + +export const author = 'biswap-dex'; +export const version = '0.0.2'; +export const examples = examplesFile; + +const abi = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address _owner) view returns (uint256 balance)', + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +const autoBswAbi = [ + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)', + 'function getPricePerFullShare() view returns (uint256)' +]; + +const bn = (num: any): BigNumber => { + return BigNumber.from(num.toString()); +}; + +const addUserBalance = (userBalances, user: string, balance) => { + if (userBalances[user]) { + return (userBalances[user] = userBalances[user].add(balance)); + } else { + return (userBalances[user] = balance); + } +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multicall = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address: any) => { + multicall.call(`token.${address}`, options.address, 'balanceOf', [address]); + multicall.call(`masterChef.${address}`, options.masterChef, 'userInfo', [ + '0', + address + ]); + }); + options.bswLPs.forEach((lp: { address: string; pid: number }) => { + multicall.call(`lp.${lp.pid}.totalSupply`, lp.address, 'totalSupply'); + multicall.call(`lp.${lp.pid}.balanceOf`, options.address, 'balanceOf', [ + lp.address + ]); + addresses.forEach((address: any) => { + multicall.call( + `lpUsers.${address}.${lp.pid}`, + options.masterChef, + 'userInfo', + [lp.pid, address] + ); + }); + }); + + const multicallAutoCompound = new Multicaller(network, provider, autoBswAbi, { + blockTag + }); + multicallAutoCompound.call( + 'priceShare_1', + options.autoBsw, + 'getPricePerFullShare' + ); + multicallAutoCompound.call( + 'priceShare_2', + options.autoBswSecond, + 'getPricePerFullShare' + ); + addresses.forEach((address) => { + multicallAutoCompound.call( + `autoPool_1.${address}`, + options.autoBsw, + 'userInfo', + [address] + ); + multicallAutoCompound.call( + `autoPool_2.${address}`, + options.autoBswSecond, + 'userInfo', + [address] + ); + }); + + const resultAutoBsw = await multicallAutoCompound.execute(); + const result = await multicall.execute(); + + const userBalances: any = []; + for (let i = 0; i < addresses.length - 1; i++) { + userBalances[addresses[i]] = bn(0); + } + + addresses.forEach((address: any) => { + addUserBalance(userBalances, address, result.token[address]); + addUserBalance(userBalances, address, result.masterChef[address][0]); + addUserBalance( + userBalances, + address, + resultAutoBsw.autoPool_1[address][0] + .mul(resultAutoBsw.priceShare_1) + .div(bn(parseUnits('1', options.decimals))) + ); + addUserBalance( + userBalances, + address, + resultAutoBsw.autoPool_2[address][0] + .mul(resultAutoBsw.priceShare_2) + .div(bn(parseUnits('1', options.decimals))) + ); + options.bswLPs.forEach((lp: { address: string; pid: number }) => { + addUserBalance( + userBalances, + address, + result.lpUsers[address][lp.pid][0] + .mul(result.lp[lp.pid].balanceOf) + .div(result.lp[lp.pid].totalSupply) + ); + }); + }); + + return Object.fromEntries( + Object.entries(userBalances).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/README.md b/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/README.md new file mode 100644 index 00000000..9bd8c17b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/README.md @@ -0,0 +1,16 @@ +# blockzerolabs-cryptonauts + +This strategy is to be used for Blockzero Labs. It will return the underlying XIO tokens held in the NFT Vault. These +XIO can then be used towards voting. + +Here is an example of parameters: + +```json +{ + "symbol": "CRYPTONAUTS", + "totalSupply": 110, + "decimals": 18, + "_nftTokenAddress": "0x1d48ddb875d1e540ee0715dbb7b019d02a3ba4db", + "_vaultAddress": "0x6d9d87fd57fb8ae3bf005e564a950124fc83dd1a" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/examples.json b/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/examples.json new file mode 100644 index 00000000..e2c7d07b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "blockzerolabs-cryptonauts", + "params": { + "symbol": "CRYPTONAUTS", + "totalSupply": 110, + "decimals": 18, + "_nftTokenAddress": "0x1d48ddb875d1e540ee0715dbb7b019d02a3ba4db", + "_vaultAddress": "0x6d9d87fd57fb8ae3bf005e564a950124fc83dd1a" + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0x693a3fc85e373dc89239e9eda24edbf4c1d8fefd", + "0x2231e35dae9f0c33141c9eab05b1c1bc4fb1df36" + ], + "snapshot": 13594719 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/index.ts b/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/index.ts new file mode 100644 index 00000000..36a23f87 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/blockzerolabs-cryptonauts/index.ts @@ -0,0 +1,88 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { getAddress } from '@ethersproject/address'; +import { Multicaller } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'blockzerolabs'; +export const version = '0.1.0'; + +const abi = [ + 'function totalSupply() external view returns (uint256)', + 'function exists(uint256) external view returns (bool)', + 'function ownerOf(uint256) external view returns (address)', + 'function balanceOf(uint256) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Step 1. Retrieve the total supply of Cryptonauts + const totalSupply = options.totalSupply; + + // Step 2. Determine which ones still exist (i.e not burned) + const nftExistsMulti = new Multicaller(network, provider, abi, { blockTag }); + for (let _nftId = 0; _nftId < totalSupply; _nftId++) { + nftExistsMulti.call( + _nftId, + options._nftTokenAddress, // This is a static address + 'exists', + [_nftId] + ); + } + const nftExists: Record = await nftExistsMulti.execute(); + + // Step 3. Determine owners for NFTs that still exist + const nftOwnersMulti = new Multicaller(network, provider, abi, { blockTag }); + for (let _nftId = 0; _nftId < totalSupply; _nftId++) { + // If the NFT exists, get the owner + if (nftExists[_nftId]) { + nftOwnersMulti.call(_nftId, options._nftTokenAddress, 'ownerOf', [ + _nftId + ]); + } + } + const nftOwners: Record = await nftOwnersMulti.execute(); + + // Step 4. Get the balance of each NFT from the Vault + const vaultBalanceMulti = new Multicaller(network, provider, abi, { + blockTag + }); + for (let _nftId = 0; _nftId < totalSupply; _nftId++) { + // If the NFT exists, get the owner + if (nftExists[_nftId]) { + vaultBalanceMulti.call(_nftId, options._vaultAddress, 'balanceOf', [ + _nftId + ]); + } + } + const vaultBalance: Record = + await vaultBalanceMulti.execute(); + + // Iterate over each address provided + const balances: Record = {}; + addresses.forEach((address) => { + let totalBalance = BigNumber.from(0); + // Iterate over each NFT + for (let _nftId = 0; _nftId < totalSupply; _nftId++) { + // Ensure the NFT Exists before continuing to add balance + if (nftExists[_nftId]) { + // Ensure this address is the owner before continuing to add balance + if (nftOwners[_nftId] == getAddress(address)) { + // Add the balance + totalBalance = totalBalance.add(vaultBalance[_nftId]); + } + } + } + // Format the balance with 18 decimals (fixed) + balances[address] = parseFloat(formatUnits(totalBalance, options.decimals)); + }); + + return balances; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/botto-dao/README.md b/Implementations/API/backend/utils/snapshot/strategies/botto-dao/README.md new file mode 100644 index 00000000..0aed1864 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/botto-dao/README.md @@ -0,0 +1,33 @@ +# Botto Dao Governance strategy + +It returns the BOTTO balance of the voters participating in the governance and in the liquidiy mining program + +Here is an example of parameters: +```json +[ + { + "name": "Botto Dao Governance strategy", + "strategy": { + "name": "botto-dao", + "params": { + "token": "0x9dfad1b7102d46b1b197b90095b5c4e9f5845bba", + "stakingAddress": "0x19CD3998f106eCC40eE7668c19C47e18b491e8a6", + "miningAddress": "0xf8515Cae6915838543bCD7756F39268CE8F853Fd", + "liquidityAddress": "0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66", + "symbol": "BOTTO", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xf2b10b41961f6bc3801e7946dabe5572158a78a5", + "0xea32cf979bdaf8d3bb4121d58515d4623a27f3e0", + "0x7173d417cfa1cdec794fa21f4ca8a71d181b2ca0", + "0x58787f1659b2fd8853918dab6d4a1565569e6044", + "0x7dbc5f88986db946f1d5987edfbf9c6917230fa2" + ], + "snapshot": 13808330 + } +] + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/botto-dao/examples.json b/Implementations/API/backend/utils/snapshot/strategies/botto-dao/examples.json new file mode 100644 index 00000000..15d9e74a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/botto-dao/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Botto Dao Governance strategy", + "strategy": { + "name": "botto-dao", + "params": { + "token": "0x9dfad1b7102d46b1b197b90095b5c4e9f5845bba", + "stakingAddress": "0x19CD3998f106eCC40eE7668c19C47e18b491e8a6", + "miningAddress": "0xf8515Cae6915838543bCD7756F39268CE8F853Fd", + "liquidityAddress": "0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66", + "symbol": "BOTTO", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xf2b10b41961f6bc3801e7946dabe5572158a78a5", + "0xea32cf979bdaf8d3bb4121d58515d4623a27f3e0", + "0x7173d417cfa1cdec794fa21f4ca8a71d181b2ca0", + "0x58787f1659b2fd8853918dab6d4a1565569e6044", + "0x7dbc5f88986db946f1d5987edfbf9c6917230fa2" + ], + "snapshot": 13803640 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/botto-dao/index.ts b/Implementations/API/backend/utils/snapshot/strategies/botto-dao/index.ts new file mode 100644 index 00000000..16decaca --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/botto-dao/index.ts @@ -0,0 +1,73 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'alfonsocarbono'; +export const version = '1.0.0'; + +const abi = [ + 'function userStakes(address) external view returns(uint256)', + 'function totalUserStake(address) external view returns(uint256)', + 'function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)', + 'function totalSupply() public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const _formatUnits = (value) => + parseFloat(formatUnits(value, options.decimals)); + + const multiLiquidityProvider = new Multicaller(network, provider, abi, { + blockTag + }); + multiLiquidityProvider.call( + 'reserves', + options.liquidityAddress, + 'getReserves' + ); + multiLiquidityProvider.call( + 'totalSupply', + options.liquidityAddress, + 'totalSupply' + ); + + const multiBalances = new Multicaller(network, provider, abi, { + blockTag + }); + addresses.forEach((address) => { + multiBalances.call( + address + '-stakedBotto', + options.stakingAddress, + 'userStakes', + [address] + ); + multiBalances.call( + address + '-stakedLPs', + options.miningAddress, + 'totalUserStake', + [address] + ); + }); + + const { reserves, totalSupply } = await multiLiquidityProvider.execute(); + const balances: Record = await multiBalances.execute(); + + return Object.fromEntries( + addresses.map((adr) => { + const stakedBotto = _formatUnits(balances[adr + '-stakedBotto']); + const stakedLPsBottos = + (_formatUnits(balances[adr + '-stakedLPs']) * + _formatUnits(reserves['_reserve0'])) / + _formatUnits(totalSupply); + return [adr, stakedBotto + stakedLPsBottos]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/brightid/README.md b/Implementations/API/backend/utils/snapshot/strategies/brightid/README.md new file mode 100644 index 00000000..6d13e591 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/brightid/README.md @@ -0,0 +1,44 @@ +# BrightID + +This strategy returns a score of 1 for voters verified in a BrightID user registry. You can learn more about the smart contract used on this strategy from the following sources: + +- Public Registry(v5): https://github.com/BrightID/BrightID-SmartContract/blob/v5/snapshot/BrightIDSnapshot.sol +- Private Registry: https://github.com/clrfund/monorepo/tree/develop/contracts/contracts/userRegistry + +Public registries are maintained by BrightID and can be used if a DAO has no interest on setting one up themselves. + +Here is an example of parameters for using an public registry contract: +Note that when using an public registry, the network is always set to IDChain(74). + +```json +{ + "registry": "v5", + "symbol": "verified" +} +``` + +Here's a list of all public registry contracts currently deployed on IDChain: + +- v5(https://explorer.idchain.one/address/0x81591DC4997A76A870c13D383F8491B288E09344/contracts) + +To provide users with a better experience, a custom built frontend site is recommended for interacting with public / private registry contracts. + +e.g. Frontend for public v5 registry built by SongADAO (https://github.com/SongADAO/songaday-brightid-registration) + +In the example below, an alternative private registry contract is used: +Note that when using a private registry, the network is set to the space's network. + +```json +{ + "registry": "0xF99e2173db1f341a947CE9Bd7779aF2245309f91", + "symbol": "verified" +} +``` + +A private registry would need to implement a similar logic to the public registry, while also implementing the following function: + +``` +function isVerifiedUser(address _user) external view returns (bool) +``` + +Where when passed in an address `_user`, the contract would return a boolean value representing the address' verification status. diff --git a/Implementations/API/backend/utils/snapshot/strategies/brightid/examples.json b/Implementations/API/backend/utils/snapshot/strategies/brightid/examples.json new file mode 100644 index 00000000..035398b7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/brightid/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query (Official Registry)", + "strategy": { + "name": "brightid", + "params": { + "registry": "v5", + "symbol": "ID" + } + }, + "network": "1", + "addresses": [ + "0x7A38760C295f1ea086005214a279fb1280010483", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 14266000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/brightid/index.ts b/Implementations/API/backend/utils/snapshot/strategies/brightid/index.ts new file mode 100644 index 00000000..906bde62 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/brightid/index.ts @@ -0,0 +1,63 @@ +import { multicall } from '../../utils'; +import { subgraphRequest } from '../../utils'; +import { getProvider } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.1'; + +const abi = [ + 'function isVerifiedUser(address _user) external view returns (bool)' +]; + +const official = new Map([ + ['v5', '0x81591DC4997A76A870c13D383F8491B288E09344'] +]); + +async function getIDChainBlock(snapshot, provider) { + const ts = (await provider.getBlock(snapshot)).timestamp; + const query = { + blocks: { + __args: { + where: { + ts: ts, + network_in: ['74'] + } + }, + number: true + } + }; + const url = 'https://blockfinder.snapshot.org'; + const data = await subgraphRequest(url, query); + return data.blocks[0].number; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const isOfficial = options.registry.charAt(0) == 'v'; + const blockTag = + typeof snapshot === 'number' + ? isOfficial + ? await getIDChainBlock(snapshot, provider) + : snapshot + : 'latest'; + const response = await multicall( + isOfficial ? '74' : network, + getProvider(isOfficial ? '74' : network), + abi, + addresses.map((address: any) => [ + isOfficial ? official.get(options.registry) : options.registry, + 'isVerifiedUser', + [address] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [addresses[i], value[0] ? 1 : 0]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/bsc-mvb/examples.json b/Implementations/API/backend/utils/snapshot/strategies/bsc-mvb/examples.json new file mode 100644 index 00000000..2652d6e5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/bsc-mvb/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "bsc-mvb", + "params": { + "symbol": "BSCDAO", + "params": { + "blacklistNFTID": ["1"], + "configs": [ + { + "name": "MVB Voting Badge - BSC Core Team Badge", + "cumulative": true, + "votingPower": 10 + } + ] + } + } + }, + "network": "56", + "addresses": [ + "0x42d228f33C31aE390A3B023515ddE8F81f214067", + "0x77e4CDF993a2F7A7973f38A5401A0E895B859366" + ], + "snapshot": 12580101 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/bsc-mvb/index.ts b/Implementations/API/backend/utils/snapshot/strategies/bsc-mvb/index.ts new file mode 100644 index 00000000..552cdedc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/bsc-mvb/index.ts @@ -0,0 +1,182 @@ +import fetch from 'cross-fetch'; +import { subgraphRequest } from '../../utils'; + +export const author = 'alberthaotan'; +export const version = '0.1.0'; + +const Endpoint: { + name: string; + graphql: string; + subgraph: string; + contract: string; +} = { + name: 'BSC', + graphql: 'https://graphigo.prd.galaxy.eco/query', + subgraph: + 'https://api.thegraph.com/subgraphs/name/nftgalaxy/bsc-ticket-erc1155', + contract: '0x56535d2273e2eC8c2CdC65f71e3981Bf6301ae8D' +}; + +interface Config { + name: string; + votingPower: number; + cumulative: boolean; +} + +interface OwnerWithNfts { + [owner: string]: { + [tokenId: string]: string; + }; +} + +interface OwnerToNftCount { + [owner: string]: { + [name: string]: number; + }; +} + +interface OwnerToScore { + [owner: string]: number; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const restoreAddress = addresses.reduce((map, address) => { + map[address.toLowerCase()] = address; + return map; + }, {}); + + const subgraphParams = { + accounts: { + __args: { + where: { + id_in: addresses.map((a) => a.toLowerCase()) + } + }, + id: true, + balances: { + token: { + id: true, + identifier: true + } + } + } + }; + if (snapshot !== 'latest') { + subgraphParams.accounts.__args['block'] = { number: snapshot }; + } + + const graphqlParams = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + operationName: 'allNFTsByOwnersCoresAndChain', + query: `query allNFTsByOwnersCoresAndChain($option: NFTsOptions!) { + allNFTsByOwnersCoresAndChain(option: $option) { + owner + nfts + { + id + name + nftCore + { + contractAddress + } + } + } + }`, + variables: { + option: { + nftCoreAddresses: options.params.NFTCoreAddress + ? options.params.NFTCoreAddress + : [Endpoint.contract], + chain: Endpoint.name, + owners: addresses + } + } + }) + }; + + const graphqlPromise = fetch(Endpoint.graphql, graphqlParams); + const subgraphPromise = subgraphRequest(Endpoint.subgraph, subgraphParams); + const promisesRes = await Promise.all([graphqlPromise, subgraphPromise]); + const graphqlData = await promisesRes[0].json(); + const subgraphData = promisesRes[1]; + + // Initialize objects + const configs: Config[] = options.params.configs; + const ownerToNftCount: OwnerToNftCount = Object.fromEntries( + addresses.map((addr) => [addr.toLowerCase(), {}]) + ); + const ownerToScore: OwnerToScore = {}; + const ownersWithNfts: OwnerWithNfts[] = + graphqlData.data.allNFTsByOwnersCoresAndChain.reduce((map, item) => { + map[item.owner.toLowerCase()] = item.nfts.reduce((m, i) => { + if (!options.params.blacklistNFTID?.includes(i.id)) { + m[ + i.nftCore.contractAddress.toLowerCase() + + '-0x' + + Number.parseInt(i.id).toString(16) + ] = i.name; + } + return m; + }, {}); + return map; + }, {}); + const subgraphOwnersWithNfts: OwnerWithNfts[] = subgraphData.accounts.reduce( + (map, item) => { + map[item.id] = item.balances.reduce((m, i) => { + m[i.token.id] = ''; + return m; + }, {}); + return map; + }, + {} + ); + + // Intersect nft holdings of owners from graphql and subgraph returns + Object.keys(subgraphOwnersWithNfts).forEach((owner) => { + Object.keys(subgraphOwnersWithNfts[owner]).forEach((tokenId) => { + if (owner in ownersWithNfts && tokenId in ownersWithNfts[owner]) { + subgraphOwnersWithNfts[owner][tokenId] = ownersWithNfts[owner][tokenId]; + } + }); + }); + + // Get owners nft counts base on nft name + Object.keys(subgraphOwnersWithNfts).forEach((owner) => { + Object.keys(subgraphOwnersWithNfts[owner]).forEach((tokenId) => { + const nftName = subgraphOwnersWithNfts[owner][tokenId]; + if (nftName in ownerToNftCount[owner]) { + ownerToNftCount[owner][nftName]++; + } else { + ownerToNftCount[owner][nftName] = 1; + } + }); + }); + + // Get owners score base on certain config + Object.keys(ownerToNftCount).forEach((owner) => { + ownerToScore[restoreAddress[owner]] = 0; + configs.forEach((config) => { + if (config.name in ownerToNftCount[owner]) { + if (config.cumulative) { + ownerToScore[restoreAddress[owner]] += + config.votingPower * ownerToNftCount[owner][config.name]; + } else { + ownerToScore[restoreAddress[owner]] += config.votingPower * 1; + } + } + }); + }); + + return ownerToScore; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/cake/README.md b/Implementations/API/backend/utils/snapshot/strategies/cake/README.md new file mode 100644 index 00000000..dfbe6b5d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cake/README.md @@ -0,0 +1,9 @@ +# Contract Call Strategy + +Fetches [CAKE](https://bscscan.com/address/0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82) balance from the following sources: + +- Wallet +- CAKE-BNB LP Farm +- CAKE Pool +- CAKE Vault +- Pools that were active at the time of the snapshot diff --git a/Implementations/API/backend/utils/snapshot/strategies/cake/examples.json b/Implementations/API/backend/utils/snapshot/strategies/cake/examples.json new file mode 100644 index 00000000..ea44a612 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cake/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "cake", + "params": { + "symbol": "CAKE" + } + }, + "network": "56", + "addresses": [ + "0x8b017905DC96B38f817473dc885F84D4C76bC113", + "0x21fF20E7e1B820020415707298b92299CF0951fE", + "0xa7A01B93B889ff0639d5ec02914A77529924a46F", + "0x68e0c606036CAcb83D1755ca31f79630468F044e" + ], + "snapshot": 16308675 + }, + { + "name": "Example query - before 16300686 snapshot", + "strategy": { + "name": "cake", + "params": { + "symbol": "CAKE" + } + }, + "network": "56", + "addresses": [ + "0x8b017905DC96B38f817473dc885F84D4C76bC113", + "0x21fF20E7e1B820020415707298b92299CF0951fE", + "0x68e0c606036CAcb83D1755ca31f79630468F044e" + ], + "snapshot": 15204010 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/cake/index.ts b/Implementations/API/backend/utils/snapshot/strategies/cake/index.ts new file mode 100644 index 00000000..497928e9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cake/index.ts @@ -0,0 +1,297 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { strategy as masterChefPoolBalanceStrategy } from '../masterchef-pool-balance'; +import { formatEther } from '@ethersproject/units'; +import { Zero, WeiPerEther } from '@ethersproject/constants'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall, subgraphRequest, Multicaller } from '../../utils'; + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); + +const PAGE_SIZE = 1000; + +export const author = 'pancake-swap'; +export const version = '0.0.1'; + +const CAKE_ADDRESS = '0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82'; +const CAKE_VAULT_ADDRESS = '0xa80240Eb5d7E05d3F250cF000eEc0891d00b51CC'; +const IFO_POOL_START_BLOCK = 13463954; +const IFO_POOL_ADDRESS = '0x1B2A2f6ed4A1401E8C73B4c2B6172455ce2f78E8'; +const CAKE_BNB_LP_ADDRESS = '0x0eD7e52944161450477ee417DE9Cd3a859b14fD0'; + +const MASTER_CHEF_ADDRESS = { + v1: '0x73feaa1eE314F8c655E354234017bE2193C9E24E' +}; + +const onChainVotingPower = { + v0: { + blockNumber: 16300686, + address: '0xc0FeBE244cE1ea66d27D23012B3D616432433F42' + }, + v1: { + blockNumber: 17137653, + address: '0x67Dfbb197602FDB9A9D305cC7A43b95fB63a0A56' + } +}; + +const abi = [ + 'function getVotingPowerWithoutPool(address _user) view returns (uint256)' +]; + +const vaultAbi = [ + 'function getPricePerFullShare() view returns (uint256)', + 'function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime, uint256 cakeAtLastUserAction, uint256 lastUserActionTime)' +]; + +const smartChefUrl = + 'https://api.thegraph.com/subgraphs/name/pancakeswap/smartchef'; + +async function getPools(provider, snapshot: any) { + let blockNumber = snapshot; + if (blockNumber === 'latest') { + blockNumber = await provider.getBlockNumber(); + } + + const params = { + smartChefs: { + __args: { + where: { + stakeToken: CAKE_ADDRESS.toLowerCase(), + endBlock_gte: blockNumber, + startBlock_lt: blockNumber + } + }, + id: true + } + }; + + const pools = await subgraphRequest(smartChefUrl, params); + + return pools.smartChefs; +} + +async function getSmartChefStakedCakeAmount( + snapshot: any, + poolAddresses: string[], + addresses: string[] +) { + const addressChunks = chunk(addresses, 1500); + let results: any[] = []; + + for (const addressChunk of addressChunks) { + const params = { + users: { + __args: { + where: { + pool_in: poolAddresses.map((addr) => addr.toLowerCase()), + address_in: addressChunk.map((addr) => addr.toLowerCase()), + stakeAmount_gt: '0' + }, + first: PAGE_SIZE + }, + address: true, + stakeAmount: true + } + }; + + let page = 0; + let triedBlockNumber = false; + + while (true) { + // @ts-ignore + params.users.__args.skip = page * PAGE_SIZE; + if (snapshot !== 'latest' && !triedBlockNumber) { + // @ts-ignore + params.users.__args.block = { number: snapshot }; + } else { + // @ts-ignore + delete params.users.__args.block; + } + let result; + try { + result = await subgraphRequest(smartChefUrl, params); + } catch (error) { + if (!triedBlockNumber) { + triedBlockNumber = true; + continue; + } else { + throw error; + } + } + if (!Array.isArray(result.users) && !triedBlockNumber) { + triedBlockNumber = true; + continue; + } + results = results.concat(result.users); + page++; + if (result.users.length < PAGE_SIZE) break; + } + } + + return results.reduce>((acc, user) => { + if (acc[user.address]) { + acc[user.address] = (acc[user.address] as BigNumber).add( + user.stakeAmount + ); + } else { + acc[user.address] = BigNumber.from(user.stakeAmount); + } + return acc; + }, {}); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const pools = await getPools(provider, snapshot); + + const userPoolBalance = await getSmartChefStakedCakeAmount( + snapshot, + pools.map((p) => p.id), + addresses + ); + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + if ( + blockTag === 'latest' || + (typeof blockTag === 'number' && + blockTag >= onChainVotingPower.v0.blockNumber) + ) { + let callData = addresses.map((address: any) => [ + typeof blockTag === 'number' && + blockTag < onChainVotingPower.v1.blockNumber + ? onChainVotingPower.v0.address + : onChainVotingPower.v1.address, + 'getVotingPowerWithoutPool', + [address.toLowerCase()] + ]); + + callData = [...chunk(callData, options.max || 300)]; + const response: any[] = []; + for (const call of callData) { + const multiRes = await multicall(network, provider, abi, call, { + blockTag + }); + response.push(...multiRes); + } + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat( + formatEther( + (userPoolBalance[addresses[i].toLowerCase()] || Zero).add( + value.toString() + ) + ) + ) + ]) + ); + } + + const erc20Balance = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + { + address: CAKE_ADDRESS, + symbol: 'CAKE', + decimals: 18 + }, + snapshot + ); + + const cakeBnbLpBalance = await masterChefPoolBalanceStrategy( + space, + network, + provider, + addresses, + { + chefAddress: MASTER_CHEF_ADDRESS.v1, + uniPairAddress: CAKE_BNB_LP_ADDRESS, + pid: '251', + symbol: 'CAKE-BNB LP', + tokenIndex: 0 + }, + snapshot + ); + + const cakeVaultBalance = await getVaultBalance( + network, + provider, + addresses, + blockTag + ); + + return Object.fromEntries( + addresses.map((address) => [ + address, + erc20Balance[address] + + cakeBnbLpBalance[address] + + parseFloat( + formatEther( + (userPoolBalance[address.toLowerCase()] || Zero).add( + cakeVaultBalance[address] || Zero + ) + ) + ) + ]) + ); +} + +async function getVaultBalance(network, provider, addresses, blockTag) { + const vaultMulti = new Multicaller(network, provider, vaultAbi, { blockTag }); + + vaultMulti.call( + CAKE_VAULT_ADDRESS, + CAKE_VAULT_ADDRESS, + 'getPricePerFullShare' + ); + + addresses.forEach((address) => + vaultMulti.call( + `${CAKE_VAULT_ADDRESS}-${address}`, + CAKE_VAULT_ADDRESS, + 'userInfo', + [address] + ) + ); + + if (blockTag >= IFO_POOL_START_BLOCK) { + vaultMulti.call(IFO_POOL_ADDRESS, IFO_POOL_ADDRESS, 'getPricePerFullShare'); + + addresses.forEach((address) => { + vaultMulti.call( + `${IFO_POOL_ADDRESS}-${address}`, + IFO_POOL_ADDRESS, + 'userInfo', + [address] + ); + }); + } + + const vaultMultiRes = await vaultMulti.execute(); + + return Object.fromEntries( + addresses.map((address) => [ + address, + (vaultMultiRes[CAKE_VAULT_ADDRESS] || Zero) + .mul(vaultMultiRes[`${CAKE_VAULT_ADDRESS}-${address}`]?.shares || Zero) + .div(WeiPerEther) + .add( + (vaultMultiRes[IFO_POOL_ADDRESS] || Zero) + .mul( + vaultMultiRes[`${IFO_POOL_ADDRESS}-${address}`]?.shares || Zero + ) + .div(WeiPerEther) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/README.md new file mode 100644 index 00000000..05236ce6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/README.md @@ -0,0 +1,62 @@ +# cap-voting-power + +This strategy is used to calculate the off-chain voting power for addresses with tokens vested in a contract. It also includes a clamping mechanism to limit the voting power of the vesting contracts based on the total voting power of the voting escrow contract. All parameters are required. + +Some things to note if you plan on using this strategy: + +1. All vestingAddresses objects MUST have address, lockedTokens, cliffMonths, vestingMonths, startDateTimestamp and initialReleasePercentage. +2. The Vesting Contract's voting power is clamped to a percentage (clampPercentage) of the total voting power. This value must be between 0 and 1. +3. Total voting power is calculated based on the Voting Escrow Contract's voting power. Therefore, you must specify the voting escrow contract address (votingEscrowContractAddress) and the number of decimals in your token (decimals) as parameters. +4. For optimal performance and memory management, the strategy imposes a limit of 500 vesting addresses in the parameters. Adhere to this limit when setting up the strategy. + +Here is an example of parameters: + +```json +{ + "votingEscrowContractAddress": "0x3362A77AC77fF5098618F8C7CFB4eA27E738229f", + "decimals": 18, + "vestingAddresses": [ + { + "address": "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "lockedTokens": 1000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + }, + { + "address": "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "lockedTokens": 2000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + }, + { + "address": "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "lockedTokens": 5000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + }, + { + "address": "0xC83df6FD76484938C10843fa37c7Cbba327c8eDC", + "lockedTokens": 10000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + }, + { + "address": "0x8E83aD3ecC12E2e2Df1021CDe01e53C9465D5883", + "lockedTokens": 20000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + } + ], + "clampPercentage": 0.4 +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/examples.json new file mode 100644 index 00000000..f44cd72d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/examples.json @@ -0,0 +1,66 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "cap-voting-power", + "params": { + "votingEscrowContractAddress": "0x3362A77AC77fF5098618F8C7CFB4eA27E738229f", + "decimals": 18, + "vestingAddresses": [ + { + "address": "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "lockedTokens": 1000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + }, + { + "address": "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "lockedTokens": 2000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + }, + { + "address": "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "lockedTokens": 5000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + }, + { + "address": "0xC83df6FD76484938C10843fa37c7Cbba327c8eDC", + "lockedTokens": 10000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + }, + { + "address": "0x8E83aD3ecC12E2e2Df1021CDe01e53C9465D5883", + "lockedTokens": 20000, + "cliffMonths": 12, + "vestingMonths": 24, + "startDateTimestamp": 1602033545, + "initialReleasePercentage": 0.25 + } + ], + "clampPercentage": 0.4 + } + }, + "network": "80001", + "addresses": [ + "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0xC83df6FD76484938C10843fa37c7Cbba327c8eDC", + "0x8E83aD3ecC12E2e2Df1021CDe01e53C9465D5883" + ], + "snapshot": 33129875 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/index.ts new file mode 100644 index 00000000..05831351 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cap-voting-power/index.ts @@ -0,0 +1,278 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Contract } from '@ethersproject/contracts'; + +export const author = 'unRealGamer28'; +export const version = '0.1.0'; + +const abi = ['function totalSupply(uint256 t) external view returns (uint256)']; + +const maxLockupDuration = 4 * 365 * 24 * 60 * 60; // Maximum lockup duration is 4 years in seconds based on Curve VE contract +const maxVestingDuration = 4 * 365 * 24 * 60 * 60; // Maximum vesting duration is 4 years in seconds +const maxVestingAddressCount = 500; // Maximum number of vesting addresses + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + // Assertions/checks to validate options + if (!options.vestingAddresses || options.vestingAddresses.length === 0) { + throw new Error( + 'Invalid options provided! Please make sure vestingAddresses is provided.' + ); + } + if (options.vestingAddresses.length > maxVestingAddressCount) { + throw new Error( + `Too many vesting addresses provided! The maximum allowed is ${maxVestingAddressCount}.` + ); + } + options.vestingAddresses.forEach((vestingAddress) => { + if ( + !('address' in vestingAddress) || + !('lockedTokens' in vestingAddress) || + !('cliffMonths' in vestingAddress) || + !('vestingMonths' in vestingAddress) || + !('startDateTimestamp' in vestingAddress) || + !('initialReleasePercentage' in vestingAddress) + ) { + throw new Error( + 'Invalid options provided! Please make sure each vesting address object has all required properties.' + ); + } + }); + if (options.clampPercentage < 0 || options.clampPercentage > 1) { + throw new Error( + 'Invalid clamp percentage! Please provide a number between 0 and 1.' + ); + } + if (!options.votingEscrowContractAddress || !options.decimals) { + throw new Error( + 'Invalid options provided! Please make sure votingEscrowContractAddress and decimals are provided.' + ); + } + + // Find current timestamp + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + let block; + try { + block = await provider.getBlock(blockTag); + } catch (error) { + throw new Error('Failed to get block information'); + } + const now = block.timestamp; + + // Calculate voting power for each vesting address and create vestingAddressesArray + const vestingVotingPower = {}; + const vestingAddressesArray: string[] = []; + options.vestingAddresses.forEach((vestingAddress) => { + const { + address, + lockedTokens, + cliffMonths, + vestingMonths, + startDateTimestamp, + initialReleasePercentage + } = vestingAddress; + + // Calculate the cliffEndDateTimestamp by adding the cliff duration (in seconds) to the startDateTimestamp. + const cliffEndDateTimestamp = + startDateTimestamp + ((cliffMonths * 365) / 12) * (24 * 60 * 60); + // Calculate the endDateTimestamp by adding the total vesting duration (in seconds) to the startDateTimestamp. + const endDateTimestamp = + startDateTimestamp + + (((cliffMonths + vestingMonths) * 365) / 12) * (24 * 60 * 60); + const votingPower = votingPowerCalc( + lockedTokens, + now, + startDateTimestamp, + cliffEndDateTimestamp, + endDateTimestamp, + initialReleasePercentage + ); + + // Add the final voting power of any duplicate addresses + vestingVotingPower[address] = + vestingVotingPower[address] === undefined + ? votingPower + : vestingVotingPower[address] + votingPower; + + vestingAddressesArray.push(address); + }); + + // Calculate total voting power for vesting addresses + let vestingTotalVotingPower = 0; + Object.values(vestingVotingPower).forEach((value) => { + if (typeof value === 'number') vestingTotalVotingPower += value; + }); + + // Get voting escrow's total voting power + const votingEscrowContract = new Contract( + options.votingEscrowContractAddress, + abi, + provider + ); + const votingEscrowTotalVotingPower: BigNumber = + await votingEscrowContract.totalSupply(now); + const escrowTotalVotingPower = parseFloat( + formatUnits(votingEscrowTotalVotingPower, options.decimals) + ); + + // Clamp the vesting voting power for each address if the total vesting voting power is greater than the clamped total vesting voting power based on the clamp percentage + const vestingClampedTotalVotingPower = calculateClampedVotingPower( + escrowTotalVotingPower, + options.clampPercentage + ); + if (vestingTotalVotingPower > vestingClampedTotalVotingPower) { + const clampRatio = vestingClampedTotalVotingPower / vestingTotalVotingPower; + + // Calculate the new clamped voting power for each vesting address + Object.keys(vestingVotingPower).forEach((address) => { + vestingVotingPower[address] = vestingVotingPower[address] * clampRatio; + }); + } + + // Return the final voting power for addresses that are in the vestingAddressesArray + const result = {}; + addresses.forEach((address: string) => { + if (vestingAddressesArray.includes(address)) { + result[address] = vestingVotingPower[address]; + } + }); + return result; +} + +/** + * Calculate the voting power based on the given parameters. + * + * @function + * @param {number} lockedToken - The number of tokens locked in the vesting contract. + * @param {number} currentDateTS - The current date timestamp (in seconds). + * @param {number} startDateTS - The start date timestamp of the vesting period (in seconds). + * @param {number} cliffEndDateTS - The end date timestamp of the cliff period (in seconds). + * @param {number} endDateTS - The end date timestamp of the vesting period (in seconds). + * @param {number} initialReleasePercentage - The initial percentage of tokens released at the end of the cliff. + * @returns {number} The calculated voting power based on the input parameters. + */ +function votingPowerCalc( + lockedToken, + currentDateTS, + startDateTS, + cliffEndDateTS, + endDateTS, + initialReleasePercentage +): number { + // Store function parameters as constants + const lockedTokenAmount = lockedToken; + let currentTimestamp = currentDateTS; + const startTimestamp = startDateTS; + const cliffEndTimestamp = cliffEndDateTS; + const endTimestamp = endDateTS; + + // Calculate initial release amount based on initial release percentage + const initialReleaseAmount = lockedTokenAmount * initialReleasePercentage; + // Calculate adjusted locked token amount + const adjustedLockedTokenAmount = lockedTokenAmount - initialReleaseAmount; + + // Assertions/checks to limit function parameters + if (startTimestamp > currentTimestamp) currentTimestamp = startTimestamp; + if (endTimestamp < currentTimestamp) return 0; // If endTimestamp is in the past, return 0 voting power + if (startTimestamp > endTimestamp) + throw new Error('Vesting start date cannot begin after the end date.'); + if (startTimestamp > cliffEndTimestamp) + throw new Error( + 'Vesting start date cannot begin after the cliff end date.' + ); + if (endTimestamp < cliffEndTimestamp) + throw new Error('Vesting end date cannot end before the cliff end date.'); + if (lockedTokenAmount <= 0) + throw new Error('Token amount must be greater than 0.'); + + // Calculate cliffDuration and vestingDuration, and asset it matches to totalDuration + const totalDuration = endTimestamp - startTimestamp; + const cliffDuration = cliffEndTimestamp - startTimestamp; + const vestingDuration = endTimestamp - cliffEndTimestamp; + if (cliffDuration + vestingDuration != totalDuration) + throw new Error( + "Cliff duration and vesting duration don't match up to total duration." + ); + + // Calculate remaining cliff duration and vesting duration + let remainingCliffDuration; + if (currentTimestamp >= cliffEndTimestamp) remainingCliffDuration = 0; + else remainingCliffDuration = cliffEndTimestamp - currentTimestamp; + + let remainingVestingDuration; + if (currentTimestamp >= cliffEndTimestamp) + remainingVestingDuration = endTimestamp - currentTimestamp; + else remainingVestingDuration = vestingDuration; + + // Clamp remaining cliff duration and vesting duration to maxLockupDuration and maxVestingDuration respectively + const clampedRemainingCliffDuration = Math.min( + remainingCliffDuration, + maxLockupDuration + ); + const clampedRemainingVestingDuration = Math.min( + remainingVestingDuration, + maxVestingDuration + ); + + // Calculate decayed voting power for the cliffDuration (25% portion) + const decayCliffVPInitialRelease = + (initialReleaseAmount / maxLockupDuration) * clampedRemainingCliffDuration; + + // Calculate decayed voting power for the cliffDuration (75% portion) + const decayCliffVPAdjustedLockedToken = + (adjustedLockedTokenAmount / maxLockupDuration) * + clampedRemainingCliffDuration; + + // Calculate the average decayed voting power for the vestingDuration (75% portion) + const tokensVestedPerSecond = adjustedLockedTokenAmount / vestingDuration; + const remainingTokenAmount = remainingVestingDuration * tokensVestedPerSecond; + + const decayVestingVPAdjustedLockedToken = + (remainingTokenAmount / maxVestingDuration) * + clampedRemainingVestingDuration * + 0.5; // Average the decayed voting power + + // Combine all decayed voting power + const currentVotingPower = + decayCliffVPInitialRelease + + decayCliffVPAdjustedLockedToken + + decayVestingVPAdjustedLockedToken; + + // Cap currentVotingPower to maximumVotingPower (lockedTokenAmount) + if (currentVotingPower >= lockedTokenAmount) { + return lockedTokenAmount; + } + + // Finally, return the currentVotingPower + return currentVotingPower; +} + +/** + * Calculate the clamped voting power of the vesting contract. + * + * @param {number} escrowVotingPower - The total voting power of the escrow contract. + * @param {number} vestingClampPercentage - The desired percentage of total voting power for the vesting contract. + * @return {number} The clamped voting power for the vesting contract. + */ +function calculateClampedVotingPower( + escrowVotingPower, + vestingClampPercentage +) { + // Calculate the percentage of voting power allocated to the escrow contract, + // which is the remaining percentage after allocating the vestingClampPercentage to the vesting contract + const escrowClampPercentage = 1 - vestingClampPercentage; + + // Calculate the combined total voting power of the escrow and vesting contracts, + // considering the escrowVotingPower and the escrowClampPercentage + const totalVotingPower = escrowVotingPower / escrowClampPercentage; + + // Return the clamped voting power for the vesting contract, + // which is the product of the total voting power and the vestingClampPercentage + return totalVotingPower * vestingClampPercentage; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/README.md b/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/README.md new file mode 100644 index 00000000..c90f799d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/README.md @@ -0,0 +1,15 @@ +# Capital Dao Staking Token + +This strategy returns balances of the underlying token in Capital Dao Staking pools + +Here is an example of parameters: + +```json +{ + "symbol": "CDS", + "decimals": 18, + "tokenAddress": "0x6139b11c7CE407fb2AAEc3bFdFa97e3A21330843", + "stakingAddress": "0xe157B35C9E8798f61F537a117a5d059A883C8A6F", + "poolIndex": 0 +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/examples.json new file mode 100644 index 00000000..8b76e2f5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Capital Dao Staked Token", + "strategy": { + "name": "capitaldao-staking", + "params": { + "symbol": "CDS", + "decimals": 18, + "tokenAddress": "0x6139b11c7CE407fb2AAEc3bFdFa97e3A21330843", + "stakingAddress": "0xe157B35C9E8798f61F537a117a5d059A883C8A6F", + "poolIndex": 0 + } + }, + "network": "4", + "addresses": [ + "0x95298790beb442f204e3864c5bd4073905185108", + "0xCe44798440952EBd75Cf1FDC62a996d28137eF30" + ], + "snapshot": 10343903 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/index.ts new file mode 100644 index 00000000..70d65a9b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/capitaldao-staking/index.ts @@ -0,0 +1,49 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'capitaldao'; +export const version = '0.1.0'; + +const masterChefAbi = [ + 'function users(uint256, address) view returns (uint256 amount, uint256 rewardDebt, uint256 lastDepositAt)' +]; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Get staked LP in staking contract + const response = await multicall( + network, + provider, + masterChefAbi, + [ + ...addresses.map((address: any) => [ + options.stakingAddress, + 'users', + [options.poolIndex, address] + ]) + ], + { blockTag } + ); + + return Object.fromEntries( + response.map((user, i) => { + const parsedAmount = parseFloat( + formatUnits(bn(user.amount), options.decimal) + ); + return [addresses[i], parsedAmount]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/README.md new file mode 100644 index 00000000..060be94d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/README.md @@ -0,0 +1,12 @@ +# celer-sgn-delegation + +Strategy to count the number of CELR tokens delegated to SGN validators. + +Here is an example of parameters: + +```json +{ + "v1StakingAddress": "0x5216db4d4cb22d1ba38866867c38d8e862974e82", + "v2StakingViewerAddress": "0x5803457E3074E727FA7F9aED60454bf2F127853b" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/examples.json new file mode 100644 index 00000000..fdfc108c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/examples.json @@ -0,0 +1,43 @@ +[ + { + "name": "Celer SGN Delegation example query", + "strategy": { + "name": "celer-sgn-delegation", + "params": { + "v1StakingAddress": "0x5216db4d4cb22d1ba38866867c38d8e862974e82", + "v2StakingViewerAddress": "0x5803457E3074E727FA7F9aED60454bf2F127853b", + "symbol": "CELR" + } + }, + "network": "1", + "addresses": [ + "0x884ad5933693d8575c37247774c9c1148a1c4e9b", + "0xe136fee678e352b5438d9c87af83d57e495cf432", + "0xa554ab2c38c1aa82281e6627ff3d2ea5a08adbb0", + "0xf977814e90da44bfa03b6295a0616a897441acec", + "0xd7539FCdC0aB79a7B688b04387cb128E75cb77Dc", + "0x6E33e22f7aC5A4b58A93C7f6D8Da8b46c50A3E20", + "0xC9dA7343583fA8Bb380A6F04A208C612F86C7701", + "0x2AC89522CB415AC333E64F52a1a5693218cEBD58", + "0xd90c6f6D37716b1Cc4dd2B116be42e8683550F45", + "0x69ABF813a683391C0ec888351912E14590B56e88", + "0x85924aA0B2cb5a0BbeC583Dd090bF7CEdBa5D2Ea", + "0x9149B2b87159c4CC9e2f10C2711357720Da4DA08", + "0xa0710d3b4BA0f848f7edf9CC827aF70A183EAd26", + "0xAE1220f6bFEb414Ed0A95fbb5A6Ecc303b10aa46", + "0x2edd637af99f6822d983bd3406d6f414e73108cc", + "0x6f69286d7c51de0f0cf5f3c03c8ca3eda8a322f9", + "0xf4c9bd3b5f6c589b3a25f8f9d7aeb0aa81c5f4b3", + "0x7e9c5d07dbdfa95e1645328c345ace45c4d43b9b", + "0x5a477cf891d4603cd40679155d09b5d37c48cf2c", + "0x804c4ac4fdc73767b54f8e69eb447d64516f912f", + "0xcec9e5318e427b7014c3a207d02e10288e333d0d", + "0x67c8db836b46e61804a744478bdc7d3804782577", + "0xd55a339509b514ef4499bbe416c94a00bbe87fcc", + "0x228b0f599ad555161b91c1f53a133bef8b664864", + "0xcbd77b27e76cc4725081e34763fac6a919898f4c", + "0xdb7bbd7445d149cbd8650901d1d76a27545044f0" + ], + "snapshot": 14887099 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/index.ts new file mode 100644 index 00000000..aa1b0978 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/celer-sgn-delegation/index.ts @@ -0,0 +1,142 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Provider } from '@ethersproject/providers'; +import { formatUnits } from '@ethersproject/units'; + +import { multicall } from '../../utils'; + +export const author = 'dominator008'; +export const version = '0.2.0'; + +const v1StakingABI = [ + 'function getValidatorNum() view returns (uint256)', + 'function validatorSet(uint256) view returns (address)', + 'function getDelegatorInfo(address _candidateAddr, address _delegatorAddr) view returns (uint256 delegatedStake, uint256 undelegatingStake, uint256[] intentAmounts, uint256[] intentProposedTimes)' +]; + +const v2StakingABI = [ + 'function getDelegatorTokens(address _delAddr) view returns (uint256, uint256)' +]; + +export async function strategy( + space: string, + network: string, + provider: Provider, + addresses: string[], + options: { v1StakingAddress: string; v2StakingViewerAddress: string }, + snapshot: string | number +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Staking V1 + // 1.1. Get the number of validators + const validatorNum: BigNumber = ( + await multicall( + network, + provider, + v1StakingABI, + [[options.v1StakingAddress, 'getValidatorNum', []]], + { blockTag } + ) + )[0][0]; + + // 1.2. Get the addresses of the validators + const validatorAddresses: string[] = ( + await multicall( + network, + provider, + v1StakingABI, + Array.from(Array(validatorNum.toNumber()).keys()).map((index: number) => [ + options.v1StakingAddress, + 'validatorSet', + [index] + ]), + { blockTag } + ) + ).map((value: string[]) => value[0]); + + // 1.3. Get the delegation of all addresses to all validators + const callInfos = validatorAddresses.reduce( + (infos, validatorAddress) => + infos.concat( + addresses.map((address: string) => [ + address, + [ + options.v1StakingAddress, + 'getDelegatorInfo', + [validatorAddress, address] + ] + ]) + ), + [] + ); + const callInfosCopy = [...callInfos]; + const batchSize = 2000; + const batches = new Array(Math.ceil(callInfos.length / batchSize)) + .fill(0) + .map(() => callInfosCopy.splice(0, batchSize)); + let delegatorInfoResponse: any[] = []; + for (let i = 0; i < batches.length; i++) { + delegatorInfoResponse = delegatorInfoResponse.concat( + await multicall( + network, + provider, + v1StakingABI, + batches[i].map((info) => info[1]), + { blockTag } + ) + ); + } + + // 1.4. For each address, aggregate the delegations to each validator + const delegations = delegatorInfoResponse.map((info, i) => [ + callInfos[i][0], + info.delegatedStake + ]); + const aggregatedDelegations = delegations.reduce((aggregates, delegation) => { + const delegatorAddress = delegation[0]; + if (aggregates[delegatorAddress]) { + aggregates[delegatorAddress] = aggregates[delegatorAddress].add( + delegation[1] + ); + } else { + aggregates[delegatorAddress] = delegation[1]; + } + return aggregates; + }, {}); + + // Staking V2 + // 2.1. Get delegator tokens for all addresses + const v2DelegatorTokens: string[] = ( + await multicall( + network, + provider, + v2StakingABI, + addresses.map((address: string) => [ + options.v2StakingViewerAddress, + 'getDelegatorTokens', + [address] + ]), + { blockTag } + ) + ).map((value: string[]) => value[0]); + const v2DelegatorTokensMap = addresses.reduce((map, address, i) => { + map[address] = v2DelegatorTokens[i]; + return map; + }, {}); + + // 3. Sum up V1 and V2 delegations + return Object.entries(aggregatedDelegations).reduce( + (transformed, [delegatorAddress, delegatedStake]) => { + transformed[delegatorAddress] = parseFloat( + formatUnits(delegatedStake.toString(), 18) + ); + if (v2DelegatorTokensMap[delegatorAddress]) { + transformed[delegatorAddress] += parseFloat( + formatUnits(v2DelegatorTokensMap[delegatorAddress].toString(), 18) + ); + } + return transformed; + }, + {} + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/README.md b/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/README.md new file mode 100644 index 00000000..fa6decec --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/README.md @@ -0,0 +1,33 @@ +# ChubbyKaijuDAO strategy + +This strategy return the balances of the voters for ChubbyKaijuDAO project from both staking pool and ERC721 NFT. +Most of codes are from the "mutant-cats-stakers-and-holders" strategy. + +## Accepted options + +- **staking:** ChubbyKaijuDAO Gen1 staking V1 pool address. + +- **token:** ChubbyKaijuDAO Gen1 ERC721 NFT address. + +## Examples + +```JSON +[ + { + "name": "chubbykaijudao", + "strategy": { + "name": "chubbykaijudao", + "params": { + "staking": "0x42299C513e442123D0903ca9e4A009dEE89Ae5de", + "token": "0x65b28ED75c12D8ce29d892DE9f8304A6D2e176A7" + } + }, + "network": "1", + "addresses": [ + "0xbA6f51199725D4f1F6B1A9E5fEFdc597eDC89B8A", + "0x9534643b04d4b51B90FcdDeeBB628efC54e58D64" + ], + "snapshot": 14129872 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/examples.json b/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/examples.json new file mode 100644 index 00000000..55e6cfb4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "chubbykaijudao", + "strategy": { + "name": "chubbykaijudao", + "params": { + "staking": "0x42299C513e442123D0903ca9e4A009dEE89Ae5de", + "token": "0x65b28ED75c12D8ce29d892DE9f8304A6D2e176A7", + "symbol": "CKAIJU", + "decimals": 0 + } + }, + "network": "1", + "addresses": [ + "0xbA6f51199725D4f1F6B1A9E5fEFdc597eDC89B8A", + "0x9534643b04d4b51B90FcdDeeBB628efC54e58D64" + ], + "snapshot": 14129872 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/index.ts b/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/index.ts new file mode 100644 index 00000000..2374592e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/chubbykaijudao/index.ts @@ -0,0 +1,49 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'yosep'; +export const version = '0.1.0'; + +const stakingAbi = [ + 'function GEN1depositsOf(address account) external view returns (uint16[] memory)' +]; + +const tokenAbi = [ + 'function balanceOf(address owner) public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPool = new Multicaller(network, provider, stakingAbi, { + blockTag + }); + const tokenPool = new Multicaller(network, provider, tokenAbi, { + blockTag + }); + + addresses.forEach((address) => { + stakingPool.call(address, options.staking, 'GEN1depositsOf', [address]); + tokenPool.call(address, options.token, 'balanceOf', [address]); + }); + + const [stakingResponse, tokenResponse]: [ + Record, + Record + ] = await Promise.all([stakingPool.execute(), tokenPool.execute()]); + + return Object.fromEntries( + addresses.map((address) => { + const stakingCount = stakingResponse[address].length; + const tokenCount = BigNumber.from(tokenResponse[address]).toNumber(); + return [address, stakingCount + tokenCount]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/README.md b/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/README.md new file mode 100644 index 00000000..99d7c3c1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/README.md @@ -0,0 +1,29 @@ +# CityDAO Square Root Strategy + +Holders of an ERC1155 token can cast a number of votes equal to the square root of their net token holdings. + +### Example + +This example uses [CityDAO's Citizen Token](https://opensea.io/assets/0x7eef591a6cc0403b9652e98e88476fe1bf31ddeb/42). + +```json +{ + "symbol": "CITIZEN", + "address": "0x7EeF591A6CC0403b9652E98E88476fe1bF31dDeb", + "tokenId": 42, + "decimals": 0 +} +``` + +### Development + +#### Testing + +```shell +yarn test --strategy=citydao-square-root +``` + +#### Changelog + +- **0.0.1** + - Makes initial commit of strategy. diff --git a/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/examples.json b/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/examples.json new file mode 100644 index 00000000..c32db5f3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/examples.json @@ -0,0 +1,41 @@ +[ + { + "name": "Example", + "strategy": { + "name": "citydao-square-root", + "params": { + "symbol": "CITIZEN", + "address": "0x7EeF591A6CC0403b9652E98E88476fe1bF31dDeb", + "tokenId": 42, + "decimals": 0 + } + }, + "network": "1", + "addresses": [ + "0xafa46468De1D6f1ab77DEFAe5F7657467911182d", + "0x944Fd37C2a46A3ad6B5EE11f7e26035Ed2F1E4FE", + "0x4c55C41Bd839B3552fb2AbecaCFdF4a5D2879Cb9" + ], + "snapshot": 14616178 + }, + { + "name": "Plural Voting Example", + "strategy": { + "name": "citydao-square-root", + "params": { + "symbol": "CITIZEN", + "address": "0x7EeF591A6CC0403b9652E98E88476fe1bF31dDeb", + "tokenId": 42, + "decimals": 0, + "voiceCredits": 9 + } + }, + "network": "1", + "addresses": [ + "0xafa46468De1D6f1ab77DEFAe5F7657467911182d", + "0x944Fd37C2a46A3ad6B5EE11f7e26035Ed2F1E4FE", + "0x4c55C41Bd839B3552fb2AbecaCFdF4a5D2879Cb9" + ], + "snapshot": 14616178 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/index.ts b/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/index.ts new file mode 100644 index 00000000..e9077821 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/citydao-square-root/index.ts @@ -0,0 +1,64 @@ +// Types +import type { StaticJsonRpcProvider } from '@ethersproject/providers'; + +// Strategies +import { strategy as erc1155BalanceOfStrategy } from '../erc1155-balance-of'; + +type ScoresByAddress = { + [address: string]: number; +}; + +type Params = { + // Token Label + symbol: string; + // Contract Address + address: string; + // Token to measure holdings of + tokenId: number; + // Decimal places used by token + decimals: number; + // Strategy is extensible to Plural Voting by setting >1. + // see https://www.radicalxchange.org/concepts/plural-voting/ + voiceCredits?: number; +}; + +const author = 'citydao'; +const version = '0.0.1'; + +/** + * CityDAO Square Root Snapshot Strategy + * @version 0.0.1 + * @summary Holders of an ERC1155 token can cast a number of votes equal to the square root of their net token holdings. + * @see https://archive.ph/beczV + * @author Will Holley + */ +async function strategy( + space: string, + network: string, + provider: StaticJsonRpcProvider, + addresses: Array, + params: Params, + snapshot: number +): Promise { + // Query default scores. + const scores: ScoresByAddress = await erc1155BalanceOfStrategy( + space, + network, + provider, + addresses, + params, + snapshot + ); + + // Support Plural Voting + const magnitude = params.voiceCredits || 1; + + // Update in place, rounding down. + for (const address in scores) { + scores[address] = Math.floor(Math.sqrt(scores[address] * magnitude)); + } + + return scores; +} + +export { author, version, strategy }; diff --git a/Implementations/API/backend/utils/snapshot/strategies/clqdr-balance-with-lp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/clqdr-balance-with-lp/examples.json new file mode 100644 index 00000000..e497c667 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/clqdr-balance-with-lp/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "clqdr-balance-with-lp", + "params": { + "address": "0x814c66594a22404e101FEcfECac1012D8d75C156", + "symbol": "cLQDR", + "decimals": 18 + } + }, + "network": "250", + "addresses": [ + "0xf4c5b06ff9cd8f685ddcc58202597e56f1c0faee", + "0x70ECC7FecAea8D67e820035ED48c53706E7F2079", + "0xb5ae3c648709913ef9739e9f6edb5a821c6ab160", + "0x9675fe51fcfa05dbff4d027706f0a97b74fe5dc7", + "0x85644679fd440cd55c7046f2748ec5479cb3c3ab", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 49467339 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/clqdr-balance-with-lp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/clqdr-balance-with-lp/index.ts new file mode 100644 index 00000000..6dcd9d74 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/clqdr-balance-with-lp/index.ts @@ -0,0 +1,148 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, multicall } from '../../utils'; + +export const author = 'LiquidDriver-finance'; +export const version = '0.0.1'; + +const liquidMasterAddress = '0x6e2ad6527901c9664f016466b8DA1357a004db0f'; +const beetsMasterAddress = '0x8166994d9ebBe5829EC86Bd81258149B87faCfd3'; +const lpAddress = '0xEAdCFa1F34308b144E96FcD7A07145E027A8467d'; +const beetsVaultAddress = '0x20dd72Ed959b6147912C2e529F0a0C651c33c9ce'; +const clqdrPoolId = + '0xeadcfa1f34308b144e96fcd7a07145e027a8467d000000000000000000000331'; + +const contractAbi = [ + 'function userInfo(uint256, address) view returns (uint256 amount, int256 rewardDebt)', + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address _owner) view returns (uint256 balance)', + 'function getPoolTokens(bytes32 poolId) view returns (uint256[], uint256[], uint256)', + 'function getVirtualSupply() external view returns (uint256)' +]; + +const bn = (num: any): BigNumber => { + return BigNumber.from(num.toString()); +}; + +const addUserBalance = (userBalances, user: string, balance) => { + if (userBalances[user]) { + return (userBalances[user] = userBalances[user].add(balance)); + } else { + return (userBalances[user] = balance); + } +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const res = await multicall( + network, + provider, + contractAbi, + [ + [beetsVaultAddress, 'getPoolTokens', [clqdrPoolId]], + [lpAddress, 'getVirtualSupply', []] + ], + { blockTag } + ); + + const totalClqdrInBeets = bn(res[0][1][1]); + + const virtualSupply = bn(res[1]); + + const userCLqdrBalances: any = []; + for (let i = 0; i < addresses.length - 1; i++) { + userCLqdrBalances[addresses[i]] = bn(0); + } + + const clqdrMulti = new Multicaller(network, provider, contractAbi, { + blockTag + }); + addresses.forEach((address) => + clqdrMulti.call(address, options.address, 'balanceOf', [address]) + ); + const clqdrToken: Record = await clqdrMulti.execute(); + + Object.fromEntries( + Object.entries(clqdrToken).map(([address, balance]) => { + return addUserBalance(userCLqdrBalances, address, balance); + }) + ); + + const userLpBalances: any = []; + for (let i = 0; i < addresses.length - 1; i++) { + userLpBalances[addresses[i]] = bn(0); + } + + const multi = new Multicaller(network, provider, contractAbi, { blockTag }); + addresses.forEach((address) => + multi.call(address, lpAddress, 'balanceOf', [address]) + ); + const resultToken: Record = await multi.execute(); + + Object.fromEntries( + Object.entries(resultToken).map(([address, balance]) => { + return addUserBalance(userLpBalances, address, balance); + }) + ); + + const multiLiquidMaster = new Multicaller(network, provider, contractAbi, { + blockTag + }); + + addresses.forEach((address) => + multiLiquidMaster.call(address, liquidMasterAddress, 'userInfo', [ + '43', + address + ]) + ); + const resultLiquidMaster: Record = + await multiLiquidMaster.execute(); + + Object.fromEntries( + Object.entries(resultLiquidMaster).map(([address, balance]) => { + return addUserBalance(userLpBalances, address, balance[0]); + }) + ); + + const multiBeetsMaster = new Multicaller(network, provider, contractAbi, { + blockTag + }); + + addresses.forEach((address) => + multiBeetsMaster.call(address, beetsMasterAddress, 'userInfo', [ + '69', + address + ]) + ); + const resultBeetsMaster: Record = + await multiBeetsMaster.execute(); + + Object.fromEntries( + Object.entries(resultBeetsMaster).map(([address, balance]) => { + return addUserBalance(userLpBalances, address, balance[0]); + }) + ); + + return Object.fromEntries( + Object.entries(userLpBalances).map(([address, balance]) => { + const clqdrBalanceInLp = totalClqdrInBeets + // @ts-ignore + .mul(balance) + .div(virtualSupply); + const totalBalance = userCLqdrBalances[address].add(clqdrBalanceInLp); + return [ + address, + // @ts-ignore + parseFloat(formatUnits(totalBalance, options.decimals)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/coinswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/coinswap/README.md new file mode 100644 index 00000000..91254249 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/coinswap/README.md @@ -0,0 +1,47 @@ +# Coinswap + +This is the most common strategy, it returns the balances of the voters for a balances CSS token +in Coinswap project(pools, farms, Liquidity, token). + +Here is an example of parameters: +```json +[ + { + "name": "Example query Coinswap", + "strategy": { + "name": "coinswap", + "params": { + "address": "0x3bc5798416c1122BcFd7cb0e055d50061F23850d", + "masterChef": "0x3A0a988D680dBBB02DECBfd35F9E0676B4bEc292", + "smartChef": [ + "0x496a0227f7f16622650DDf2601B6842e845203C5", + "0xa25EA2B60c1a1365f195Cfda61b9fb7Eb8fcC38B", + "0x5Ca94e1b35C0a726E5431F66DBECDD2253cA6cb1", + "0x29a888e301A9fF0f4420a115F61E6ad0750Db9dE" + ], + + "cssLPs": [ + { + "address": "0xfA8E0C0568edcDD3D9b12B48792a5B00018FdB57", + "pid": 1 + }, + { + "address": "0x2f32E2252a6979704F0f540b7988Fa8B8C36B292", + "pid": 2 + } + ], + "symbol": "CSS", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0x3a675f2fff053494aa0e9f753f912c4be314d1e7", + "0x2101e095244b4fd2ff228725faa692fd7261c074" + ], + "snapshot": 9837410 + } +] + + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/coinswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/coinswap/examples.json new file mode 100644 index 00000000..3c329498 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/coinswap/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query Coinswap", + "strategy": { + "name": "coinswap", + "params": { + "address": "0x3bc5798416c1122BcFd7cb0e055d50061F23850d", + "masterCSS": "0x3A0a988D680dBBB02DECBfd35F9E0676B4bEc292", + "communityStake": [ + "0xa25EA2B60c1a1365f195Cfda61b9fb7Eb8fcC38B", + "0x5Ca94e1b35C0a726E5431F66DBECDD2253cA6cb1", + "0x29a888e301A9fF0f4420a115F61E6ad0750Db9dE" + ], + + "cssLPs": [ + { + "address": "0xfA8E0C0568edcDD3D9b12B48792a5B00018FdB57", + "pid": 1 + }, + { + "address": "0x2f32E2252a6979704F0f540b7988Fa8B8C36B292", + "pid": 2 + } + ], + "symbol": "CSS", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0xa327D4b88a23c75BdB74aeA0F8f6493584405ed7", + "0x12d8E4C93949FEdfb798bd665dE99c47B8F4C8b0" + ], + "snapshot": 12664197 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/coinswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/coinswap/index.ts new file mode 100644 index 00000000..589a167c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/coinswap/index.ts @@ -0,0 +1,164 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import examplesFile from './examples.json'; + +export const author = 'CoinSwap-Space'; +export const version = '0.0.1'; +export const examples = examplesFile; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +const masterCSSAbi = [ + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)', + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address _owner) view returns (uint256 balance)' +]; + +const communityStakeAbi = [ + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +const bn = (num: any): BigNumber => { + return BigNumber.from(num.toString()); +}; + +const addUserBalance = (userBalances, user: string, balance) => { + if (userBalances[user]) { + return (userBalances[user] = userBalances[user].add(balance)); + } else { + return (userBalances[user] = balance); + } +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + /* + Balance in CSS token + from params.address + */ + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + multi.call(address, options.address, 'balanceOf', [address]) + ); + const resultToken: Record = await multi.execute(); + + /* + Balance in MasterCSS pool CSS - CSS + from params.masterCSS + */ + const multiMasterCSS = new Multicaller(network, provider, masterCSSAbi, { + blockTag + }); + + addresses.forEach((address) => + multiMasterCSS.call(address, options.masterCSS, 'userInfo', ['0', address]) + ); + const resultMasterCSS: Record = + await multiMasterCSS.execute(); + + /* + Balance in Launch pools + from params.communityStakeCSS + */ + const multiCommunityStake = new Multicaller( + network, + provider, + communityStakeAbi, + { + blockTag + } + ); + options.communityStake.forEach((communityStakeAddress) => { + addresses.forEach((address) => + multiCommunityStake.call( + communityStakeAddress + '-' + address, + communityStakeAddress, + 'userInfo', + [address] + ) + ); + }); + const communityStakeCSS: Record = + await multiCommunityStake.execute(); + + /* + Staked LPs in CSS farms + */ + const multiCssLPs = new Multicaller(network, provider, masterCSSAbi, { + blockTag + }); + options.cssLPs.forEach((cssLpAddr) => { + multiCssLPs.call('balanceOf', options.address, 'balanceOf', [ + cssLpAddr.address + ]); + multiCssLPs.call('totalSupply', cssLpAddr.address, 'totalSupply'); + addresses.forEach((address) => + multiCssLPs.call( + cssLpAddr.address + '-' + address, + options.masterCSS, + 'userInfo', + [cssLpAddr.pid, address] + ) + ); + }); + const resultCssLPs: Record = + await multiCssLPs.execute(); + + const userBalances: any = []; + for (let i = 0; i < addresses.length - 1; i++) { + userBalances[addresses[i]] = bn(0); + } + + Object.fromEntries( + Object.entries(resultMasterCSS).map(([address, balance]) => { + return addUserBalance(userBalances, address, balance[0]); + }) + ); + + Object.fromEntries( + Object.entries(resultToken).map(([address, balance]) => { + return addUserBalance(userBalances, address, balance); + }) + ); + + options.communityStake.forEach((communityStakeAddr) => { + addresses.forEach((userAddr) => { + addUserBalance( + userBalances, + userAddr, + communityStakeCSS[communityStakeAddr + '-' + userAddr][0] + ); + }); + }); + + options.cssLPs.forEach((cssLPAddr) => { + addresses.forEach((userAddr) => { + addUserBalance( + userBalances, + userAddr, + bn(resultCssLPs[cssLPAddr.address + '-' + userAddr][0]) + .mul(bn(resultCssLPs.balanceOf)) + .div(bn(resultCssLPs.totalSupply)) + ); + }); + }); + + return Object.fromEntries( + Object.entries(userBalances).map(([address, balance]) => [ + address, + // @ts-ignore + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/README.md b/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/README.md new file mode 100644 index 00000000..c4a5e40f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/README.md @@ -0,0 +1,13 @@ +# colony-reputation + +Uses reputation in a colony as voting power. + +Here is an example of parameters: + +```json +{ + "colonyAddress": "0x51A1fC3f8B1D46e66691C1a7F1A84C9b863dE8c2", + "colonyNetworkAddress": "0x78163f593D1Fa151B4B7cacD146586aD2b686294", + "domainId": 1 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/examples.json new file mode 100644 index 00000000..939a424d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Colony Reputation", + "strategy": { + "name": "colony-reputation", + "params": { + "colonyAddress": "0x51A1fC3f8B1D46e66691C1a7F1A84C9b863dE8c2", + "colonyNetworkAddress": "0x78163f593D1Fa151B4B7cacD146586aD2b686294", + "domainId": 1, + "symbol": "CLNY-REP" + } + }, + "network": "100", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x00Da8d2F207E34B9523798FaF36fC9ff04cA9588" + ], + "snapshot": 18701900 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/index.ts new file mode 100644 index 00000000..9ea86738 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/colony-reputation/index.ts @@ -0,0 +1,82 @@ +import { call } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import fetch from 'cross-fetch'; + +export const author = 'colony'; +export const version = '0.1'; + +const colonyAbi = [ + 'function getDomain(uint256 domainId) external view returns (uint256, uint256)', + 'function getToken() external view returns (address)' +]; + +const colonyNetworkAbi = [ + 'function getReputationRootHash() external view returns (bytes32)' +]; + +const tokenAbi = ['function decimals() external view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const rootHashAtBlock = await call( + provider, + colonyNetworkAbi, + [options.colonyNetworkAddress, 'getReputationRootHash', []], + { blockTag } + ); + + const domain = await call( + provider, + colonyAbi, + [options.colonyAddress, 'getDomain', [options.domainId]], + { blockTag } + ); + + const tokenAddress = await call( + provider, + colonyAbi, + [options.colonyAddress, 'getToken', []], + { blockTag } + ); + + const decimals = await call( + provider, + tokenAbi, + [tokenAddress, 'decimals', []], + { blockTag } + ); + + const url = `https://xdai.colony.io/reputation/xdai/${rootHashAtBlock}/${options.colonyAddress}/${domain[0]}`; + + const res = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }); + const data = await res.json(); + + const retVal = {}; + + addresses.forEach(function (address) { + const loc = data.addresses.indexOf(address.toLowerCase()); + if (loc > -1) { + retVal[address] = parseFloat( + formatUnits(data.reputations[loc], decimals) + ); + } else { + retVal[address] = 0; + } + }); + + return retVal; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/README.md b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/README.md new file mode 100644 index 00000000..81ba6bfd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/README.md @@ -0,0 +1,3 @@ +# Comp-like Votes Inclusive + +Uses comp-like voting delegation to assign scores while also crediting accounts for non-delegated token balances. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/examples.json b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/examples.json new file mode 100644 index 00000000..9d05560a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Comp Like Votes Inclusive", + "strategy": { + "name": "comp-like-votes-inclusive", + "params": { + "address": "0x86772b1409b61c639EaAc9Ba0AcfBb6E238e5F83", + "symbol": "NDX", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x0000006daea1723962647b7e189d311d757Fb793", + "0x7e388444731C38189C0685F6D98605107fF59282" + ], + "snapshot": 13340820 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/index.ts b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/index.ts new file mode 100644 index 00000000..19c1dec0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes-inclusive/index.ts @@ -0,0 +1,40 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { call } from '../../utils'; + +export const author = 'd1ll0nk'; +export const version = '0.1.0'; + +const abi = [ + 'function getMultipleVotesInclusive(address token, address[] accounts) external view returns (uint256[] scores)' +]; + +const CompLikeVotesInclusive = '0x95Cb39a64390994dd8C1cC5D8a29dFfDE4212298'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response: BigNumberish[] = await call( + provider, + abi, + [ + CompLikeVotesInclusive, + 'getMultipleVotesInclusive', + [options.address, addresses] + ], + { blockTag } + ); + return response.reduce( + (obj, value, i) => ({ + ...obj, + [addresses[i]]: parseFloat(formatUnits(value, options.decimals)) + }), + {} as Record + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes/examples.json b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes/examples.json new file mode 100644 index 00000000..25550f1d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example COMP like votes query", + "strategy": { + "name": "comp-like-votes", + "params": { + "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", + "symbol": "COMP" + } + }, + "network": "1", + "addresses": [ + "0x2B384212EDc04Ae8bB41738D05BA20E33277bf33", + "0xAC5720d6EE2d7872b88914C9c5Fa9BF38e72FaF6" + ], + "snapshot": 12050071 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes/index.ts b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes/index.ts new file mode 100644 index 00000000..7d72b444 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/comp-like-votes/index.ts @@ -0,0 +1,57 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'Arr00'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'getCurrentVotes', + outputs: [ + { + internalType: 'uint96', + name: '', + type: 'uint96' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'getCurrentVotes', + [address.toLowerCase()] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/contract-call/README.md b/Implementations/API/backend/utils/snapshot/strategies/contract-call/README.md new file mode 100644 index 00000000..40becdc7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/contract-call/README.md @@ -0,0 +1,81 @@ +# Contract call strategy + +Allows any contract method to be used to calculate voter scores. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["contract-call", { + // token address + "address": "0x6887DF2f4296e8B772cb19479472A16E836dB9e0", + // token decimals + "decimals": 18, + // token symbol + "symbol": "DAI", + // ABI for balanceOf method + "methodABI": { + "constant": true, + "inputs": [{ + "internalType": "address", + "name": "account", + "type": "address" + }], + "name": "balanceOf", + "outputs": [{ + "internalType": "uint256", + "name": "", + "type": "uint256" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + } + }], + ] +} +``` + +You can call methods with multiple inputs in any contract: + +```JSON +{ + "strategies": [ + ["contract-call", { + // contract address + "address": "0x6887DF2f4296e8B772cb19479472A16E836dB9e0", + // output decimals + "decimals": 18, + // strategy symbol + "symbol": "mySCORE", + // arguments are passed to the method; "%{address}" is replaced with the voter's address; default value ["%{address}"] + "args": ["0x6887DF2f4296e8B772cb19479472A16E836dB9e0", "%{address}"], + // method ABI, output type should be uint256 + "methodABI": { + "constant": true, + "inputs": [{ + "internalType": "address", + "name": "_someAddress", + "type": "address" + }, { + "internalType": "address", + "name": "_voterAddress", + "type": "address" + }], + "name": "totalScoresFor", + "outputs": [{ + "internalType": "uint256", + "name": "", + "type": "uint256" + }], + "payable": false, + "stateMutability": "view", + "type": "function" + } + }], + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/contract-call/examples.json b/Implementations/API/backend/utils/snapshot/strategies/contract-call/examples.json new file mode 100644 index 00000000..246b5118 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/contract-call/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "contract-call", + "params": { + "address": "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82", + "symbol": "Cake", + "decimals": 18, + "methodABI": { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + } + }, + "network": "56", + "addresses": ["0x009cF7bC57584b7998236eff51b98A168DceA9B0"], + "snapshot": 13839867 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/contract-call/index.ts b/Implementations/API/backend/utils/snapshot/strategies/contract-call/index.ts new file mode 100644 index 00000000..506a11ba --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/contract-call/index.ts @@ -0,0 +1,45 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'in19farkt'; +export const version = '0.1.0'; + +function getArgs(options, address: string) { + const args: Array = options.args || ['%{address}']; + return args.map((arg) => + typeof arg === 'string' ? arg.replace(/%{address}/g, address) : arg + ); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + [options.methodABI], + addresses.map((address: any) => [ + options.address, + options.methodABI.name, + getArgs(options, address) + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat( + formatUnits( + options?.output ? value[options.output].toString() : value.toString(), + options.decimals + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/conv-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/conv-finance/examples.json new file mode 100644 index 00000000..09596c14 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/conv-finance/examples.json @@ -0,0 +1,64 @@ +[ + { + "name": "Convergence finance LP and staking pool balance", + "strategy": { + "name": "conv-finance", + "params": { + "address": "0x8006320739fC281da67Ee62eB9b4Ef8ADD5C903a", + "symbol": "CONV", + "decimals": 18, + "lpTokenAddresses": [ + "0x30059f41bdde4256bf94c29e1ab74648b2a23e7b", + "0x7ed6102f2a4d3aa47dcbf6a85abfee6fd2921a27", + "0x083a91f24D02201946Ab6B1Ef3ACb60851432D1E", + "0xACe46AeC3b4C5a353F44f89fBf0A9d886f27D6a2", + "0xfaa5b559cd3dbE9329a5c4E4483c8DcC4B2C07C4" + ], + "stakingPools": [ + { + "address": "0x72D7965fD3CD2b81Af638BbB0752DFdA362910B5", + "pools": [ + { + "poolId": "1", + "rewarderIdx": "1" + }, + { + "poolId": "7", + "rewarderIdx": "0" + }, + { + "poolId": "8", + "rewarderIdx": "0" + } + ] + } + ], + "stakingPoolsVersion": "2", + "rewarder": [ + { + "address": "0x5fC40FAd9728B45be4A762C02Fdb574b1E5E9e93", + "poolIds": [7] + }, + { + "address": "0x823c9f78DD5cf945DB8d81Ac3A959fC0E6233bb9", + "poolIds": [1] + }, + { + "address": "0x88Cb1456b8325aC99f90eBdadB1826516BfFc79a", + "poolIds": [2] + } + ], + "rewarderVersion": "2" + } + }, + "network": "1284", + "addresses": [ + "0xa63982283bc992744f8ad978b09bf254aa964c0b", + "0x76a743237fdab1c0d80d84fd08ef558dac8776ab", + "0x941df80bc611ef10736f16441c36cb232cd07814", + "0x873ca57e8236a3fd3834a7ef3fa624afad14a36e", + "0xe51231daa306acf16eac34a864564ca36b262a1f" + ], + "snapshot": 563524 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/conv-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/conv-finance/index.ts new file mode 100644 index 00000000..86ea3c27 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/conv-finance/index.ts @@ -0,0 +1,350 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'eric-convergence'; +export const version = '0.1.0'; + +interface STRATEGY_OPTIONS { + address: string; + symbol: string; + decimals: number; + lpTokenAddresses: string[]; + stakingPools: STAKING_POOL[]; + stakingPoolsVersion: string; + rewarder: REWARDER[]; + rewarderVersion: string; +} + +interface LP_TOKEN { + [address: string]: { + totalSupply: BigNumber; + totalToken: BigNumber; + }; +} + +interface STAKING_POOL { + address: string; + pools: STAKING_POOL_INFO[]; +} + +interface STAKING_POOL_INFO { + poolId: string; + rewarderIdx?: string; +} + +interface REWARDER { + address: string; + poolIds: number[]; +} + +const lpTokenContractAbi = [ + 'function getReserves() public view returns (uint112, uint112, uint32)', + 'function token0() public view returns (address)', + 'function token1() public view returns (address)', + 'function balanceOf(address account) view returns (uint256)', + 'function totalSupply() public view returns (uint256)' +]; + +const stakingPoolsV2ContractAbi = [ + 'function getReward(uint256 poolId, address staker, uint8 rewarderIdx) external view returns (uint256)', + 'function userData(uint256 poolId, address staker) view returns (uint256, uint32)', + 'function poolInfos(uint256 poolId) view returns (uint256, uint256, uint256, address)' +]; + +const rewarderV2ContractAbi = [ + 'function calculateTotalReward(address user, uint256 poolId) external view returns (uint256)' +]; + +const stakingPoolsV1ContractAbi = [ + 'function getReward(uint256 poolId, address staker) external view returns (uint256)', + 'function userData(uint256 poolId, address staker) view returns (uint256, uint256, uint256)', + 'function poolInfos(uint256 poolId) view returns (uint256, uint256, uint256, uint256, address)' +]; + +const rewarderV1ContractAbi = [ + 'function vestingSchedules(address user, uint256 poolId) view returns(uint128, uint32, uint32, uint32, uint32)' +]; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options: STRATEGY_OPTIONS, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const lpMultiCalls = options.lpTokenAddresses.flatMap((lpAddress: any) => { + return [ + [lpAddress, 'token0', []], + [lpAddress, 'token1', []], + [lpAddress, 'getReserves', []], + [lpAddress, 'totalSupply', []], + ...addresses.map((userAddress: any) => [ + lpAddress, + 'balanceOf', + [userAddress] + ]) + ]; + }); + + const stakingPoolsMultiCalls: any[] = []; + + options.stakingPools.forEach((stakingPool: STAKING_POOL) => { + // Staking pool version: + // 1: Single reward + // 2: Multi reward + if (options.stakingPoolsVersion === '2') { + stakingPool.pools.forEach((poolInfo: STAKING_POOL_INFO) => { + stakingPoolsMultiCalls.push( + [stakingPool.address, 'poolInfos', [poolInfo.poolId]], // Get pool token + ...addresses.map((userAddress: any) => [ + stakingPool.address, + 'userData', + [poolInfo.poolId, userAddress] + ]), + ...addresses.map((userAddress: any) => [ + stakingPool.address, + 'getReward', + [poolInfo.poolId, userAddress, poolInfo.rewarderIdx] + ]) + ); + }); + } else { + stakingPool.pools.forEach((poolInfo: STAKING_POOL_INFO) => { + stakingPoolsMultiCalls.push( + [stakingPool.address, 'poolInfos', [poolInfo.poolId]], // Get pool token + ...addresses.map((userAddress: any) => [ + stakingPool.address, + 'userData', + [poolInfo.poolId, userAddress] + ]), + ...addresses.map((userAddress: any) => [ + stakingPool.address, + 'getReward', + [poolInfo.poolId, userAddress] + ]) + ); + }); + } + }); + + const stakingPoolRewarderMultiCalls: any[] = []; + + options.rewarder.forEach((rewarder: REWARDER) => { + // Staking pool rewarder version: + // 1: Old rewarder + // 2. New rewarder + if (options.rewarderVersion === '2') { + rewarder.poolIds.forEach((id: number) => { + stakingPoolRewarderMultiCalls.push( + ...addresses.map((userAddress: any) => [ + rewarder.address, + 'calculateTotalReward', + [userAddress, id] + ]) + ); + }); + } else { + rewarder.poolIds.forEach((id: number) => { + stakingPoolRewarderMultiCalls.push( + ...addresses.map((userAddress: any) => [ + rewarder.address, + 'vestingSchedules', + [userAddress, id] + ]) + ); + }); + } + }); + + const promiseArray = [ + multicall(network, provider, lpTokenContractAbi, lpMultiCalls, { blockTag }) + ]; + + if (options.stakingPoolsVersion === '2') { + promiseArray.push( + multicall( + network, + provider, + stakingPoolsV2ContractAbi, + stakingPoolsMultiCalls, + { blockTag } + ) + ); + } else { + promiseArray.push( + multicall( + network, + provider, + stakingPoolsV1ContractAbi, + stakingPoolsMultiCalls, + { blockTag } + ) + ); + } + + if (options.rewarderVersion === '2') { + promiseArray.push( + multicall( + network, + provider, + rewarderV2ContractAbi, + stakingPoolRewarderMultiCalls, + { blockTag } + ) + ); + } else { + promiseArray.push( + multicall( + network, + provider, + rewarderV1ContractAbi, + stakingPoolRewarderMultiCalls, + { blockTag } + ) + ); + } + + const res = await Promise.all(promiseArray); + + const usersTokensFromLp: BigNumber[] = []; + const lpTokens: LP_TOKEN = {}; + + // LP Token Calculation + + let lpTokenResult = res[0]; + + options.lpTokenAddresses.forEach((lpAddress: string, idx) => { + const token0Addr = lpTokenResult[0][0]; + const token1Addr = lpTokenResult[1][0]; + const reserve0 = bn(lpTokenResult[2][0]); + const reserve1 = bn(lpTokenResult[2][1]); + const totalSupply = bn(lpTokenResult[3][0]); + + lpTokenResult = lpTokenResult.slice(4); + + let tokenInLP = bn(0); + if (token0Addr === options.address) { + tokenInLP = reserve0.mul(bn(2)); + } else if (token1Addr === options.address) { + tokenInLP = reserve1.mul(bn(2)); + } + + lpTokenResult + .slice(0, addresses.length) + .map((num: BigNumber, i: number) => { + const lpTokenBal = bn(num[0]); + if (usersTokensFromLp[i] === undefined) { + usersTokensFromLp[i] = lpTokenBal.mul(tokenInLP).div(totalSupply); + } else { + usersTokensFromLp[i] = usersTokensFromLp[i].add( + lpTokenBal.mul(tokenInLP).div(totalSupply) + ); + } + }); + + lpTokens[options.lpTokenAddresses[idx].toLowerCase()] = { + totalSupply, + totalToken: tokenInLP + }; + + lpTokenResult = lpTokenResult.slice(addresses.length); + }); + + let stakingPoolResult = res[1]; + + // Staking Pools Calculation + + options.stakingPools.forEach((stakingPool: STAKING_POOL) => { + stakingPool.pools.forEach(() => { + let poolToken = ''; + if (options.stakingPoolsVersion === '2') { + poolToken = stakingPoolResult[0][3]; + } else { + poolToken = stakingPoolResult[0][4]; + } + stakingPoolResult = stakingPoolResult.slice(1); + + if (poolToken === options.address) { + // single side staking + stakingPoolResult.slice(0, addresses.length).map((userData, idx) => { + const stakingBal = bn(userData[0]); + usersTokensFromLp[idx] = usersTokensFromLp[idx].add(stakingBal); + }); + stakingPoolResult = stakingPoolResult.slice(addresses.length); + stakingPoolResult.slice(0, addresses.length).map((num, idx) => { + const pendingReward = bn(num); + usersTokensFromLp[idx] = usersTokensFromLp[idx].add(pendingReward); + }); + } else if (lpTokens[poolToken.toLowerCase()] !== undefined) { + // CONV LP token staking + const totalSupply = lpTokens[poolToken.toLowerCase()].totalSupply; + const totalToken = lpTokens[poolToken.toLowerCase()].totalToken; + stakingPoolResult.slice(0, addresses.length).map((userData, idx) => { + const stakedLPBal = bn(userData[0]); + usersTokensFromLp[idx] = usersTokensFromLp[idx].add( + stakedLPBal.mul(totalToken).div(totalSupply) + ); + }); + stakingPoolResult = stakingPoolResult.slice(addresses.length); + stakingPoolResult.slice(0, addresses.length).map((num, idx) => { + const pendingReward = bn(num); + usersTokensFromLp[idx] = usersTokensFromLp[idx].add(pendingReward); + }); + } else { + // Non-CONV LP token staking, only calculates pending reward + stakingPoolResult = stakingPoolResult.slice(addresses.length); + stakingPoolResult.slice(0, addresses.length).map((num, idx) => { + const pendingReward = bn(num); + usersTokensFromLp[idx] = usersTokensFromLp[idx].add(pendingReward); + }); + } + stakingPoolResult = stakingPoolResult.slice(addresses.length); + }); + }); + + let rewarderResult = res[2]; + + // Rewarder Calculation + options.rewarder.forEach((rewarder: REWARDER) => { + rewarder.poolIds.forEach(() => { + if (options.rewarderVersion === '2') { + rewarderResult.slice(0, addresses.length).map((num, i) => { + const rewarderBal = bn(num); + usersTokensFromLp[i] = usersTokensFromLp[i].add(rewarderBal); + }); + } else { + rewarderResult.slice(0, addresses.length).map((vestingSchedule, i) => { + const vestingAmount = bn(vestingSchedule[0]); + if (vestingAmount.gt(bn(0))) { + const startTime = parseInt(vestingSchedule[1]); + const endTime = parseInt(vestingSchedule[2]); + const lastClaimTime = parseInt(vestingSchedule[4]); + const step = parseInt(vestingSchedule[3]); + const totalStep = (endTime - startTime) / step; + const remainingStep = (endTime - lastClaimTime) / step; + const rewarderBal = vestingAmount + .div(bn(totalStep)) + .mul(bn(remainingStep)); + usersTokensFromLp[i] = usersTokensFromLp[i].add(rewarderBal); + } + }); + } + rewarderResult = rewarderResult.slice(addresses.length); + }); + }); + + return Object.fromEntries( + usersTokensFromLp.map((sum, i) => { + const parsedSum = parseFloat(formatUnits(sum, options.decimals)); + return [addresses[i], parsedSum]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/coordinape/README.md b/Implementations/API/backend/utils/snapshot/strategies/coordinape/README.md new file mode 100644 index 00000000..a318bd02 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/coordinape/README.md @@ -0,0 +1,11 @@ +# Coordinape + +Use Coordinape circle epoch tokens as voting power. + +Here is an example of parameters: +```json +{ + "symbol": "CIRCLE", + "circle": "92" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/coordinape/examples.json b/Implementations/API/backend/utils/snapshot/strategies/coordinape/examples.json new file mode 100644 index 00000000..181bc2a5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/coordinape/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "coordinape", + "params": { + "symbol": "CIRCLE", + "circle": "92" + } + }, + "network": "1", + "addresses": [ + "0x823b92d6a4b2AED4b15675c7917c9f922ea8ADAD", + "0xd337fccaec6ea113baacca3a41eb8766706a0706", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7" + ], + "snapshot": 13219000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/coordinape/index.ts b/Implementations/API/backend/utils/snapshot/strategies/coordinape/index.ts new file mode 100644 index 00000000..620881e1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/coordinape/index.ts @@ -0,0 +1,34 @@ +import fetch from 'cross-fetch'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const ts = (await provider.getBlock(snapshot)).timestamp; + const url = `https://api.coordinape.com/api/${options.circle}/token-gifts?latest_epoch=1×tamp=${ts}&snapshot=${snapshot}`; + const res = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }); + const gifts = await res.json(); + const scores = {}; + gifts.forEach((gift) => { + const address = getAddress(gift.recipient_address); + if (!scores[address]) scores[address] = 0; + scores[address] += gift.tokens; + }); + return Object.fromEntries( + addresses.map((address) => [address, scores[getAddress(address)] || 0]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/cream/examples.json b/Implementations/API/backend/utils/snapshot/strategies/cream/examples.json new file mode 100644 index 00000000..56ead3db --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cream/examples.json @@ -0,0 +1,58 @@ +[ + { + "name": "CREAM", + "strategy": { + "name": "cream", + "params": { + "token": "0x2ba592F78dB6436527729929AAf6c908497cB200", + "symbol": "CREAM", + "crCREAM": "0x892B14321a4FCba80669aE30Bd0cd99a7ECF6aC0", + "sushiswap": "0xf169CeA51EB51774cF107c88309717ddA20be167", + "uniswap": "0xddF9b7a31b32EBAF5c064C80900046C9e5b7C65F", + "balancer": "0x280267901C175565C64ACBD9A3c8F60705A72639", + "masterChef": "0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd", + "pid": 22, + "periods": 3, + "minVote": 1, + "pools": [ + { + "name": "CREAM", + "address": "0x2ba592F78dB6436527729929AAf6c908497cB200" + }, + { + "name": "1 Year", + "address": "0x780F75ad0B02afeb6039672E6a6CEDe7447a8b45" + }, + { + "name": "2 Year", + "address": "0xBdc3372161dfd0361161e06083eE5D52a9cE7595" + }, + { + "name": "3 Year", + "address": "0xD5586C1804D2e1795f3FBbAfB1FBB9099ee20A6c" + }, + { + "name": "4 Year", + "address": "0xE618C25f580684770f2578FAca31fb7aCB2F5945" + } + ] + } + }, + "network": "1", + "addresses": [ + "0x7dd508a1e4Da1243789B799a480f8B45e58b1B5b", + "0xB157ba30e3467DdBC844f14F02b4ba741f1d549F", + "0x6595732468A241312bc307F327bA0D64F02b3c20", + "0xdd4C3B2860f7C747C4F69414022D5FA7354Eef28", + "0xC51FA42503942cafa7b1ffc02F0Cd9564189773e", + "0x0430605323465E26Dc21fBAaA9A1A4Be6ae9d496", + "0xAdC24d7b630759A3AF7f52716d91299c999a2213", + "0x5E0b772FC4E58C470CE4551EeF2391A3B5dA5bb4", + "0x90aBCf1598ed3077861bCFb3B11EFcd1D7277223", + "0xF800d8407b1488BB6Dc3789c2D45147c25C38AF5", + "0xB662ACEAF435C5F21568FC138Ab202C6a43FFc13", + "0x99eb33756a2eAa32f5964A747722c4b59e6aF351" + ], + "snapshot": 12315029 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/cream/index.ts b/Implementations/API/backend/utils/snapshot/strategies/cream/index.ts new file mode 100644 index 00000000..16e01637 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cream/index.ts @@ -0,0 +1,301 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { strategy as erc20BalanceOf } from '../erc20-balance-of'; +import { getBlockNumber } from '../../utils'; +import { Multicaller } from '../../utils'; + +export const author = 'jeremyHD'; +export const version = '0.2.1'; + +const ONE_E18 = parseUnits('1', 18); + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + }, + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'exchangeRateStored', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'borrowBalanceStored', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +const CREAM_VOTING_POWER = '0xb146BF59f30a54750209EF529a766D952720D0f9'; +const CREAM_VOTING_POWER_DEPLOY_BLOCK = 12315028; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const snapshotBlock = + typeof snapshot === 'number' ? snapshot : await getBlockNumber(provider); + const snapshotBlocks: number[] = []; + + for (let i = 0; i < options.periods; i++) { + const blocksPerPeriod = 80640; // 2 weeks per period, assume 15s per block + const blockTag = + snapshotBlock > blocksPerPeriod * i + ? snapshotBlock - blocksPerPeriod * i + : snapshotBlock; + snapshotBlocks.push(blockTag); + } + + const scores = await Promise.all([ + ...snapshotBlocks.map((blockTag) => + blockTag > CREAM_VOTING_POWER_DEPLOY_BLOCK + ? getScores(provider, addresses, options, blockTag) + : getLegacyScores(provider, addresses, options, blockTag) + ) + ]); + + const averageScore = {}; + addresses.forEach((address) => { + const userScore = scores + .map((score) => score[address]) + .reduce((accumulator, score) => (accumulator += score), 0); + averageScore[address] = userScore / options.periods; + }); + + return Object.fromEntries( + Array(addresses.length) + .fill('') + .map((_, i) => { + const score = averageScore[addresses[i]]; + // ignore score < minimum voting amount + if (score < options.minVote) { + return [addresses[i], 0]; + } + return [addresses[i], score]; + }) + ); +} + +async function getScores(provider, addresses, options, blockTag) { + return erc20BalanceOf( + 'cream', + '1', + provider, + addresses, + { + address: CREAM_VOTING_POWER, + decimals: 18 + }, + blockTag + ); +} + +async function getLegacyScores(provider, addresses, options, blockTag) { + const score = {}; + // Ethereum only + const multi1 = new Multicaller('1', provider, abi, { blockTag }); + multi1.call('sushiswap.cream', options.token, 'balanceOf', [ + options.sushiswap + ]); + multi1.call('sushiswap.totalSupply', options.sushiswap, 'totalSupply'); + + addresses.forEach((address) => { + multi1.call( + `sushiswap.${address}.balanceOf`, + options.sushiswap, + 'balanceOf', + [address] + ); + multi1.call( + `sushiswap.${address}.userInfo`, + options.masterChef, + 'userInfo', + [options.pid, address] + ); + }); + + const multi2 = new Multicaller('1', provider, abi, { blockTag }); + multi2.call('uniswap.cream', options.token, 'balanceOf', [options.uniswap]); + multi2.call('uniswap.totalSupply', options.uniswap, 'totalSupply'); + multi2.call('balancer.cream', options.token, 'balanceOf', [options.balancer]); + multi2.call('balancer.totalSupply', options.balancer, 'totalSupply'); + addresses.forEach((address) => { + multi2.call(`uniswap.${address}.balanceOf`, options.uniswap, 'balanceOf', [ + address + ]); + multi2.call( + `balancer.${address}.balanceOf`, + options.balancer, + 'balanceOf', + [address] + ); + }); + + const multi3 = new Multicaller('1', provider, abi, { blockTag }); + multi3.call('crCREAM.exchangeRate', options.crCREAM, 'exchangeRateStored'); + addresses.forEach((address) => { + multi3.call(`crCREAM.${address}.balanceOf`, options.crCREAM, 'balanceOf', [ + address + ]); + multi3.call( + `crCREAM.${address}.borrow`, + options.crCREAM, + 'borrowBalanceStored', + [address] + ); + }); + + const multi4 = new Multicaller('1', provider, abi, { blockTag }); + addresses.forEach((address) => { + options.pools.forEach((pool) => { + multi4.call(`pool.${address}.${pool.name}`, pool.address, 'balanceOf', [ + address + ]); + }); + }); + + const results = await Promise.all([ + multi1.execute(), + multi2.execute(), + multi3.execute(), + multi4.execute() + ]); + + const result = results.reduce((sumResult, partialResult) => { + Object.entries(partialResult).forEach(([key, value]) => { + sumResult[key] = value; + }); + return sumResult; + }, {}); + + const creamPerSushiswapLP = parseUnits( + result.sushiswap.cream.toString(), + 18 + ).div(result.sushiswap.totalSupply); + const creamPerUniswapLP = parseUnits(result.uniswap.cream.toString(), 18).div( + result.uniswap.totalSupply + ); + const creamPerBalancerLP = parseUnits( + result.balancer.cream.toString(), + 18 + ).div(result.balancer.totalSupply); + + addresses.forEach((address) => { + const userScore = score[address] || BigNumber.from(0); + const sushi = result.sushiswap[address].balanceOf + .add(result.sushiswap[address].userInfo.amount) + .mul(creamPerSushiswapLP) + .div(ONE_E18); + const uniswap = result.uniswap[address].balanceOf + .mul(creamPerUniswapLP) + .div(ONE_E18); + const balancer = result.balancer[address].balanceOf + .mul(creamPerBalancerLP) + .div(ONE_E18); + const crCREAM = result.crCREAM[address].balanceOf + .mul(result.crCREAM.exchangeRate) + .div(ONE_E18) + .sub(result.crCREAM[address].borrow); + const pools = Object.values(result.pool[address]).reduce( + (accumulator: any, poolBalance: any) => { + return accumulator.add(poolBalance); + }, + BigNumber.from(0) + ); + + score[address] = userScore + .add(sushi) + .add(uniswap) + .add(balancer) + .add(crCREAM) + .add(pools); + }); + + Object.keys(score).map((address) => { + score[address] = parseFloat(formatUnits(score[address], 18)); + }); + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/credit-lp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/credit-lp/examples.json new file mode 100644 index 00000000..668b5151 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/credit-lp/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Credit LP", + "strategy": { + "name": "credit-lp", + "params": { + "address": "0x77128DFdD0ac859B33F44050c6fa272F34872B5E", + "lpToken": "0x06F3Cb227781A836feFAEa7E686Bdc857e80eAa7", + "masterchef": "0xe0c43105235C1f18EA15fdb60Bb6d54814299938", + "crypt": "0xAD48320c7e3D3e9fF0c7e51608869cbbfFE7422C", + "pid": 1, + "symbol": "CREDIT", + "decimals": 18 + } + }, + "network": "250", + "addresses": [ + "0x70f8892B9AEd192b3E794f9398B62D50Cf2fbBf8", + "0x865b59FeE0Fd5230BED5Cf9D0A15151C21BF0499", + "0xa0AD4e0111bE3d664262488fb54e2e543C9C2B54", + "0xaC207c599e4A07F9A8cc5E9cf49B02E20AB7ba69", + "0x6D5C8b593958eE0cB7aD6Ac2531AB1b818186857" + ], + "snapshot": 39939923 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/credit-lp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/credit-lp/index.ts new file mode 100644 index 00000000..96aaccd0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/credit-lp/index.ts @@ -0,0 +1,56 @@ +// import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = '0xEntropy'; +export const version = '0.1.0'; + +const abi = [ + 'function getUserInfo(uint256 _pid, address _user) view returns (tuple(uint256 amount, uint256[] RewardDebt, uint256[] RemainingRewards))', + 'function getPricePerFullShare() view returns (uint256)', + 'function balanceOf(address account) view returns (uint256)', + 'function totalSupply() view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + multi.call('lpTotalSupply', options.lpToken, 'totalSupply', []); + multi.call('creditInLp', options.address, 'balanceOf', [options.lpToken]); + multi.call('pricePerShare', options.crypt, 'getPricePerFullShare', []); + addresses.forEach((address: any) => { + multi.call(`chef.${address}`, options.masterchef, 'getUserInfo', [ + options.pid, + address + ]); + multi.call(`lp.${address}`, options.lpToken, 'balanceOf', [address]); + multi.call(`reaper.${address}`, options.crypt, 'balanceOf', [address]); + }); + const result = await multi.execute(); + const creditInLp = parseFloat( + formatUnits(result.creditInLp, options.decimals) + ); + const lpTotalSupply = parseFloat(formatUnits(result.lpTotalSupply)); + const creditPerLp = creditInLp / lpTotalSupply; + + return Object.fromEntries( + addresses.map((address) => { + const reaperVal = result.reaper[address]; + const raw = reaperVal.div(result.pricePerShare); + return [ + address, + parseFloat(formatUnits(result.lp[address])) * creditPerLp + + parseFloat(formatUnits(result.chef[address].amount)) * creditPerLp + + parseFloat(formatUnits(raw)) * creditPerLp + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/cronaswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/cronaswap/README.md new file mode 100644 index 00000000..00894b4f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cronaswap/README.md @@ -0,0 +1,45 @@ +# CronaSwap + +This is the most common strategy, it returns the balances of the voters for a balances CRONA token +in CronaSwap project(pools, farms, Liquidity, token). + +Here is an example of parameters: +```json +[ + { + "name": "Example query CronaSwap", + "strategy": { + "name": "cronaswap", + "params": { + "address": "0xadbd1231fb360047525BEdF962581F3eee7b49fe", + "masterChef": "0x77ea4a4cF9F77A034E4291E8f457Af7772c2B254", + "autoCrona": "0xDf3EBc46F283eF9bdD149Bb24c9b201a70d59389", + "cronaLPs": [ + { + "address": "0xeD75347fFBe08d5cce4858C70Df4dB4Bbe8532a0", + "pid": 1 + }, + { + "address": "0x482E0eEb877091cfca439D131321bDE23ddf9bB5", + "pid": 13 + }, + { + "address": "0x0427F9C304b0028f67A5fD61ffdD613186c1894B", + "pid": 14 + } + ], + "symbol": "CRONA", + "decimals": 18 + } + }, + "network": "25", + "addresses": [ + "0xd758B37Aff75F8Ee847D606fcEfE7BD18C8ed029", + "0xB6E6d031db616cF8aC338293dC2ecFa0F01C55EC" + ], + "snapshot": 599576 + } +] + + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/cronaswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/cronaswap/examples.json new file mode 100644 index 00000000..90a6c5ab --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cronaswap/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query CronaSwap", + "strategy": { + "name": "cronaswap", + "params": { + "address": "0xadbd1231fb360047525BEdF962581F3eee7b49fe", + "masterChef": "0x77ea4a4cF9F77A034E4291E8f457Af7772c2B254", + "autoCrona": "0xDf3EBc46F283eF9bdD149Bb24c9b201a70d59389", + "cronaLPs": [ + { + "address": "0xeD75347fFBe08d5cce4858C70Df4dB4Bbe8532a0", + "pid": 1 + }, + { + "address": "0x482E0eEb877091cfca439D131321bDE23ddf9bB5", + "pid": 13 + }, + { + "address": "0x0427F9C304b0028f67A5fD61ffdD613186c1894B", + "pid": 14 + } + ], + "symbol": "CRONA", + "decimals": 18 + } + }, + "network": "25", + "addresses": [ + "0xd758B37Aff75F8Ee847D606fcEfE7BD18C8ed029", + "0xB6E6d031db616cF8aC338293dC2ecFa0F01C55EC" + ], + "snapshot": 599576 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/cronaswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/cronaswap/index.ts new file mode 100644 index 00000000..85ce5518 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cronaswap/index.ts @@ -0,0 +1,120 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import examplesFile from './examples.json'; + +export const author = 'CronaChef'; +export const version = '0.0.1'; +export const examples = examplesFile; + +const abi = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address _owner) view returns (uint256 balance)', + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +const autoCronaSwapAbi = [ + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)', + 'function getPricePerFullShare() view returns (uint256)' +]; + +const bn = (num: any): BigNumber => { + return BigNumber.from(num.toString()); +}; + +const addUserBalance = (userBalances, user: string, balance) => { + if (userBalances[user]) { + return (userBalances[user] = userBalances[user].add(balance)); + } else { + return (userBalances[user] = balance); + } +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multicall = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address: any) => { + multicall.call(`token.${address}`, options.address, 'balanceOf', [address]); + multicall.call(`masterChef.${address}`, options.masterChef, 'userInfo', [ + '0', + address + ]); + }); + options.cronaLPs.forEach((lp: { address: string; pid: number }) => { + multicall.call(`lp.${lp.pid}.totalSupply`, lp.address, 'totalSupply'); + multicall.call(`lp.${lp.pid}.balanceOf`, options.address, 'balanceOf', [ + lp.address + ]); + addresses.forEach((address: any) => { + multicall.call( + `lpUsers.${address}.${lp.pid}`, + options.masterChef, + 'userInfo', + [lp.pid, address] + ); + }); + }); + + const multicallAutoCompound = new Multicaller( + network, + provider, + autoCronaSwapAbi, + { + blockTag + } + ); + multicallAutoCompound.call( + 'priceShare', + options.autoCrona, + 'getPricePerFullShare' + ); + addresses.forEach((address) => { + multicallAutoCompound.call(address, options.autoCrona, 'userInfo', [ + address + ]); + }); + + const resultAutoCrona = await multicallAutoCompound.execute(); + const result = await multicall.execute(); + + const userBalances: any = []; + for (let i = 0; i < addresses.length - 1; i++) { + userBalances[addresses[i]] = bn(0); + } + + addresses.forEach((address: any) => { + addUserBalance(userBalances, address, result.token[address]); + addUserBalance(userBalances, address, result.masterChef[address][0]); + addUserBalance( + userBalances, + address, + resultAutoCrona[address][0] + .mul(resultAutoCrona.priceShare) + .div(bn(parseUnits('1', options.decimals))) + ); + options.cronaLPs.forEach((lp: { address: string; pid: number }) => { + addUserBalance( + userBalances, + address, + result.lpUsers[address][lp.pid][0] + .mul(result.lp[lp.pid].balanceOf) + .div(result.lp[lp.pid].totalSupply) + ); + }); + }); + + return Object.fromEntries( + Object.entries(userBalances).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/README.md new file mode 100644 index 00000000..fe3814fa --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/README.md @@ -0,0 +1,13 @@ +# crucible-erc20-balance-of + +This strategy weighs votes by the sum of an ERC20 held in a crucible vault. + +Here is an example of parameters: + +```json +{ + "crucible_factory": "0x54e0395CFB4f39beF66DBCd5bD93Cca4E9273D56", + "erc20_address": "0xCD6bcca48069f8588780dFA274960F15685aEe0e", + "erc20_decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/examples.json new file mode 100644 index 00000000..9e928216 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/examples.json @@ -0,0 +1,28 @@ +[ + { + "name": "Crucible LP Balance", + "strategy": { + "name": "crucible-erc20-balance-of", + "params": { + "symbol": "MIST-WETH LP (Uni V2)", + "crucible_factory": "0x54e0395CFB4f39beF66DBCd5bD93Cca4E9273D56", + "erc20_address": "0xCD6bcca48069f8588780dFA274960F15685aEe0e", + "erc20_decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x4d4902BD7E080159964f46B10feeb6482d148E5a", + "0xbD7B1a13149Da69059e4591F040D7D7dAda740c5", + "0x7F8aE988796890454A1007a6dD15eaedC549ad1e", + "0xfca399b892f4e8306fc31b312a3399f422976886", + "0x97a6c796FE543cABC2cA7aE026206e8B260C4dA0", + "0x5E91d547A6f279E6d59086E30e25C964EFE4b463", + "0xB59212Bd19aE722F1cc97A3A93542D573534cf70", + "0x777B0884f97Fd361c55e472530272Be61cEb87c8", + "0x63060f713b377AF8D7D50669ec0fDcE1D31E3f28", + "0xA5109D7E4790143a91D673Ba545405Bf396806CF" + ], + "snapshot": 13062462 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/index.ts new file mode 100644 index 00000000..d18eea6c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-balance-of/index.ts @@ -0,0 +1,107 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { hexZeroPad } from '@ethersproject/bytes'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'thegostep'; +export const version = '0.1.1'; + +const abi = [ + 'function balanceOf(address owner) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)' +]; + +// options +// { +// "crucible_factory": "0x54e0395CFB4f39beF66DBCd5bD93Cca4E9273D56", +// "erc20_address": "0xCD6bcca48069f8588780dFA274960F15685aEe0e", +// "erc20_decimals": 18 +// } + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // get the number of crucibles owned by the wallet + // wallet_address => crucible_count + + const callWalletToCrucibleCount = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToCrucibleCount.call( + walletAddress, + options.crucible_factory, + 'balanceOf', + [walletAddress] + ); + } + const walletToCrucibleCount: Record = + await callWalletToCrucibleCount.execute(); + + // get the address of each crucible + // wallet_address : crucible_index => crucible_address + + const callWalletToCrucibleAddresses = new Multicaller( + network, + provider, + abi, + { + blockTag + } + ); + for (const [walletAddress, crucibleCount] of Object.entries( + walletToCrucibleCount + )) { + for (let index = 0; index < crucibleCount.toNumber(); index++) { + callWalletToCrucibleAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.crucible_factory, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToCrucibleAddresses: Record = + await callWalletToCrucibleAddresses.execute(); + + // get the balance of each crucible + // crucible_address => lp_balance + + const callCrucibleToLpBalance = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletID, crucibleAddress] of Object.entries( + walletIDToCrucibleAddresses + )) { + callCrucibleToLpBalance.call(walletID, options.erc20_address, 'balanceOf', [ + hexZeroPad(crucibleAddress.toHexString(), 20) + ]); + } + const walletIDToLpBalance: Record = + await callCrucibleToLpBalance.execute(); + + // sum the amount of LP tokens held across all crucibles + // wallet_address => lp_balance + + const walletToLpBalance = {} as Record; + for (const [walletID, lpBalance] of Object.entries(walletIDToLpBalance)) { + const address = walletID.split('-')[0]; + walletToLpBalance[address] = walletToLpBalance[address] + ? walletToLpBalance[address].add(lpBalance) + : lpBalance; + } + + return Object.fromEntries( + Object.entries(walletToLpBalance).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.erc20_decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/README.md new file mode 100644 index 00000000..56ded742 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/README.md @@ -0,0 +1,22 @@ +# crucible-erc20-token-and-lp-weighted + +This strategy works on Uniswap v2 style pools or contracts utilising token0/token1 and reserves. + +This strategy calculates the qty of the specified token within a single LP, doubles it to account for both sides, and then uses it as a weight against the sum of a users LP balance across all of their Crucibles. + +This strategy also additionally adds the sum of the users token balance in all Crucibles to give a token weighted score. + +This is useful if you want to be inclusive of LP and token holdings and need to scale them to be balanced with each other. + +Here is an example of parameters: + +```json +{ + "symbol": "MIST", + "crucibleFactory": "0x54e0395CFB4f39beF66DBCd5bD93Cca4E9273D56", + "tokenAddress": "0x88ACDd2a6425c3FaAE4Bc9650Fd7E27e0Bebb7aB", + "tokenDecimals" : "18", + "lpTokenAddress": "0xcd6bcca48069f8588780dfa274960f15685aee0e", + "lpTokenDecimals" : "18" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/examples.json new file mode 100644 index 00000000..317f35c9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/examples.json @@ -0,0 +1,28 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "crucible-erc20-token-and-lp-weighted", + "params": { + "symbol": "MIST", + "crucibleFactory": "0x54e0395CFB4f39beF66DBCd5bD93Cca4E9273D56", + "tokenAddress": "0x88ACDd2a6425c3FaAE4Bc9650Fd7E27e0Bebb7aB", + "lpTokenAddress": "0xcd6bcca48069f8588780dfa274960f15685aee0e" + } + }, + "network": "1", + "addresses": [ + "0x4d4902BD7E080159964f46B10feeb6482d148E5a", + "0xbD7B1a13149Da69059e4591F040D7D7dAda740c5", + "0x7F8aE988796890454A1007a6dD15eaedC549ad1e", + "0xfca399b892f4e8306fc31b312a3399f422976886", + "0x97a6c796FE543cABC2cA7aE026206e8B260C4dA0", + "0x5E91d547A6f279E6d59086E30e25C964EFE4b463", + "0xB59212Bd19aE722F1cc97A3A93542D573534cf70", + "0x777B0884f97Fd361c55e472530272Be61cEb87c8", + "0x63060f713b377AF8D7D50669ec0fDcE1D31E3f28", + "0xA5109D7E4790143a91D673Ba545405Bf396806CF" + ], + "snapshot": 13062462 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/index.ts new file mode 100644 index 00000000..5b53b2e8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/index.ts @@ -0,0 +1,185 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { hexZeroPad } from '@ethersproject/bytes'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, multicall } from '../../utils'; + +export const author = 'joehquak'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function decimals() external view returns (uint8)', + 'function token0() external view returns (address)', + 'function token1() external view returns (address)', + 'function totalSupply() external view returns (uint256)', + 'function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // fetch all token and lp contract data + + const fetchContractData = await multicall( + network, + provider, + abi, + [ + [options.lpTokenAddress, 'token0', []], + [options.lpTokenAddress, 'token1', []], + [options.lpTokenAddress, 'getReserves', []], + [options.lpTokenAddress, 'totalSupply', []], + [options.lpTokenAddress, 'decimals', []], + [options.tokenAddress, 'decimals', []] + ], + { blockTag } + ); + + // assign multicall data to variables + + const token0Address = fetchContractData[0][0]; + const token1Address = fetchContractData[1][0]; + const lpTokenReserves = fetchContractData[2]; + const lpTokenTotalSupply = fetchContractData[3][0]; + const lpTokenDecimals = fetchContractData[4][0]; + const tokenDecimals = fetchContractData[5][0]; + + // calculate single lp token weight + + let tokenWeight; + + if (token0Address === options.tokenAddress) { + tokenWeight = + (parseFloat(formatUnits(lpTokenReserves._reserve0, tokenDecimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals))) * + 2; + } else if (token1Address === options.tokenAddress) { + tokenWeight = + (parseFloat(formatUnits(lpTokenReserves._reserve1, tokenDecimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals))) * + 2; + } else { + tokenWeight = 0; + } + + // get the number of crucibles owned by the wallet + // wallet_address => crucible_count + + const callWalletToCrucibleCount = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToCrucibleCount.call( + walletAddress, + options.crucibleFactory, + 'balanceOf', + [walletAddress] + ); + } + const walletToCrucibleCount: Record = + await callWalletToCrucibleCount.execute(); + + // get the address of each crucible + // wallet_address : crucible_index => crucible_address + + const callWalletToCrucibleAddresses = new Multicaller( + network, + provider, + abi, + { + blockTag + } + ); + for (const [walletAddress, crucibleCount] of Object.entries( + walletToCrucibleCount + )) { + for (let index = 0; index < crucibleCount.toNumber(); index++) { + callWalletToCrucibleAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.crucibleFactory, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToCrucibleAddresses: Record = + await callWalletToCrucibleAddresses.execute(); + + // get the lp token balance of each crucible + // crucible_address => lp_balance + + const callCrucibleToLpBalance = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletID, crucibleAddress] of Object.entries( + walletIDToCrucibleAddresses + )) { + callCrucibleToLpBalance.call( + walletID, + options.lpTokenAddress, + 'balanceOf', + [hexZeroPad(crucibleAddress.toHexString(), 20)] + ); + } + const walletIDToLpBalance: Record = + await callCrucibleToLpBalance.execute(); + + // get the token balance of each crucible + // crucible_address => token_balance + + const callCrucibleToTokenBalance = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletID, crucibleAddress] of Object.entries( + walletIDToCrucibleAddresses + )) { + callCrucibleToTokenBalance.call( + walletID, + options.tokenAddress, + 'balanceOf', + [hexZeroPad(crucibleAddress.toHexString(), 20)] + ); + } + const walletIDToTokenBalance: Record = + await callCrucibleToTokenBalance.execute(); + + // sum the amount of LP tokens held across all crucibles + // wallet_address => lp_balance + + const walletToLpBalance = {} as Record; + for (const [walletID, lpBalance] of Object.entries(walletIDToLpBalance)) { + const address = walletID.split('-')[0]; + walletToLpBalance[address] = walletToLpBalance[address] + ? walletToLpBalance[address].add(lpBalance) + : lpBalance; + } + + // sum the amount of tokens held across all crucibles + // wallet_address => token_balance + + const walletToTokenBalance = {} as Record; + for (const [walletID, tokenBalance] of Object.entries( + walletIDToTokenBalance + )) { + const address = walletID.split('-')[0]; + walletToTokenBalance[address] = walletToTokenBalance[address] + ? walletToTokenBalance[address].add(tokenBalance) + : tokenBalance; + } + + return Object.fromEntries( + Object.entries(walletToLpBalance).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, lpTokenDecimals)) * tokenWeight + + parseFloat(formatUnits(walletToTokenBalance[address], tokenDecimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/schema.json b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/schema.json new file mode 100644 index 00000000..fe453aba --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/crucible-erc20-token-and-lp-weighted/schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "symbol": { + "type": "string", + "title": "Token symbol", + "examples": ["e.g. MIST"], + "maxLength": 16 + }, + "crucibleFactory": { + "type": "string", + "title": "Crucible factory address", + "examples": ["e.g. 0x54e0395CFB4f39beF66DBCd5bD93Cca4E9273D56"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "tokenAddress": { + "type": "string", + "title": "Token address", + "examples": ["e.g. 0x88ACDd2a6425c3FaAE4Bc9650Fd7E27e0Bebb7aB"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "lpTokenAddress": { + "type": "string", + "title": "LP address", + "examples": ["e.g. 0xCD6bcca48069f8588780dFA274960F15685aEe0e"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["crucibleFactory", "tokenAddress", "lpTokenAddress"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctoken/README.md b/Implementations/API/backend/utils/snapshot/strategies/ctoken/README.md new file mode 100644 index 00000000..c7f47714 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctoken/README.md @@ -0,0 +1,27 @@ +# Contract call strategy + +Allows for calculating the voting weight of cToken holders. This strategy allows for invalidating borrowers from voting and incorperating a waiting period between minting (or receiving) cTokens and votes becoming availible. + +## Params + +- `offsetCheck` - Offset (or waiting period) between minting and voting becoming availible +- `borrowingRestricted` - If true, borrowers will have a 0 voting weight + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +"strategies": [ + { + "name": "ctoken", + "params": { + "address": "0x35A18000230DA775CAc24873d00Ff85BccdeD550", + "symbol": "cUNI", + "decimals": 8, + "offsetCheck":40320, + "borrowingRestricted":true + } + } + ] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctoken/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ctoken/examples.json new file mode 100644 index 00000000..46ce3c67 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctoken/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ctoken", + "params": { + "symbol": "cUNI", + "address": "0x35A18000230DA775CAc24873d00Ff85BccdeD550", + "decimals": 8, + "offsetCheck": 40320, + "borrowingRestricted": true + } + }, + "network": "1", + "addresses": [ + "0x614812d04526C0C882A6cB993a135fcD559F33F9", + "0xff91AcCd5277cc3de3E73DD77e3102b1980e439e", + "0x05ff2b0db69458a0750badebc4f9e13add608c7f", + "0xd1a8Dd23e356B9fAE27dF5DeF9ea025A602EC81e", + "0x48f7cb174a2333de834452ad240ce8d3d827dc55", + "0x0d3cae1ff719d6a2ac0fcd7cd22f599cac64b6ec", + "0xE0e710A907a8E44c078e6212Efa336C1191F4CD1" + ], + "snapshot": 13494433 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctoken/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ctoken/index.ts new file mode 100644 index 00000000..b5d63dce --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctoken/index.ts @@ -0,0 +1,112 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'arr00'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'borrowBalanceStored', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const oldBlockTag = + typeof snapshot === 'number' + ? snapshot - options.offsetCheck + : (await provider.getBlockNumber()) - options.offsetCheck; + + const balanceOfCalls = addresses.map((address: any) => [ + options.address, + 'balanceOf', + [address] + ]); + const borrowBalanceCalls = addresses.map((address: any) => [ + options.address, + 'borrowBalanceStored', + [address] + ]); + const calls = balanceOfCalls.concat(borrowBalanceCalls); + + const [response, balancesOldResponse] = await Promise.all([ + multicall(network, provider, abi, calls, { blockTag }), + multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'balanceOf', + [address] + ]), + { blockTag: oldBlockTag } + ) + ]); + + const balancesNowResponse = response.slice(0, addresses.length); + const borrowsNowResponse = response.slice(addresses.length); + + const resultData = {}; + for (let i = 0; i < balancesNowResponse.length; i++) { + let noBorrow = 1; + if (options.borrowingRestricted) { + noBorrow = + borrowsNowResponse[i].toString().localeCompare('0') == 0 ? 1 : 0; + } + const balanceNow = parseFloat( + formatUnits(balancesNowResponse[i].toString(), options.decimals) + ); + const balanceOld = parseFloat( + formatUnits(balancesOldResponse[i].toString(), options.decimals) + ); + resultData[addresses[i]] = Math.min(balanceNow, balanceOld) * noBorrow; + } + return resultData; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/README.md b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/README.md new file mode 100644 index 00000000..185b2b32 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/README.md @@ -0,0 +1,7 @@ +# ctsi-staking-pool + +This strategy implements the rules of CTSI Staking through Staking Pools, returning the staked balance of the voters in all pools they stake to. + +There are no parameters to configure. + +Direct stakers or pool operators are not accounted in this strategy. For those use strategy `ctsi-staking`. diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/examples.json new file mode 100644 index 00000000..75bd0e32 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ctsi-staking-pool", + "params": { + "expectedResults": { + "scores": { + "0x4af926a219e09fe5dd1ee16d7039d82ecf979148": 1001.2293481992707, + "0x0b47fef7cb50ad5abbc10265152aacc3b4b5e6d1": 19243.216830099187, + "0xff26ccf9058b9bd8facfb6a8876864fec193285d": 4272385.58482901 + } + } + } + }, + "network": "1", + "addresses": [ + "0x4af926a219e09fe5dd1ee16d7039d82ecf979148", + "0x0b47fef7cb50ad5abbc10265152aacc3b4b5e6d1", + "0xff26ccf9058b9bd8facfb6a8876864fec193285d" + ], + "snapshot": 16053852 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/index.ts new file mode 100644 index 00000000..4e541dac --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/index.ts @@ -0,0 +1,125 @@ +import { BigNumber, FixedNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'cartesi'; +export const version = '0.1.0'; + +const SUBGRAPH_URL_ROOT = 'https://api.thegraph.com/subgraphs/name/cartesi/pos'; + +const NETWORK_KEY = { + '1': '', + '5': '-goerli' +}; + +function buildSubgraphUrl(chainId) { + const networkString = NETWORK_KEY[chainId]; + return `${SUBGRAPH_URL_ROOT}${networkString}`; +} + +async function getStakingPoolDelegatorBalance( + url, + addresses, + options, + snapshot +): Promise> { + // query for StakingPool balance of voters + const query = { + poolBalances: { + __args: { + where: { + user_in: addresses + }, + first: 1000 + }, + pool: { + amount: true, + shares: true, + manager: true + }, + user: { + id: true + }, + shares: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + query.poolBalances.__args.block = { number: snapshot }; + } + const score: Record = {}; + const result = await subgraphRequest(url, query); + if (result && result.poolBalances) { + result.poolBalances.forEach((poolBalance) => { + const address = getAddress(poolBalance.user.id); + const poolShares = FixedNumber.from(poolBalance.pool.shares); + const poolAmount = FixedNumber.from(poolBalance.pool.amount); + const shares = FixedNumber.from(poolBalance.shares); + const balance = BigNumber.from( + poolAmount + .mulUnsafe(shares.divUnsafe(poolShares)) + .floor() + .toFormat('ufixed128x0') + .toString() + ); + // a staker can stake to several pools, so we must add the value if there is already one + score[address] = score[address] ? score[address].add(balance) : balance; + }); + } + + return score; +} + +function verifyResults( + results: Record, + expectedResults: Record +): void { + Object.entries(results).forEach(([address, score]) => { + const expectedScore = + expectedResults[address.toLowerCase()] ?? + expectedResults[getAddress(address)]; + if (score !== expectedScore) { + console.error( + `>>> ERROR: Score do not match for address ${address}, expected ${expectedScore}, got ${score}` + ); + } + }); +} + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +): Promise> { + // convert addresses to lowercase, as in subgraph they are all lowercase + addresses = addresses.map((address) => address.toLowerCase()); + + // build subgraph URL based on network, as we have one for mainnet and another for goerli + const url = buildSubgraphUrl(network); + + // get balance staked in pools + const results = await getStakingPoolDelegatorBalance( + url, + addresses, + options, + snapshot + ); + + const scores = Object.fromEntries( + Object.entries(results).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, 18)) + ]) + ); + + if (options.expectedResults && snapshot !== 'latest') { + // validate testing expected results + verifyResults(scores, options.expectedResults.scores); + } + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/schema.json new file mode 100644 index 00000000..006ba35f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking-pool/schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "expectedResults": { + "type": "object", + "properties": { + "scores": { + "type": "object", + "additionalProperties": { "type": "number" } + } + } + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/README.md b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/README.md new file mode 100644 index 00000000..4281a8ea --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/README.md @@ -0,0 +1,23 @@ +# ctsi-staking + +This strategy implements the rules of CTSI Staking, returning the staked balance of the voters. It supports direct staking, as well as delegated staking through staking pools. + +There are no parameters to configure. + +The diagram below represents Cartesi Staking system: + +```mermaid +graph TD + U1[User 1] -->|stake| Staking{Staking} + U2[User 2] -->|operate| P1 + P1(Pool 1) -->|stake| Staking + U3[User 3] -->|stake| P1 +``` + +Cartesi staking system is primarily consolidated in the [StakingImpl contract](https://etherscan.io/address/0x9EdEAdFDE65BCfD0907db3AcdB3445229c764A69#readContract). Stakers to this contract can be EOA who stake directly, as the `User 1` above, or can be a [Staking Pool](https://github.com/cartesi/staking-pool), as the `Pool 1` above. + +Staking Pools are smart contracts, so in this case the voting power is delegated to its operator, given by the `owner` of the pool smart contract, represented by `User 2` above. + +Those users who do not wish to stake directly or operate a pool can stake to a pool instead, like the `User 3` represented in the diagram. As described above, pool operators accumulates the voting power of all its stakers. + +Note that an EOA can be at the same time a direct staker and a pool operator. Voting powers are accumulated. diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/examples.json new file mode 100644 index 00000000..38920938 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ctsi-staking", + "params": { + "expectedResults": { + "scores": { + "0xf977814e90da44bfa03b6295a0616a897441acec": 51704200, + "0xc71c504d2a2938a173660ae71b3e97b563196bea": 36765082, + "0x2942aa4356783892c624125acfbbb80d29629a9d": 0, + "0xb5ba4a130f9e30036d1c1db11a8913caf3acdeba": 34404167.25798815 + } + } + } + }, + "network": "1", + "addresses": [ + "0xf977814e90da44bfa03b6295a0616a897441acec", + "0xc71c504d2a2938a173660ae71b3e97b563196bea", + "0x2942aa4356783892c624125acfbbb80d29629a9d", + "0xb5ba4a130f9e30036d1c1db11a8913caf3acdeba" + ], + "snapshot": 15951825 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/index.ts new file mode 100644 index 00000000..802c55ee --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/index.ts @@ -0,0 +1,172 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'cartesi'; +export const version = '0.1.0'; + +const SUBGRAPH_URL_ROOT = 'https://api.thegraph.com/subgraphs/name/cartesi/pos'; + +const NETWORK_KEY = { + '1': '', + '5': '-goerli' +}; + +function buildSubgraphUrl(chainId) { + const networkString = NETWORK_KEY[chainId]; + return `${SUBGRAPH_URL_ROOT}${networkString}`; +} + +async function getStakingBalance( + url, + addresses, + options, + snapshot +): Promise> { + // query for direct stakers (no pools) + const query = { + users: { + __args: { + where: { + id_in: addresses, + pool: null + }, + first: 1000 + }, + id: true, + balance: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + query.users.__args.block = { number: snapshot }; + } + const score: Record = {}; + const result = await subgraphRequest(url, query); + if (result && result.users) { + result.users.forEach((user) => { + const address = getAddress(user.id); + const balance = BigNumber.from(user.balance); + score[address] = balance; + }); + } + + return score; +} + +async function getStakingPoolOperatorBalance( + url, + addresses, + options, + snapshot +): Promise> { + // query for StakingPools by manager (pool operator) + const query = { + stakingPools: { + __args: { + where: { + manager_in: addresses + }, + first: 1000 + }, + manager: true, + user: { + balance: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + query.stakingPools.__args.block = { number: snapshot }; + } + const score: Record = {}; + const result = await subgraphRequest(url, query); + if (result && result.stakingPools) { + result.stakingPools.forEach((pool) => { + const address = getAddress(pool.manager); + const balance = BigNumber.from(pool.user.balance); + // a pool operator can operate more than one pool, so we must add the value if there is already one + score[address] = score[address] ? score[address].add(balance) : balance; + }); + } + + return score; +} + +function combineBalanceScores( + records: Record[] +): Record { + return records.reduce((aggScore, currScore) => { + for (const [address, balance] of Object.entries(currScore)) { + if (!aggScore[address]) { + aggScore[address] = balance; + } else { + aggScore[address] = aggScore[address].add(balance); // sum(L1, L2) + } + } + return aggScore; + }, {}); +} + +function verifyResults( + results: Record, + expectedResults: Record +): void { + Object.entries(results).forEach(([address, score]) => { + const expectedScore = + expectedResults[address.toLowerCase()] ?? + expectedResults[getAddress(address)]; + if (score !== expectedScore) { + console.error( + `>>> ERROR: Score do not match for address ${address}, expected ${expectedScore}, got ${score}` + ); + } + }); +} + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +): Promise> { + // convert addresses to lowercase, as in subgraph they are all lowercase + addresses = addresses.map((address) => address.toLowerCase()); + + // build subgraph URL based on network, as we have one for mainnet and another for goerli + const url = buildSubgraphUrl(network); + + // get staking balance for all direct stakers as voters (no pools, no delegators) + const directStaking = await getStakingBalance( + url, + addresses, + options, + snapshot + ); + + // get balance of pools operated by voters + const operators = await getStakingPoolOperatorBalance( + url, + addresses, + options, + snapshot + ); + + const results = combineBalanceScores([directStaking, operators]); + const scores = Object.fromEntries( + Object.entries(results).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, 18)) + ]) + ); + + if (options.expectedResults && snapshot !== 'latest') { + // validate testing expected results + verifyResults(scores, options.expectedResults.scores); + } + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/schema.json new file mode 100644 index 00000000..006ba35f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ctsi-staking/schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "expectedResults": { + "type": "object", + "properties": { + "scores": { + "type": "object", + "additionalProperties": { "type": "number" } + } + } + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/README.md new file mode 100644 index 00000000..c97695dc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/README.md @@ -0,0 +1,5 @@ +# Curio Cards ERC20 Weighted Voting Strategy + +Curio cards are a pseudo-NFT that predates the erc721 and erc1155 specifications. Each card is implemented as an erc20 with a max supply between 100-2000 tokens. + +This strategy weights the vote according to the holdings of each card and relative scarcity of each card. diff --git a/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/examples.json new file mode 100644 index 00000000..32fd19d2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "curio-cards-erc20-weighted", + "params": { + "symbol": "CARD" + } + }, + "network": "1", + "addresses": [ + "0x518e5A942Ed7Db4B45e9A491ce318373346dB240", + "0xb618aacb9dcdc21ca69d310a6fc04674d293a193" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/index.ts new file mode 100644 index 00000000..de47b5fc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/curio-cards-erc20-weighted/index.ts @@ -0,0 +1,171 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'fafrd'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const curioAddresses = { + CRO1: '0x6aa2044c7a0f9e2758edae97247b03a0d7e73d6c', + CRO2: '0xe9a6a26598b05db855483ff5ecc5f1d0c81140c8', + CRO3: '0x3f8131b6e62472ceea9cb8aa67d87425248a3702', + CRO4: '0x4f1694be039e447b729ab11653304232ae143c69', + CRO5: '0x5a3d4a8575a688b53e8b270b5c1f26fd63065219', + CRO6: '0x1ca6ac0ce771094f0f8a383d46bf3acc9a5bf27f', + CRO7: '0x2647bd8777e0c66819d74ab3479372ea690912c3', + CRO8: '0x2fce2713a561bb019bc5a110be0a19d10581ee9e', + CRO9: '0xbf4cc966f1e726087c5c55aac374e687000d4d45', + CRO10: '0x72b34d637c0d14ace58359ef1bf472e4b4c57125', + CRO11: '0xb36c87f1f1539c5fc6f6e7b1c632e1840c9b66b4', + CRO12: '0xd15af10a258432e7227367499e785c3532b50271', + CRO13: '0x2d922712f5e99428c65b44f09ea389373d185bb3', + CRO14: '0x0565ac44e5119a3224b897de761a46a92aa28ae8', + CRO15: '0xdb7f262237ad8acca8922aa2c693a34d0d13e8fe', + CRO16: '0x1b63532ccb1fee0595c7fe2cb35cfd70ddf862cd', + CRO17: '0xf59536290906f204c3c7918d40c1cc5f99643d0b', + CRO18: '0xa507d9d28bbca54cbcffad4bb770c2ea0519f4f0', + CRO19: '0xf26bc97aa8afe176e275cf3b08c363f09de371fa', + CRO20: '0xd0ec99e99ce22f2487283a087614aee37f6b1283', + CRO21: '0xb7a5a84ff90e8ef91250fb56c50a7bb92a6306ee', + CRO22: '0x148ff761d16632da89f3d30ef3dfe34bc50ca765', + CRO23: '0xcde7185b5c3ed9ea68605a960f6653aa1a5b5c6c', + CRO24: '0xe67dad99c44547b54367e3e60fc251fc45a145c6', + CRO25: '0xc7f60c2b1dbdfd511685501edeb05c4194d67018', + CRO26: '0x1cb5bf4be53eb141b56f7e4bb36345a353b5488c', + CRO27: '0xfb9f3fa2502d01d43167a0a6e80be03171df407e', + CRO28: '0x59d190e8a2583c67e62eec8da5ea7f050d8bf27e', + CRO29: '0xd3540bcd9c2819771f9d765edc189cbd915feabd', + CRO30: '0x7f5b230dc580d1e67df6ed30dee82684dd113d1f' + }; + + // Total supply pulled from https://fafrd.github.io/curio-gallery data + // Note that the total supply values here are TENTATIVE and NEED TO BE VERIFIED!!! + // 1. Many tokens have been burned or are locked in the vendor contract. + // 2. The card contracts do not fully follow ERC20 spec- they do not emit 'Transfer' events, + // which causes tools like etherscan to mis-report p2p transfers. + // 3. Several hundred tokens have been locked in the bugged erc1155 contract and are irretrievable + // The other option is to pull supply data from http://dogestreet.com/curio/, but I also don't know where this is sourced from + // for example, the CRO7 total supply on that site looks lower than it should be. + // For now this data should be considered a rough approximation of total supply. + const curioSupply = { + CRO1: 1817, + CRO2: 1576, + CRO3: 1627, + CRO4: 465, + CRO5: 438, + CRO6: 424, + CRO7: 2006, + CRO8: 2002, + CRO9: 2009, + CRO10: 1999, + CRO11: 1999, + CRO12: 1998, + CRO13: 2000, + CRO14: 500, + CRO15: 499, + CRO16: 497, + CRO17: 508, + CRO18: 500, + CRO19: 500, + CRO20: 1996, + CRO21: 497, + CRO22: 500, + CRO23: 250, + CRO24: 333, + CRO25: 222, + CRO26: 106, + CRO27: 603, + CRO28: 397, + CRO29: 197, + CRO30: 823 + }; + + // Calculate weights for all cards. + // for each card, vote weight = 1/(total supply) * constant + const curioWeights = {}; + Object.keys(curioSupply).map((k) => { + curioWeights[k] = 1000 / curioSupply[k]; + }); + + // Given a card and address-tokencount mapping, + // calculate the weight for this card. + // i.e., assuming card CRO01 had weight 1.5, + // given {'0x123': 2, '0x456': 4} + // return {'0x123': 3, '0x456': 6} + function applyWeightForCard(cardName, addressHoldings) { + const weight = curioWeights[cardName]; + const result = {}; + + Object.keys(addressHoldings).map((k) => { + result[k] = addressHoldings[k] * weight; + }); + + return result; + } + + // Sums multiple address-voteweight mappings into a single object. + // i.e., given [{'0x123': 1, '0x456': 2}, {'0x123': 10, '0x456': 20}] + // return {'0x123': 11, '0x456': 22} + function sumAddressWeights(arrayOfWeights) { + return arrayOfWeights.reduce((sum, current) => { + for (const k in current) { + sum[k] = (sum[k] || 0) + current[k]; + } + return sum; + }, {}); + } + + // helper + async function returnPromiseWithContext(promise, context) { + const ret = await promise; + return { rv: ret, context: context }; + } + + // Prepare to fetch erc20 balances + const cardBalancePromises: Promise<{ rv: any; context: any }>[] = []; + Object.keys(curioAddresses).forEach((cardName) => + cardBalancePromises.push( + returnPromiseWithContext( + erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + { address: curioAddresses[cardName], decimals: 0, start: 3678637 }, + snapshot + ), + cardName + ) + ) + ); + + // Execute erc20 balance fetch in parallel + return await Promise.all(cardBalancePromises) + .then((cardBalances) => { + // then transform token balance -> vote weight + const cardBalancesWeighted: Array = []; + + cardBalances.forEach((cb) => { + const cbWeighted = applyWeightForCard(cb.context, cb.rv); + console.debug( + 'Weighting for card ' + + cb.context + + ':\n' + + JSON.stringify(cbWeighted, null, 2) + ); + cardBalancesWeighted.push(cbWeighted); + }); + + return cardBalancesWeighted; + }) + .then((cardBalancesWeighted) => { + // finally, sum card balances + return sumAddressWeights(cardBalancesWeighted); + }); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v2/examples.json new file mode 100644 index 00000000..6a483a93 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v2/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "CyberKongz, CyberKongz VX and Banana holdings", + "strategy": { + "name": "cyberkongz-v2", + "params": { + "symbol": "KONGZ", + "registries": [ + "0x57a204aa1042f6e66dd7730813f4024114d74f37", + "0x7ea3cca10668b8346aec0bf1844a49e995527c8b", + "0xe2311ae37502105b442bbef831e9b53c5d2e9b3b" + ] + } + }, + "network": "1", + "addresses": [ + "0xf521Bb7437bEc77b0B15286dC3f49A87b9946773", + "0x721931508df2764fd4f70c53da646cb8aed16ace", + "0xa63571f2ce7cf4e9a566a1f248f5d0ad3ba78726", + "0x9279c4cfb0e85e2dff8825ce141f9794c7c7170a", + "0x6f35b0cfc58eb1e21eef8a439bbb0ce4c929d32a", + "0xe34bded2b256430a9be53cbf5cba3b6d866d55f3", + "0xb14b87790643d2dab44b06692d37dd95b4b30e56" + ], + "snapshot": 13322697 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v2/index.ts new file mode 100644 index 00000000..059338e4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v2/index.ts @@ -0,0 +1,77 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'frosti-eth'; +export const version = '0.1.0'; + +const bananaContract = '0xe2311ae37502105b442bbef831e9b53c5d2e9b3b'; + +// addresses are blacklisted across all registries instead of just one, but that should be ok +const blacklistedAddresses = [ + '0xb14b87790643d2dab44b06692d37dd95b4b30e56', // WGK + '0x9d59eba4deaee09466ba9d4073bf912bc72982b0', // NFT20 Gen + '0x0f4676178b5c53ae0a655f1b19a96387e4b8b5f2', // NFT20 VX + '0xcfa9a297a406a48d1137172c18de04c944b47ba9', // Banana Sushi + '0x820f92c1b3ad8e962e6c6d9d7caf2a550aec46fb', // Banana Tip.cc + '0x9ffad2ff3a59d8579e3b0edc6c8f2f591c94dfab', // Banana cyberkongz.eth + '0xe058d87fc1185e38ab68893136834715b30961e1', // Banana Rewarder + '0xe2311ae37502105b442bbef831e9b53c5d2e9b3b' // CyberKongZ: BANANA Token +]; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const calls: any[] = []; + options.registries.forEach((registry) => { + addresses.forEach((address: any) => { + calls.push([registry, 'balanceOf', [address]]); + }); + }); + + const response = await multicall(network, provider, abi, calls, { blockTag }); + const nanaCall = [[bananaContract, 'totalSupply', []]]; + let nanaSupply = await multicall(network, provider, abi, nanaCall, { + blockTag + }); + + response.forEach((value: any, i: number) => { + const address = calls[i][2][0]; + if ( + Math.floor(i / addresses.length) == 2 && + blacklistedAddresses.find((add) => add === address) + ) { + nanaSupply -= value; + } + }); + + const merged = {}; + response.map((value: any, i: number) => { + const address = calls[i][2][0]; + if (blacklistedAddresses.find((add) => add === address)) { + return; + } + merged[address] = (merged[address] || 0) as number; + if (Math.floor(i / addresses.length) == 0) + merged[address] += parseFloat(formatUnits((3 * value).toString(), 0)); + else if (Math.floor(i / addresses.length) == 1) + merged[address] += parseFloat(formatUnits(value.toString(), 0)); + else if (Math.floor(i / addresses.length) == 2) + merged[address] += parseFloat( + formatUnits(Math.floor((15000 * value) / nanaSupply).toString(), 0) + ); + }); + + return merged; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v3/examples.json b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v3/examples.json new file mode 100644 index 00000000..608d4028 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v3/examples.json @@ -0,0 +1,58 @@ +[ + { + "name": "Multichain CyberKongz, CyberKongz VX and Banana holdings", + "strategy": { + "name": "cyberkongz-v3", + "params": { + "symbol": "KONGZ", + "chains": [ + { + "network": "1", + "registries": { + "0x57a204aa1042f6e66dd7730813f4024114d74f37": "OG", + "0x7ea3cca10668b8346aec0bf1844a49e995527c8b": "VX", + "0xe2311ae37502105b442bbef831e9b53c5d2e9b3b": "BANANA" + } + }, + { + "network": "137", + "registries": { + "0xbc91347e80886453f3f8bbd6d7ac07c122d87735": "BANANA", + "0x05df72d911e52ab122f7d9955728bc96a718782c": "VX" + } + } + ], + "skipList": [ + "0xb14b87790643d2dab44b06692d37dd95b4b30e56", + "0x9d59eba4deaee09466ba9d4073bf912bc72982b0", + "0x0f4676178b5c53ae0a655f1b19a96387e4b8b5f2", + "0xcfa9a297a406a48d1137172c18de04c944b47ba9", + "0x820f92c1b3ad8e962e6c6d9d7caf2a550aec46fb", + "0x9ffad2ff3a59d8579e3b0edc6c8f2f591c94dfab", + "0xe058d87fc1185e38ab68893136834715b30961e1", + "0xe2311ae37502105b442bbef831e9b53c5d2e9b3b", + "0x7a08865A3E7c291f3b794210Bc51D559B49DFd15", + "0xe6f45376f64e1f568bd1404c155e5ffd2f80f7ad", + "0x40ec5b33f54e0e8a33a975908c5ba1c14e5bbbdf", + "0x0000000000000000000000000000000000000000", + "0xD6a92755Ac5384867083Abd79aD007DE389b955e", + "0x000000000000000000000000000000000000dead", + "0x70C575588B98C1F46B1382c706AdAf398A874e3E", + "0xab8eee3493a55a7bd8126865fd662b7097928088" + ] + } + }, + "network": "1", + "addresses": [ + "0xf521Bb7437bEc77b0B15286dC3f49A87b9946773", + "0x721931508df2764fd4f70c53da646cb8aed16ace", + "0xa63571f2ce7cf4e9a566a1f248f5d0ad3ba78726", + "0x9279c4cfb0e85e2dff8825ce141f9794c7c7170a", + "0x6f35b0cfc58eb1e21eef8a439bbb0ce4c929d32a", + "0xe34bded2b256430a9be53cbf5cba3b6d866d55f3", + "0xb14b87790643d2dab44b06692d37dd95b4b30e56", + "0xd32f25Dfa932b8064A81B8254E7997CAeBc85F97" + ], + "snapshot": 15021651 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v3/index.ts b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v3/index.ts new file mode 100644 index 00000000..5788b438 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz-v3/index.ts @@ -0,0 +1,97 @@ +import { formatUnits } from '@ethersproject/units'; +import { getProvider, getSnapshots, multicall } from '../../utils'; + +export const author = 'maxbrand99'; +export const version = '1.0.0'; + +const bananaContract = '0xe2311ae37502105b442bbef831e9b53c5d2e9b3b'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const allAddresses = {}; + const promises: any = []; + const blocks = await getSnapshots( + network, + snapshot, + provider, + options.chains.map((s) => s.network || network) + ); + const allCalls: any[] = []; + const chainCalls = { 1: [], 137: [] }; + options.chains.forEach((chain) => { + if (chain.network == 1 || chain.network == 137) { + Object.keys(chain.registries).forEach((registry) => { + allAddresses[registry] = chain.registries[registry]; + addresses.forEach((address: any) => { + chainCalls[chain.network].push([registry, 'balanceOf', [address]]); + allCalls.push([registry, 'balanceOf', [address]]); + }); + }); + } + }); + + Object.keys(chainCalls).forEach((chainID) => { + const blockTag = + typeof blocks[chainID] === 'number' ? blocks[chainID] : 'latest'; + promises.push( + multicall(chainID, getProvider(chainID), abi, chainCalls[chainID], { + blockTag + }) + ); + }); + + const results = await Promise.all(promises); + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const nanaCall = [[bananaContract, 'totalSupply', []]]; + let nanaSupply = await multicall(network, provider, abi, nanaCall, { + blockTag + }); + + const response: any[] = []; + results.forEach((result) => { + result.forEach((value) => { + response.push(value); + }); + }); + + response.forEach((value: any, i: number) => { + const address = allCalls[i][2][0]; + if ( + allAddresses[allCalls[i][0]] == 'BANANA' && + options.skipList.find((add) => add === address) + ) { + nanaSupply -= value; + } + }); + + const merged = {}; + response.forEach((value: any, i: number) => { + const address = allCalls[i][2][0]; + if (options.skipList.find((add) => add === address)) { + return; + } + merged[address] = (merged[address] || 0) as number; + if (allAddresses[allCalls[i][0]] == 'OG') + merged[address] += parseFloat(formatUnits((3 * value).toString(), 0)); + else if (allAddresses[allCalls[i][0]] == 'VX') + merged[address] += parseFloat(formatUnits(value.toString(), 0)); + else if (allAddresses[allCalls[i][0]] == 'BANANA') + merged[address] += parseFloat( + formatUnits(Math.floor((15000 * value) / nanaSupply).toString(), 0) + ); + }); + + return merged; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/cyberkongz/examples.json b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz/examples.json new file mode 100644 index 00000000..9a51c16b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "CyberKongz, CyberKongz VX and Banana holdings", + "strategy": { + "name": "cyberkongz", + "params": { + "symbol": "KONGZ", + "registries": [ + "0x57a204aa1042f6e66dd7730813f4024114d74f37", + "0x7ea3cca10668b8346aec0bf1844a49e995527c8b", + "0xe2311ae37502105b442bbef831e9b53c5d2e9b3b" + ] + } + }, + "network": "1", + "addresses": [ + "0xf521Bb7437bEc77b0B15286dC3f49A87b9946773", + "0x721931508df2764fd4f70c53da646cb8aed16ace", + "0xa63571f2ce7cf4e9a566a1f248f5d0ad3ba78726", + "0x9279c4cfb0e85e2dff8825ce141f9794c7c7170a", + "0x6f35b0cfc58eb1e21eef8a439bbb0ce4c929d32a", + "0xe34bded2b256430a9be53cbf5cba3b6d866d55f3", + "0x031c690be2932403cbdd85f8853f596794cff6c3" + ], + "snapshot": 13322697 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/cyberkongz/index.ts b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz/index.ts new file mode 100644 index 00000000..a47a1348 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/cyberkongz/index.ts @@ -0,0 +1,52 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'cesarsld'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const calls: any[] = []; + options.registries.forEach((registry) => { + addresses.forEach((address: any) => { + calls.push([registry, 'balanceOf', [address]]); + }); + }); + + const response = await multicall(network, provider, abi, calls, { blockTag }); + const nanaCall = [ + ['0xe2311ae37502105b442bbef831e9b53c5d2e9b3b', 'totalSupply', []] + ]; + const nanaSupply = await multicall(network, provider, abi, nanaCall, { + blockTag + }); + + const merged = {}; + response.map((value: any, i: number) => { + const address = calls[i][2][0]; + merged[address] = (merged[address] || 0) as number; + if (Math.floor(i / addresses.length) == 0) + merged[address] += parseFloat(formatUnits((3 * value).toString(), 0)); + else if (Math.floor(i / addresses.length) == 1) + merged[address] += parseFloat(formatUnits(value.toString(), 0)); + else if (Math.floor(i / addresses.length) == 2) + merged[address] += parseFloat( + formatUnits(Math.floor((15000 * value) / nanaSupply).toString(), 0) + ); + }); + + return merged; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/README.md b/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/README.md new file mode 100644 index 00000000..fb94c074 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/README.md @@ -0,0 +1,8 @@ +# Dark Forest Score strategy + +Gets player score from Dark Forest Contract. *Must be from a round with a `score` field on the `Player` struct* + +Due to the exponential nature of Dark Forest scoring, this stategy takes the log2 of each player's score. + +The only input parameter is `"graph_url"`, which requires the subgraph API URL for the round in question. + diff --git a/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/examples.json b/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/examples.json new file mode 100644 index 00000000..414738ca --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/examples.json @@ -0,0 +1,29 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "darkforest-score", + "params": { + "graph_url": "https://api.thegraph.com/subgraphs/name/darkforest-eth/dark-forest-v06-round-4", + "symbol": "log2(score)" + } + }, + "network": "100", + "addresses": [ + "0x0804bd21d754b33fed53c7335385697e7b87fdab", + "0xe99b5d080a1e0256726e106ab51d0a1799ef2519", + "0xee32d8021ed88a59eb6f1fd1b8367b8e9c75a6f5", + "0x1396364c0f00c814c97d1406c2b3250f9ff561ee", + "0x73cf8b7819ba17bc986611d093510296b46e4d33", + "0xb5ce86c2ab9e2403ab47acfbe501845e2480fad9", + "0x102e277c34668e96cbed6169fa1195002c11d746", + "0x14d2b918ae4d9bcf383a64931ec750c2d04a600a", + "0xd70c35c779d0055f1bccdc24b92fa58dcdad24b4", + "0x61f7bf167bd32bcf7042bc823bfcd24327426da7", + "0x8459c6bebe2d53b4dcaa71499a1ae4274c0e4df9", + "0x021004341db64a77a01eaead4c1cbfba8bedf589", + "0xe732654bA181fC97A42dC35Cd137CdeE2B17930F" + ], + "snapshot": 19546839 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/index.ts b/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/index.ts new file mode 100644 index 00000000..b5de6763 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/darkforest-score/index.ts @@ -0,0 +1,46 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'cha0sg0d'; +export const version = '0.1.0'; + +const calcScore = (score: number) => { + return score == 0 ? 0 : Math.floor(Math.log2(score)); +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const DF_SUBGRAPH_URL = options.graph_url; + + const params = { + players: { + __args: { + where: { + id_in: addresses.map((addr: string) => addr.toLowerCase()) + }, + first: 1000 + }, + id: true, + score: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.players.__args.block = { number: snapshot }; + } + + const result = await subgraphRequest(DF_SUBGRAPH_URL, { + ...params + }); + + return Object.fromEntries( + result.players.map((p) => [getAddress(p.id), calcScore(parseInt(p.score))]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/README.md b/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/README.md new file mode 100644 index 00000000..b8ed3787 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/README.md @@ -0,0 +1,14 @@ +# Decentraland Wearable Rarity + +This strategy allows users to calculate the total amount of Decentraland lands in estates and apply a multiplier to the result. + +## Example + +The parameters should look like this: + +```json +{ + "symbol": "ESTATE", + "multiplier": 2000 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/examples.json b/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/examples.json new file mode 100644 index 00000000..351ae975 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "decentraland-estate-size", + "params": { + "symbol": "ESTATE", + "multiplier": 2000 + } + }, + "network": "1", + "addresses": [ + "0x4eac6325e1dbf1ac90434d39766e164dca71139e", + "0xa65be351527ebcf8c1707d1e444dac38b41a5faf", + "0xa89473630bc2d2724279d1002ebdcfb000ad708c" + ], + "snapshot": 12453375 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/index.ts b/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/index.ts new file mode 100644 index 00000000..57b6956a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-estate-size/index.ts @@ -0,0 +1,87 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = '2fd'; +export const version = '0.1.0'; + +const SUBGRAPH_QUERY_ADDRESSES_LIMIT = 2000; +const DECENTRALAND_MARKETPLACE_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/decentraland/marketplace', + '3': 'https://api.thegraph.com/subgraphs/name/decentraland/marketplaceropsten' +}; + +function chunk(_array: string[], pageSize: number): string[][] { + const chunks: string[][] = []; + for (let i = 0; i < _array.length; i += pageSize) { + chunks.push(_array.slice(i, i + pageSize)); + } + return chunks; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + // initialize scores + const scores = {}; + for (const address of addresses) { + scores[getAddress(address)] = 0; + } + + // if graph doesn't exist return automatically + if (!DECENTRALAND_MARKETPLACE_SUBGRAPH_URL[network]) { + return scores; + } + + const chunks = chunk(addresses, SUBGRAPH_QUERY_ADDRESSES_LIMIT); + const multipler = options.multiplier || 1; + + for (const chunk of chunks) { + const params = { + nfts: { + __args: { + where: { + owner_in: chunk.map((address) => address.toLowerCase()), + category: 'estate', + searchEstateSize_gt: 0 + }, + first: 1000, + skip: 0 + }, + owner: { + id: true + }, + searchEstateSize: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.nfts.__args.block = { number: snapshot }; + } + + let hasNext = true; + while (hasNext) { + const result = await subgraphRequest( + DECENTRALAND_MARKETPLACE_SUBGRAPH_URL[network], + params + ); + + const nfts = result && result.nfts ? result.nfts : []; + for (const estate of nfts) { + const userAddress = getAddress(estate.owner.id); + scores[userAddress] = + (scores[userAddress] || 0) + estate.searchEstateSize * multipler; + } + + params.nfts.__args.skip += params.nfts.__args.first; + hasNext = nfts.length === params.nfts.__args.first; + } + } + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/README.md b/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/README.md new file mode 100644 index 00000000..678ec1e7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/README.md @@ -0,0 +1,30 @@ +# Decentraland Rental Lessors + +This strategy allows calculating the VP of Land/Estate owners after ownership is transferred to the Rentals contract. + +Thanks to this, the VP that the user had before utilizing the rentals feature will not be lost. + +## Example + +The following example params are for obtaining the VP users have after sending their Lands/Estates to the Rentals contract in the Goerli network. + +```json +{ + "subgraphs": { + "rentals": "https://api.thegraph.com/subgraphs/name/decentraland/rentals-ethereum-goerli", + "marketplace": "https://api.thegraph.com/subgraphs/name/decentraland/marketplace-goerli" + }, + "addresses": { + "estate": "0xc9a46712e6913c24d15b46ff12221a79c4e251dc", + "land": "0x25b6b4bac4adb582a0abd475439da6730777fbf7" + }, + "multipliers": { + "estateSize": 2000, + "land": 2000 + } +} +``` + +The land multiplier determines how much VP is given by each Land the address possesses in the Rentals contract. For example, if the user has 5 Lands in the Rentals contract, it will be given 5 \* 2000 VP. + +The estateSize multiplier determines how much VP is given to the original owner according to the size of their Estates currently on the Rentals contract. For example, if the address has 1 Estate in the Rentals contract composed of 5 Lands, the user will be given 5 \* 2000 VP as well. diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/examples.json b/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/examples.json new file mode 100644 index 00000000..e2f8512e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/examples.json @@ -0,0 +1,30 @@ +[ + { + "name": "Get VP of LAND and Estate owner's that have their assets locked in the Rentals contract", + "strategy": { + "name": "decentraland-rental-lessors", + "params": { + "subgraphs": { + "rentals": "https://api.thegraph.com/subgraphs/name/decentraland/rentals-ethereum-goerli", + "marketplace": "https://api.thegraph.com/subgraphs/name/decentraland/marketplace-goerli" + }, + "addresses": { + "estate": "0xc9a46712e6913c24d15b46ff12221a79c4e251dc", + "land": "0x25b6b4bac4adb582a0abd475439da6730777fbf7" + }, + "multipliers": { + "estateSize": 2000, + "land": 2000 + } + } + }, + "network": "5", + "addresses": [ + "0x747c6f502272129bf1ba872a1903045b837ee86c", + "0xbad79d832671d91b4bba85f600932faec0e5fd7c", + "0x24e5f44999c151f08609f8e27b2238c773c4d020", + "0x2f89ec84e0413950d9adf8e56dd56c2b2f5066cb" + ], + "snapshot": 7866054 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/index.ts b/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/index.ts new file mode 100644 index 00000000..72b77efe --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/index.ts @@ -0,0 +1,223 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; +import { MarketplaceEstate, RentalsLandOrEstate, Scores } from './types'; + +export const author = 'fzavalia'; +export const version = '0.1.0'; + +const SUBGRAPH_QUERY_IN_FILTER_MAX_LENGTH = 500; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const scores: Scores = {}; + + // Initialize scores for every provided address as 0 + for (const address of addresses) { + scores[getAddress(address)] = 0; + } + + // For the provided addresses, fetch all their Lands and Estates that have been transferred to the rentals contract. + const rentalLandsAndEstates = await fetchLandsAndEstatesInRentalsContract( + addresses, + options, + snapshot + ); + + const rentalLands: RentalsLandOrEstate[] = []; + const rentalEstates: RentalsLandOrEstate[] = []; + + // Separate the assets into Lands and Estates + for (const rentalLandOrEstate of rentalLandsAndEstates) { + switch (rentalLandOrEstate.contractAddress) { + case options.addresses.land.toLowerCase(): + rentalLands.push(rentalLandOrEstate); + break; + case options.addresses.estate.toLowerCase(): + rentalEstates.push(rentalLandOrEstate); + } + } + + // For each Land, increase the score of the original owner by the land multiplier. + for (const land of rentalLands) { + scores[getAddress(land.lessor)] += options.multipliers.land; + } + + // Fetch and match Estates from the marketplace subgraph with the ones from the rentals subgraph. + const rentalAndMarketplaceEstates = + await fetchMarketplaceEstatesForProvidedRentalAssets( + rentalEstates, + options, + snapshot + ); + + // For each Estate, increase the score of the original owner by the size of the estate times the multiplier. + for (const [rentalEstate, marketplaceEstate] of rentalAndMarketplaceEstates) { + scores[getAddress(rentalEstate.lessor)] += + marketplaceEstate.size * options.multipliers.estateSize; + } + + return scores; +} + +// For a given list of addresses, fetch all the lands and estates that belonged to them before being transferred to the Rentals contract. +async function fetchLandsAndEstatesInRentalsContract( + addresses, + options, + snapshot +): Promise { + // Separate the addresses in batches to optimize the subgraph query. + const addressBatches = batchify( + addresses, + SUBGRAPH_QUERY_IN_FILTER_MAX_LENGTH + ); + + let finalRentalLandsAndEstates: RentalsLandOrEstate[] = []; + + for (const addressBatch of addressBatches) { + const query: any = { + rentalAssets: { + __args: { + where: { + contractAddress_in: [ + options.addresses.estate.toLowerCase(), + options.addresses.land.toLowerCase() + ], + lessor_in: addressBatch.map((address) => address.toLowerCase()), + isClaimed: false + }, + first: 1000, + skip: 0 + }, + id: true, + contractAddress: true, + tokenId: true, + lessor: true + } + }; + + // If a snapshot is provided, use it as another filter of the query. + if (typeof snapshot === 'number') { + query.rentalAssets.__args.block = { number: snapshot }; + } + + let hasMoreResults = true; + + while (hasMoreResults) { + const result = await subgraphRequest(options.subgraphs.rentals, query); + + const rentalLandsAndEstates: RentalsLandOrEstate[] = result.rentalAssets; + + // If the received length matches the requested length, there might be more results. + hasMoreResults = + rentalLandsAndEstates.length === query.rentalAssets.__args.first; + // If there are more results, skip the ones we already have on the next query. + query.rentalAssets.__args.skip += query.rentalAssets.__args.first; + + finalRentalLandsAndEstates = [ + ...finalRentalLandsAndEstates, + ...rentalLandsAndEstates + ]; + } + } + + return finalRentalLandsAndEstates; +} + +// For a given list of estates obtained from the rentals subgraph, fetch the estates that correspond to them in the marketplace subgraph. +async function fetchMarketplaceEstatesForProvidedRentalAssets( + rentalEstates: RentalsLandOrEstate[], + options, + snapshot +): Promise<[RentalsLandOrEstate, MarketplaceEstate][]> { + const rentalEstatesTokenIds: string[] = []; + + // Keep a map of rental estates to optimize the lookup later. + const rentalEstatesByTokenId = new Map(); + + for (const rentalEstate of rentalEstates) { + const tokenId = rentalEstate.tokenId; + rentalEstatesTokenIds.push(tokenId); + rentalEstatesByTokenId.set(tokenId, rentalEstate); + } + + // Separate the estate token ids in batches to optimize the subgraph query. + const rentalEstateTokenIdBatches = batchify( + rentalEstatesTokenIds, + SUBGRAPH_QUERY_IN_FILTER_MAX_LENGTH + ); + + const rentalAndMarketplaceEstates: [ + RentalsLandOrEstate, + MarketplaceEstate + ][] = []; + + for (const rentalEstateTokenIdBatch of rentalEstateTokenIdBatches) { + const query: any = { + estates: { + __args: { + where: { + tokenId_in: rentalEstateTokenIdBatch, + size_gt: 0 + }, + first: 1000, + skip: 0 + }, + tokenId: true, + size: true + } + }; + + // If a snapshot is provided, use it as another filter of the query. + if (typeof snapshot === 'number') { + query.estates.__args.block = { number: snapshot }; + } + + let hasMoreResults = true; + + while (hasMoreResults) { + const result = await subgraphRequest( + options.subgraphs.marketplace, + query + ); + + const marketplaceEstates: MarketplaceEstate[] = result.estates; + + // If the received length matches the requested length, there might be more results. + hasMoreResults = marketplaceEstates.length === query.estates.__args.first; + // If there are more results, skip the ones we already have on the next query. + query.estates.__args.skip += query.estates.__args.first; + + for (const marketplaceEstate of marketplaceEstates) { + const rentalEstate = rentalEstatesByTokenId.get( + marketplaceEstate.tokenId + ); + + if (rentalEstate) { + rentalAndMarketplaceEstates.push([rentalEstate, marketplaceEstate]); + } + } + } + } + + return rentalAndMarketplaceEstates; +} + +function batchify(elements: T[], batchSize: number): T[][] { + const batches: T[][] = []; + + for (let i = 0; i < elements.length; i++) { + if (i % batchSize === 0) { + batches.push([]); + } + + batches[batches.length - 1].push(elements[i]); + } + + return batches; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/types.ts b/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/types.ts new file mode 100644 index 00000000..412d22ed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-rental-lessors/types.ts @@ -0,0 +1,15 @@ +export type Scores = { + [address: string]: number; +}; + +export type RentalsLandOrEstate = { + id: string; + contractAddress: string; + tokenId: string; + lessor: string; +}; + +export type MarketplaceEstate = { + tokenId: string; + size: number; +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/README.md b/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/README.md new file mode 100644 index 00000000..04741de1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/README.md @@ -0,0 +1,47 @@ +# Decentraland Wearable Rarity + +This strategy allows users to calculate the total amount of Decentraland wearables and apply a multiplier to each rarity. Additionally you can limit the collections that will be taken into account using a list of ids. + +## Example + +The parameters should look like this: + +```json +{ + "symbol": "WEARABLE", + "multipliers": { + "unique": 1000000, + "mythic": 100000, + "legendary": 10000, + "epic": 1000, + "rare": 100, + "uncommon": 10, + "common": 1 + } +} +``` + +If you want to restrict which collection is taken into account, parameters should look like this: + +```json +{ + "symbol": "WEARABLE", + "collections": [ + "0x32b7495895264ac9d0b12d32afd435453458b1c6", + "0xd35147be6401dcb20811f2104c33de8e97ed6818", + "0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd", + "0xc1f4b0eea2bd6690930e6c66efd3e197d620b9c2", + "0xf64dc33a192e056bb5f0e5049356a0498b502d50", + "0xc3af02c0fd486c8e9da5788b915d6fff3f049866" + ], + "multipliers": { + "unique": 1000000, + "mythic": 100000, + "legendary": 10000, + "epic": 1000, + "rare": 100, + "uncommon": 10, + "common": 1 + } +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/examples.json b/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/examples.json new file mode 100644 index 00000000..2104c70c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "decentraland-wearable-rarity", + "params": { + "symbol": "WEARABLE", + "collections": [ + "0x32b7495895264ac9d0b12d32afd435453458b1c6", + "0xd35147be6401dcb20811f2104c33de8e97ed6818", + "0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd", + "0xc1f4b0eea2bd6690930e6c66efd3e197d620b9c2", + "0xf64dc33a192e056bb5f0e5049356a0498b502d50", + "0xc3af02c0fd486c8e9da5788b915d6fff3f049866" + ], + "multipliers": { + "unique": 1000000, + "mythic": 100000, + "legendary": 10000, + "epic": 1000, + "rare": 100, + "uncommon": 10, + "common": 1 + } + } + }, + "network": "1", + "addresses": [ + "0xd210dc1dd26751503cbf1b8c9154224707820da8", + "0x8cff6832174091dae86f0244e3fd92d4ced2fe07", + "0xFC9C4C0e17c3A3139a77d86282eCf18687C14780" + ], + "snapshot": 12453375 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/index.ts b/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/index.ts new file mode 100644 index 00000000..4baaf43d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/decentraland-wearable-rarity/index.ts @@ -0,0 +1,112 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = '2fd'; +export const version = '0.1.0'; + +const SUBGRAPH_QUERY_ADDRESSES_LIMIT = 2000; +const DECENTRALAND_COLLECTIONS_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/decentraland/collections-ethereum-mainnet', + '3': 'https://api.thegraph.com/subgraphs/name/decentraland/collections-ethereum-ropsten', + '137': + 'https://api.thegraph.com/subgraphs/name/decentraland/collections-matic-mainnet', + '80001': + 'https://api.thegraph.com/subgraphs/name/decentraland/collections-matic-mumbai' +}; + +function chunk(_array: string[], pageSize: number): string[][] { + const chunks: string[][] = []; + for (let i = 0; i < _array.length; i += pageSize) { + chunks.push(_array.slice(i, i + pageSize)); + } + return chunks; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + // initialize scores + const scores = {}; + for (const address of addresses) { + scores[getAddress(address)] = 0; + } + + // if graph doesn't exist return automatically + if (!DECENTRALAND_COLLECTIONS_SUBGRAPH_URL[network]) { + return scores; + } + + const chunks = chunk(addresses, SUBGRAPH_QUERY_ADDRESSES_LIMIT); + // initialize multipliers and params + const multiplers = options.multipliers || {}; + + for (const chunk of chunks) { + const params = { + nfts: { + __args: { + where: { + itemType_in: [ + 'wearable_v1', + 'wearable_v2', + 'smart_wearable_v1', + 'emote_v1' + ], + owner_in: chunk.map((address) => address.toLowerCase()), + id_gt: '' + }, + orderBy: 'id', + orderDirection: 'asc', + first: 1000 + }, + id: true, + owner: { + id: true + }, + searchWearableRarity: true + } + }; + + if (options.collections) { + // @ts-ignore + params.nfts.__args.where.collection_in = options.collections; + } + + if (snapshot !== 'latest') { + // @ts-ignore + params.nfts.__args.block = { number: snapshot }; + } + + // load and add each wearable by rarity + let hasNext = true; + while (hasNext) { + const result = await subgraphRequest( + DECENTRALAND_COLLECTIONS_SUBGRAPH_URL[network], + params + ); + + const nfts = result && result.nfts ? result.nfts : []; + const latest = nfts[nfts.length - 1]; + for (const wearable of nfts) { + const userAddress = getAddress(wearable.owner.id); + const rarity = String(wearable.searchWearableRarity) + .toLowerCase() + .trim(); + scores[userAddress] = + (scores[userAddress] ?? 0) + (multiplers[rarity] ?? 0); + } + + hasNext = nfts.length === params.nfts.__args.first; + if (hasNext) { + params.nfts.__args.where.id_gt = latest?.id || ''; + } + } + } + + // return result + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/defidollar/examples.json b/Implementations/API/backend/utils/snapshot/strategies/defidollar/examples.json new file mode 100644 index 00000000..99a5c6be --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/defidollar/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "defidollar", + "strategy": { + "name": "defidollar", + "params": { + "address": "0xde157688a36ac94b6e5f52e99c196f79ac71cea3", + "symbol": "ibDFD" + } + }, + "network": "1", + "addresses": [ + "0x562574AF66836b1d30e69815bDf0740A7BD7C437", + "0x6d3ee34a020e7565e78540c74300218104c8e4a9", + "0xa91194363b0a498e9d8602a866c473cb7bc9467d" + ], + "snapshot": 12050071 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/defidollar/index.ts b/Implementations/API/backend/utils/snapshot/strategies/defidollar/index.ts new file mode 100644 index 00000000..6cd3a699 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/defidollar/index.ts @@ -0,0 +1,72 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'atvanguard'; +export const version = '1.0.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'getPricePerFullShare', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const queries: any[] = []; + + addresses.forEach((voter) => { + queries.push([options.address, 'balanceOf', [voter]]); + }); + queries.push([options.address, 'getPricePerFullShare']); + + const response = ( + await multicall(network, provider, abi, queries, { blockTag }) + ).map((r) => r[0]); + const pps = response[response.length - 1]; + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + return [ + addresses[i], + parseFloat(formatUnits(response[i].mul(pps), 36 /* decimals */)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/defiplaza/README.md b/Implementations/API/backend/utils/snapshot/strategies/defiplaza/README.md new file mode 100644 index 00000000..5f7b4ca0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/defiplaza/README.md @@ -0,0 +1,20 @@ +# DefiPlaza + +This new strategy looks at `balanceOf` and the `quoteRewards` function of the DFP2 governance contract to be able to use the balance of unclaimed staking rewards in the voting process, next to the balance of DFP2 in a wallet. + +It also looks at the `stakerData` function of the new StablePlaza pool contract (if given as an option) to be able to use the staked DFP2 tokens in the voting process. + +The addresses in the `examples.json` are the current biggest DFP2 holders to create a real-world test scenario. + +This strategy is based on the `erc20-balance-of` strategy. + +Here is an example of the parameters: + +```json +{ + "address": "0x2F57430a6ceDA85a67121757785877b4a71b8E6D", + "stableplaza": "0x3A2b8cC91aF8bf45F3Ec61E779ee1c2ba6b7E694", + "symbol": "DFP2", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/defiplaza/examples.json b/Implementations/API/backend/utils/snapshot/strategies/defiplaza/examples.json new file mode 100644 index 00000000..6124eeb1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/defiplaza/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "defiplaza", + "params": { + "address": "0x2F57430a6ceDA85a67121757785877b4a71b8E6D", + "stableplaza": "0x3A2b8cC91aF8bf45F3Ec61E779ee1c2ba6b7E694", + "symbol": "DFP2", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xe461cc7406607866082b1385bf66c8d1d794e892", + "0xef7ddc0852baa45cc8ae7c99d30c47b75147c2ff", + "0xac0f1af3b5439d0954e83ede9b7c4a0772178217", + "0xf8c95d77e4c7668c0982917b3a7f47ae3902c095", + "0x2f7ab204f3675353f37c70f180944a65b9890a9a", + "0x29961513051affe355f6db49cb7e81b4970b4492", + "0x4bc760c7997a2833c974a85420c5a35d93f26be8", + "0x77fc54677f6cd1c3a669b9c2ff032836af59c5b8", + "0x9de1d0c4ec447242a5a13e26ff7fe68e8158e489", + "0x4d08800317e9aabffb815014d8d7054878663665", + "0x720de10fea55f74e58f9478ee4225087d1260048", + "0x7a9608f2e24d78a2e90104a1a31526a057dd8b10", + "0xea51de3bcfcda969eae74f27b9785611b0c43550", + "0xe579ac2dce69f44cc85d8d66e5eecc8fc4fe292a", + "0x5fa483d02a78fcf0e5dd0670b5d4b60a0be1de33", + "0x40d0440af173578d0ce4e959f822fca215cad7bb", + "0x27f2be297489e813dae3895a30c9ac72b01bf57a", + "0x1eA44f3176B480cD03a8c9B7e924B2bB9a49a9cA" + ], + "snapshot": 15839572 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/defiplaza/index.ts b/Implementations/API/backend/utils/snapshot/strategies/defiplaza/index.ts new file mode 100644 index 00000000..8fcf80cf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/defiplaza/index.ts @@ -0,0 +1,85 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, customFetch } from '../../utils'; + +export const author = 'timanrebel'; +export const version = '0.1.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function rewardsQuote(address stakerAddress) external view returns (uint256 rewards)', + 'function stakerData(address address) external view returns (uint64 stakedAmount, uint64 sharesEquivalent, uint96 rewardsPerShareWhenStaked, uint32 unlockTime)' +]; + +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) => { + // request balance + multi.call(`balanceOf.${address}`, options.address, 'balanceOf', [address]); + + // request balance of unclaimed staking rewards + multi.call(`rewardsQuote.${address}`, options.address, 'rewardsQuote', [ + address + ]); + + // request balance of staked tokens on StablePlaza + if (options.stableplaza) { + multi.call(`stableplaza.${address}`, options.stableplaza, 'stakerData', [ + address + ]); + } + }); + const result = await multi.execute(); + + const returnObject = {}; + + addresses.forEach((address) => { + if (!returnObject.hasOwnProperty(address)) { + returnObject[address] = 0; + } + + returnObject[address] += parseFloat( + formatUnits(result.balanceOf[address], options.decimals) + ); + returnObject[address] += parseFloat( + formatUnits(result.rewardsQuote[address], options.decimals) + ); + + if (options.stableplaza) { + returnObject[address] += parseFloat( + formatUnits( + result.stableplaza[address][0].mul(4294967296), + options.decimals + ) + ); // * 2^32 + } + }); + + // request balance on Radix + const res = await customFetch( + `https://radix.defiplaza.net/voting/${snapshot}`, + {}, + 8000 + ); + const radixLinks = await res.json(); + + for (const wallet of radixLinks) { + if (!returnObject.hasOwnProperty(wallet.address)) { + returnObject[wallet.address] = 0; + } + + returnObject[wallet.address] += parseFloat( + formatUnits(wallet.balance, options.decimals) + ); + } + + return returnObject; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/defiplaza/schema.json b/Implementations/API/backend/utils/snapshot/strategies/defiplaza/schema.json new file mode 100644 index 00000000..f4f54080 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/defiplaza/schema.json @@ -0,0 +1,35 @@ +{ + "$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. DFP2"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Governance contract address", + "examples": ["e.g. 0x2F57430a6ceDA85a67121757785877b4a71b8E6D"] + }, + "stableplaza": { + "type": "string", + "title": "StablePlaza contract address", + "examples": ["e.g. 0x3A2b8cC91aF8bf45F3Ec61E779ee1c2ba6b7E694"] + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/README.md new file mode 100644 index 00000000..9efe27f5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/README.md @@ -0,0 +1,36 @@ +# degenzoo-erc721-animals-weighted + +This strategy allows you to determine the voting power by reading the metadata attribute of holding NFT. +`tokenURI(tokenID)` returns the individual metadata URI in ERC721. + +Each animal has it's own "Staked Tokens" attribute. + +The voting power is the sum of all Staked tokens of each Zoo owner. + + +Example: + +```tokenURI(36) +[ + { + "0x91646c2c2fF05C4e9822740b0aD9d8B3DA51382b-36": "data:application/json;base64,eyJuYW1lIjogIkRlZ2VuWm9vICM2NDAxIiwgImRlc2NyaXB0aW9uIjogIkRlZ2Vuem9vICM2NDAxIGlzIHN0aWxsIGFuIGVnZy4uLiB3aHkgbm90IGhhdGNoIGl0PyIsICJpbWFnZSI6ICJpcGZzOi8vYmFmeWJlaWNqaXltb3VneHVqczJmenN4anVrbTdlNjU0eXRlcjRhb2hkcDc0ZWJvbXhjZDN5dnJzcWEvZWdnLnBuZyIsImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkxldmVsIiwgInZhbHVlIjogIjAifSwgeyJ0cmFpdF90eXBlIjogIlJhcml0eSIsICJ2YWx1ZSI6ICIwIn1dfQ==", + } +] +``` + +This data are encoded onchain and decoding them is providing the metadata attribute of each animal. The 'Staked Tokens' value is used to calculate the weight of each user + +``` example metadata of an animal +[ + { trait_type: 'Rank', value: '42' }, + { trait_type: 'Rarity', value: 'Endangered' }, + { trait_type: 'Shininess', value: 'false' }, + { trait_type: 'Variant', value: '2' }, + { trait_type: 'Multiplier', value: '12012' }, + { trait_type: 'Staked Tokens', value: '12012' }, + { trait_type: 'Level', value: '1' }, + { trait_type: 'Evolve Time', value: '1987200' }, + { trait_type: 'Hatch Timestamp', value: '1680706434' } + ] + +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/examples.json new file mode 100644 index 00000000..94feba35 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "degenzoo-erc721-animals-weighted", + "params": { + "address": "0x6defa7058d517f2dada60d79b7e26ab8a1219aca", + "symbol": "NFT" + } + }, + "network": "56", + "addresses": [ + "0x91646c2c2fF05C4e9822740b0aD9d8B3DA51382b", + "0xb9cc4a34BaA26D27363eE224CD652a6eBc59b49b", + "0x7219Fc75f990d0147F51960c8E4Dc868c6f3670C" + ], + "snapshot": 27121184 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/index.ts new file mode 100644 index 00000000..3626fe06 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/degenzoo-erc721-animals-weighted/index.ts @@ -0,0 +1,90 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'aorfevre'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function tokenURI(uint256 tokenId) external view returns (string)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Fetch the balanceOf the addresses i.e. how many vouchers do they hold? + const balanceOfMulti = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + balanceOfMulti.call(address, options.address, 'balanceOf', [address]) + ); + const ownedCounts: Record = await balanceOfMulti.execute(); + + // Fetch the voucher token IDs held for each address + const tokenIdsMulti = new Multicaller(network, provider, abi, { blockTag }); + addresses.map((address) => { + let ownedCount = ownedCounts[address]; + while (ownedCount.gt(0)) { + const index = ownedCount.sub(1); + tokenIdsMulti.call( + `${address}-${index.toString()}`, + options.address, + 'tokenOfOwnerByIndex', + [address, index.toNumber()] + ); + ownedCount = index; + } + }); + const ownerTokenIds: Record = await tokenIdsMulti.execute(); + + // Fetch the voucher data for each voucher held by an address among the address + const tokenURIMulti = new Multicaller(network, provider, abi, { blockTag }); + Object.entries(ownerTokenIds).map(([addressWithIndex, tokenId]) => { + tokenURIMulti.call(`${addressWithIndex}`, options.address, `tokenURI`, [ + tokenId + ]); + }); + const ownerTokenURIs: Record = await tokenURIMulti.execute(); + + // Go through the list of results and sum up claimable values + const claimableVotingPower: Record = {}; + Object.entries(ownerTokenURIs).map(([addressWithIndex, tokenURI]) => { + const address = addressWithIndex.split('-')[0]; + + if (tokenURI.split(',')[0] == 'data:application/json;base64') { + const decoded = JSON.parse( + atob(tokenURI.slice('data:application/json;base64'.length + 1)) + ); + if (decoded.attributes) { + for (const i in decoded.attributes) { + if ( + decoded.attributes[i] && + decoded.attributes[i].trait_type === 'Staked Tokens' + ) { + if (!claimableVotingPower[address]) { + claimableVotingPower[address] = 0; + } + claimableVotingPower[address] += Number( + decoded.attributes[i].value + ); + } + } + } + } + }); + + // Return the computed values + return Object.fromEntries( + Object.entries(claimableVotingPower).map(([address, votingPower]) => [ + address, + votingPower + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/README.md b/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/README.md new file mode 100644 index 00000000..ee3c8fc0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/README.md @@ -0,0 +1,50 @@ +# delegation-with-overrides + +This strategy is based on the [delegation strategy](https://github.com/snapshot-labs/snapshot-strategies/tree/master/src/strategies/delegation), but with an optional `overrides` parameter: an address to address mapping, where the delegated voting power of each key will be forwarded to the corresponding value. + +For example: + +```json lines +{ + "overrides": { + // The delegated votes of: 0xAD9992f3631028CEF19e6D6C31e822C5bc2442CC + // will be forwarded to: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + "0xAD9992f3631028CEF19e6D6C31e822C5bc2442CC": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + } +} +``` + +| Param Name | Description | +|----------------------------|------------------------------------------------------------------------------------------| +| strategies | list of sub strategies to calculate voting power based on delegation | +| delegationSpace (optional) | Get delegations of a particular space (by default it takes delegations of current space) | +| overrides (optional) | Address mapping used to override delegated votes and forward to another address | + +Here is an example of parameters: + +```json +{ + "symbol": "veBAL (delegated)", + "strategies": [ + { + "symbol": "veBAL (delegated)", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "symbol": "veBAL", + "address": "0xC128a9954e6c874eA3d62ce62B468bA073093F25", + "decimals": 18 + } + } + ], + "delegationSpace": "balancer.eth" + } + ], + "delegationSpace": "balancer.eth", + "overrides": { + "0xAD9992f3631028CEF19e6D6C31e822C5bc2442CC": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + } +} + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/examples.json b/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/examples.json new file mode 100644 index 00000000..53023be9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "space": "balancer.eth", + "strategy": { + "name": "delegation-with-overrides", + "params": { + "symbol": "veBAL (delegated)", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "symbol": "veBAL", + "address": "0xC128a9954e6c874eA3d62ce62B468bA073093F25", + "decimals": 18 + }, + "delegationSpace": "balancer.eth" + } + ], + "overrides": { + "0xAD9992f3631028CEF19e6D6C31e822C5bc2442CC": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + }, + "delegationSpace": "balancer.eth" + } + }, + "network": "1", + "addresses": [ + "0xAD9992f3631028CEF19e6D6C31e822C5bc2442CC", + "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "0x512fce9B07Ce64590849115EE6B32fd40eC0f5F3", + "0xE58585B22cC2aC4270a2b92c2D2d8C5dB5A3330E" + ], + "snapshot": 16693494 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/index.ts b/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/index.ts new file mode 100644 index 00000000..ed466630 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/delegation-with-overrides/index.ts @@ -0,0 +1,75 @@ +import { getDelegations } from '../../utils/delegation'; +import { getScoresDirect } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = '0xbutterfield'; +export const version = '0.1.0'; +export const dependOnOtherAddress = true; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const delegationSpace = options.delegationSpace || space; + const overrides: { [delegatee: string]: string } = Object.fromEntries( + Object.entries(options.overrides ?? {}).map(([key, value]) => [ + getAddress(key), + getAddress(value as string) + ]) + ); + + // Remove duplicates + const allAddresses = addresses + .concat(Object.keys(overrides)) + .filter((v, i, a) => a.indexOf(v) === i); + + const delegations = await getDelegations( + delegationSpace, + network, + allAddresses, + snapshot + ); + if (Object.keys(delegations).length === 0) return {}; + + const scores = ( + await getScoresDirect( + space, + options.strategies, + network, + provider, + Object.values(delegations).reduce((a: string[], b: string[]) => + a.concat(b) + ), + snapshot + ) + ).filter((score) => Object.keys(score).length !== 0); + + return allAddresses + .map((address) => { + const addressScore = delegations[address] + ? delegations[address].reduce( + (a, b) => a + scores.reduce((x, y) => (y[b] ? x + y[b] : x), 0), + 0 + ) + : 0; + return [address, addressScore]; + }) + .reduce((acc, [address, addressScore]) => { + const delegatee = overrides[address]; + if (delegatee) { + return { + ...acc, + // Redirect the votes for address to delegatee + [address]: 0, + [delegatee]: (acc[delegatee] ?? 0) + addressScore + }; + } + // It is possible that address has already been set with an override, + // so add the score to that value (or zero) + return { ...acc, [address]: (acc[address] ?? 0) + addressScore }; + }, {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/delegation/README.md new file mode 100644 index 00000000..c1906e36 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/delegation/README.md @@ -0,0 +1,37 @@ +# delegation + +If you want to delegate your voting power to another wallet address, you can do this using the “delegation strategy”. In delegation strategy, if A delegates to B and both of them vote, then the delegated voting power is not calculated. Only the vote of A will be calculated. The vote of B will be counted if A does not vote. + +In delegation strategy, the sub strategies defined in params are used to delegate vote from one address to another. + +| Param Name | Description | +| ----------- | ----------- | +| strategies | list of sub strategies to calculate voting power based on delegation | +| delegationSpace (optional) | Get delegations of a particular space (by default it take delegations of current space) | + +Here is an example of parameters: + +```json +{ + "symbol": "YFI (delegated)", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0xBa37B002AbaFDd8E89a1995dA52740bbC013D992", + "symbol": "YFI", + "decimals": 18 + } + }, + { + "name": "yearn-vault", + "params": { + "address": "0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1", + "symbol": "YFI (yYFI)", + "decimals": 18 + } + } + ] +} + +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/delegation/examples.json new file mode 100644 index 00000000..e2f824d5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/delegation/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "delegation", + "params": { + "symbol": "POH (delegated)", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0x1dAD862095d40d43c2109370121cf087632874dB", + "decimals": 0 + } + } + ] + } + }, + "network": "1", + "addresses": [ + "0x3c13f2B56AF614aC6381265EcB3B619bA26CC641", + "0x048fee7c3279a24af0790b6b002ded42be021d2b", + "0x139a9032a46c3afe3456eb5f0a35183b5f189cae" + ], + "snapshot": 15705816 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/delegation/index.ts new file mode 100644 index 00000000..7ea2d7ff --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/delegation/index.ts @@ -0,0 +1,59 @@ +import { getDelegations } from '../../utils/delegation'; +import { getScoresDirect } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; +export const dependOnOtherAddress = true; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const invalidStrategies = [ + '{"name":"erc20-balance-of","params":{"symbol":"HOP","address":"0xed8Bdb5895B8B7f9Fdb3C087628FD8410E853D48","decimals":18}}' //https://snapshot.org/#/hop.eth/proposal/0x603f0f6e54c7be8d5db7e16ae7145e6df4b439b8aac49654cdfd6b0c03eb6492 + ]; + + if ( + options.strategies.some((s) => + invalidStrategies.includes(JSON.stringify(s)) + ) + ) + return {}; + const delegationSpace = options.delegationSpace || space; + const delegations = await getDelegations( + delegationSpace, + network, + addresses, + snapshot + ); + if (Object.keys(delegations).length === 0) return {}; + + const scores = ( + await getScoresDirect( + space, + options.strategies, + network, + provider, + Object.values(delegations).reduce((a: string[], b: string[]) => + a.concat(b) + ), + snapshot + ) + ).filter((score) => Object.keys(score).length !== 0); + + return Object.fromEntries( + addresses.map((address) => { + const addressScore = delegations[address] + ? delegations[address].reduce( + (a, b) => a + scores.reduce((x, y) => (y[b] ? x + y[b] : x), 0), + 0 + ) + : 0; + return [address, addressScore]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/README.md b/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/README.md new file mode 100644 index 00000000..9c60aac4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/README.md @@ -0,0 +1,12 @@ +# Deposit in Sablier Stream + +This strategy returns the score for any voter as the sum of all deposits made by a sender towards the voters for a specific ERC20 token; + +Here is an example of parameters: + +```JSON +{ + "sender": "0xC9F2D9adfa6C24ce0D5a999F2BA3c6b06E36F75E", + "token": "0x7f8F6E42C169B294A384F5667c303fd8Eedb3CF3" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/examples.json b/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/examples.json new file mode 100644 index 00000000..e0eea6f0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query 0", + "strategy": { + "name": "deposit-in-sablier-stream", + "params": { + "sender": "0xC9F2D9adfa6C24ce0D5a999F2BA3c6b06E36F75E", + "token": "0x7f8F6E42C169B294A384F5667c303fd8Eedb3CF3" + } + }, + "network": "5", + "addresses": [ + "0x3f9b2fea60325d733e61bc76598725c5430cd751", + "0x7f8F6E42C169B294A384F5667c303fd8Eedb3CF3" + ], + "snapshot": 7572600 + }, + { + "name": "Example query 1", + "strategy": { + "name": "deposit-in-sablier-stream", + "params": { + "sender": "0xC9F2D9adfa6C24ce0D5a999F2BA3c6b06E36F75E", + "token": "0x7f8F6E42C169B294A384F5667c303fd8Eedb3CF3", + "subGraphURL": "https://api.thegraph.com/subgraphs/name/sablierhq/sablier-goerli" + } + }, + "network": "5", + "addresses": [ + "0x1206b51217271FC3ffCa57d0678121983ce0390E", + "0x7f8F6E42C169B294A384F5667c303fd8Eedb3CF3" + ], + "snapshot": 7572688 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/index.ts b/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/index.ts new file mode 100644 index 00000000..afad5ab8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/index.ts @@ -0,0 +1,69 @@ +import { getAddress } from '@ethersproject/address'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; + +const SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier', // mainnet + '3': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-ropsten', // ropsten + '4': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-rinkeby', // rinkeby + '5': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-goerli', // goerli + '10': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-optimism', // optimism + '42': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-kovan', // kovan + '56': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-bsc', // bsc + '137': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-matic', // polygon + '42161': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-arbitrum', // arbitrum + '43114': 'https://api.thegraph.com/subgraphs/name/sablierhq/sablier-avalanche' // avalanche +}; + +export const author = 'dan13ram'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + streams: { + __args: { + limit: 1000, + where: { + recipient_in: addresses.map((address) => address.toLowerCase()), + sender: options.sender.toLowerCase(), + token: options.token.toLowerCase(), + cancellation: null + } + }, + recipient: true, + deposit: true, + token: { + decimals: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.streams.__args.block = { number: snapshot }; + } + const result = await subgraphRequest( + options.subGraphURL ? options.subGraphURL : SUBGRAPH_URL[network], + params + ); + const score = Object.fromEntries( + addresses.map((address) => [getAddress(address), 0]) + ); + if (result && result.streams) { + result.streams.forEach((stream) => { + const userAddress = getAddress(stream.recipient); + const userScore = parseFloat( + formatUnits(stream.deposit, stream.token.decimals) + ); + + score[userAddress] = score[userAddress] + userScore; + }); + } + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/schema.json b/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/schema.json new file mode 100644 index 00000000..583a6148 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/deposit-in-sablier-stream/schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "sender": { + "type": "string", + "title": "Sender address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "token": { + "type": "string", + "title": "ERC20 token address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "subGraphURL": { + "type": "string", + "title": "Optional subgraph url", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"] + } + }, + "required": ["sender", "token"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/README.md b/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/README.md new file mode 100644 index 00000000..08c2686a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/README.md @@ -0,0 +1,42 @@ +# Multiple contract call strategy + +This strategy allows users to call the 'balanceOf' function across multiple DEXTF contracts (vaults) and performs summation over the results. By calling 'balanceOf', DEXTF vaults return the amount of $DEXTF staked by the user in that vault. This strategy will make a single multicall which will retrieve all users' staked balances in all of DEXTF vaults. + +## Example + +The space config will look like this: + +```JSON +{ + "strategies": [ + ["dextf-staked-in-vaults", { + // vault contracts across which token balance needs to be calculated + "contractAddresses": [ + "0x42a05787584ec09dDDe46f8CE6a715c93049ee88" + ], + // scoreMultiplier can be used to increase users' scores by a certain magnitude + "scoreMultiplier": 1, + // ABI for balanceOf method + "methodABI_1": { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + }], + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/examples.json new file mode 100644 index 00000000..bed1a9e1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "dextf-staked-in-vaults", + "params": { + "name": "DEXTF Vaults", + "contractAddresses": ["0x42a05787584ec09dDDe46f8CE6a715c93049ee88"], + "symbol": "DEXTF", + "scoreMultiplier": 1, + "methodABI": { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + } + }, + "network": "1", + "addresses": ["0x75527f00EC786dCCd6CEa82C9B90C81781C42E92"], + "snapshot": 13230652 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/index.ts new file mode 100644 index 00000000..a81f0339 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dextf-staked-in-vaults/index.ts @@ -0,0 +1,61 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'dextf'; +export const version = '1.0.0'; + +function chunk(array, chunkSize) { + const tempArray: any[] = []; + for (let i = 0, len = array.length; i < len; i += chunkSize) + tempArray.push(array.slice(i, i + chunkSize)); + return tempArray; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + let callData: [any, string, [any]][] = []; + addresses.map((userAddress: any) => { + options.contractAddresses.map((vaultAddress: any) => { + callData.push([vaultAddress, options.methodABI.name, [userAddress]]); + }); + }); + callData = [...chunk(callData, 2000)]; // chunking the callData into multiple arrays of 2000 requests + let response: any[] = []; + for (let i = 0; i < callData.length; i++) { + const tempArray = await multicall( + network, + provider, + [options.methodABI], + callData[i], + { blockTag } + ); + response.push(...tempArray); + } + if (options.contractAddresses.length > 1) { + // grouping all balances of a particular address together + const result: any = []; + response = [].concat.apply([], response); + for (let i = addresses.length; i > 0; i--) { + result.push(response.splice(0, Math.ceil(response.length / i))); + } + // performing summation over all balances of the user + response = []; + result.map((item, index) => { + let sum = 0; + result[index].map((element) => { + sum = sum + parseFloat(formatUnits(element.toString(), 18)); + }); + response.push(sum); + }); + } + return Object.fromEntries( + response.map((value, i) => [addresses[i], options.scoreMultiplier * value]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/README.md b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/README.md new file mode 100644 index 00000000..08843d32 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/README.md @@ -0,0 +1,67 @@ +# Staked Dfyn in Farming Contracts + +Allows fetching the amount of $DFYN staked in different farming contracts. By changing 'tokenAddress', 'decimals' and 'symbol' field, this strategy can also be used for other tokens. This strategy can be used for all contracts which return the amount of LP tokens in 'balanceOf' call. This strategy uses pair data from Dfyn's subgraph to calculate the amount of $DFYN present in users' LP token balance using the calcTokenBalance() function. A total of three calls have been made: 1) a subgraphRequest to fetch Dfyn's subgraph data 2) a multicall to fetch the staked LP token address corresponding to each farming contract and 3) a multicall to fetch all the users' staked LP token balance across all the farming contracts. + +The strategy will also handle the case wherein only one contractAddress is to be used, however that address should also be put in an array. + +## Example + +The space config will look like this: + +```JSON +{ + "strategies": [ + ["dfyn-staked-in-farms", { + // farming contracts across which token balance needs to be calculated + "contractAddresses": [ + "0xEdBB73C0ccD3F00cD75c2749b0df40A1BE394EE2", + "0x52b965ccd44A98A8aa064bC597C895adCD02e9BC", + "0x001A4e27CCDfe8ed6BBaFfEc9AE0985aB5542BEf", + "0xEAb0FD1FE0926E43b61612d65002Ba6320AA1080" + ], + // token address + "tokenAddress": "0xc168e40227e4ebd8c1cae80f7a55a4f0e6d66c97", + // token decimals + "decimals": 18, + // token symbol + "symbol": "DFYN", + // scoreMultiplier can be used to increase users' scores by a certain magnitude + "scoreMultiplier": 1, + // ABI for balanceOf method + "methodABI_1": { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + // ABI for stakingToken method + "methodABI_2": { + "inputs": [], + "name": "stakingToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } + }], + ] +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/examples.json new file mode 100644 index 00000000..8aa30b26 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/examples.json @@ -0,0 +1,63 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "dfyn-staked-in-farms", + "params": { + "name": "Dfyn Farms", + "contractAddresses": [ + "0xEdBB73C0ccD3F00cD75c2749b0df40A1BE394EE2", + "0x52b965ccd44A98A8aa064bC597C895adCD02e9BC", + "0x001A4e27CCDfe8ed6BBaFfEc9AE0985aB5542BEf", + "0xEAb0FD1FE0926E43b61612d65002Ba6320AA1080" + ], + "tokenAddress": "0xc168e40227e4ebd8c1cae80f7a55a4f0e6d66c97", + "decimal": 18, + "symbol": "DFYN", + "scoreMultiplier": 1, + "methodABI_1": { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + "methodABI_2": { + "inputs": [], + "name": "stakingToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } + } + }, + "network": "137", + "addresses": [ + "0x41CdE29bF9aea095fDD204D150451678b2c6A736", + "0xD7B26f34775fa41D6dCE182851642573aBF9B532", + "0xd7539FCdC0aB79a7B688b04387cb128E75cb77Dc", + "0x6E33e22f7aC5A4b58A93C7f6D8Da8b46c50A3E20", + "0xC9dA7343583fA8Bb380A6F04A208C612F86C7701", + "0x2AC89522CB415AC333E64F52a1a5693218cEBD58" + ], + "snapshot": 18342938 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/index.ts new file mode 100644 index 00000000..91c63ab5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-farms/index.ts @@ -0,0 +1,158 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { subgraphRequest } from '../../utils'; + +export const author = 'vatsalgupta13'; +export const version = '0.1.0'; + +const DFYN_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/ss-sonic/dfyn-v5'; + +const getAllLP = async (skip, stakedLP, snapshot) => { + const params = { + pairs: { + __args: { + where: { + id_in: stakedLP.map((address) => address.toLowerCase()) + }, + skip: skip, + first: 1000 + }, + id: true, + reserve0: true, + reserve1: true, + totalSupply: true, + token0: { + id: true, + symbol: true + }, + token1: { + id: true, + symbol: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.pairs.__args.block = { number: snapshot }; + } + try { + const response = await subgraphRequest(DFYN_SUBGRAPH_URL, params); + return response.pairs; + } catch (error) { + console.error(error); + } +}; + +const getLP = async (stakedLP, snapshot) => { + let lp = []; + let results = []; + do { + results = await getAllLP(lp.length, stakedLP, snapshot); + lp = lp.concat(results); + } while (results.length === 1000); + return lp; +}; + +function chunk(array, chunkSize) { + const tempArray: any[] = []; + for (let i = 0, len = array.length; i < len; i += chunkSize) + tempArray.push(array.slice(i, i + chunkSize)); + return tempArray; +} + +function calcTokenBalance( + user_lp_balance, + total_lp_supply, + total_token_reserve +) { + return total_lp_supply === 0 + ? 0 + : (total_token_reserve / total_lp_supply) * user_lp_balance; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + let callData: [any, string, [any]][] = []; + const callStakedAddress: [any, string][] = []; + options.contractAddresses.map((contractAddress: any) => { + callStakedAddress.push([contractAddress, options.methodABI_2.name]); + }); + addresses.map((userAddress: any) => { + options.contractAddresses.map((contractAddress: any) => { + callData.push([contractAddress, options.methodABI_1.name, [userAddress]]); + }); + }); + callData = [...chunk(callData, 2000)]; // chunking the callData into multiple arrays of 2000 requests + + let LP_balances: any[] = []; + for (let i = 0; i < callData.length; i++) { + const tempArray = await multicall( + network, + provider, + [options.methodABI_1], + callData[i], + { blockTag } + ); + LP_balances.push(...tempArray); + } + + let stakedLP = await multicall( + network, + provider, + [options.methodABI_2], + callStakedAddress, + { blockTag } + ); + + stakedLP = [].concat.apply([], stakedLP); + const dfyn_lp: any[] = await getLP(stakedLP, snapshot); + let temp: any = []; + if (options.contractAddresses.length > 1) { + // sorting dfyn_lp such that it aligns with users' lp balances + const itemPositions = {}; + for (let i = 0; i < stakedLP.length; i++) { + itemPositions[stakedLP[i].toLowerCase()] = i; + } + dfyn_lp.sort((a, b) => itemPositions[a.id] - itemPositions[b.id]); + + // grouping all balances of a particular address together + LP_balances = [].concat.apply([], LP_balances); + for (let i = addresses.length; i > 0; i--) { + temp.push(LP_balances.splice(0, Math.ceil(LP_balances.length / i))); + } + } else { + temp = [...LP_balances]; + } + + // finding users token balance from their lp balances + LP_balances = []; + temp.map((item, index) => { + let sum = 0; + temp[index].map((element, i) => { + element = calcTokenBalance( + parseFloat(formatUnits(element.toString(), 18)), + parseFloat(dfyn_lp[i].totalSupply), + options.tokenAddress.toLowerCase() === dfyn_lp[i].token0.id + ? parseFloat(dfyn_lp[i].reserve0) + : parseFloat(dfyn_lp[i].reserve1) + ); + sum = sum + element; + }); + LP_balances.push(sum); + }); + + return Object.fromEntries( + LP_balances.map((value, i) => [ + addresses[i], + options.scoreMultiplier * value + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/README.md b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/README.md new file mode 100644 index 00000000..9816e5e0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/README.md @@ -0,0 +1,43 @@ +# Multiple contract call strategy + +This strategy allows users to call a function like 'balanceOf' across multiple contracts and performs summation over the results. By calling 'balanceOf', Dfyn vaults return the amount of Dfyn staked by the user in that vault. This strategy will make a single multicall which will retrieve all users' staked balances in all of Dfyn's vaults. + +## Example + +The space config will look like this: + +```JSON +{ + "strategies": [ + ["dfyn-staked-in-vaults", { + // vault contracts across which token balance needs to be calculated + "contractAddresses": [ + "0x8b016E4f714451f3aFF88B82Ec9dfAe13D664d42", + "0x07E3f04903aBd6506A6E41246Da7d39dA0D6a8CA" + ], + // scoreMultiplier can be used to increase users' scores by a certain magnitude + "scoreMultiplier": 1, + // ABI for balanceOf method + "methodABI_1": { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + }], + ] +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/examples.json new file mode 100644 index 00000000..a68bb7c0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/examples.json @@ -0,0 +1,42 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "dfyn-staked-in-vaults", + "params": { + "name": "Dfyn Vaults", + "contractAddresses": [ + "0x8b016E4f714451f3aFF88B82Ec9dfAe13D664d42", + "0x07E3f04903aBd6506A6E41246Da7d39dA0D6a8CA" + ], + "symbol": "DFYN", + "scoreMultiplier": 1, + "methodABI": { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + } + }, + "network": "137", + "addresses": [ + "0x41CdE29bF9aea095fDD204D150451678b2c6A736", + "0xD7B26f34775fa41D6dCE182851642573aBF9B532" + ], + "snapshot": 18342938 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/index.ts new file mode 100644 index 00000000..54d43094 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dfyn-staked-in-vaults/index.ts @@ -0,0 +1,61 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'vatsalgupta13'; +export const version = '0.1.0'; + +function chunk(array, chunkSize) { + const tempArray: any[] = []; + for (let i = 0, len = array.length; i < len; i += chunkSize) + tempArray.push(array.slice(i, i + chunkSize)); + return tempArray; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + let callData: [any, string, [any]][] = []; + addresses.map((userAddress: any) => { + options.contractAddresses.map((vaultAddress: any) => { + callData.push([vaultAddress, options.methodABI.name, [userAddress]]); + }); + }); + callData = [...chunk(callData, 2000)]; // chunking the callData into multiple arrays of 2000 requests + let response: any[] = []; + for (let i = 0; i < callData.length; i++) { + const tempArray = await multicall( + network, + provider, + [options.methodABI], + callData[i], + { blockTag } + ); + response.push(...tempArray); + } + if (options.contractAddresses.length > 1) { + // grouping all balances of a particular address together + const result: any = []; + response = [].concat.apply([], response); + for (let i = addresses.length; i > 0; i--) { + result.push(response.splice(0, Math.ceil(response.length / i))); + } + // performing summation over all balances of the user + response = []; + result.map((item, index) => { + let sum = 0; + result[index].map((element) => { + sum = sum + parseFloat(formatUnits(element.toString(), 18)); + }); + response.push(sum); + }); + } + return Object.fromEntries( + response.map((value, i) => [addresses[i], options.scoreMultiplier * value]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dgenesis/README.md b/Implementations/API/backend/utils/snapshot/strategies/dgenesis/README.md new file mode 100644 index 00000000..9a49f4a6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dgenesis/README.md @@ -0,0 +1,3 @@ +# dGenesis Governance Strategy + +Calculates voting power based on staked and unstaked DGV tokens on the Arbitrum network. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/dgenesis/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dgenesis/examples.json new file mode 100644 index 00000000..e76348f8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dgenesis/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "dGenesis Query", + "strategy": { + "name": "dgenesis", + "params": { + "symbol": "DGV" + } + }, + "network": "42161", + "addresses": [ + "0xa52093DC50f2596abadb56dC643630E61944aBA9", + "0x987c781938d0881306069ec9e3f7e914cf23c9fe", + "0x3d9fe7337f067dd52d1dc8cb45490b1ad6c2f65b", + "0x9ee73ec148b8f68da65598ebb497f1e7e9957219", + "0xef764BAC8a438E7E498c2E5fcCf0f174c3E3F8dB" + ], + "snapshot": 3213870 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dgenesis/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dgenesis/index.ts new file mode 100644 index 00000000..bc48ea8c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dgenesis/index.ts @@ -0,0 +1,52 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; + +const DGENESIS_SUBGRAPH_URL = { + '42161': 'https://api.thegraph.com/subgraphs/name/callikai/dgenesisarbitrum' +}; + +export const author = 'callikai'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + tokenUsers: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + totalBalance: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.tokenUsers.__args.block = { number: snapshot }; + } + const result = await subgraphRequest(DGENESIS_SUBGRAPH_URL[network], params); + + const score = {}; + + if (result && result.tokenUsers) { + result.tokenUsers.map((_data) => { + const address = getAddress(_data.id); + score[address] = Number( + formatUnits(BigNumber.from(_data.totalBalance), 18) + ); + }); + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/README.md b/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/README.md new file mode 100644 index 00000000..76c03f24 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/README.md @@ -0,0 +1,14 @@ +# DIGITALAX DECO to MONA conversion + +This is the DIGITALAX DECO to MONA conversion for voting purposes + +Here is an example of parameters: + +```json +{ + "symbol": "MONA", + "address": "0x200f9621cbce6ed740071ba34fde85ee03f2e113" +} +``` + + diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/examples.json b/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/examples.json new file mode 100644 index 00000000..86fbc176 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Digitalax Deco to Mona conversion", + "strategy": { + "name": "digitalax-deco-to-mona", + "params": { + "symbol": "MONA", + "address": "0x200f9621cbce6ed740071ba34fde85ee03f2e113" + } + }, + "network": "137", + "addresses": [ + "0xd4c4f5e108d09f4383f431d143e75ecabb703f2a", + "0x0e091edbc59d7089451b19bd54bddd3576232edc", + "0x5fc1f31a084ef781b436885d10ab8c3e24a29642", + "0x5e4038eb6f1e7316a45ed569e94c8cd1706a5ec5", + "0xab48edd90bdf367d326d827758bacd2460c59d17" + ], + "snapshot": 23986420 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/index.ts b/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/index.ts new file mode 100644 index 00000000..5558c0ad --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-deco-to-mona/index.ts @@ -0,0 +1,55 @@ +import { multicall } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'onigiri-x'; +export const version = '0.1.0'; + +const abi = [ + 'function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const pairAddresses = [ + '0x7ecb3be21714114d912469810aedd34e6fc27736', + '0x3203bf44d434452b4605c7657c51bfeaf2a0847c' + ]; + const priceResponse = await multicall( + network, + provider, + abi, + pairAddresses.map((address: any) => [address, 'getReserves', []]), + { blockTag } + ); + + const priceDecoToEth = + parseFloat(priceResponse[0]._reserve1) / + parseFloat(priceResponse[0]._reserve0); + const priceEthToMona = + parseFloat(priceResponse[1]._reserve0) / + parseFloat(priceResponse[1]._reserve1); + + const erc20Balances = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + return Object.fromEntries( + addresses.map((address) => [ + address, + erc20Balances[address] * priceDecoToEth * priceEthToMona + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/README.md b/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/README.md new file mode 100644 index 00000000..98baa2ec --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/README.md @@ -0,0 +1,13 @@ +# DIGITALAX Genesis Contributions + +This is the DIGITALAX Genesis Contribution + +Here is an example of parameters: + +```json +{ + "symbol": "DXG", + "decimals": 18 +} +``` +If you use decimals: 0, it will show up in wei format on the scores. diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/examples.json b/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/examples.json new file mode 100644 index 00000000..6017a949 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Digitalax Genesis Contribution", + "strategy": { + "name": "digitalax-genesis-contribution", + "params": { + "symbol": "MONA", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x801e12b93a570930cb68ac29f3450bc5afd78350", + "0x4702baa85c236d204230a439c72625cd014b17ea", + "0xca11d10ceb098f597a0cab28117fc3465991a63c", + "0xa48f120e2a69c7848f7e841412fe494a5fddaede", + "0x294803520a1e7b671c105c8a3d94836f40b2b557", + "0xfaa1eb7e8d449a4b065f90028170108f54feb877", + "0x033cd8e9e400ac9d67375a08ff1fffb91e8100c0", + "0xd854d3800e0ce85273789a0cc70215d415a8c945", + "0x09e0ba2596677a84cc3b419c648ed42d47a42d6f" + ], + "snapshot": 13513882 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/index.ts b/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/index.ts new file mode 100644 index 00000000..1945b990 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-genesis-contribution/index.ts @@ -0,0 +1,154 @@ +import { subgraphRequest, multicall } from '../../utils'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import fetch from 'cross-fetch'; + +export const author = 'onigiri-x'; +export const version = '0.1.0'; + +const abiStaking = [ + 'function getGenesisContribution(uint256 _tokenId) external view returns (uint256)' +]; + +const DIGITALAX_SUBGRAPH = + 'https://api.thegraph.com/subgraphs/name/digitalax/digitalax'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const block = await provider.getBlock(blockTag); + + const genesisStaking = '0xa202d5b0892f2981ba86c981884ceba49b8ae096'; + + // Set up the GraphQL parameters and necessary variables + const holderParams = { + digitalaxGenesisNFTs: { + __args: { + where: { + owner_in: addresses + }, + first: 1000 + }, + id: true, + owner: true + } + }; + + const stakerParams = { + digitalaxGenesisStakedTokens: { + __args: { + where: { + staker_in: addresses + }, + first: 1000 + }, + id: true, + staker: true + } + }; + + // Query subgraph for the holders and the stakers based on input addresses + const resultOwners = await subgraphRequest(DIGITALAX_SUBGRAPH, holderParams); + const resultStakers = await subgraphRequest(DIGITALAX_SUBGRAPH, stakerParams); + + const tokenIdsForAddress: Record = {}; + let tokenIdsToCheck: number[] = []; + const tokenIdsWithContribution: Record = {}; + + // For each nft in user wallets, take note of the token ids + resultOwners.digitalaxGenesisNFTs.forEach((nft) => { + if (!tokenIdsForAddress[nft.owner]) { + tokenIdsForAddress[nft.owner] = [nft.id]; + } else { + tokenIdsForAddress[nft.owner] = tokenIdsForAddress[nft.owner].concat( + nft.id + ); + } + tokenIdsToCheck.push(nft.id); + // Make double sure no duplicates + tokenIdsForAddress[nft.owner] = [...new Set(tokenIdsForAddress[nft.owner])]; + }); + + // For each nft in staking contract, take note of the token ids + resultStakers.digitalaxGenesisStakedTokens.forEach((nft) => { + if (!tokenIdsForAddress[nft.staker]) { + tokenIdsForAddress[nft.staker] = [nft.id]; + } else { + tokenIdsForAddress[nft.staker] = tokenIdsForAddress[nft.staker].concat( + nft.id + ); + } + tokenIdsToCheck.push(nft.id); + // Make double sure no duplicates + tokenIdsForAddress[nft.staker] = [ + ...new Set(tokenIdsForAddress[nft.staker]) + ]; + }); + + // Make sure no duplicates + tokenIdsToCheck = [...new Set(tokenIdsToCheck)]; + + // Get the genesis contribution for all the token ID's we recorded from the staking contract itself + const result2 = await multicall( + network, + provider, + abiStaking, + tokenIdsToCheck.map((tokenId: any) => [ + genesisStaking, + 'getGenesisContribution', + [tokenId] + ]), + { blockTag } + ); + result2.forEach((x, i) => { + tokenIdsWithContribution[tokenIdsToCheck[i]] = x; + }); + + const monaPriceConversion = await getConversionMonaPerETH(block); + + // Set up a record list for all the genesis tokens with their summed up contribution + const genesisRecord: Record = {}; + addresses.forEach((addr) => { + const tokenIds = tokenIdsForAddress[addr]; + if (tokenIds) { + const decimalContributions = tokenIds.map((x) => { + return parseFloat( + formatUnits(tokenIdsWithContribution[x][0], options.decimals) + ); + }); + genesisRecord[addr] = + decimalContributions.reduce((a, b) => a + b, 0) * monaPriceConversion; + } else { + genesisRecord[addr] = 0; + } + }); + + return genesisRecord; +} + +async function getConversionMonaPerETH(block) { + // Find the coingecko for monavale to eth + const coingeckoApiURL = `https://api.coingecko.com/api/v3/coins/monavale/market_chart/range?vs_currency=eth&from=${ + block.timestamp - 100000 + }&to=${block.timestamp}`; + const coingeckoData = await fetch(coingeckoApiURL) + .then(async (r) => { + const json = await r.json(); + return json; + }) + .catch((e) => { + console.error(e); + throw new Error( + 'Strategy digitalax-genesis-contribution: coingecko api failed' + ); + }); + + const priceConversion = parseFloat(coingeckoData.prices?.pop()[1]); + return 1 / priceConversion; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers-matic/examples.json b/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers-matic/examples.json new file mode 100644 index 00000000..c681e1ca --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers-matic/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "digitalax-lp-stakers-matic", + "params": { + "address": "0xF795c2abB0E7A56A0C011993C37A51626756B4BD", + "symbol": "MONA" + } + }, + "network": "137", + "addresses": [ + "0xb9f1942edb0e0b9fdf77cb85397e75437aa37999", + "0xd4c4f5e108d09f4383f431d143e75ecabb703f2a", + "0x2f55acce6958d390dc32606230b6bda2dfec2102" + ], + "snapshot": 22757476 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers-matic/index.ts b/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers-matic/index.ts new file mode 100644 index 00000000..29d07dcf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers-matic/index.ts @@ -0,0 +1,68 @@ +import { multicall } from '../../utils'; +const bignumber_1 = require('@ethersproject/bignumber'); +const quickswap_1 = require('../digitalax-mona-quickswap'); +const bn = (num) => { + return bignumber_1.BigNumber.from(num.toString()); +}; + +export const author = 'onigiri-x'; +export const version = '0.1.0'; + +const abiStaking = [ + 'function getStakedLPBalance(address _user) external view returns (uint256)', + 'function stakedLPTotalForPool() public view returns (uint256)' +]; + +const STAKING_ADDRESS = '0xF795c2abB0E7A56A0C011993C37A51626756B4BD'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const totalPoolShares = await multicall( + network, + _provider, + abiStaking, + [[STAKING_ADDRESS.toLowerCase(), 'stakedLPTotalForPool']], + { blockTag } + ); + + const totalStakedLP = bn(totalPoolShares[0]); + + const stakeResponse = await multicall( + network, + _provider, + abiStaking, + addresses.map((address: any) => [ + STAKING_ADDRESS.toLowerCase(), + 'getStakedLPBalance', + [address] + ]), + { blockTag } + ); + + // Get just the LP MONA equivalent for just the LP Staking contract + const uniswap = await quickswap_1.strategy( + _space, + network, + _provider, + [STAKING_ADDRESS], + { strategyType: 'monausd', ...options }, + snapshot + ); + + // The score is LP staking contract uniswap MONA value * (user LP staked / total LP staked) + return Object.fromEntries( + stakeResponse.map((value, i) => [ + addresses[i], + (parseFloat(bn(value)) * uniswap[STAKING_ADDRESS]) / + parseFloat(totalStakedLP) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers/examples.json b/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers/examples.json new file mode 100644 index 00000000..6040e420 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "digitalax-lp-stakers", + "params": { + "address": "0x275f5Ad03be0Fa221B4C6649B8AeE09a42D9412A", + "symbol": "MONA" + } + }, + "network": "1", + "addresses": [ + "0xd9854F3ab29A69c89Cc57fb675C48Ecd69b61Aec", + "0xa59ba4eebfbd28e5a61dea4b9bcf358320e8d53b", + "0xaa3e5ee4fdc831e5274fe7836c95d670dc2502e6", + "0xea41cd3f972db6237ffa2918df9199b547172420", + "0xbfd5493b94ead64d3dcada5700dbee9409acc345" + ], + "snapshot": 13526042 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers/index.ts b/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers/index.ts new file mode 100644 index 00000000..0ba69b07 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-lp-stakers/index.ts @@ -0,0 +1,68 @@ +import { multicall } from '../../utils'; +const bignumber_1 = require('@ethersproject/bignumber'); +const uniswap_1 = require('../uniswap'); +const bn = (num) => { + return bignumber_1.BigNumber.from(num.toString()); +}; + +export const author = 'onigiri-x'; +export const version = '0.1.0'; + +const abiStaking = [ + 'function getStakedBalance(address _user) external view returns (uint256)', + 'function stakedLPTotal() public view returns (uint256)' +]; + +const STAKING_ADDRESS = '0xA0d1345244C89b5574ba50bd6530d4EBd237e826'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const totalPoolShares = await multicall( + network, + _provider, + abiStaking, + [[STAKING_ADDRESS.toLowerCase(), 'stakedLPTotal']], + { blockTag } + ); + + const totalStakedLP = bn(totalPoolShares[0]); + + const stakeResponse = await multicall( + network, + _provider, + abiStaking, + addresses.map((address: any) => [ + STAKING_ADDRESS.toLowerCase(), + 'getStakedBalance', + [address] + ]), + { blockTag } + ); + + // Get just the LP MONA equivalent for just the LP Staking contract + const uniswap = await uniswap_1.strategy( + _space, + network, + _provider, + [STAKING_ADDRESS], + options, + snapshot + ); + + // The score is LP staking contract uniswap MONA value * (user LP staked / total LP staked) + return Object.fromEntries( + stakeResponse.map((value, i) => [ + addresses[i], + (parseFloat(bn(value)) * uniswap[STAKING_ADDRESS]) / + parseFloat(totalStakedLP) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/README.md new file mode 100644 index 00000000..52366e25 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/README.md @@ -0,0 +1,21 @@ +# DIGITALAX MATIC Stakers on Quickswap + +This is the DIGITALAX MONA value of LP held for stakers on quickswap + +Here is an example of parameters: + +```json +{ + "symbol": "MONA", + "address": "0x82f1676ef298db09da935f4cb7bd3c44fb73d83a" +} +``` +There are two pools that are currently supported. + +Mona Quick Pool: +``` "address": "0x82f1676ef298db09da935f4cb7bd3c44fb73d83a"``` + +Mona USDT Pool: +``` "address": "0x856ad56defbb685db8392d9e54441df609bc5ce1"``` + + diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/examples.json new file mode 100644 index 00000000..64b9eaba --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Digitalax MONA LP on Quickswap", + "strategy": { + "name": "digitalax-mona-quickswap", + "params": { + "symbol": "MONA", + "address": "0x82f1676ef298db09da935f4cb7bd3c44fb73d83a" + } + }, + "network": "137", + "addresses": [ + "0x0dcdc99c8f20c8825cc906dbbafe72f15e6ebcf4", + "0xb98f29ffff030efd58e75a625d1f6dca58a49bfa", + "0x806ef73ac66c4b669f2ba1403c770a2c503f6764", + "0xaa3e5ee4fdc831e5274fe7836c95d670dc2502e6", + "0x4879712c5d1a98c0b88fb700daff5c65d12fd729" + ], + "snapshot": 20828135 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/index.ts new file mode 100644 index 00000000..2aabf90f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-quickswap/index.ts @@ -0,0 +1,56 @@ +import { subgraphRequest } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'onigiri-x'; +export const version = '0.1.0'; + +const QUICKSWAP_SUBGRAPH = 'https://api.fura.org/subgraphs/name/quickswap'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + // Set up the GraphQL parameters and necessary variables + if (options.strategyType) { + if (options.strategyType == 'monausd') { + options.address = '0x856ad56defbb685db8392d9e54441df609bc5ce1'; + } + } + const holderParams = { + pair: { + __args: { + id: options.address + }, + reserve0: true, + totalSupply: true + } + }; + + // Query subgraph for the holders and the stakers based on input addresses + const monaReserve = await subgraphRequest(QUICKSWAP_SUBGRAPH, holderParams); + + const erc20Balances = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const totalLPSupply = + monaReserve.pair.totalSupply < 0 + ? monaReserve.pair.totalSupply * -1 + : monaReserve.pair.totalSupply; + + return Object.fromEntries( + addresses.map((address) => [ + address, + (erc20Balances[address] * monaReserve.pair.reserve0) / totalLPSupply + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-stakers-matic/examples.json b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-stakers-matic/examples.json new file mode 100644 index 00000000..439b6e07 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-stakers-matic/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "digitalax-mona-stakers-matic", + "params": { + "address": "0xF795c2abB0E7A56A0C011993C37A51626756B4BD", + "symbol": "MONA" + } + }, + "network": "137", + "addresses": [ + "0xba658ae386566745ee8b661e148460b0ece00144", + "0xbff466b48073bb1ffda6c644f4c0cdb61287c75f", + "0x5359ed6ef235be8ef0f25ad0a5c0b521bcf8d663", + "0x71284ddcaba20f2386c09b9ccd3ad1d369bf4e30", + "0x03ea64a5ced21e2cccfaea0aec283d0a0afe9424" + ], + "snapshot": 22757476 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-stakers-matic/index.ts b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-stakers-matic/index.ts new file mode 100644 index 00000000..bab855a8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/digitalax-mona-stakers-matic/index.ts @@ -0,0 +1,46 @@ +import { multicall } from '../../utils'; +const bignumber_1 = require('@ethersproject/bignumber'); +const bn = (num) => { + return bignumber_1.BigNumber.from(num.toString()); +}; + +export const author = 'onigiri-x'; +export const version = '0.1.0'; + +const abiStaking = [ + 'function getStakedBalance(address _user) external view returns (uint256)' +]; + +const STAKING_ADDRESS = '0xF795c2abB0E7A56A0C011993C37A51626756B4BD'; +export const ETH_IN_WEI = 1000000000000000000; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakeResponse = await multicall( + network, + _provider, + abiStaking, + addresses.map((address: any) => [ + STAKING_ADDRESS.toLowerCase(), + 'getStakedBalance', + [address] + ]), + { blockTag } + ); + + // The score is LP staking contract uniswap MONA value * (user LP staked / total LP staked) + return Object.fromEntries( + stakeResponse.map((value, i) => [ + addresses[i], + parseFloat(bn(value)) / ETH_IN_WEI + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dittomoney/README.md b/Implementations/API/backend/utils/snapshot/strategies/dittomoney/README.md new file mode 100644 index 00000000..6ba6a318 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dittomoney/README.md @@ -0,0 +1,35 @@ +# Contract call strategy + +This strategy checks the amount of LP tokens staked and calculates the amount of DITTO associated with the stake. voting rights are the same for those who stake 1 DITTO and for those holding 1 DITTO + +## Params + +**Pancake** :[0x470BC451810B312BBb1256f96B0895D95eA659B1](https://bscscan.com/address/0x470BC451810B312BBb1256f96B0895D95eA659B1) + +**sharePool**: [0x27Da7Bc5CcB7c31baaeEA8a04CC8Bf0085017208](https://bscscan.com/address/0x27Da7Bc5CcB7c31baaeEA8a04CC8Bf0085017208) + +**Token[Ditto]**: [0x233d91a0713155003fc4dce0afa871b508b3b715](https://bscscan.com/address/0x233d91a0713155003fc4dce0afa871b508b3b715) + +## Examples + +```json +[ + { + "name": "Ditto.money", + "strategy": { + "name": "dittomoney", + "params": { + "pancake": "0x470BC451810B312BBb1256f96B0895D95eA659B1", + "sharePool": "0x27Da7Bc5CcB7c31baaeEA8a04CC8Bf0085017208", + "token": "0x233d91a0713155003fc4dce0afa871b508b3b715", + "decimals": 18, + "minBalance": 1 + } + }, + "network": "56", + "addresses": [ + ], + "snapshot": 11605878 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/dittomoney/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dittomoney/examples.json new file mode 100644 index 00000000..3735f54e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dittomoney/examples.json @@ -0,0 +1,29 @@ +[ + { + "name": "Ditto.money", + "strategy": { + "name": "dittomoney", + "params": { + "autofarm": "0x0895196562C7868C5Be92459FaE7f877ED450452", + "jetfuel": "0x3d6D415be40159f207540f95E398F29a7173dC20", + "cafeswap": "0xc772955c33088a97D56d0BBf473d05267bC4feBB", + "pancake": "0x470BC451810B312BBb1256f96B0895D95eA659B1", + "sharePool": "0x27Da7Bc5CcB7c31baaeEA8a04CC8Bf0085017208", + "token": "0x233d91a0713155003fc4dce0afa871b508b3b715", + "symbol": "DITTO", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0x614812d04526C0C882A6cB993a135fcD559F33F9", + "0xff91AcCd5277cc3de3E73DD77e3102b1980e439e", + "0x05ff2b0db69458a0750badebc4f9e13add608c7f", + "0xd1a8Dd23e356B9fAE27dF5DeF9ea025A602EC81e", + "0x48f7cb174a2333de834452ad240ce8d3d827dc55", + "0x0d3cae1ff719d6a2ac0fcd7cd22f599cac64b6ec", + "0xE0e710A907a8E44c078e6212Efa336C1191F4CD1" + ], + "snapshot": 5248103 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dittomoney/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dittomoney/index.ts new file mode 100644 index 00000000..c936651b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dittomoney/index.ts @@ -0,0 +1,139 @@ +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'codingsh'; +export const version = '0.1.1'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'addr', + type: 'address' + } + ], + name: 'totalStakedFor', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'getPricePerFullShare', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + multi.call('cafeswapBalance', options.token, 'balanceOf', [options.cafeswap]); + multi.call('jetfuelBalance', options.token, 'balanceOf', [options.jetfuel]); + multi.call('jetfuelTotalSupply', options.jetfuel, 'totalSupply'); + multi.call('autofarmBalance', options.token, 'balanceOf', [options.autofarm]); + multi.call('pancakeBalance', options.token, 'balanceOf', [options.pancake]); + multi.call('pancakeTotalSupply', options.pancake, 'totalSupply'); + multi.call('pricePerFullShare', options.jetfuel, 'getPricePerFullShare'); + addresses.forEach((address) => { + multi.call( + `scores.${address}.totalStaked`, + options.sharePool, + 'totalStakedFor', + [address] + ); + multi.call(`scores.${address}.pancake`, options.pancake, 'balanceOf', [ + address + ]); + multi.call(`scores.${address}.jetfuel`, options.jetfuel, 'balanceOf', [ + address + ]); + multi.call(`scores.${address}.balance`, options.token, 'balanceOf', [ + address + ]); + }); + + const result = await multi.execute(); + const dittoPerLP = result.pancakeBalance; + const autofarmBalance = result.autofarmBalance; + const cafeswapBalance = result.cafeswapBalance; + const pricePerFullShare = result.pricePerFullShare; + + return Object.fromEntries( + Array(addresses.length) + .fill('') + .map((_, i) => { + const lpBalances = result.scores[addresses[i]].pancake; + const lpBalancesJetFuel = result.scores[addresses[i]].jetfuel; + const stakedLpBalances = result.scores[addresses[i]].totalStaked; + const tokenBalances = result.scores[addresses[i]].balance; + const lpBalance = lpBalances.add(stakedLpBalances); + const dittoLpBalance = lpBalance + .add(tokenBalances) + .mul(dittoPerLP) + .div(parseUnits('1', 9)); + const dittoFuelBalance = lpBalancesJetFuel.mul(pricePerFullShare); + return [ + addresses[i], + parseFloat( + formatUnits( + dittoLpBalance + .add(dittoFuelBalance) + .add(autofarmBalance) + .add(cafeswapBalance), + options.decimals + ) + ) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/README.md b/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/README.md new file mode 100644 index 00000000..2ff9907b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/README.md @@ -0,0 +1,5 @@ +# DU + +This strategy pulls ownership of BOOM, Dogs, Steaks, and Puppies. It checks for ownership both in-wallet and stake via direct contract calls and subgraph info. + +It weights BOOM voting as 50% of all voting value, regardless of current BOOM supply. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/examples.json new file mode 100644 index 00000000..0c70933e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/examples.json @@ -0,0 +1,33 @@ +[ + { + "name": "DU Global Voting Query", + "strategy": { + "name": "dogs-unchained", + "params": { + "boom": "0x1f17996beacc0803841d5f8cecc753e688fb5084", + "dogs": "0x9c0ffc9088abeb2ea220d642218874639229fa7a", + "steaks": "0xcab65c60d9dc47e1d450c7e9074f73f1ff75f181", + "puppies": "0x948bc723d7e33575427270ac4f26c73b8ba938aa", + "staking": "0xD742193C84062c1e0488545FB91A32D220Ec6c76", + "decimals": 18 + } + }, + "network": "1", + "snapshot": 14616017, + "addresses": [ + "0x99484019bdcd444af57a365bea0188f937fe751d", + "0x34dde35b7a88a1634c5e5e7ab8f18e67974997c8", + "0x8a91de08552c5b16930a25b3d88af17dfa44ba0c", + "0x625a840144253a7dcb638de03454d6f281616ed3", + "0x6795474c42d4562e7242b042d4b91b86a3e96889", + "0x74c75f96f183e4553df09151de69b39c63a95fef", + "0x9002b80e3fd2cfe9bb04d89438c55a0d08dd218c", + "0xdcf3d0d56804bc4c04d034e323aa4ec77015e76c", + "0xfd9395acb188ae0166d8ed970e1117410b67ad6d", + "0x18b4ddab23483770af0b105e21145dfdaa047ab5", + "0x1cc0c0921463ec1fa70b883df209d753f55e07e3", + "0x30d02630998d983e4c3e07aa5bff2ee200c5ab64", + "0x439c5deac76813e49a97362bb52ccdf649ea0511" + ] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/index.ts new file mode 100644 index 00000000..e26b7cb9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dogs-unchained/index.ts @@ -0,0 +1,141 @@ +import { getAddress } from '@ethersproject/address'; +import { formatUnits } from '@ethersproject/units'; +import { multicall, subgraphRequest } from '../../utils'; + +export const author = 'dogsunchained'; +export const version = '0.1.1'; + +const abi721or20 = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; + +const SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/dogsunchained/dogs-unchained'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const NFT_VALUE = 1; + + const calls721: any[] = []; + const calls20: any[] = []; + addresses.forEach((address: any) => { + calls721.push([options.steaks, 'balanceOf', [address]]); + calls20.push([options.boom, 'balanceOf', [address]]); + }); + calls721.push([options.steaks, 'totalSupply', []]); + calls20.push([options.boom, 'totalSupply', []]); + + const response721 = await multicall(network, provider, abi721or20, calls721, { + blockTag + }); + const response20 = await multicall(network, provider, abi721or20, calls20, { + blockTag + }); + let totalNFTs = parseInt(response721.pop().toString()) * NFT_VALUE; + const totalBOOM = parseFloat(formatUnits(response20.pop().toString(), 18)); + + const paginate = (skip, reverse = false, where = {}) => { + const req = { + __args: { + first: 1000, + skip, + orderBy: 'id', + orderDirection: reverse ? 'desc' : 'asc', + where: where + }, + id: true, + owner: { + id: true + } + }; + if (typeof snapshot === 'number') + req.__args['block'] = { number: snapshot }; + return req; + }; + + const dogParams = { + 'd1:dogs': paginate(0, false, { burned: false }), + 'd2:dogs': paginate(1000, false, { burned: false }), + 'd3:dogs': paginate(2000, false, { burned: false }), + 'd4:dogs': paginate(3000, false, { burned: false }), + 'd5:dogs': paginate(4000, false, { burned: false }), + 'd6:dogs': paginate(5000, false, { burned: false }) + }; + + const dogsResult = await subgraphRequest(SUBGRAPH_URL, dogParams); + + const puppyParams = { + 'p1:puppies': paginate(0), + 'p2:puppies': paginate(1000), + 'p3:puppies': paginate(2000), + 'p4:puppies': paginate(3000), + 'p5:puppies': paginate(4000), + 'p6:puppies': paginate(5000) + }; + + const puppiesResult = await subgraphRequest(SUBGRAPH_URL, puppyParams); + + const stakeParams = { + 'p1:stakes': paginate(0), + 'p2:stakes': paginate(1000) + }; + + const stakesResult = await subgraphRequest(SUBGRAPH_URL, stakeParams); + + const merged = {}; + + response721.map((value: any, i: number) => { + const address = getAddress(calls721[i][2][0]); + if (address == options.staking) return; + merged[address] = (merged[address] || 0) as number; + merged[address] += parseFloat(formatUnits(value.toString(), 0)) * NFT_VALUE; + }); + + for (const key in dogsResult) { + dogsResult[key].map((value: any) => { + if (!value.owner) return; + const address = getAddress(value.owner.id); + merged[address] = (merged[address] || 0) as number; + merged[address] += NFT_VALUE; + totalNFTs += NFT_VALUE; + }); + } + + for (const key in puppiesResult) { + puppiesResult[key].map((value: any) => { + if (!value.owner) return; + const address = getAddress(value.owner.id); + merged[address] = (merged[address] || 0) as number; + merged[address] += NFT_VALUE; + totalNFTs += NFT_VALUE; + }); + } + + for (const key in stakesResult) { + stakesResult[key].map((value: any) => { + if (!value.owner) return; + const address = getAddress(value.owner.id); + merged[address] = (merged[address] || 0) as number; + merged[address] += NFT_VALUE; + }); + } + + const BOOM_TO_NFT = totalNFTs / totalBOOM; + response20.map((value: any, i: number) => { + const address = getAddress(calls20[i][2][0]); + merged[address] = (merged[address] || 0) as number; + merged[address] += + parseFloat(formatUnits(value.toString(), 18)) * BOOM_TO_NFT; + }); + + return merged; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dopamine/README.md b/Implementations/API/backend/utils/snapshot/strategies/dopamine/README.md new file mode 100644 index 00000000..87262fd4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dopamine/README.md @@ -0,0 +1,14 @@ +# dopamine + +Combines veDope governance token holders and NFT holders, returning a combined voting power as a percentage of the total. + +Here is an example of parameters: + +```json +{ + "erc20Address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "erc721Address": "0x96b0f2cf48ad9ab21bb7a8a052a3d8391ee64798", + "nftMultiplier": 10000, + "decimals": 4 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/dopamine/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dopamine/examples.json new file mode 100644 index 00000000..43038956 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dopamine/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "dopamine", + "params": { + "tokenAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", + "nftAddress": "0x96b0f2cf48ad9ab21bb7a8a052a3d8391ee64798", + "nftMultiplier": 10000, + "decimals": 8 + } + }, + "network": "1", + "addresses": [ + "0xd99c7975ef9b339d93fec21a4ab24a567d686d73", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 15092760 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dopamine/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dopamine/index.ts new file mode 100644 index 00000000..9fe5ef5e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dopamine/index.ts @@ -0,0 +1,86 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, call } from '../../utils'; + +export const author = 'crypto-dump'; +export const version = '0.1.0'; + +const nftContractAbi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; + +const tokenContractAbi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)', + 'function decimals() external view returns (uint256)' +]; + +interface StrategyOptions { + decimals: number; + tokenAddress: string; + nftAddress: string; + nftMultiplier: number; +} + +type MultiCallResult = Record; + +export async function strategy( + space, + network, + provider, + addresses, + options: StrategyOptions, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const callTokenDecimal = () => { + return call(provider, tokenContractAbi, [ + options.tokenAddress, + 'decimals', + [] + ]); + }; + + const makeMulticaller = (abi, contractAddress) => { + const multiCaller = new Multicaller(network, provider, abi, { + blockTag + }); + addresses.forEach((address) => + multiCaller.call(address, contractAddress, 'balanceOf', [address]) + ); + return multiCaller; + }; + + const erc20Multi = makeMulticaller(tokenContractAbi, options.tokenAddress); + const erc721Multi = makeMulticaller(nftContractAbi, options.nftAddress); + + const [tokenDecimal, tokenResults, nftResults]: [ + BigNumber, + MultiCallResult, + MultiCallResult + ] = await Promise.all([ + callTokenDecimal(), + erc20Multi.execute(), + erc721Multi.execute() + ]); + + const scores: Record = {}; + + for (const address of addresses) { + const tokenScore = BigNumber.from(tokenResults[address] || 0); + + const nftScore = BigNumber.from(nftResults[address] || 0) + .mul(options.nftMultiplier) + .mul(BigNumber.from(10).pow(tokenDecimal)); + scores[address] = tokenScore.add(nftScore); + } + + return Object.fromEntries( + Object.entries(scores).map(([address, score]) => [ + address, + parseFloat(formatUnits(score, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dopamine/schema.json b/Implementations/API/backend/utils/snapshot/strategies/dopamine/schema.json new file mode 100644 index 00000000..f09f221c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dopamine/schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Dopamine", + "type": "object", + "properties": { + "nftMultiplier": { + "type": "number", + "title": "Symbol", + "examples": ["e.g. 10000"] + }, + "tokenAddress": { + "type": "string", + "title": "ERC20 Token Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "nftAddress": { + "type": "string", + "title": "ERC721 NFT Token Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 4"] + } + }, + "required": ["tokenAddress", "nftAddress", "nftMultiplier", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/README.md b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/README.md new file mode 100644 index 00000000..a210779a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/README.md @@ -0,0 +1,15 @@ +# dps-nft-strategy-nova + +This is a strategy similar with ERC721, which it calculates the voting power as the balances of the voters for a specific ERC721 token, but this takes into account locked NFTs +and claimed NFTs. It is a strategy using for a game, where users can lock their NFTs in order to achive different things. + +Here is an example for calculating voting power: + +Bob balance: 40 NFTs + +Bob locked NFTs: 7 + +Bob claimed NFTs: 3 + +=> Bob voting power: 40 + 7 - 3 = 44 + diff --git a/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/examples.json new file mode 100644 index 00000000..f29dafed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "DPS NFTs query Nova", + "strategy": { + "name": "dps-nft-strategy-nova", + "params": { + "symbol": "DPS" + } + }, + "network": "42170", + "addresses": [ + "0x926ce67776a621f64cce3bfdd8d7dcc627b5134e", + "0x5114e0a7a2fc85685bc114477d7c3035d47daf8b", + "0x73b0d4b2507f867c3ecf567128a2265b084d918c", + "0x8c6fdbca2070590d7f2ce5b2c1ed1c305d968e5d", + "0x899831d937937d011305e73ee782cce0455df15a" + ], + "snapshot": 8703862 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/index.ts new file mode 100644 index 00000000..2bef9ff8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/index.ts @@ -0,0 +1,88 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'andreibadea20'; +export const version = '0.1.0'; + +const DPS_SUBGRAPH_URL_NOVA = { + '42170': + 'https://api.goldsky.com/api/public/project_clg4w9cwqdk8c3rz73mqr0z91/subgraphs/voting-subgraph/1.0.0/gn' +}; + +const PAGE_SIZE = 1000; + +const params_nova = { + holders: { + __args: { + block: { number: 927357 }, + first: PAGE_SIZE, + skip: 0 + }, + id: true, + numberOfDPSOwned: true, + listOfDPSLocked: { + tokenId: true + }, + listOfDPSReturned: { + tokenId: true + } + } +}; + +export async function strategy( + space: any, + network: string | number, + provider: any, + addresses: any, + options: any, + snapshot: string +) { + //calculate score for moonbeam + const score = {}; + + // calculate score for nova + if (snapshot !== 'latest') { + // @ts-ignore + params_nova.holders.__args.block = { number: snapshot }; + } + + // eslint-disable-next-line prefer-const + let page_nova = 0; + + while (page_nova !== -1) { + params_nova.holders.__args.skip = page_nova * PAGE_SIZE; + + const result = await subgraphRequest( + DPS_SUBGRAPH_URL_NOVA[network], + params_nova + ); + + if (result && result.holders) { + result.holders.forEach( + (holder: { + id: string; + numberOfDPSOwned: string; + listOfDPSLocked: string | any[]; + listOfDPSReturned: string | any[]; + }) => { + const userAddress = getAddress(holder.id); + + let userScore = Number(holder.numberOfDPSOwned); + + const lockedNFTs = holder.listOfDPSLocked.length; + const claimedNFTs = holder.listOfDPSReturned.length; + + userScore = userScore + lockedNFTs - claimedNFTs; + + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = userScore; + } + ); + page_nova = result.holders.length < PAGE_SIZE ? -1 : page_nova + 1; + } else { + page_nova = -1; + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/schema.json b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/schema.json new file mode 100644 index 00000000..09d5ee24 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy-nova/schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "symbol": { + "title": "DPS" + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/README.md b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/README.md new file mode 100644 index 00000000..8a44292f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/README.md @@ -0,0 +1,15 @@ +# dps-nft-strategy + +This is a strategy similar with ERC721, which it calculates the voting power as the balances * 3 of the voters for a specific ERC721 token, but this takes into account locked NFTs +and claimed NFTs. It is a strategy using for a game, where users can lock their NFTs in order to achive different things. + +Here is an example for calculating voting power: + +Bob balance: 40 NFTs + +Bob locked NFTs: 7 + +Bob claimed NFTs: 3 + +=> Bob voting power: (40 + 7 - 3) * 3 = 44 * 3 + diff --git a/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/examples.json new file mode 100644 index 00000000..0425e854 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "DPS NFTs query", + "strategy": { + "name": "dps-nft-strategy", + "params": { + "symbol": "DPS" + } + }, + "network": "1284", + "addresses": [ + "0x926ce67776a621f64cce3bfdd8d7dcc627b5134e", + "0x5114e0a7a2fc85685bc114477d7c3035d47daf8b", + "0x73b0d4b2507f867c3ecf567128a2265b084d918c", + "0x8c6fdbca2070590d7f2ce5b2c1ed1c305d968e5d", + "0x899831d937937d011305e73ee782cce0455df15a" + ], + "snapshot": 3516443 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/index.ts new file mode 100644 index 00000000..6eda761e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/index.ts @@ -0,0 +1,84 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'andreibadea20'; +export const version = '0.3.1'; + +const DPS_SUBGRAPH_URL_MOONBEAM = { + '1284': + 'https://api.thegraph.com/subgraphs/name/andreibadea20/dps-holders-moonbeam' +}; + +const PAGE_SIZE = 1000; + +const params = { + holders: { + __args: { + block: { number: 2847359 }, + first: PAGE_SIZE, + skip: 0 + }, + id: true, + listOfDPSOwned: true, + listOfDPSLocked: { + tokenId: true + }, + listOfDPSReturned: { + tokenId: true + } + } +}; + +export async function strategy( + space: any, + network: string | number, + provider: any, + addresses: any, + options: any, + snapshot: string +) { + if (snapshot !== 'latest') { + // @ts-ignore + params.holders.__args.block = { number: snapshot }; + } + + const score = {}; + let page = 0; + + while (page !== -1) { + params.holders.__args.skip = page * PAGE_SIZE; + + const result = await subgraphRequest( + DPS_SUBGRAPH_URL_MOONBEAM[network], + params + ); + + if (result && result.holders) { + result.holders.forEach( + (holder: { + id: string; + listOfDPSOwned: string; + listOfDPSLocked: string | any[]; + listOfDPSReturned: string | any[]; + }) => { + const userAddress = getAddress(holder.id); + + let userScore = holder.listOfDPSOwned.length; + + const lockedNFTs = holder.listOfDPSLocked.length; + const claimedNFTs = holder.listOfDPSReturned.length; + + userScore = userScore + lockedNFTs - claimedNFTs; + + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = 3 * userScore; + } + ); + page = result.holders.length < PAGE_SIZE ? -1 : page + 1; + } else { + page = -1; + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/schema.json b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/schema.json new file mode 100644 index 00000000..09d5ee24 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dps-nft-strategy/schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "symbol": { + "title": "DPS" + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/README.md b/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/README.md new file mode 100644 index 00000000..ec872419 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/README.md @@ -0,0 +1,15 @@ +# dsla + +DSLA's custom snapshot strategy, calculates voting score based of the staked balances of the voters for users/provider pools of Staking SLA. + +Here is an example of parameters: + +```json +{ + "LP_TOKEN": "0xAC104C0438A7bb15C714503537c6FA271FDB284E", + "SP_TOKEN": "0xcf4ea46eba95fe3643b6c954d29516d7376913dc", + "DSLA": "0x3aFfCCa64c2A6f4e3B6Bd9c64CD2C969EFd1ECBe", + "StakingSLA": "0x091ee4d4D8FfD00698c003C21F1ba69EA6310241", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/examples.json b/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/examples.json new file mode 100644 index 00000000..bdf26712 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/examples.json @@ -0,0 +1,37 @@ +[ + { + "name": "Example query of DSLA Staking", + "strategy": { + "name": "dsla-parametric-staking-service-credits", + "params": { + "xDSLA": "0xcf4ea46eba95fe3643b6c954d29516d7376913dc", + "xDSLA_LP": "0xAC104C0438A7bb15C714503537c6FA271FDB284E", + "DSLA": "0x3aFfCCa64c2A6f4e3B6Bd9c64CD2C969EFd1ECBe", + "StakingSLA": "0x091ee4d4D8FfD00698c003C21F1ba69EA6310241", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x8b0049beb72893c290a026398571a26ea4a03892", + "0x4d5c314adac838d1906992d83894fe47d6931165", + "0xe7a3b897fa52fe3e7f7d0e9803bea00fab78d12f", + "0xb6d568233e81442ded126fad8e8230ed02b6be3e", + "0x5d64d4c35345e9588f3f2233cf6e93978763012c", + "0x9e1b10bf75f339805ed3b266070001100bf87a85", + "0x57a43c9bc2b4d4447bfc1cf4e050bc6dcb72d753", + "0xb44b476a7f6a83b5e0559433b0d3ad374a0c1682", + "0x4baac6507bdcb0c9d059284c539bcf35d433d22e", + "0x66fc061aa4d8d3809876f5a6ddb3506d2d16168f", + "0x443a391e33241798d69295149cdf018e543049ea", + "0x645b28857b19c0046b10caa3bd93fcd31ce15905", + "0x20850bcea7476a0623e276c2cb9ec2e3024dcb0c", + "0xce049d2afc746f96d2ca2b6425efcbf9d44fda2c", + "0x397ef1462cf750ae2e4c387a70474c1f1c5a1637", + "0xb270814532f90969b62b37ccd04dfadfadcd3267", + "0x12b50bdab12a633d992ebfecff6eb9785aa5a09a", + "0x88c0356a46823938bcb233be05d7634aab7f40d9" + ], + "snapshot": 15825878 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/index.ts b/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/index.ts new file mode 100644 index 00000000..4e768ea6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/index.ts @@ -0,0 +1,79 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'gmtesla'; +export const version = '0.1.1'; + +// const DSLA = '0x3aFfCCa64c2A6f4e3B6Bd9c64CD2C969EFd1ECBe'; +// const StakingSLA = '0x091ee4d4D8FfD00698c003C21F1ba69EA6310241'; +// const LP_TOKEN = '0xAC104C0438A7bb15C714503537c6FA271FDB284E'; // dpToken +// const SP_TOKEN = '0xcf4ea46eba95fe3643b6c954d29516d7376913dc' // duToken + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)', + 'function usersPool(address token) external view returns (uint256)', + 'function providersPool(address token) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Get dpToken Balances + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + multi.call(address, options.xDSLA_LP, 'balanceOf', [address]) + ); + const dpTokenBalances: Record = await multi.execute(); + + // Get duToken Balances + addresses.forEach((address) => + multi.call(address, options.xDSLA, 'balanceOf', [address]) + ); + const duTokenBalances: Record = await multi.execute(); + + // Get totalSupply of user/provider pools + const multi2 = new Multicaller(network, provider, abi, { blockTag }); + multi2.call('userTotalSupply', options.xDSLA, 'totalSupply', []); + multi2.call('providerTotalSupply', options.xDSLA_LP, 'totalSupply', []); + multi2.call('usersPool', options.StakingSLA, 'usersPool', [options.DSLA]); + multi2.call('providersPool', options.StakingSLA, 'providersPool', [ + options.DSLA + ]); + const res2: Record = await multi2.execute(); + + // Sum up duTokenBalance and dpTokenBalances + // dTokenBalance = staked amount * total supply / (userPools or providerPools) + // staked amount = dTokenBalance * (userPools or providerPools) / total supply + const balances = Object.fromEntries( + Object.entries(dpTokenBalances).map(([address, balance]) => [ + address, + BigNumber.from(balance) + .mul(res2.providersPool) + .div(res2.providerTotalSupply) + ]) + ); + Object.entries(duTokenBalances).forEach(([address, balance]) => { + const prevBal = BigNumber.from(balances[address]); + balances[address] = prevBal.add( + BigNumber.from(balance).mul(res2.usersPool).div(res2.userTotalSupply) + ); + }); + + const result = Object.fromEntries( + Object.entries(balances).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); + + return result; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/schema.json b/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/schema.json new file mode 100644 index 00000000..4b27e279 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/dsla-parametric-staking-service-credits/schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "DSLA", + "type": "object", + "properties": { + "xDSLA": { + "type": "string", + "title": "xDSLA", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "xDSLA_LP": { + "type": "string", + "title": "xDSLA_LP", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "DSLA": { + "type": "string", + "title": "DSLA", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "StakingSLA": { + "type": "string", + "title": "StakingSLA", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["xDSLA", "xDSLA_LP", "StakingSLA", "DSLA", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/README.md new file mode 100644 index 00000000..fccf8b79 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/README.md @@ -0,0 +1,12 @@ +# earthfund-child-dao-staking-balance + +Earthfund's custom snapshot strategy, calculates voting score based of the staked balances of the voters for a specific child dao token in the staking rewards contract. + +Here is an example of parameters: + +```json +{ + "stakingRewardsContractAddress": "0x351e9c4de57a1854397E97Df909Ea0b3D7cbd219", + "childDaoTokenAddress": "0xbEdAf8563DE032A8A48ae8B04552c18c6Cb60B85" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/examples.json new file mode 100644 index 00000000..f1c66897 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "earthfund-child-dao-staking-balance", + "params": { + "stakingRewardsContractAddress": "0x351e9c4de57a1854397E97Df909Ea0b3D7cbd219", + "childDaoTokenAddress": "0xbEdAf8563DE032A8A48ae8B04552c18c6Cb60B85" + } + }, + "network": "4", + "addresses": [ + "0xB300ecae675213d6889d93c0Bf0B27DD04d8eaa0", + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 10928132 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/index.ts new file mode 100644 index 00000000..46be05ed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/index.ts @@ -0,0 +1,38 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatEther } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.1'; + +const abi = [ + 'function userStakes(address _daoToken, address _user) external view returns(uint256 stakedAmount, uint256 rewardEntry, uint256 pendingRewards, uint256 timeStaked)' +]; + +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) => + multi.call(address, options.stakingRewardsContractAddress, 'userStakes', [ + options.childDaoTokenAddress, + address + ]) + ); + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, userStake]) => [ + address, + parseFloat(formatEther(userStake[0])) // staked balance is the first item in the returned tuple from the contract call + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/schema.json b/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/schema.json new file mode 100644 index 00000000..be58e1d5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/earthfund-child-dao-staking-balance/schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "stakingRewardsContractAddress": { + "type": "string", + "title": "Address of the staking rewards contract", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "childDaoTokenAddress": { + "type": "string", + "title": "Child DAO token contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["stakingRewardsContractAddress", "childDaoTokenAddress"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/README.md b/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/README.md new file mode 100644 index 00000000..6d80e7d9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/README.md @@ -0,0 +1,24 @@ +# echelon-cached-erc1155-decay + +This strategy looks at an ERC1155 caching (staking) contract and assigns a linearly decaying amount of voting power. The business context behind this is that each cached asset is eligible to claim a set amount of ERC20s over the same period; and thus can use those tokens to vote as well. + +For example, at block 0, the voting should have equivalent to 4000 units of voting power. A year later, they should have 0 units. + +As parameters, we pass in the base amount of voting power (e.g. 4000), starting block where there's no decay, and number of months until complete decay (e.g. 12). + +At a high level, the strategy grabs the UNIX timestamp in seconds for starting block, current block, and project timestamp of final block. It then queries the contract for the amount of cached ERC1155s. A simple slope formula is then applied to calculate the decay rate; which is then applied to determine the voting power per asset at current block. + +The final value is square rooted. + +Example of parameters: + +```json + "params": { + "symbol": "PK - PRIME", + "address": "0x3399eff96D4b6Bae8a56F4852EB55736c9C2b041", + "baseValue": 4000, + "startingBlock": 15166749, + "monthsToDecay": 12 + } +``` + diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/examples.json b/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/examples.json new file mode 100644 index 00000000..fb289d27 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/examples.json @@ -0,0 +1,28 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "echelon-cached-erc1155-decay", + "params": { + "symbol": "PK - PRIME", + "address": "0x3399eff96D4b6Bae8a56F4852EB55736c9C2b041", + "baseValue": 4000, + "startingBlock": 15166749, + "monthsToDecay": 12 + } + }, + "network": "1", + "addresses": [ + "0xE6be99cbC7796F90baff870a2ffE838a540E27C9", + "0xf98A4A42853cC611eED664627087d4ae19740ED8", + "0xbdc3C931387e2c6647b0D7237Ed30c702260fa80", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 15169750 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/index.ts b/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/index.ts new file mode 100644 index 00000000..1f932adc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-cached-erc1155-decay/index.ts @@ -0,0 +1,51 @@ +import { Multicaller } from '../../utils'; + +export const author = 'brandonleung'; +export const version = '1.0.0'; + +const cachingAbi = [ + 'function cacheInfo(uint256, address) view returns (uint256 amount, int256 rewardDebt)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPool = new Multicaller(network, provider, cachingAbi, { + blockTag + }); + + const startingBlockTimestamp = ( + await provider.getBlock(options.startingBlock) + ).timestamp; + const endingBlockTimestamp = + startingBlockTimestamp + 2628288 * options.monthsToDecay; + const currentBlockTimestamp = (await provider.getBlock(snapshot)).timestamp; + + const decayRate = + (0 - options.baseValue) / (endingBlockTimestamp - startingBlockTimestamp); + + const votingPowerPerKey = + options.baseValue + + decayRate * (currentBlockTimestamp - startingBlockTimestamp); + + addresses.forEach((address) => { + stakingPool.call(address, options.address, 'cacheInfo', [0, address]); + }); + const response = await stakingPool.execute(); + + return Object.fromEntries( + addresses.map((address) => { + return [ + address, + Math.sqrt(response[address][0].toNumber() * votingPowerPerKey) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/README.md b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/README.md new file mode 100644 index 00000000..6824fa49 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/README.md @@ -0,0 +1,36 @@ +# echelon-wallet-prime-cached-key-gated + +This strategy looks at an ERC1155 caching (staking) contract and assigns a linearly decaying amount of voting power. The business context behind this is that each cached asset is eligible to claim a set amount of ERC20s over the same period; and thus can use those tokens to vote as well. + +For example, at block 0, the voting should have equivalent to 4000 units of voting power. A year later, they should have 0 units. + +As parameters, we pass in the base amount of voting power (e.g. 4000), starting block where there's no decay, and number of months until complete decay (e.g. 12). + +At a high level, the strategy grabs the UNIX timestamp in seconds for starting block, current block, and project timestamp of final block. It then queries the contract for the amount of cached ERC1155s. A simple slope formula is then applied to calculate the decay rate; which is then applied to determine the voting power per asset at current block. + +This strategy also makes use of the `erc20-balance-of` strategy. The erc20 balance is added to the equivalent value of the cached NFT. + +The weighted voting power is square rooted. + +In order to be eligible to vote, the address has to have a non-zero wallet erc1155 balance (using the `erc1155-all-balances-of` strategy) or be whitelisted. Additionally, the address cannot be blacklisted. + +Example of parameters: + +```json + "params": { + "symbol": "PRIME VOTE", + "address": "0xb23d80f5FefcDDaa212212F028021B41DEd428CF", + "decimals": 18, + "stakingAddress": "0x3399eff96D4b6Bae8a56F4852EB55736c9C2b041", + "baseValue": 4000, + "startingBlock": 15166749, + "monthsToDecay": 12, + "erc1155Address": "0x76BE3b62873462d2142405439777e971754E8E77", + "whitelist": [ + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0xFCfcC87E312f323768f9553255250A9357a04109" + ], + "blacklist": ["0xE6be99cbC7796F90baff870a2ffE838a540E27C9"] + } +``` + diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/examples.json b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/examples.json new file mode 100644 index 00000000..387c32c6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/examples.json @@ -0,0 +1,37 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "echelon-wallet-prime-and-cached-key-gated", + "params": { + "symbol": "PRIME VOTE", + "address": "0xb23d80f5FefcDDaa212212F028021B41DEd428CF", + "decimals": 18, + "stakingAddress": "0x3399eff96D4b6Bae8a56F4852EB55736c9C2b041", + "baseValue": 4000, + "startingBlock": 15166749, + "monthsToDecay": 12, + "erc1155Address": "0x76BE3b62873462d2142405439777e971754E8E77", + "whitelist": [ + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0xFCfcC87E312f323768f9553255250A9357a04109" + ], + "blacklist": ["0xE6be99cbC7796F90baff870a2ffE838a540E27C9"] + } + }, + "network": "1", + "addresses": [ + "0xE6be99cbC7796F90baff870a2ffE838a540E27C9", + "0xf98A4A42853cC611eED664627087d4ae19740ED8", + "0xbdc3C931387e2c6647b0D7237Ed30c702260fa80", + "0x5566eec3684F3ED896740590cc372758f25f056f", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0xFCfcC87E312f323768f9553255250A9357a04109", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 15540462 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/index.ts b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/index.ts new file mode 100644 index 00000000..79c7068a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key-gated/index.ts @@ -0,0 +1,99 @@ +import { Multicaller } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { strategy as erc1155AllBalancesOf } from '../erc1155-all-balances-of'; + +export const author = 'brandonleung'; +export const version = '1.0.0'; + +const cachingAbi = [ + 'function cacheInfo(uint256, address) view returns (uint256 amount, int256 rewardDebt)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPool = new Multicaller(network, provider, cachingAbi, { + blockTag + }); + + const startingBlockTimestamp = ( + await provider.getBlock(options.startingBlock) + ).timestamp; + const endingBlockTimestamp = + startingBlockTimestamp + 2628288 * options.monthsToDecay; + const currentBlockTimestamp = (await provider.getBlock(snapshot)).timestamp; + + const decayRate = + (0 - options.baseValue) / (endingBlockTimestamp - startingBlockTimestamp); + + const votingPowerPerKey = + options.baseValue + + decayRate * (currentBlockTimestamp - startingBlockTimestamp); + + addresses.forEach((address) => { + stakingPool.call(address, options.stakingAddress, 'cacheInfo', [ + 0, + address + ]); + }); + const contractResponse = await stakingPool.execute(); + + const cachedKeyScore = Object.fromEntries( + addresses.map((address) => { + return [ + address, + contractResponse[address][0].toNumber() * votingPowerPerKey + ]; + }) + ); + + const walletScore = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const erc1155Options = { + address: options.erc1155Address + }; + const erc1155Balances = await erc1155AllBalancesOf( + space, + network, + provider, + addresses, + erc1155Options, + snapshot + ); + + const votingPower = Object.entries(walletScore).reduce( + (address, [key, value]) => ({ + ...address, + [key]: (address[key] || 0) + value + }), + { ...cachedKeyScore } + ); + + Object.keys(votingPower).forEach((key) => { + const whitelistedAddress = + options.whitelist.indexOf(key) !== -1 || + (key in erc1155Balances && erc1155Balances[key] > 0); + const blacklistedAddress = options.blacklist.indexOf(key) !== -1; + + votingPower[key] = + whitelistedAddress && !blacklistedAddress + ? Math.sqrt(votingPower[key]) + : 0; + }); + + return votingPower; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/README.md b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/README.md new file mode 100644 index 00000000..8b296221 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/README.md @@ -0,0 +1,28 @@ +# echelon-wallet-prime-cached-key + +This strategy looks at an ERC1155 caching (staking) contract and assigns a linearly decaying amount of voting power. The business context behind this is that each cached asset is eligible to claim a set amount of ERC20s over the same period; and thus can use those tokens to vote as well. + +For example, at block 0, the voting should have equivalent to 4000 units of voting power. A year later, they should have 0 units. + +As parameters, we pass in the base amount of voting power (e.g. 4000), starting block where there's no decay, and number of months until complete decay (e.g. 12). + +At a high level, the strategy grabs the UNIX timestamp in seconds for starting block, current block, and project timestamp of final block. It then queries the contract for the amount of cached ERC1155s. A simple slope formula is then applied to calculate the decay rate; which is then applied to determine the voting power per asset at current block. + +This strategy also makes use of the `erc20-balance-of` strategy. The erc20 balance is added to the equivalent value of the cached NFT. + +The final value is square rooted. + +Example of parameters: + +```json + "params": { + "symbol": "PRIME VOTE", + "address": "0xb23d80f5FefcDDaa212212F028021B41DEd428CF", + "decimals": 18, + "stakingAddress": "0x3399eff96D4b6Bae8a56F4852EB55736c9C2b041", + "baseValue": 4000, + "startingBlock": 15166749, + "monthsToDecay": 12 + } +``` + diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/examples.json b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/examples.json new file mode 100644 index 00000000..7e868e06 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/examples.json @@ -0,0 +1,30 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "echelon-wallet-prime-and-cached-key", + "params": { + "symbol": "PRIME VOTE", + "address": "0xb23d80f5FefcDDaa212212F028021B41DEd428CF", + "decimals": 18, + "stakingAddress": "0x3399eff96D4b6Bae8a56F4852EB55736c9C2b041", + "baseValue": 4000, + "startingBlock": 15166749, + "monthsToDecay": 12 + } + }, + "network": "1", + "addresses": [ + "0xE6be99cbC7796F90baff870a2ffE838a540E27C9", + "0xf98A4A42853cC611eED664627087d4ae19740ED8", + "0xbdc3C931387e2c6647b0D7237Ed30c702260fa80", + "0x5566eec3684F3ED896740590cc372758f25f056f", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 15540462 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/index.ts b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/index.ts new file mode 100644 index 00000000..e6268362 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/echelon-wallet-prime-and-cached-key/index.ts @@ -0,0 +1,77 @@ +import { Multicaller } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'brandonleung'; +export const version = '1.0.0'; + +const cachingAbi = [ + 'function cacheInfo(uint256, address) view returns (uint256 amount, int256 rewardDebt)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPool = new Multicaller(network, provider, cachingAbi, { + blockTag + }); + + const startingBlockTimestamp = ( + await provider.getBlock(options.startingBlock) + ).timestamp; + const endingBlockTimestamp = + startingBlockTimestamp + 2628288 * options.monthsToDecay; + const currentBlockTimestamp = (await provider.getBlock(snapshot)).timestamp; + + const decayRate = + (0 - options.baseValue) / (endingBlockTimestamp - startingBlockTimestamp); + + const votingPowerPerKey = + options.baseValue + + decayRate * (currentBlockTimestamp - startingBlockTimestamp); + + addresses.forEach((address) => { + stakingPool.call(address, options.stakingAddress, 'cacheInfo', [ + 0, + address + ]); + }); + const contractResponse = await stakingPool.execute(); + + const cachedKeyScore = Object.fromEntries( + addresses.map((address) => { + return [ + address, + contractResponse[address][0].toNumber() * votingPowerPerKey + ]; + }) + ); + + const walletScore = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + const votingPower = Object.entries(walletScore).reduce( + (address, [key, value]) => ({ + ...address, + [key]: (address[key] || 0) + value + }), + { ...cachedKeyScore } + ); + + Object.keys(votingPower).forEach((key) => { + votingPower[key] = Math.sqrt(votingPower[key]); + }); + + return votingPower; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/README.md new file mode 100644 index 00000000..e3ace8df --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/README.md @@ -0,0 +1,11 @@ +# eco-voting-power + +This is strategy returns the ECO voting power delegated to the `delegatee`. + +Here is an example of parameters: + +```json +{ + "delegatee": "0x6b175474e89094c44da98b954eedeac495271d0f" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/examples.json new file mode 100644 index 00000000..8742a596 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example Eco Voting Power", + "strategy": { + "name": "eco-voting-power", + "params": { + "delegatee": "0xbFB97a6e13E42518522379C94e0Ddd99F7b70b8b" + } + }, + "network": "5", + "addresses": [ + "0x18ab8b4fba1c770b279b97686f4265e882aa1e94", + "0x40051DE3036d76Aee7B16Cb2c21c86eB0dFa3f4c", + "0x35244c622e5034dc1bcf2ff3931cfa57192572ff", + "0x3ec87777b22d12ea95c55456324433a4c5a7bfe8", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0xE3DE066aC4CFa2C6169C6e8ccc2afe7b4E440C1D", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 8620080 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/index.ts new file mode 100644 index 00000000..2191d91c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/index.ts @@ -0,0 +1,192 @@ +import { WeiPerEther, Zero, One } from '@ethersproject/constants'; +import { formatEther } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; + +import { getBlockNumber, subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'carlosfebres'; +export const version = '1.0.1'; + +const ECO_SUBGRAPH_BY_CHAIN_ID = { + '1': 'https://api.thegraph.com/subgraphs/name/ecographs/the-eco-currency-subgraphs', + '5': 'https://api.thegraph.com/subgraphs/name/ecographs/staging-subgraphs' +}; + +const TOKEN_DELEGATEES_FIELDS = { + amount: true, + delegator: { + id: true + } +}; + +interface TokenDelegateesResult { + amount: string; + delegator: { + id: string; + }; +} + +interface QueryResult { + account: { + ecoTokenDelegatees: TokenDelegateesResult[]; + ecoCurrentTokenDelegatees: TokenDelegateesResult[]; + stakedEcoXTokenDelegatees: TokenDelegateesResult[]; + stakedEcoXCurrentTokenDelegatees: TokenDelegateesResult[]; + }; + inflationMultipliers: { + value: string; + }[]; +} + +function calculateVotingPower( + ecoVp: BigNumber, + stakedEcoXVp: BigNumber +): BigNumber { + return stakedEcoXVp.add(ecoVp.div(10)); +} + +function createDelegationList( + items: TokenDelegateesResult[], + inflationMultiplier = One +): Record { + return Object.fromEntries( + items.map((item) => [ + item.delegator.id, + BigNumber.from(item.amount).div(inflationMultiplier) + ]) + ); +} + +export async function strategy( + space: string, + network: string, + provider: StaticJsonRpcProvider, + addresses: string[], + options, + snapshot: number | 'latest' +): Promise> { + const _addresses = addresses.map((addr) => addr.toLowerCase()); + const blockNumber = + snapshot !== 'latest' ? snapshot : await getBlockNumber(provider); + + const baseFilter = { + blockStarted_lte: blockNumber, + delegator_in: _addresses + }; + + const query = { + account: { + __args: { id: options.delegatee.toLowerCase() }, + ecoTokenDelegatees: { + __aliasFor: 'tokenDelegatees', + __args: { + where: { + ...baseFilter, + token: 'eco', + blockEnded_gt: blockNumber + } + }, + ...TOKEN_DELEGATEES_FIELDS + }, + ecoCurrentTokenDelegatees: { + __aliasFor: 'tokenDelegatees', + __args: { + where: { + ...baseFilter, + token: 'eco', + blockEnded: null + } + }, + ...TOKEN_DELEGATEES_FIELDS + }, + stakedEcoXTokenDelegatees: { + __aliasFor: 'tokenDelegatees', + __args: { + where: { + ...baseFilter, + token: 'sEcox', + blockEnded_gt: blockNumber + } + }, + ...TOKEN_DELEGATEES_FIELDS + }, + stakedEcoXCurrentTokenDelegatees: { + __aliasFor: 'tokenDelegatees', + __args: { + where: { + ...baseFilter, + token: 'sEcox', + blockEnded: null + } + }, + ...TOKEN_DELEGATEES_FIELDS + } + }, + inflationMultipliers: { + __args: { + first: 1, + orderBy: 'blockNumber', + orderDirection: 'desc', + where: { blockNumber_lte: blockNumber } + }, + value: true + } + }; + + const subgraphUrl = ECO_SUBGRAPH_BY_CHAIN_ID[network]; + if (subgraphUrl == undefined) { + throw new Error(`Unsupported network with id: ${network}`); + } + + const { account: account, inflationMultipliers }: QueryResult = + await subgraphRequest(subgraphUrl, query); + + if (!account) { + return Object.fromEntries( + _addresses.map((address) => [getAddress(address), 0]) + ); + } + + const inflationMultiplier = inflationMultipliers.length + ? BigNumber.from(inflationMultipliers[0].value) + : WeiPerEther; + + const ecoHistoricalDelegations = createDelegationList( + account.ecoTokenDelegatees, + inflationMultiplier + ); + const ecoCurrentDelegations = createDelegationList( + account.ecoCurrentTokenDelegatees, + inflationMultiplier + ); + const stakedEcoXHistoricalDelegations = createDelegationList( + account.stakedEcoXTokenDelegatees + ); + const stakedEcoXCurrentDelegations = createDelegationList( + account.stakedEcoXCurrentTokenDelegatees + ); + + return Object.fromEntries( + _addresses.map((address) => { + const ecoHistorical = ecoHistoricalDelegations[address] || Zero; + const ecoCurrent = ecoCurrentDelegations[address] || Zero; + const stakedEcoXHistorical = + stakedEcoXHistoricalDelegations[address] || Zero; + const stakedEcoXCurrent = stakedEcoXCurrentDelegations[address] || Zero; + + return [ + getAddress(address), + parseInt( + formatEther( + calculateVotingPower( + ecoHistorical.add(ecoCurrent), + stakedEcoXHistorical.add(stakedEcoXCurrent) + ) + ) + ) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/schema.json b/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/schema.json new file mode 100644 index 00000000..44e25088 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eco-voting-power/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "delegatee": { + "type": "string", + "title": "Delegatee Address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["delegatee"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/egl-vote/README.md b/Implementations/API/backend/utils/snapshot/strategies/egl-vote/README.md new file mode 100644 index 00000000..9e08ccb0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/egl-vote/README.md @@ -0,0 +1,15 @@ +# egl-vote + +Checks for the number of EGL's locked in an EGL vote and adds it to the balance of the +voters EGL ERC20 token + +Here is an example of parameters: + +```json +{ + "address": "0x0baD6775A3581ef6ED9a70F5798642FAfc9ecbba", + "symbol": "EGL", + "decimals": 18, + "eglVotingAddress": "0xFF238bb166Fd031A36302B0190eaF55e240c5B7E" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/egl-vote/examples.json b/Implementations/API/backend/utils/snapshot/strategies/egl-vote/examples.json new file mode 100644 index 00000000..2bd221eb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/egl-vote/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "egl-vote", + "params": { + "address": "0x0baD6775A3581ef6ED9a70F5798642FAfc9ecbba", + "symbol": "EGL", + "decimals": 18, + "eglVotingAddress": "0xFF238bb166Fd031A36302B0190eaF55e240c5B7E" + } + }, + "network": "3", + "addresses": [ + "0x6606689c5423Ee00E6576876CB1CDC002883bcD0", + "0x98d6E440b54E31aA9bd46b20002Fa44c9b084633", + "0xFF238bb166Fd031A36302B0190eaF55e240c5B7E", + "0x12b709eB54Ad3417400Cc1A5e6F64174391143Ed" + ], + "snapshot": 10813553 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/egl-vote/index.ts b/Implementations/API/backend/utils/snapshot/strategies/egl-vote/index.ts new file mode 100644 index 00000000..16cfcbef --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/egl-vote/index.ts @@ -0,0 +1,54 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'shanevc'; +export const version = '0.1'; + +const lockedTokenBalance = [ + 'function voters(address) view returns (uint8,uint16,uint256,uint256,uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const { eglVotingAddress, decimals } = options; + + const eglBalances = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const lockedTokenBalances = new Multicaller( + network, + provider, + lockedTokenBalance, + { blockTag } + ); + + addresses.forEach((address) => + lockedTokenBalances.call(address, eglVotingAddress, 'voters', [address]) + ); + const result: Record< + string, + Array + > = await lockedTokenBalances.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, voter]) => [ + address, + parseFloat(formatUnits(voter[3], decimals)) + eglBalances[address] + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/README.md b/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/README.md new file mode 100644 index 00000000..8562b269 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/README.md @@ -0,0 +1,16 @@ +# ens-10k-club + +This strategy is for 10K Club members, holders of ENS names 000.eth - 9999.eth. +This script queries The Graph for all ENS names owned by voter and checks for 10K Club names. +By default, 999 Club members' votes are weighted 10x. + +Here is an example of parameters: + +```json +{ + "address": "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85", + "symbol": "NUMS", + "clubWeight10k": 1, + "clubWeight999": 10 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/examples.json new file mode 100644 index 00000000..25fcaa00 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "ENS 10k Club", + "strategy": { + "name": "ens-10k-club", + "params": { + "address": "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85", + "symbol": "NUMS", + "clubWeight10k": 1, + "clubWeight999": 10 + } + }, + "network": "1", + "addresses": [ + "0xF5F8977348f592DB9773C373fEf5bE362D103776", + "0xE3D2b657c45613d30292Fd67Ef40DAB043B70706", + "0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9" + ], + "snapshot": 14799078 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/index.ts new file mode 100644 index 00000000..4720800b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-10k-club/index.ts @@ -0,0 +1,83 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +const ENS_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/ensdomains/ens', + '3': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensropsten', + '4': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensrinkeby', + '5': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensgoerli' +}; + +// 999 Club: 000-999 as strings, 1000 total +const NAMES_999_CLUB = [...Array(1000).keys()] + .map((i) => -1 + i + 1) + .map((x) => x.toString().padStart(3, '0')); + +// 10K Club: 0000-9999 as strings, 10000 total +const NAMES_10K_CLUB = [...Array(10000).keys()] + .map((i) => -1 + i + 1) + .map((x) => x.toString().padStart(4, '0')); + +export const author = 'paste'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const { clubWeight10k, clubWeight999 } = options; + const max = 10; + const count = Math.ceil(addresses.length / max); + const pages = Array.from(Array(count)).map((x, i) => + addresses.slice(max * i, max * (i + 1)) + ); + const params = Object.fromEntries( + pages + .map((page, i) => `_${i}`) + .map((q, i) => [ + q, + { + __aliasFor: 'registrations', + __args: { + block: snapshot !== 'latest' ? { number: snapshot } : null, + where: { + registrant_in: pages[i].map((address) => address.toLowerCase()) + }, + first: 1000, + orderBy: 'registrationDate', + orderDirection: 'desc' + }, + registrant: { + id: true + }, + domain: { + // labelhash: true, + labelName: true + } + } + ]) + ); + + let result = await subgraphRequest(ENS_SUBGRAPH_URL[network], params); + result = [].concat.apply([], Object.values(result)); + const votes = {}; + if (result) { + result.forEach((registration) => { + const owner = getAddress(registration.registrant.id); + const label = registration.domain.labelName; + if (!votes[owner]) { + votes[owner] = 0; + } + if (NAMES_999_CLUB.includes(label)) { + votes[owner] = votes[owner] + clubWeight999; + } else if (NAMES_10K_CLUB.includes(label)) { + votes[owner] = votes[owner] + clubWeight10k; + } + }); + } + return votes; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/README.md b/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/README.md new file mode 100644 index 00000000..14ed3264 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/README.md @@ -0,0 +1,16 @@ +# ens-all-club-digits + +This strategy is for All Club Digits members, holders of ENS names 000.eth - 999.eth, 0000.eth - 9999.eth, 00000.eth to 99999.eth and beyond +This script queries The Graph for all ENS names owned by voter and checks for 10K Club names. +Pass the number of digits as the requirement + + +Here is an example of parameters, we want 999 so we put numberOfDigits set to 3: + +```json +{ + "address": "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85", + "symbol": "DIGITS", + "numberOfDigits": 3 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/examples.json new file mode 100644 index 00000000..1931a5ff --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "ENS ALL Club Digits", + "strategy": { + "name": "ens-all-club-digits", + "params": { + "address": "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85", + "symbol": "NUMS", + "numberOfDigits": 7 + } + }, + "network": "1", + "addresses": [ + "0xa2e87bf4e4edc28922736887772c351de5554b77", + "0xf8ebe2b23ed74bee2b35da9de63c2380902b7cb5", + "0xae099ada728a5abf8b31f2b126cc5bf8e70fbc2c" + ], + "snapshot": 15818456 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/index.ts new file mode 100644 index 00000000..d1885ba4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-all-club-digits/index.ts @@ -0,0 +1,87 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +const ENS_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/ensdomains/ens', + '3': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensropsten', + '4': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensrinkeby', + '5': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensgoerli' +}; + +export const author = 'onigiri-x'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + // 999 Club = 3 , 10k club = 4, 100k club = 5, etc + const { numberOfDigits } = options; + + const max = 10; + const count = Math.ceil(addresses.length / max); + const pages = Array.from(Array(count)).map((x, i) => + addresses.slice(max * i, max * (i + 1)) + ); + let page = 0; + const votes = {}; + // This will iterate until there are no more full pages (1000 domains being returned), + while (true) { + const params = Object.fromEntries( + pages + .map((page, i) => `_${i}`) + .map((q, i) => [ + q, + { + __aliasFor: 'registrations', + __args: { + block: snapshot !== 'latest' ? { number: snapshot } : null, + where: { + registrant_in: pages[i].map((address) => address.toLowerCase()) + }, + first: 1000, + skip: page * 1000, + orderBy: 'registrationDate', + orderDirection: 'desc' + }, + registrant: { + id: true + }, + domain: { + // labelhash: true, + labelName: true + } + } + ]) + ); + + let result = await subgraphRequest(ENS_SUBGRAPH_URL[network], params); + result = [].concat.apply([], Object.values(result)); + + if (result) { + result.forEach((registration) => { + const owner = getAddress(registration.registrant.id); + const label = registration.domain.labelName; + if (!votes[owner]) { + votes[owner] = 0; + } + const reg = new RegExp('^[0-9]*$'); // only number 0 to 9 + if (label && label.length === numberOfDigits && reg.test(label)) { + votes[owner] = votes[owner] + 1; + } + }); + if (result.length >= 1000) { + page++; + } else { + break; + } + } else { + break; + } + } + return votes; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/README.md b/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/README.md new file mode 100644 index 00000000..210d7ea9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/README.md @@ -0,0 +1,39 @@ +# Contract call strategy + +Allows subdomain owners of a ENS domain to vote depending on the number of domains one own + + +## Params + +- `domain` - The name of the parent domain. + +## Examples + +```JSON +"strategies": [ + { + "name": "Example query", + "strategy": { + "name": "ens-domains-owned", + "params": { + "domain": "ismoney.eth" + } + }, + "network": "1", + "addresses": [ + "0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9", + "0x0239769A1aDF4DeF9f07Da824B80B9C4fCB59593" + ], + "snapshot": 11414195 + } +] +``` + +Valid test addresses and snapshot block number: +```typescript +const addresses = [ + '0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9' +]; + +const snapshot = 11414195; +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/examples.json new file mode 100644 index 00000000..c2f2842f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ens-domains-owned", + "params": { + "symbol": "ENS", + "domain": "ismoney.eth" + } + }, + "network": "1", + "addresses": [ + "0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9", + "0x0239769A1aDF4DeF9f07Da824B80B9C4fCB59593" + ], + "snapshot": 11414195 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/index.ts new file mode 100644 index 00000000..985bf057 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-domains-owned/index.ts @@ -0,0 +1,71 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const ENS_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/ensdomains/ens', + '3': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensropsten', + '4': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensrinkeby', + '5': 'https://api.thegraph.com/subgraphs/name/ensdomains/ensgoerli' +}; + +export const author = 'makoto'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const max = 10; + const count = Math.ceil(addresses.length / max); + const pages = Array.from(Array(count)).map((x, i) => + addresses.slice(max * i, max * (i + 1)) + ); + const params = Object.fromEntries( + pages + .map((page, i) => `_${i}`) + .map((q, i) => [ + q, + { + __aliasFor: 'domains', + __args: { + block: snapshot !== 'latest' ? { number: snapshot } : undefined, + where: { + name: options.domain + }, + first: 1000 + }, + id: true, + labelName: true, + subdomains: { + __args: { + where: { + owner_in: pages[i].map((address) => address.toLowerCase()) + } + }, + owner: { + id: true + } + } + } + ]) + ); + + let result = await subgraphRequest(ENS_SUBGRAPH_URL[network], params); + result = [].concat.apply([], Object.values(result)); + + const score = {}; + if (result) { + result.forEach((u) => { + u.subdomains.forEach((domain) => { + const userAddress = getAddress(domain.owner.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + 1; + }); + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/README.md b/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/README.md new file mode 100644 index 00000000..e3eb3e68 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/README.md @@ -0,0 +1,35 @@ +# Contract call strategy + +Allows users with ENS reverse record address to vote + +## Params + +None + +## Examples + +```JSON +"strategies": [ + { + "name": "Example query", + "strategy": { + "name": "ens-reverse-record", + }, + "network": "1", + "addresses": [ + "0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9", + "0x0239769A1aDF4DeF9f07Da824B80B9C4fCB59593" + ], + "snapshot": 12011880 + } +] +``` + +Valid test addresses and snapshot block number: +```typescript +const addresses = [ + '0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9' +]; + +const snapshot = 12011880; +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/examples.json new file mode 100644 index 00000000..25bf54dd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "ENS Reverse Record", + "strategy": { + "name": "ens-reverse-record", + "params": { + "symbol": "ENS" + } + }, + "network": "1", + "addresses": [ + "0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9", + "0x0239769A1aDF4DeF9f07Da824B80B9C4fCB59593" + ], + "snapshot": 12011880 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/index.ts new file mode 100644 index 00000000..c449d8cb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ens-reverse-record/index.ts @@ -0,0 +1,64 @@ +import { call } from '../../utils'; +import namehash from 'eth-ens-namehash'; + +export const author = 'makoto'; +export const version = '0.1.0'; + +const abi = [ + { + inputs: [ + { + internalType: 'address[]', + name: 'addresses', + type: 'address[]' + } + ], + name: 'getNames', + outputs: [ + { + internalType: 'string[]', + name: 'r', + type: 'string[]' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +const CONTRACTS = { + '1': '0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C', + '3': '0x5bBFe410e18DCcaebbf5fD7A00844d4255615258', + '4': '0x196eC7109e127A353B709a20da25052617295F6f', + '5': '0x333Fc8f550043f239a2CF79aEd5e9cF4A20Eb41e' +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const contractAdress = CONTRACTS[network]; + const response = await call( + provider, + abi, + [contractAdress, 'getNames', [addresses]], + { blockTag } + ); + const r = Object.fromEntries( + response.map((value, i) => { + const isEligible = + value !== '' && + namehash.normalize(value) === value && + value.split('.').length === 2; // no subdomain + + const number = isEligible ? 1 : 0; + return [addresses[i], number]; + }) + ); + return r; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/README.md new file mode 100644 index 00000000..116339fc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/README.md @@ -0,0 +1,6 @@ +# erc1155-all-balances-of + +This strategy return the balances of the voters for all tokens in a ERC1155 contract. + +### Limit: +On polygon, works only with contracts with total tokenIds less than 6000 \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/examples.json new file mode 100644 index 00000000..ed11b31c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/examples.json @@ -0,0 +1,33 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc1155-all-balances-of", + "params": { + "address": "0x71eb5c179ceb640160853144cbb8df5bd24ab5cc", + "symbol": "CORGI" + } + }, + "network": "1", + "addresses": ["0x06658Fd70023f527BFAc1A6d9141C56d99c65129"], + "snapshot": 14597922 + }, + { + "name": "Example query", + "strategy": { + "name": "erc1155-all-balances-of", + "params": { + "address": "0x2939b94BDc377e66A377cfc15028DF3Bd6aC6C28", + "symbol": "CORGI" + } + }, + "network": "137", + "addresses": [ + "0xb165014c736c50da0638283eac2e19c88eab74f3", + "0x28adbf7bd37c08965746669471a1dcef6dc91009", + "0xe6fecee69a9e3726324498830bb0e898a866eccc", + "0x715c624ff8a8ebae3a2030c0b0868a5b447c956f" + ], + "snapshot": 27088229 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/index.ts new file mode 100644 index 00000000..d73fce64 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-all-balances-of/index.ts @@ -0,0 +1,87 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'snapshot-labs'; +export const version = '0.2.0'; + +const SUBGRAPH_URL = { + '1': 'https://gateway.thegraph.com/api/94c3f5dd3947e2f62fc6e0e757549ee7/subgraphs/id/GCQVLurkeZrdMf4t5v5NyeWJY8pHhfE9sinjFMjLYd9C' +}; + +const HOSTED_SUBGRAPH_URL = { + '137': 'https://api.thegraph.com/subgraphs/name/tranchien2002/eip1155-matic' +}; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const PAGE_SIZE = 1000; + let result = []; + let page = 0; + + const isHosted = HOSTED_SUBGRAPH_URL[network] !== undefined; + const subgraphURL = isHosted + ? HOSTED_SUBGRAPH_URL[network] + : SUBGRAPH_URL[network]; + const eip1155BalancesParams: any = { + balances: { + __aliasFor: 'erc1155Balances', + __args: { + first: PAGE_SIZE, + skip: 0, + where: { + account_in: addresses.map((a) => a.toLowerCase()), + token_starts_with: options.address.toLowerCase(), + value_not: '0' + } + }, + account: { + id: true + }, + value: true, + valueExact: true + } + }; + if (snapshot !== 'latest') { + eip1155BalancesParams.balances.__args.block = { number: snapshot }; + } + + // No erc1155balances alias and valueExact for hosted subgraph + if (isHosted) { + delete eip1155BalancesParams.balances.__aliasFor; + delete eip1155BalancesParams.balances.valueExact; + } + + while (true) { + eip1155BalancesParams.balances.__args.skip = page * PAGE_SIZE; + const pageResult = await subgraphRequest( + subgraphURL, + eip1155BalancesParams + ); + const pageERC1155Balances = pageResult.balances || []; + result = result.concat(pageERC1155Balances); + page++; + if (pageERC1155Balances.length < PAGE_SIZE) break; + // hosted subgraph doesn't support skip more than 5000 + if (isHosted && page === 6) break; + } + + return result.reduce( + ( + acc, + val: { value: string; valueExact: string; account: { id: string } } + ) => { + const address = getAddress(val.account.id); + const value = parseInt(isHosted ? val.value : val.valueExact, 10); + if (!acc[address]) acc[address] = 0; + acc[address] += value; + return acc; + }, + {} + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-cv/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-cv/examples.json new file mode 100644 index 00000000..631bb6c0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-cv/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc1155-balance-of-cv", + "params": { + "symbol": "ABC", + "address": "0xE18a32192ED95b0FE9D70D19e5025f103475d7BA", + "tokenId": "0x8000000000000000000000000000000200000000000000000000000000000000", + "decimals": 0 + } + }, + "network": "1", + "addresses": [ + "0x0B7056e2D9064f2ec8647F1ae556BAcc06da6Db4", + "0xcc5Ddc8CCD5B1E90Bc42F998ec864Ead0090A12B", + "0x0154d25120ed20a516fe43991702e7463c5a6f6e" + ], + "snapshot": 11992257 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-cv/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-cv/index.ts new file mode 100644 index 00000000..f1c6879b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-cv/index.ts @@ -0,0 +1,25 @@ +import { strategy as erc1155BalanceOfStrategy } from '../erc1155-balance-of'; + +export const author = 'dave4506'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc1155BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address) => [address[0], Math.sqrt(address[1])]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids-weighted/examples.json new file mode 100644 index 00000000..750c92c6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids-weighted/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc1155-balance-of-ids-weighted", + "params": { + "symbol": "ABC", + "address": "0x28472a58A490c5e09A238847F66A68a47cC76f0f", + "ids": ["0", "1"], + "weight": 10 + } + }, + "network": "1", + "addresses": ["0x34518f5559425a7bb06f66196920af10e1938b5f"], + "snapshot": 14039001 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids-weighted/index.ts new file mode 100644 index 00000000..f2be3a06 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids-weighted/index.ts @@ -0,0 +1,29 @@ +import { strategy as erc1155BalanceOfIdsStrategy } from '../erc1155-balance-of-ids'; + +export const author = 'naomsa'; +export const version = '1.0.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc1155BalanceOfIdsStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + return Object.fromEntries( + Object.entries(score).map((address) => [ + address[0], + address[1] * options.weight + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids/examples.json new file mode 100644 index 00000000..5b6b6478 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc1155-balance-of-ids", + "params": { + "symbol": "ABC", + "address": "0x28472a58A490c5e09A238847F66A68a47cC76f0f", + "ids": ["0"] + } + }, + "network": "1", + "addresses": ["0x34518f5559425a7bb06f66196920af10e1938b5f"], + "snapshot": 14039001 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids/index.ts new file mode 100644 index 00000000..6c56fb36 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of-ids/index.ts @@ -0,0 +1,38 @@ +import { multicall } from '../../utils'; + +export const author = 'naomsa'; +export const version = '1.0.0'; + +const abi = [ + 'function balanceOfBatch(address[], uint256[]) external view returns (uint256[])' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'balanceOfBatch', + [Array(options.ids.length).fill(address), options.ids] + ]), + { blockTag } + ); + + return Object.fromEntries( + response.map((values, i) => [ + addresses[i], + values[0].reduce((prev, curr) => prev + curr.toNumber(), 0) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/README.md new file mode 100644 index 00000000..967fc915 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/README.md @@ -0,0 +1,14 @@ +# erc1155-balances-of + +This strategy return the balances of the voters, for a specific token in a ERC1155 contract. + +> Note: If you want to get balance of all tokenIds in the contract, you can use the `erc1155-balance-of-all` strategy. If you have multiple tokenIds, you can use the `erc1155-balance-of-ids` or `erc1155-balance-of-ids-weighted` strategy. + +## Params + +| param | type | description | +| --- | --- | --- | +| `address` | `string` | The address of the ERC1155 contract | +| `tokenId` | `string` | The tokenId of the token to check | +| `decimals` | `number` | The number of decimals of the token | +| `symbol`(optional) | `string` | The symbol of the token | diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/examples.json new file mode 100644 index 00000000..f655f0c4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc1155-balance-of", + "params": { + "symbol": "ABC", + "address": "0xE18a32192ED95b0FE9D70D19e5025f103475d7BA", + "tokenId": "0x8000000000000000000000000000000200000000000000000000000000000000", + "decimals": 0 + } + }, + "network": "1", + "addresses": [ + "0x0B7056e2D9064f2ec8647F1ae556BAcc06da6Db4", + "0xcc5Ddc8CCD5B1E90Bc42F998ec864Ead0090A12B", + "0x0154d25120Ed20A516fE43991702e7463c5A6F6e" + ], + "snapshot": 11992257 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/index.ts new file mode 100644 index 00000000..32ce8bd9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-balance-of/index.ts @@ -0,0 +1,37 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'dave4506'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner, uint256 id) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'balanceOf', + [address, options.tokenId] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/README.md new file mode 100644 index 00000000..f3006fb1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/README.md @@ -0,0 +1,11 @@ +# erc1155-weighted-by-id + +This strategy takes in an array of ERC1155 token ids and the weight attached to each token ID. It returns the highest value a wallet holds, and does not calculate a sum. + +## Params + +| Param | Description | Required | +| --------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------- | +| ids | Array of ERC1155 Token IDs | Yes | +| weight | Array of Weights that map to the TokenID of the same index. | +| The balance of an owners token is multiplied by the weight to calculate the score | Yes | diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/examples.json new file mode 100644 index 00000000..c71d5e1c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/examples.json @@ -0,0 +1,40 @@ +[ + { + "name": "ERC1155 Weighted by id", + "strategy": { + "name": "erc1155-weighted-by-id", + "params": { + "symbol": "COINAGE", + "address": "0x4776defcf622c60c6419cccc9ee9e9042fadf3f7", + "ids": ["1", "3"], + "weight": [25, 1] + } + }, + "network": "1", + "addresses": [ + "0x2274091DAad5B7bF734f426B9aA8513955075b9E", + "0x65A922561638015E394A20356C31E9A6217513B8", + "0x1A9deC77516BA05316938D7Fb32F51D646f8d91f" + ], + "snapshot": 16648731 + }, + { + "name": "Example query", + "strategy": { + "name": "erc1155-weighted-by-id", + "params": { + "symbol": "COINAGE", + "address": "0x4776defcf622c60c6419cccc9ee9e9042fadf3f7", + "ids": ["2", "1"], + "weight": [50, 10] + } + }, + "network": "1", + "addresses": [ + "0x2274091DAad5B7bF734f426B9aA8513955075b9E", + "0x65A922561638015E394A20356C31E9A6217513B8", + "0x1A9deC77516BA05316938D7Fb32F51D646f8d91f" + ], + "snapshot": 16648731 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/index.ts new file mode 100644 index 00000000..2342662c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-weighted-by-id/index.ts @@ -0,0 +1,48 @@ +import { multicall } from '../../utils'; + +export const author = 'isaac-martin'; +export const version = '1.0.0'; + +const abi = [ + 'function balanceOfBatch(address[], uint256[]) external view returns (uint256[])' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'balanceOfBatch', + [Array(options.ids.length).fill(address), options.ids] + ]), + { blockTag } + ); + + const getWeightCount = (balance: number, currentIndex: number): number => { + // will either return 0 or 1 depending if the user holds one of these tokens + const count = Math.min(balance, 1); + return count * options.weight[currentIndex]; + }; + + return Object.fromEntries( + response.map((values, i) => [ + addresses[i], + values[0].reduce( + (prev, curr, currentIndex) => + Math.max(prev, getWeightCount(curr.toNumber(), currentIndex)), + 0 + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-with-multiplier/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc1155-with-multiplier/examples.json new file mode 100644 index 00000000..f78e84a1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-with-multiplier/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc1155-with-multiplier", + "params": { + "address": "0x2C56b43983Ca77cc29b27B4a731F0f0d54ae7e52", + "tokenId": 1, + "symbol": "DAWN", + "decimals": 0, + "multiplier": 5 + } + }, + "network": "4", + "addresses": [ + "0x9cA70B93CaE5576645F5F069524A9B9c3aef5006", + "0xb5aE5169F4D750e802884d81b4f9eC66c525396F" + ], + "snapshot": 9694956 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc1155-with-multiplier/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc1155-with-multiplier/index.ts new file mode 100644 index 00000000..49310576 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc1155-with-multiplier/index.ts @@ -0,0 +1,38 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'fabianschu'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner, uint256 id) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const multiplier = options.multiplier || 1; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'balanceOf', + [address, options.tokenId] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) * multiplier + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/README.md new file mode 100644 index 00000000..e54cc6d3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/README.md @@ -0,0 +1,14 @@ +# erc20-balance-of-at + +This strategy returns the balances of the voters for a specific ERC20 token at a specified snapshotID. The ERC20 token contract must implement ERC20 Snapshot (https://docs.openzeppelin.com/contracts/3.x/api/token/erc20#ERC20Snapshot) + +Here is an example of parameters: + +```json +{ + "address": "0xb4B486496469B3269c8907543706C377daAA4dD9", + "symbol": "PYE", + "decimals": 9, + "snapshotId": 1 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/examples.json new file mode 100644 index 00000000..c0dcdd40 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/examples.json @@ -0,0 +1,28 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-at", + "params": { + "address": "0xb4B486496469B3269c8907543706C377daAA4dD9", + "symbol": "PYE", + "decimals": 9, + "snapshotId": 1 + } + }, + "network": "56", + "addresses": [ + "0x2E9012CeE533a78D822ed3cdb0fe5C6F3A76092F", + "0x848AE3318A798bA0354c4B817e4005a5DE72ca69", + "0xA4F971216d178C840F03cEcbe945811378aFb25d", + "0x76Cb562A83C71B8457c9705072201740A4463100", + "0x08386c3e469420Ed44522C3AF38fC91713D2Cbbf", + "0x7EFc2150058EB91B3DDd885f6a6991C28E0De39d", + "0x59444C2bfB8b5ef089d630701d467AB6656D05Bc", + "0xf6f2B0C7f0c806Fb27FD2b16DC298997B957E0B5", + "0xac0ebcd2f8ed5a56F9765EBB820D3C081a1030A4", + "0x595f6cf9086Bc85d574100962ac201cDA7e5315F" + ], + "snapshot": 23711285 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/index.ts new file mode 100644 index 00000000..34049d01 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/index.ts @@ -0,0 +1,34 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'OfficialDevTeamSix'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOfAt(address account, uint256 snapshotId) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options +): Promise> { + const multi = new Multicaller(network, provider, abi); + addresses.forEach((address) => + multi.call(address, options.address, 'balanceOfAt', [ + address, + options.snapshotId + ]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/schema.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/schema.json new file mode 100644 index 00000000..094c4266 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-at/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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + }, + "snapshotId": { + "type": "number", + "title": "SnapshotID", + "examples": ["e.g. 5"] + } + }, + "required": ["address", "decimals", "snapshotId"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-coeff/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-coeff/examples.json new file mode 100644 index 00000000..771f6ab7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-coeff/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-coeff", + "params": { + "coeff": 2, + "symbol": "HYPR", + "address": "0x03d6bd3d48f956d783456695698c407a46ecd54d", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 13494433 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-coeff/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-coeff/index.ts new file mode 100644 index 00000000..3dac7f37 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-coeff/index.ts @@ -0,0 +1,28 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'jxde'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address: any) => [ + address[0], + address[1] * options.coeff + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/README.md new file mode 100644 index 00000000..c0b16697 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/README.md @@ -0,0 +1,29 @@ +# erc20-balance-of-contract-multiplier + +This returns the balances of the voters for a specific ERC20 token with a multiplier obtained from a contract call. + +Here is an example of parameters: + +```json +{ + "address": "0xb352A324283a51259f74fc9133b56A582671c836", + "symbol": "wTROVE", + "decimals": 9, + "contract_address": "0x4ef0191De9E85154161E6AD0E96fe0bb8D95892D", + // Arguments passed to the method + "args": [], + "methodABI": { + "inputs": [], + "name": "index", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/examples.json new file mode 100644 index 00000000..5f7eb396 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-contract-multiplier", + "params": { + "address": "0xb352A324283a51259f74fc9133b56A582671c836", + "symbol": "wTROVE", + "decimals": 9, + "contract_address": "0x4ef0191De9E85154161E6AD0E96fe0bb8D95892D", + "args": [], + "methodABI": { + "inputs": [], + "name": "index", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + } + }, + "network": "4", + "addresses": [ + "0x0B72513E73BB60375a4329FABF9BA16f861C0f12", + "0x9a82d59f46913913808bFE905F6157F967AAa28E" + ], + "snapshot": 9711086 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/index.ts new file mode 100644 index 00000000..b4936b64 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-contract-multiplier/index.ts @@ -0,0 +1,33 @@ +import { call } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'phoenix-keeper'; +export const version = '1.0.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const multiplier = await call( + provider, + [options.methodABI], + [options.contract_address, options.methodABI.name, options.args], + { blockTag } + ); + const scores = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(scores).map((score) => [score[0], score[1] * multiplier]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-cv/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-cv/examples.json new file mode 100644 index 00000000..276c9023 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-cv/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-cv", + "params": { + "symbol": "ICAP", + "address": "0xd83C569268930fadAd4cdE6D0cB64450fef32b65", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 13494433 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-cv/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-cv/index.ts new file mode 100644 index 00000000..b2612e4e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-cv/index.ts @@ -0,0 +1,25 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address) => [address[0], Math.sqrt(address[1])]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-delegation/examples.json new file mode 100644 index 00000000..df88e9b9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-delegation/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-delegation", + "params": { + "symbol": "POHD", + "address": "0x1dAD862095d40d43c2109370121cf087632874dB", + "decimals": 0, + "delegationSpace": "poh.eth" + } + }, + "network": "1", + "addresses": [ + "0x3c13f2B56AF614aC6381265EcB3B619bA26CC641", + "0x048fee7c3279a24af0790b6b002ded42be021d2b", + "0x139a9032a46c3afe3456eb5f0a35183b5f189cae" + ], + "snapshot": 15705816 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-delegation/index.ts new file mode 100644 index 00000000..a7efca43 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-delegation/index.ts @@ -0,0 +1,44 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { getDelegations } from '../../utils/delegation'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; +export const dependOnOtherAddress = true; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const delegationSpace = options.delegationSpace || space; + const delegations = await getDelegations( + delegationSpace, + network, + addresses, + snapshot + ); + if (Object.keys(delegations).length === 0) return {}; + + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + Object.values(delegations).reduce((a: string[], b: string[]) => + a.concat(b) + ), + options, + snapshot + ); + + return Object.fromEntries( + addresses.map((address) => { + const addressScore = delegations[address] + ? delegations[address].reduce((a, b) => a + score[b], 0) + : 0; + return [address, addressScore]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-fixed-total/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-fixed-total/examples.json new file mode 100644 index 00000000..b4eacfd9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-fixed-total/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-fixed-total", + "params": { + "total": 50, + "symbol": "%SWRV", + "address": "0xB8BAa0e4287890a5F79863aB62b7F175ceCbD433", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 13494433 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-fixed-total/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-fixed-total/index.ts new file mode 100644 index 00000000..e4fc5e7a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-fixed-total/index.ts @@ -0,0 +1,29 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + const totalScore = Object.values(score).reduce((a, b) => a + b, 0); + return Object.fromEntries( + Object.entries(score).map((address) => [ + address[0], + (options.total * address[1]) / totalScore + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/README.md new file mode 100644 index 00000000..1629db0f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/README.md @@ -0,0 +1,23 @@ +# erc20-balance-of-indexed + +Extends the `erc20-balance-of` strategy to scale each voter's balance by an index value that increases over time. + +This is particularly useful for Olympus-style protocols that have a wrapped staked token whose value is +tied to an index. + +The contract located at the `indexAddress` parameter must have a function called `index` that returns a single +uint256 value, the result of which will be downscaled by the provided `decimals` and multiplied by +each user's token balance to arrive at their voting power. + +The index value may have a different number of decimals than the wrapped staked token, so configured this +via the `indexDecimals` parameter. + +Here is an example of parameters: + +{ + "symbol": "wsKLIMA", + "address": "0x6f370dba99E32A3cAD959b341120DB3C9E280bA6", + "indexAddress": "0x25d28a24Ceb6F81015bB0b2007D795ACAc411b4d", + "decimals": 18, + "indexDecimals": 9 +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/examples.json new file mode 100644 index 00000000..a543fdfb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Indexed Example", + "strategy": { + "name": "erc20-balance-of-indexed", + "params": { + "symbol": "wsKLIMA", + "address": "0x6f370dba99E32A3cAD959b341120DB3C9E280bA6", + "indexAddress": "0x25d28a24Ceb6F81015bB0b2007D795ACAc411b4d", + "decimals": 18, + "indexDecimals": 9 + } + }, + "network": "137", + "addresses": [ + "0x981d9439472DdF3f42613548b5589e46D6BBB8E8", + "0x1069B123853f835c0F12334f67771BDd5B3F4f45", + "0xbe80fd79e26ce0a605e5d2803e876f1b009d70cc", + "0x55c307cbe54a1c1c105838a9d0fd60b75d7ff951", + "0xD24FF5e32DE12be154B1C2eE7B731CBC45540E50" + ], + "snapshot": 23375927 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/index.ts new file mode 100644 index 00000000..3dce8e6c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-indexed/index.ts @@ -0,0 +1,37 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = '0xAurelius'; +export const version = '0.0.1'; + +const abi = ['function index() public view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + multi.call('index', options.indexAddress, 'index'); + + const result = await multi.execute(); + const index = parseFloat(formatUnits(result.index, options.indexDecimals)); + + const scores = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(scores).map((score) => [score[0], score[1] * index]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/README.md new file mode 100644 index 00000000..205dfdce --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/README.md @@ -0,0 +1,19 @@ +This is a variant of erc20-balance-of-delegation strategy that preserves voting power in case of quadratic voting. + +Problem with erc20-balance-of-delegation is that if used with quadratic voting, total voting power that delegate has is less than expected. +In other words, delegation causes voting power loss. To fix this, we created this strategy. + +Say there are two persons, with 100 vote credits each, in a quadratic voting setting. Person A delegates to person B. + +Using erc-20-balance-of-delegation: + +Voting Credits of Person B = Voting Credits of Person A + Voting Credits of Person B = 200 + +Person A could cast 10 (10^2=100) votes individually, similarly Person B could cast 10 (10^2=100) votes individually, so in total they could cast 20 votes. +But now, delegate can't cast 20 votes (20^2=400) because it needs 400 credits. + +Solution: + +Voting Credits of Person B = (Sqrt(Voting Credits of Person A) + Sqrt(Voting Credits of Person B))^2 = 400 + +Now the delegate can represent the sum of individual voting powers. diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/examples.json new file mode 100644 index 00000000..f05d25a5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-quadratic-delegation", + "params": { + "address": "0xd0a1e359811322d97991e03f863a0c30c2cf029c", + "symbol": "WETH", + "decimals": 18 + } + }, + "network": "42", + "addresses": [ + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0xe7Bf80807514d91B8B6321044CA2785d59541d5c", + "0xe105766f0156DAec8a46E407E35634542fA012E6", + "0xeEFA7451c03d52ce909A93654664c46cf81DdD21" + ], + "snapshot": 21977746 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/index.ts new file mode 100644 index 00000000..3fd04869 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-quadratic-delegation/index.ts @@ -0,0 +1,40 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { getDelegations } from '../../utils/delegation'; + +export const author = 'ferittuncer'; +export const version = '1.0.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const delegations = await getDelegations(space, network, addresses, snapshot); + if (Object.keys(delegations).length === 0) return {}; + + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + Object.values(delegations).reduce((a: string[], b: string[]) => + a.concat(b) + ), + options, + snapshot + ); + + return Object.fromEntries( + addresses.map((address) => { + const addressScore = delegations[address] + ? delegations[address].reduce( + (a, b) => (Math.sqrt(a) + Math.sqrt(score[b])) ^ 2, + 0 + ) + : 0; + return [address, addressScore]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/README.md new file mode 100644 index 00000000..bccd152a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/README.md @@ -0,0 +1,14 @@ +# erc20-balance-of-weighted + +This returns the balances of the voters for a specific ERC20 token with a weight multiplier applied. + +Here is an example of parameters: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18, + "weight": 0.5 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/examples.json new file mode 100644 index 00000000..473f48d6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-weighted", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18, + "weight": 0.5 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/index.ts new file mode 100644 index 00000000..750079e5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-weighted/index.ts @@ -0,0 +1,25 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'Tanz0rz'; +export const version = '1.0.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const scores = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(scores).map((score) => [score[0], score[1] * options.weight]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/README.md new file mode 100644 index 00000000..6c1014d5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/README.md @@ -0,0 +1,13 @@ +# erc20-balance-of-with-delegation + +Similar to `with-delegation` strategy, but it uses `erc20-balance-of` strategy to calculate voting power + +## Params + +| Param Name | Description | +| ---------- | ----------- | +| address | The address of the token contract | +| symbol | The symbol of the token | +| decimals | The number of decimals of the token | +| delegationSpace (optional) | Get delegations of a particular space (by default it take delegations of current space) | +| delegationNetwork (optional) | Get delegations of a particular network (by default it take delegations of current network) | diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/examples.json new file mode 100644 index 00000000..065aca67 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-with-delegation", + "params": { + "symbol": "POHD", + "address": "0x1dAD862095d40d43c2109370121cf087632874dB", + "decimals": 0, + "delegationSpace": "poh.eth" + } + }, + "network": "1", + "addresses": [ + "0x3c13f2B56AF614aC6381265EcB3B619bA26CC641", + "0x048fee7c3279a24af0790b6b002ded42be021d2b", + "0x139a9032a46c3afe3456eb5f0a35183b5f189cae" + ], + "snapshot": 15705816 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/index.ts new file mode 100644 index 00000000..f4096b7d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of-with-delegation/index.ts @@ -0,0 +1,33 @@ +import { strategy as withDelegationStrategy } from '../with-delegation'; + +export const author = 'snapshot-labs'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + return await withDelegationStrategy( + space, + network, + provider, + addresses, + { + ...options, + strategies: [ + { + name: 'erc20-balance-of', + params: { + address: options.address, + decimals: options.decimals + } + } + ] + }, + snapshot + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/README.md new file mode 100644 index 00000000..e0af42a1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/README.md @@ -0,0 +1,13 @@ +# erc20-balance-of + +This is the most common strategy, it returns the balances of the voters for a specific ERC20 token. + +Here is an example of parameters: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/examples.json new file mode 100644 index 00000000..a763c30b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/index.ts new file mode 100644 index 00000000..ead8f551 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/index.ts @@ -0,0 +1,34 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +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) => + multi.call(address, options.address, 'balanceOf', [address]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/schema.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/schema.json new file mode 100644 index 00000000..2113da8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-balance-of/schema.json @@ -0,0 +1,33 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-price/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-price/README.md new file mode 100644 index 00000000..8bcf5ec0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-price/README.md @@ -0,0 +1,20 @@ +# erc20-price + +This strategy returns the balances of the voters for a specific ERC20 token multiplied by price of the token. +Strategy uses coingecko api to fetch the price from particular timestamp of the block + +Here is an example of parameters: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 +} +``` + +Other Parameters: + +`platform` parameter for coingecko api, which can be found [here](https://api.coingecko.com/api/v3/asset_platforms) + + `currency` parameter (defaulted to `usd`) to change the currency of the price \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-price/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-price/examples.json new file mode 100644 index 00000000..7a4a59fc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-price/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-price", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-price/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-price/index.ts new file mode 100644 index 00000000..9c64ccdc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-price/index.ts @@ -0,0 +1,63 @@ +import fetch from 'cross-fetch'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'snapshot-labs'; +export const version = '0.0.1'; + +const networksWithPlatforms = { + 1: 'ethereum', + 56: 'binance-smart-chain', + 66: 'okex-chain', + 88: 'tomochain', + 100: 'xdai', + 128: 'huobi-token', + 137: 'polygon-pos', + 250: 'fantom', + 42220: 'celo', + 43114: 'avalanche', + 1666600000: 'harmony-shard-0' +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const block = await provider.getBlock(blockTag); + const platform = options.platform + ? options.platform + : networksWithPlatforms[network]; + const { address, currency = 'usd' } = options; + const coingeckoApiURL = `https://api.coingecko.com/api/v3/coins/${platform}/contract/${address}/market_chart/range?vs_currency=${currency}&from=${ + block.timestamp - 100000 + }&to=${block.timestamp}`; + const coingeckoData = await fetch(coingeckoApiURL) + .then(async (r) => { + const json = await r.json(); + return json; + }) + .catch((e) => { + console.error(e); + throw new Error('Strategy er20-price: coingecko api failed'); + }); + + const latestPriceFromBlock = coingeckoData.prices?.pop()?.pop() || 0; + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address: any) => [ + address[0], + address[1] * latestPriceFromBlock + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/README.md new file mode 100644 index 00000000..72e244f1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/README.md @@ -0,0 +1,13 @@ +# erc20-rebase-wrapper + +This returns the underlying token balance from an ERC20 rebasing wrapper that exposes `exchangeRate` and `exchangeRatePrecision` methods. + +Here is an example of parameters: + +```json +{ + "wrapperAddress": "0x93Dede06AE3B5590aF1d4c111BC54C3f717E4b35", + "symbol": "gALCX", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/examples.json new file mode 100644 index 00000000..b9b291ea --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-rebase-wrapper", + "params": { + "wrapperAddress": "0x93Dede06AE3B5590aF1d4c111BC54C3f717E4b35", + "symbol": "gALCX", + "decimals": 18 + } + }, + "network": "1", + "addresses": ["0x628Ece38B0FF97e1e98F4F79aD5500596deF66F9"], + "snapshot": 14399823 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/index.ts new file mode 100644 index 00000000..f1bc31b0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-rebase-wrapper/index.ts @@ -0,0 +1,45 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = '0xfoobar'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function exchangeRate() external view returns (uint256)', + 'function exchangeRatePrecision() external view returns (uint256)' +]; + +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 }); + multi.call('exchangeRate', options.wrapperAddress, 'exchangeRate'); + multi.call( + 'exchangeRatePrecision', + options.wrapperAddress, + 'exchangeRatePrecision' + ); + const { exchangeRate, exchangeRatePrecision } = await multi.execute(); + const rate = parseFloat(exchangeRate) / parseFloat(exchangeRatePrecision); + + addresses.forEach((address) => + multi.call(address, options.wrapperAddress, 'balanceOf', [address]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) * rate + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-received/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-received/README.md new file mode 100644 index 00000000..fc81c66d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-received/README.md @@ -0,0 +1,81 @@ +# Contract call strategy + +Scores addresses by how much ERC20 token they have sent to `params.receivingAddresses`, adding `params.coeff` tokens to their score for every 1 token sent. + +This creates a new fundraising opportunity for projects & organizations, levels the playing field for less wealthy participants, and encourages voters to "put their money where their mouth is". + +## Params + +- `contractAddress` - (**Required**, `string`) Address of ERC20 token contract +- `decimals` - (**Required**, `number`) Decimal precision for ERC20 token +- `receivingAddresses` - (**Required**, `string[]`) Array of addresses to check for ERC20 transactions from voters +- `coeff` - (**Optional**, `number`, Default: `1`) Amount to multiply the sum of a voter's ERC20 sent to `receivingAddresses`. When used in conjunction with other strategies, this enables the increase or decrease of leverage given to voter who send tokens. +- `dfuseApiKey` - (**Optional**, `string`, Default: contributor's test key) - Customize Dfuse API key + + +## Examples + +Can be used instead of, or in conjunction with erc20-balance-of strategy. +In this example, the `params.coeff` of `100` makes a 1 token donation equivalent to a balance of 100 tokens. Thus, giving voters a massive incentive to donate. + +The space config will look like this: + +```JSON +"strategies": [ + { + "name": "Example ERC20-Received Strategy", + "strategy": { + "name": "erc20-received", + "params": { + "coeff": 100, + "decimals": 18, + "contractAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", + "receivingAddresses": [ + "0x65689471339798e1dae0d2ffb61073bf4e3765e4", + "0x42b5d4a11c8fe76d114759f7f3d8e94ea28bdbe5", + "0xcad6f7e74a34a5adf550e30b1e397d2c82bb1b1c", + "0x95b2271039b020aba31b933039e042b60b063800" + ] + } + }, + "network": "1", + "addresses": [ + "0x062d413463fc6b5c4a096cb1cebae77c6d834222", + "0xca00e2502f713ebf5e58bbb21930594af6988a4c", + "0xb83074760468be269e4dc834862525f8daf6626a", + "0x88fb0e1c8367af92d90b534888c789233360e53d" + ], + "snapshot": 11414195 + }, + { + "name": "Example ERC20-Balance Strategy", + "strategy": { + "name": "erc20-balance", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x062d413463fc6b5c4a096cb1cebae77c6d834222", + "0xca00e2502f713ebf5e58bbb21930594af6988a4c", + "0xb83074760468be269e4dc834862525f8daf6626a", + "0x88fb0e1c8367af92d90b534888c789233360e53d" + ], + "snapshot": 11414195 + } +] +``` + +Valid test addresses and snapshot block number: +```typescript +const addresses = [ + "0x062d413463fc6b5c4a096cb1cebae77c6d834222", + "0xca00e2502f713ebf5e58bbb21930594af6988a4c", + "0xb83074760468be269e4dc834862525f8daf6626a", + "0x88fb0e1c8367af92d90b534888c789233360e53d" +]; + +const snapshot = 11414195; +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-received/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-received/examples.json new file mode 100644 index 00000000..0d95d671 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-received/examples.json @@ -0,0 +1,52 @@ +[ + { + "name": "Example ERC20 Received Strategy", + "strategy": { + "name": "erc20-received", + "params": { + "decimals": 18, + "contractAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "receivingAddresses": [ + "0x65689471339798e1dae0d2ffb61073bf4e3765e4", + "0x42b5d4a11c8fe76d114759f7f3d8e94ea28bdbe5", + "0xcad6f7e74a34a5adf550e30b1e397d2c82bb1b1c", + "0x95b2271039b020aba31b933039e042b60b063800" + ] + } + }, + "network": "1", + "addresses": [ + "0x062d413463fc6b5c4a096cb1cebae77c6d834222", + "0xca00e2502f713ebf5e58bbb21930594af6988a4c", + "0xb83074760468be269e4dc834862525f8daf6626a", + "0x88fb0e1c8367af92d90b534888c789233360e53d" + ], + "snapshot": 11486650 + }, + { + "name": "Example ERC20 Received Strategy", + "strategy": { + "name": "erc20-received", + "params": { + "coeff": 100, + "decimals": 18, + "contractAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", + "receivingAddresses": [ + "0x65689471339798e1dae0d2ffb61073bf4e3765e4", + "0x42b5d4a11c8fe76d114759f7f3d8e94ea28bdbe5", + "0xcad6f7e74a34a5adf550e30b1e397d2c82bb1b1c", + "0x95b2271039b020aba31b933039e042b60b063800" + ] + } + }, + "network": "1", + "addresses": [ + "0x062d413463fc6b5c4a096cb1cebae77c6d834222", + "0xca00e2502f713ebf5e58bbb21930594af6988a4c", + "0xb83074760468be269e4dc834862525f8daf6626a", + "0x88fb0e1c8367af92d90b534888c789233360e53d" + ], + "snapshot": 11486650 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-received/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-received/index.ts new file mode 100644 index 00000000..99c8bba0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-received/index.ts @@ -0,0 +1,143 @@ +import fetch from 'cross-fetch'; +import { Web3Provider } from '@ethersproject/providers'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'mccallofthewild'; +export const version = '0.1.0'; + +export async function strategy( + ...args: [ + string, + string, + Web3Provider, + string[], + { + coeff?: number; + receivingAddresses: string[]; + contractAddress: string; + decimals: number; + dfuseApiKey?: string; + }, + number + ] +) { + const [, , provider, addresses, options] = args; + const { + coeff = 1, + dfuseApiKey = 'server_806bdc9bb370dad11ec5807e82e57fa0', + receivingAddresses, + contractAddress, + decimals + } = options; + + const loadJWT = async (dfuseApiKey: string): Promise => + fetch('https://auth.dfuse.io/v1/auth/issue', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ api_key: dfuseApiKey }) + }) + .then((r) => r.json()) + .then((r) => r.token); + + const { + data: { + searchTransactions: { edges } + } + } = await fetch('https://mainnet.eth.dfuse.io/graphql', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${await loadJWT(dfuseApiKey)}` + }, + body: JSON.stringify({ + query: /* GraphQL */ ` + query ( + $query: String! + $sort: SORT + $low: Int64 + $high: Int64 + $limit: Int64 + ) { + searchTransactions( + indexName: LOGS + query: $query + sort: $sort + lowBlockNum: $low + highBlockNum: $high + limit: $limit + ) { + edges { + node { + matchingLogs { + topics + data + } + } + } + } + } + `, + variables: { + query: `address: '${contractAddress}' topic.0:'Transfer(address,address,uint256)' (${addresses + .map((a) => `topic.1:'${a}'`) + .join(' OR ')}) (${receivingAddresses + .map((a) => `topic.2:'${a}'`) + .join(' OR ')})`, + sort: 'ASC', + limit: 0, + high: await provider.getBlockNumber() + } + }) + }) + .then(async (r) => { + const json = await r.json(); + if (json.errors) throw json.errors; + return json; + }) + .catch((e) => { + console.error(e); + throw new Error('Strategy ERC20-Received: Dfuse Query Failed'); + }); + + const matchingLogs = edges.reduce( + (prev, edge) => [...prev, ...edge.node.matchingLogs], + [] + ); + + const txLogs: { + from: string; + to: string; + amount: BigNumber; + }[] = matchingLogs.map((log) => { + const [, from, to] = log.topics.map((t) => + t.replace('0x000000000000000000000000', '0x') + ); + const amount = BigNumber.from(log.data); + return { + from, + to, + amount + }; + }); + + const scores = {}; + for (const address of addresses) { + const logsWithAddress = txLogs.filter((log) => { + const validAddress = log.from.toLowerCase() == address.toLowerCase(); + return validAddress; + }); + // Sum of all transfers + scores[address] = logsWithAddress.reduce((prev, curr) => { + return ( + prev + + parseFloat(formatUnits(curr.amount, BigNumber.from(decimals))) * coeff + ); + }, 0); + } + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/README.md new file mode 100644 index 00000000..6518e713 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/README.md @@ -0,0 +1,19 @@ +# erc20-token-and-lp-weighted + +This strategy works on Uniswap v2 style pools or contracts utilising token0/token1 and reserves. + +This strategy calculates the qty of the specified token within a single LP, doubles it to account for both sides, and then uses it as a weight against the users LP balance. + +This strategy also additionally adds the users token balance to give a token weighted score. + +This is useful if you want to be inclusive of LP and token holdings and need to scale them to be balanced with each other. + +Here is an example of parameters: + +```json +{ + "tokenAddress": "0x88ACDd2a6425c3FaAE4Bc9650Fd7E27e0Bebb7aB", + "symbol": "MIST", + "lpTokenAddress": "0xcd6bcca48069f8588780dfa274960f15685aee0e" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/examples.json new file mode 100644 index 00000000..20b2f8ba --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-token-and-lp-weighted", + "params": { + "tokenAddress": "0x88ACDd2a6425c3FaAE4Bc9650Fd7E27e0Bebb7aB", + "symbol": "MIST", + "lpTokenAddress": "0xcd6bcca48069f8588780dfa274960f15685aee0e" + } + }, + "network": "1", + "addresses": [ + "0xa7396dbd79c26f923faf144b7c77c9eb1ae0b634", + "0x9eedca1b19aa0d438fcf890524e6af056d95dd58", + "0xf7b4ddd4805455e108f0be5b417434c0aac88ddf", + "0xf4b1b77f3f56b58555634b64fb7ed31c60413754", + "0x1cd5698dc7cbe5b80c8b60791a8e2fb857a53a58", + "0xcf576f74ba3b01cdc04e04305055446d1649bd07", + "0x6f1234e31830097b04a8f9b3eeedf9eb8008837a", + "0xc5690c1f0f0642e76c956580b748b64cc5b3dc73", + "0x0504c6e9563d8756cb823fcbbf342301ddbc8066", + "0xee7f963391afb7d1f1ec872dbe2f80ea025d0add" + ], + "snapshot": 15206855 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/index.ts new file mode 100644 index 00000000..59c3099e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/index.ts @@ -0,0 +1,92 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, multicall } from '../../utils'; + +export const author = 'joehquak'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function decimals() external view returns (uint8)', + 'function token0() external view returns (address)', + 'function token1() external view returns (address)', + 'function totalSupply() external view returns (uint256)', + 'function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // fetch all token and lp contract data + + const fetchContractData = await multicall( + network, + provider, + abi, + [ + [options.lpTokenAddress, 'token0', []], + [options.lpTokenAddress, 'token1', []], + [options.lpTokenAddress, 'getReserves', []], + [options.lpTokenAddress, 'totalSupply', []], + [options.lpTokenAddress, 'decimals', []], + [options.tokenAddress, 'decimals', []] + ], + { blockTag } + ); + + // assign multicall data to variables + + const token0Address = fetchContractData[0][0]; + const token1Address = fetchContractData[1][0]; + const lpTokenReserves = fetchContractData[2]; + const lpTokenTotalSupply = fetchContractData[3][0]; + const lpTokenDecimals = fetchContractData[4][0]; + const tokenDecimals = fetchContractData[5][0]; + + // calculate single lp token weight + + let tokenWeight; + + if (token0Address === options.tokenAddress) { + tokenWeight = + (parseFloat(formatUnits(lpTokenReserves._reserve0, tokenDecimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals))) * + 2; + } else if (token1Address === options.tokenAddress) { + tokenWeight = + (parseFloat(formatUnits(lpTokenReserves._reserve1, tokenDecimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals))) * + 2; + } else { + tokenWeight = 0; + } + + const lpBalances = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + lpBalances.call(address, options.lpTokenAddress, 'balanceOf', [address]) + ); + const lpBalancesResult: Record = + await lpBalances.execute(); + + const tokenBalances = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + tokenBalances.call(address, options.tokenAddress, 'balanceOf', [address]) + ); + const tokenBalancesResult: Record = + await tokenBalances.execute(); + + return Object.fromEntries( + Object.entries(lpBalancesResult).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, lpTokenDecimals)) * tokenWeight + + parseFloat(formatUnits(tokenBalancesResult[address], tokenDecimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/schema.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/schema.json new file mode 100644 index 00000000..f937c97e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-token-and-lp-weighted/schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "symbol": { + "type": "string", + "title": "Token symbol", + "examples": ["e.g. MIST"], + "maxLength": 16 + }, + "tokenAddress": { + "type": "string", + "title": "Token address", + "examples": ["e.g. 0x88ACDd2a6425c3FaAE4Bc9650Fd7E27e0Bebb7aB"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "lpTokenAddress": { + "type": "string", + "title": "LP address", + "examples": ["e.g. 0xcd6bcca48069f8588780dfa274960f15685aee0e"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["tokenAddress", "lpTokenAddress"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/README.md new file mode 100644 index 00000000..e28c312f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/README.md @@ -0,0 +1,7 @@ +# ERC20 Tokens Per Uni + +## Description + +This snapshot strategy provides one voting power unit for each unit of a given ERC20 token included in the LP. + +It is a generic strategy that can be re-used for any ERC20 token and LP token. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/examples.json new file mode 100644 index 00000000..a2e8c6a3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Strategy provides one voting power unit for each unit of a given ERC20 token included in the LP.", + "strategy": { + "name": "erc20-tokens-per-uni", + "params": { + "erc20TokenAddress": "0x385Eeac5cB85A38A9a07A70c73e0a3271CfB54A7", + "poolTokenAddress": "0xb0E35478a389dD20050D66a67FB761678af99678" + } + }, + "network": "137", + "addresses": [ + "0x26cf02F892B04aF4Cf350539CE2C77FCF79Ec172", + "0x027Ffd3c119567e85998f4E6B9c3d83D5702660c" + ], + "snapshot": 33423178 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/index.ts new file mode 100644 index 00000000..4ed23398 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-tokens-per-uni/index.ts @@ -0,0 +1,56 @@ +import { multicall } from '../../utils'; + +export const author = 'programmablewealth'; +export const version = '0.0.1'; + +const tokenAbi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function totalSupply() view returns (uint256)' +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const poolTokensBalanceQueries = addresses.map((address: string) => [ + options.poolTokenAddress, + 'balanceOf', + [address] + ]); + + const res = await multicall( + network, + provider, + tokenAbi, + [ + ...poolTokensBalanceQueries, + [options.poolTokenAddress, 'totalSupply', []], + [options.erc20TokenAddress, 'balanceOf', [options.poolTokenAddress]] + ], + { blockTag } + ); + + const tokensPerUni = (balanceInUni: number, totalSupply: number) => { + return balanceInUni / 1e18 / (totalSupply / 1e18); + }; + + const entries = {}; + for (let addressIndex = 0; addressIndex < addresses.length; addressIndex++) { + const address = addresses[addressIndex]; + const result = + res[addressIndex] * + tokensPerUni( + res[poolTokensBalanceQueries.length + 1], + res[poolTokensBalanceQueries.length] + ); + entries[address] = Number(result.toString()) / 1e18; + } + + return entries; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/README.md new file mode 100644 index 00000000..2af72266 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/README.md @@ -0,0 +1,137 @@ +# ERC-20 Votes with Override + +- [Overview](#overview) +- [Example](#example) +- [Snapshot Delegations](#snapshot-delegations) +- [Example With Snapshot Delegations](#example-with-snapshot-delegations) +- [Options](#options) + + +## Overview + +This strategy is similar to [ERC-20 Votes](../erc20-votes), except that it also allows individual delegators to **override** their vote on a particular proposal if they wish. This is most useful for social (off-chain only) proposals. + +If an account has any delegated voting power returned from getVotes, adds that value, minus the balances from any delegators that have also individually voted. + +If an account is delegating to itself, then its own token balance will already be included in the getVotes return value. + +If an account is delegating to a different valid address, adds the local token balance. The account must be delegated to another valid address, otherwise the local token balance will not be added. + + +## Example + +Say you have accounts [A,B,C], each with token balances [100,200,300], and they are delegated on-chain like so: +![Delegation Example 1](https://i.imgur.com/loMPDiu.png) + +The on-chain voting power with these delegations (using the standard [OpenZeppelin ERC20Votes](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Votes.sol) system) is then: +| A | B | C | +| ------------- | ------------- | ------------- | +| 0 | 100 | 500 | + +With the regular [erc20-votes strategy](https://github.com/snapshot-labs/snapshot-strategies/tree/master/src/strategies/erc20-votes), each account would only have access to its own on-chain delegated voting power, and delegators without voting power will not be able to vote on Snapshot proposals. In the scenario above, account A would not be able to vote at all. And account B would still be able to vote even though it is delegating to C, but it would only have access to the 100 voting power from A. + +With this **erc20-votes-with-override** strategy, now individual delegators will be able to vote on Snapshot proposals and access their own token balance as voting power, without needing to perform any additional on-chain transactions. + +Here are the scores that would be given using this override strategy, depending on who votes: +| Voters | Score A | Score B | Score C | +| ------------- | ------------- | ------------- | ------------- | +| A | 100 | | | +| B | | 300 | | +| C | | | 500 | +| A,B | 100 | 200 | | +| A,C | 100 | | 500 | +| B,C | | 300 | 300 | +| A,B,C | 100 | 200 | 300 | + +When an account votes, it will have access to its own token balance, and also any delegated voting power, _minus_ the balances of any delegators that have also voted. That's how it ensures that delegators can "override" their delegates but still no double-counting happens. + + +## Snapshot Delegations + +Accounts can also delegate [via Snapshot](https://docs.snapshot.org/guides/delegation) as well. + +If the `includeSnapshotDelegations` option is enabled, then Snapshot delegations will also be taken into account. In this case, the `isSnapshotDelegatedScore` option will determine whether the delegated or non-delegated scores will be returned. This is done because the overridden voting power calculation is not compatible with the standard [delegation](../delegation) strategy ([see below](#example-with-snapshot-delegations)). So instead, space admins can use this strategy **twice**, each with `includeSnapshotDelegations` enabled and the `isSnapshotDelegatedScore` enabled or disabled. + +Here is an example of enabling Snapshot delegations using this strategy twice: + +Delegated strategy: + +``` +{ + "symbol": "ENS (delegated)", + "address": "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + "decimals": 18, + "includeSnapshotDelegations": true, + "isSnapshotDelegatedScore": true +} +``` + +Non-delegated strategy: + +``` +{ + "symbol": "ENS", + "address": "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + "decimals": 18, + "includeSnapshotDelegations": true, + "isSnapshotDelegatedScore": false +} +``` + + +## Example With Snapshot Delegations + +Take the same example [from above](#example), with accounts [A,B,C], each with token balances [100,200,300]. This time we'll also add some Snapshot delegations: +![Delegation Example 2](https://i.imgur.com/bb2rC5J.png) + +The regular [delegation](../delegation) strategy doesn't work here, specifically because of the "override" mechanism. It calculates the list of delegators that were not in the original address list, and passes **only those delegators** into the underlying strategy. So the underlying strategy has no idea that other addresses voted as well. Because of that, using the standard delegation strategy with this override strategy can lead to double-counting. + +Taking our example from above, here are the Snapshot delegated scores that occur when using the regular [delegation](../delegation) strategy: +| Voters | Score A | Score B | Score C | +| ------------- | ------------- | ------------- | ------------- | +| A | 0 | | | +| B | | 0 | | +| C | | | 300 | +| A,B | 0 | 0 | | +| A,C | 0 | | 300 | +| B,C | | 0 | 100 | +| A,B,C | 0 | 0 | 0 | + +So in this case the total scores, when both the regular and delegated strategies are used together, are: +| Voters | Score A | Score B | Score C | +| ------------- | ------------- | ------------- | ------------- | +| A | 100 | | | +| B | | 300 | | +| C | | | 800 | +| A,B | 100 | 200 | | +| A,C | 100 | | 800 | +| B,C | | 300 | 400 | +| A,B,C | 100 | 200 | 300 | + +You can see that in some scenarios double-counting occurs, in [C], [A,C], and [B,C]. + +This override strategy addresses this by retrieving the Snapshot delegations directly, and then calculating the scores as if all those delegators were also voting. Take the scenario where only C votes. This strategy will: + +- Retrieve Snapshot delegators [A,B] +- [A,B] are added to the total address list, so it becomes [A,B,C] +- The regular calculations are done, so: {A: 100, B: 200, C: 300} +- Depending on the value of `isSnapshotDelegatedScore`: + - If false, only the score for C is returned: {C: 300} + - If true, the scores for all Snapshot delegators of C are summed up, so {A:100,B:200} is summed and the strategy returns {C: 300} +- The final score is then C: 300 + 300 delegated, which is correct + + +## Options + +- **address:** The address of the ERC-20 token contract. +- **symbol:** The display symbol for the token, e.g. "ENS". +- **decimals:** Used to display the correct base units for the token. +- **includeSnapshotDelegations:** Optional. If enabled, Snapshot delegations are taken into account. See description above. +- **isSnapshotDelegatedScore:** Optional. Only used if `includeSnapshotDelegations` is enabled. If true, the delegated score will be returned, otherwise the non-delegated score will be returned. See description above. +- **delegationSpace:** Optional. Only used if `includeSnapshotDelegations` is enabled. Determines what specific Snapshot space to retrieve delegations for. +- **getVotesName:** Optional. The function name of the [getVotes](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Votes.sol#L64) function, e.g. "getVotes". +- **getVotesABI:** Optional. The ABI specification for the getVotes function. +- **balanceOfName:** Optional. The function name of the [balanceOf](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol#L18) function, e.g. "balanceOf". +- **balanceOfABI:** Optional. The ABI specification for the balanceOf function. +- **delegatesName:** Optional. The function name of the [delegates](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Votes.sol#L57) function, e.g. "delegates". +- **delegatesABI:** Optional. The ABI specification for the delegates function. diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/examples.json new file mode 100644 index 00000000..111e6ac6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-votes-with-override", + "params": { + "address": "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + "symbol": "ENS", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x0000000000000000000000000000000000000001", + "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85", + "0xc23ca051fc6ad307bb07c1b27e22391bc8f22a44", + "0x983110309620d911731ac0932219af06091b6744" + ], + "snapshot": 14346755 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/index.ts new file mode 100644 index 00000000..783d8dfe --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes-with-override/index.ts @@ -0,0 +1,271 @@ +import { formatUnits } from '@ethersproject/units'; +import { isAddress } from '@ethersproject/address'; +import { multicall } from '../../utils'; +import { getDelegations } from '../../utils/delegation'; + +export const author = 'serenae-fansubs'; +export const version = '0.1.0'; + +const getVotesName = 'getVotes'; +const getVotesABI = [ + 'function getVotes(address account) view returns (uint256)' +]; +const balanceOfName = 'balanceOf'; +const balanceOfABI = [ + 'function balanceOf(address account) view returns (uint256)' +]; +const delegatesName = 'delegates'; +const delegatesABI = [ + 'function delegates(address account) view returns (address)' +]; + +/* + Counts votes from delegates, and also from individual delegators who wish + to override the vote of their delegate. + + Makes three multicalls for votes, delegates, and balances. + + If an account has any delegated voting power returned from getVotes, + adds that value, minus the balances from any delegators that have also + individually voted. + + If an account is delegating to itself, then its own token balance will + already be included in the getVotes return value. + + If an account is delegating to a different valid address, adds the local + token balance. The account must be delegated to another valid address, + otherwise the local token balance will not be added. + + The function names/ABI can be overridden in the options. + + If the "includeSnapshotDelegations" option is enabled, then one additional + request will be made to retrieve Snapshot delegations from the subgraph. + In this case, the "isSnapshotDelegatedScore" option will determine whether + the delegated or non-delegated scores will be returned. This is done + because the overridden voting power calculation is not compatible with the + standard "delegation" strategy. So instead, space admins can use this + strategy twice, each with "includeSnapshotDelegations" enabled and the + "isSnapshotDelegatedScore" enabled or disabled. +*/ +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const addressesLc = addresses.map((address: any) => lowerCase(address)); + + const includeSnapshotDelegations = !!options.includeSnapshotDelegations; + const isSnapshotDelegatedScore = + includeSnapshotDelegations && !!options.isSnapshotDelegatedScore; + + // If enabled, get Snapshot delegations. This will not include any delegators that are already in the addresses list. + const snapshotDelegations = includeSnapshotDelegations + ? await getDelegations( + options.delegationSpace || space, + network, + addressesLc, + snapshot + ) + : {}; + if (Object.keys(snapshotDelegations).length > 0) { + /* + If any Snapshot delegations were retrieved, add the delegators to the addresses list. + + The on-chain delegations, balances, and overridden voting power will be retrieved and + calculated with all these addresses present. + */ + Object.entries(snapshotDelegations).forEach(([, delegators]) => + delegators.forEach((delegator: string) => + addressesLc.push(lowerCase(delegator)) + ) + ); + } + + const delegatesResponse = await multicall( + network, + provider, + options.delegatesABI || delegatesABI, + addressesLc.map((address: any) => [ + options.address, + options.delegatesName || delegatesName, + [address] + ]), + { blockTag } + ); + const delegators = Object.fromEntries( + delegatesResponse + .map((value: any, i: number) => [ + addressesLc[i], + lowerCase(getFirst(value)) + ]) + .filter(([, delegate]) => isValidAddress(delegate)) + ); + + /* + Create reverse map from delegate to [delegators]. + The delegate itself will not be included in the delegators list. + */ + const delegates = Object.fromEntries( + addressesLc.map((address: string) => [ + address, + Object.entries(delegators) + .filter( + ([delegator, delegate]) => + address === delegate && delegator !== delegate + ) + .map(([delegator]) => delegator) + ]) + ); + + const balanceOfResponse = await multicall( + network, + provider, + options.balanceOfABI || balanceOfABI, + addressesLc.map((address: any) => [ + options.address, + options.balanceOfName || balanceOfName, + [address] + ]), + { blockTag } + ); + const balances = Object.fromEntries( + balanceOfResponse.map((value: any, i: number) => [ + addressesLc[i], + parseValue(value, options.decimals) + ]) + ); + + const getVotesResponse = await multicall( + network, + provider, + options.getVotesABI || getVotesABI, + addressesLc.map((address: any) => [ + options.address, + options.getVotesName || getVotesName, + [address] + ]), + { blockTag } + ); + // Calculate overridden voting power for all addresses, including the added delegators + const votes = Object.fromEntries( + getVotesResponse.map((value: any, i: number) => [ + addressesLc[i], + getVotesWithOverride( + addressesLc[i], + parseValue(value, options.decimals), + delegators, + delegates, + balances + ) + ]) + ); + + // Only return scores for the original address list + return Object.fromEntries( + addresses.map((address: any) => [ + address, + getScore( + isSnapshotDelegatedScore, + lowerCase(address), + votes, + snapshotDelegations + ) + ]) + ); +} + +function getScore( + isSnapshotDelegatedScore: boolean, + address: string, + votes: Record, + snapshotDelegations: Record> +): number { + /* + If the Snapshot delegated score is being used, defer to that method to calculate it. + Otherwise, just return the voting power calculated before. + */ + if (isSnapshotDelegatedScore) { + return getSnapshotDelegatedScore(address, votes, snapshotDelegations); + } else { + return votes[address]; + } +} + +function getSnapshotDelegatedScore( + address: string, + votes: Record, + snapshotDelegations: Record> +): number { + const delegatedScore = { score: 0 }; + + // Sum up the voting power from all accounts that have delegated to this address via Snapshot + const snapshotDelegators = snapshotDelegations[address]; + if (snapshotDelegators) { + snapshotDelegators.forEach( + (delegator: string) => + (delegatedScore.score += votes[lowerCase(delegator)]) + ); + } + + return delegatedScore.score; +} + +function getVotesWithOverride( + address: string, + votes: number, + delegators: Record, + delegates: Record>, + balances: Record +): number { + const adjustedVotes = { votes }; + + if (votes > 0) { + // Subtract any overridden votes + delegates[address].forEach( + (delegator: string) => (adjustedVotes.votes -= balances[delegator]) + ); + + /* + This should never happen because the OpenZeppelin getVotes method + returns the aggregated balances from all delegators. + + However, still checking just in case a different contract is used. + */ + if (adjustedVotes.votes < 0) { + adjustedVotes.votes = 0; + } + } + + if (isValidAddress(delegators[address]) && address !== delegators[address]) { + // Delegating to someone else, so add the local balance + adjustedVotes.votes += balances[address]; + } + + return adjustedVotes.votes; +} + +function parseValue(value: any, decimals: number): number { + return parseFloat(formatUnits(value.toString(), decimals)); +} + +function getFirst(value: any): any { + if (Array.isArray(value)) { + return value.length > 0 ? value[0] : null; + } + return value; +} + +function lowerCase(value: any): any { + return value ? value.toLowerCase() : value; +} + +function isValidAddress(address: string): boolean { + return ( + isAddress(address) && + address != '0x0000000000000000000000000000000000000000' + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-votes/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes/examples.json new file mode 100644 index 00000000..7c7d3d96 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-votes", + "params": { + "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", + "symbol": "ENS" + } + }, + "network": "1", + "addresses": [ + "0x2B384212EDc04Ae8bB41738D05BA20E33277bf33", + "0xAC5720d6EE2d7872b88914C9c5Fa9BF38e72FaF6" + ], + "snapshot": 12050071 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-votes/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes/index.ts new file mode 100644 index 00000000..2554aabd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-votes/index.ts @@ -0,0 +1,35 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +const abi = ['function getVotes(address account) view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'getVotes', + [address.toLowerCase()] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/README.md new file mode 100644 index 00000000..ab071578 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/README.md @@ -0,0 +1,25 @@ +# with-balance + +> Important Note: This strategy works for ERC721 contract too. + +This strategy checks if the voter has a balance of the token in the contract. returns `1` if the voter has a balance of the token in the contract. else returns `0`. + +## Parameters + +| Param Name | Description | +| ----------- | ----------- | +| address | Address of the contract | +| symbol (optional) | symbol | +| decimals (optional) | decimals | +| minBalance (optional) | Minimum balance check (Note that this value is exclusive, For example if you pass `1`, balance should be more than `1`) Default is `0` | + +Here is an example of parameters: + +```json +{ + "address": "0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419", + "symbol": "DIA", + "decimals": 18, + "minBalance": 10 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/examples.json new file mode 100644 index 00000000..66313593 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/examples.json @@ -0,0 +1,49 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-with-balance", + "params": { + "address": "0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419", + "symbol": "DIA", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x0000000000000000000000000000000000baddad", + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1", + "0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1", + "0x4C7909d6F029b3a5798143C843F4f8e5341a3473", + "0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419", + "0x72ac1760daf52986421b1552bdca04707e78950e" + ], + "snapshot": 11184248 + }, + { + "name": "Example query with minimum balance", + "strategy": { + "name": "erc20-with-balance", + "params": { + "address": "0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419", + "symbol": "DIA", + "decimals": 18, + "minBalance": 10 + } + }, + "network": "1", + "addresses": [ + "0x0000000000000000000000000000000000baddad", + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1", + "0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1", + "0x4C7909d6F029b3a5798143C843F4f8e5341a3473", + "0x84cA8bc7997272c7CfB4D0Cd3D55cd942B3c9419", + "0x72ac1760daf52986421b1552bdca04707e78950e" + ], + "snapshot": 11184248 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/index.ts new file mode 100644 index 00000000..8d29a46a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc20-with-balance/index.ts @@ -0,0 +1,28 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'BenjaminLu'; +export const version = '0.1.1'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address: any) => [ + address[0], + address[1] > (options.minBalance || 0) ? 1 : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/examples.json new file mode 100644 index 00000000..d35be8f4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc3525-flexible-voucher", + "params": { + "symbol": "fvSOLV", + "address": "0xb77Ca8CB421fAE5d4790a6f5EdAD97Cfb7868aD0", + "decimals": 18 + } + }, + "network": "4", + "addresses": ["0x1a71c8EF63aB6f578b1702a35367cA81c9281A8c"], + "snapshot": 11096688 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/index.ts new file mode 100644 index 00000000..b40c54de --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/index.ts @@ -0,0 +1,106 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { hexZeroPad } from '@ethersproject/bytes'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { claimCoefficient, maturitiesCoefficient } from './utils'; + +export const author = 'buchaoqun'; +export const version = '0.1.2'; + +const abi = [ + 'function getSnapshot(uint256 tokenId) view returns (tuple(tuple(address issuer, uint8 claimType, uint64 startTime, uint64 latestStartTime, uint64[] terms, uint32[] percentages, bool isValid), uint256 tokenId, uint256 vestingAmount))', + 'function balanceOf(address owner) view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner,uint256 index) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // flexible voucher banlanceOf + const callWalletToCrucibleCount = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToCrucibleCount.call( + walletAddress, + options.address, + 'balanceOf', + [walletAddress] + ); + } + + // wallet Owner Index + const walletToCrucibleCount: Record = + await callWalletToCrucibleCount.execute(); + + const callWalletToCrucibleAddresses = new Multicaller( + network, + provider, + abi, + { + blockTag + } + ); + for (const [walletAddress, crucibleCount] of Object.entries( + walletToCrucibleCount + )) { + for (let index = 0; index < crucibleCount.toNumber(); index++) { + callWalletToCrucibleAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToCrucibleAddresses: Record = + await callWalletToCrucibleAddresses.execute(); + + // voucher snapshot + const callCrucibleToSnapshot = new Multicaller(network, provider, abi, { + blockTag + }); + // walletID: walletAddress-index + for (const [walletID, crucibleAddress] of Object.entries( + walletIDToCrucibleAddresses + )) { + callCrucibleToSnapshot.call(walletID, options.address, 'getSnapshot', [ + hexZeroPad(crucibleAddress.toHexString(), 20) + ]); + } + const walletIDToSnapshot: Record< + string, + Array + > = await callCrucibleToSnapshot.execute(); + + const walletToWeights = {} as Record; + for (const [walletID, snapshot] of Object.entries(walletIDToSnapshot)) { + const address = walletID.split('-')[0]; + const value = + parseFloat(formatUnits(snapshot[2].toString(), options.decimals)) * + claimCoefficient(snapshot[0][1]) * + maturitiesCoefficient( + snapshot[0][2] == 0 + ? snapshot[0][3].toNumber() + : snapshot[0][2].toNumber(), + snapshot[0][4] + ); + walletToWeights[address] = walletToWeights[address] + ? walletToWeights[address] + value + : value; + } + + return Object.fromEntries( + Object.entries(walletToWeights).map(([address, balance]) => [ + address, + balance + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/utils.ts b/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/utils.ts new file mode 100644 index 00000000..4dbb6421 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc3525-flexible-voucher/utils.ts @@ -0,0 +1,40 @@ +const oneDaySeconds = 86400; + +export const maturitiesCoefficient = ( + latestStartTime: number, + terms: Array +) => { + const nowData = Date.parse(new Date().toString()) / 1000; + const difference = + latestStartTime + terms[terms.length - 1].toNumber() - nowData; + + if (difference <= 0) { + return 1; + } else if (difference > 0 && difference <= 90 * oneDaySeconds) { + return 1.1; + } else if ( + difference > 90 * oneDaySeconds && + difference <= 183 * oneDaySeconds + ) { + return 1.2; + } else if ( + difference > 183 * oneDaySeconds && + difference <= 365 * oneDaySeconds + ) { + return 1.5; + } else { + return 2; + } +}; + +export const claimCoefficient = (claimType: number) => { + if (claimType == 0) { + return 1.2; + } else if (claimType == 1) { + return 2; + } else if (claimType == 2) { + return 1.5; + } else { + return 1; + } +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/examples.json new file mode 100644 index 00000000..3e51d498 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc3525-vesting-voucher", + "params": { + "symbol": "fvSOLV", + "address": "0x4B0dd1aDEdA251ACec75140608bAd663fB0c4cAB", + "decimals": 18 + } + }, + "network": "4", + "addresses": ["0x1a71c8EF63aB6f578b1702a35367cA81c9281A8c"], + "snapshot": 11090903 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/index.ts new file mode 100644 index 00000000..edf5d5ca --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/index.ts @@ -0,0 +1,102 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { hexZeroPad } from '@ethersproject/bytes'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { claimCoefficient, maturitiesCoefficient } from './utils'; + +export const author = 'buchaoqun'; +export const version = '0.1.3'; + +const abi = [ + 'function getSnapshot(uint256 tokenId_) view returns (uint8 claimType_, uint64 term_, uint256 vestingAmount_, uint256 principal_, uint64[] maturities_, uint32[] percentages_, uint256 availableWithdrawAmount_, string originalInvestor_, bool isValid_)', + 'function balanceOf(address owner) view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner,uint256 index) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // vesting voucher banlanceOf + const callWalletToCrucibleCount = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToCrucibleCount.call( + walletAddress, + options.address, + 'balanceOf', + [walletAddress] + ); + } + + // wallet Owner Index + const walletToCrucibleCount: Record = + await callWalletToCrucibleCount.execute(); + + const callWalletToCrucibleAddresses = new Multicaller( + network, + provider, + abi, + { + blockTag + } + ); + for (const [walletAddress, crucibleCount] of Object.entries( + walletToCrucibleCount + )) { + for (let index = 0; index < crucibleCount.toNumber(); index++) { + callWalletToCrucibleAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToCrucibleAddresses: Record = + await callWalletToCrucibleAddresses.execute(); + + // voucher snapshot + const callCrucibleToSnapshot = new Multicaller(network, provider, abi, { + blockTag + }); + // walletID: walletAddress-index + for (const [walletID, crucibleAddress] of Object.entries( + walletIDToCrucibleAddresses + )) { + callCrucibleToSnapshot.call(walletID, options.address, 'getSnapshot', [ + hexZeroPad(crucibleAddress.toHexString(), 20) + ]); + } + const walletIDToSnapshot: Record< + string, + Array + > = await callCrucibleToSnapshot.execute(); + + const walletToWeights = {} as Record; + for (const [walletID, snapshot] of Object.entries(walletIDToSnapshot)) { + const address = walletID.split('-')[0]; + + const value = + parseFloat(formatUnits(snapshot[3].toString(), options.decimals)) * + claimCoefficient(snapshot[0]) * + maturitiesCoefficient(snapshot[4]); + walletToWeights[address] = walletToWeights[address] + ? walletToWeights[address] + value + : value; + } + + return Object.fromEntries( + Object.entries(walletToWeights).map(([address, balance]) => [ + address, + balance + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/utils.ts b/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/utils.ts new file mode 100644 index 00000000..417785e1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc3525-vesting-voucher/utils.ts @@ -0,0 +1,36 @@ +const oneDaySeconds = 86400; + +export const maturitiesCoefficient = (maturities: Array) => { + const nowData = Date.parse(new Date().toString()) / 1000; + const difference = maturities[maturities.length - 1].toNumber() - nowData; + + if (difference <= 0) { + return 1; + } else if (difference > 0 && difference <= 90 * oneDaySeconds) { + return 1.1; + } else if ( + difference > 90 * oneDaySeconds && + difference <= 183 * oneDaySeconds + ) { + return 1.2; + } else if ( + difference > 183 * oneDaySeconds && + difference <= 365 * oneDaySeconds + ) { + return 1.5; + } else { + return 2; + } +}; + +export const claimCoefficient = (claimType: number) => { + if (claimType == 0) { + return 1.2; + } else if (claimType == 1) { + return 2; + } else if (claimType == 2) { + return 1.5; + } else { + return 1; + } +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/README.md new file mode 100644 index 00000000..7325368f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/README.md @@ -0,0 +1,12 @@ +# erc721-collateralHeld + +This strategy returns the total collateral held by voters in ERC721 vaults. It takes into account the ERC721 tokens owned by each voter and then calculates the sum of the collateral held in the associated vaults. + +Here is an example of parameters: + +```json +{ + "address": "0xc76a3cbefe490ae4450b2fcc2c38666aa99f7aa0", + "symbol": "WEAMVT" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/examples.json new file mode 100644 index 00000000..5c0c21f4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-collateral-held", + "params": { + "address": "0x950eceee9e7d7366a24fc9d2ed4c0c37d17a0fa9", + "symbol": "AAMVT" + } + }, + "network": "42161", + "addresses": [ + "0x20798fd64a342d1ee640348e42c14181fdc842d8", + "0x985a29e88e75394dbdae41a269409f701ccf6a43", + "0x825c657e9e72c04b7dc1e92b947f2fa33d1127fb", + "0x14ccd6fb7c6d5693969f38c8ef5c08d77d260ce4" + ], + "snapshot": 84179668 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/index.ts new file mode 100644 index 00000000..126bde09 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/index.ts @@ -0,0 +1,81 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'publu'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function vaultCollateral(uint256 vaultId) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi1 = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + multi1.call(address, options.address, 'balanceOf', [address]) + ); + const result: Record = await multi1.execute(); + + // get the vault number owned by each user. + const multi2 = new Multicaller(network, provider, abi, { blockTag }); + const tokenIds: Record = Object.fromEntries( + addresses.map((address) => [address, []]) + ); + for (const address of addresses) { + const balance = result[address]; + for (let i = 0; i < balance; i++) { + multi2.call(`${address}_${i}`, options.address, 'tokenOfOwnerByIndex', [ + address, + i + ]); + } + } + const tokenIdsResult = await multi2.execute(); + + for (const key in tokenIdsResult) { + const [address] = key.split('_'); + tokenIds[address].push(tokenIdsResult[key]); + } + + // get the vaultCollateral by the tokenIds and store that as the amount held by the address + const multi3 = new Multicaller(network, provider, abi, { blockTag }); + for (const address in tokenIds) { + for (const tokenId of tokenIds[address]) { + multi3.call(address, options.address, 'vaultCollateral', [tokenId]); + } + } + const collaterals: Record = await multi3.execute(); + + const resultWithDefault = {}; + for (const address of addresses) { + const balance = collaterals[address] || 0; + resultWithDefault[address] = parseFloat( + formatUnits(balance, options.decimals) + ); + } + + // Convert addresses to checksum format before returning + const resultWithChecksum = {}; + for (const address in resultWithDefault) { + resultWithChecksum[getAddress(address)] = resultWithDefault[address]; + } + + // Check that at least one object with an address is returned + if (Object.keys(resultWithChecksum).length === 0) { + resultWithChecksum[getAddress(addresses[0])] = 0; + } + + return resultWithChecksum; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/schema.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/schema.json new file mode 100644 index 00000000..0017eeaa --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-collateral-held/schema.json @@ -0,0 +1,28 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["address"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-enumerable/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-enumerable/examples.json new file mode 100644 index 00000000..403f6e11 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-enumerable/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-enumerable", + "params": { + "address": "0x2216d47494e516d8206b70fca8585820ed3c4946", + "symbol": "WT" + } + }, + "network": "1", + "addresses": [ + "0x4A7998DF2Cd16815271bb6b7d3aE7EB30f50a73a", + "0x08D816526BdC9d077DD685Bd9FA49F58A5Ab8e48" + ], + "snapshot": 12353494 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-enumerable/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-enumerable/index.ts new file mode 100644 index 00000000..82fac782 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-enumerable/index.ts @@ -0,0 +1,23 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + options.decimals = 0; + return await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/README.md new file mode 100644 index 00000000..0bfe2ad9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/README.md @@ -0,0 +1,18 @@ +# erc721 + +This strategy returns the balances of the voters for a list of ERC721 NFT tokens applying a different weight for each. +Token at position `i` is assigned weight at position `i`. + +Here is an example of parameters [wizards, ponies, souls]: + +```json +{ + "symbol": "FRWC", + "tokens": [ + "0x521f9c7505005cfa19a8e5786a9c3c9c9f5e6f42", + "0xf55b615b479482440135ebf1b907fd4c37ed9420", + "0x251b5f14a825c537ff788604ea1b58e49b70726f" + ], + "weights": [2, 4, 8] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/examples.json new file mode 100644 index 00000000..5fe6f56b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/examples.json @@ -0,0 +1,30 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-multi-registry-weighted", + "params": { + "symbol": "FRWC", + "tokens": [ + "0x521f9c7505005cfa19a8e5786a9c3c9c9f5e6f42", + "0xf55b615b479482440135ebf1b907fd4c37ed9420", + "0x251b5f14a825c537ff788604ea1b58e49b70726f" + ], + "weights": [2, 4, 8] + } + }, + "network": "1", + "addresses": [ + "0x6bf97f2534be2242ddb3a29bfb24d498212dcded", + "0x7908a20be7f8ce24baa5c9cea46e3678c1ce3f4e", + "0x0abb75f676d0c131c04324dd98150a39213edd41", + "0x87931e7ad81914e7898d07c68f145fc0a553d8fb", + "0x17bc9268f8abb454f0c7ae4d76d969a37235480d", + "0xcaed230e3a8ac5fa16595810ade9cb739f47f6ba", + "0xf296178d553c8ec21a2fbd2c5dda8ca9ac905a00", + "0xd584fe736e5aad97c437c579e884d15b17a54a51", + "0x90e5aa59a9df2add394df81521dbbed5f3c4a1a3" + ], + "snapshot": 14173407 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/index.ts new file mode 100644 index 00000000..84595cf5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry-weighted/index.ts @@ -0,0 +1,41 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'Meph1587'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const calls: any[] = []; + const multipliers: any[] = []; + options.tokens.map((token, idx) => { + addresses.forEach((address: any) => { + calls.push([token, 'balanceOf', [address]]); + multipliers.push(options.weights[idx] || 1); + }); + }); + + const response = await multicall(network, provider, abi, calls, { blockTag }); + + const merged = {}; + response.map((value: any, i: number) => { + const address = calls[i][2][0]; + merged[address] = (merged[address] || 0) as number; + merged[address] += + parseFloat(formatUnits(value.toString(), 0)) * multipliers[i]; + }); + + return merged; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/README.md new file mode 100644 index 00000000..ad6d0cf4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/README.md @@ -0,0 +1,15 @@ +# erc721 + +This strategy returns the balances of the voters for a list of ERC721 NFT registries. + +Here is an example of parameters: + +```json +{ + "symbol": "PUNKS", + "registries": [ + "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", + "0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6" + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/examples.json new file mode 100644 index 00000000..40bacb41 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-multi-registry", + "params": { + "symbol": "PUNKS", + "registries": [ + "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", + "0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6" + ] + } + }, + "network": "1", + "addresses": [ + "0x51688cd36c18891167e8036bde2a8fb10ec80c43", + "0x3e17fac953de2cd729b0ace7f6d4353387717e9e", + "0x23f67feb67a3aa1e376d23beaa3f241217e427c9", + "0x54685c62db8e16b1484768db8e0daf3c644d50bf", + "0x766bc61d3150232f6f4e1d81633d68f3a94879e3", + "0xe34bded2b256430a9be53cbf5cba3b6d866d55f3", + "0x031c690be2932403cbdd85f8853f596794cff6c3" + ], + "snapshot": 12995362 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/index.ts new file mode 100644 index 00000000..13222605 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-multi-registry/index.ts @@ -0,0 +1,38 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'dievardump'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const calls: any[] = []; + options.registries.forEach((registry) => { + addresses.forEach((address: any) => { + calls.push([registry, 'balanceOf', [address]]); + }); + }); + + const response = await multicall(network, provider, abi, calls, { blockTag }); + + const merged = {}; + response.map((value: any, i: number) => { + const address = calls[i][2][0]; + merged[address] = (merged[address] || 0) as number; + merged[address] += parseFloat(formatUnits(value.toString(), 0)); + }); + + return merged; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/README.md new file mode 100644 index 00000000..40941e7b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/README.md @@ -0,0 +1,38 @@ +# ERC721 Weighted Pairs + +This strategy determines the voting powers for a pair of ERC721 NFT registries. This strategy supports 2 different NFT collections and it provides different voting powers to the holders of these collections, and also supports bonus powers for holders which have items from both the collections. + +```json +{ + "symbol": "OCMONK", + "registries": [ + "0x960b7a6bcd451c9968473f7bbfd9be826efd549a", + "0x86cc280d0bac0bd4ea38ba7d31e895aa20cceb4b" + ], + "weights": [ + 2, + 3 + ], + "pairWeight": 4 + } +``` + +In `example.json`, address `0xE052113bd7D7700d623414a0a4585BCaE754E9d5` has 31 items of `collection1` NFT and 35 items of `collection2` NFT. + +As we can see, there are 31 pairs, 0 items exclusively for collection1 and 4 items exclusively for collection2. So, the voting power of the address will be + +``` +(31 * pairWeight) + (collection1 * weights[0]) + (collection2 * weights[1])` += 31*4 + 0*2 + 4*3 += 136 +``` +which can be seen in the snapshot: + +``` + [ + { + '0x5F9345FdAd91Bf9757445ADc82e477B33FD3349c': 6, + '0xE052113bd7D7700d623414a0a4585BCaE754E9d5': 136 + } + ] +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/examples.json new file mode 100644 index 00000000..7ca4ec22 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-pair-weights", + "params": { + "symbol": "OCMONK", + "registries": [ + "0x960b7a6bcd451c9968473f7bbfd9be826efd549a", + "0x86cc280d0bac0bd4ea38ba7d31e895aa20cceb4b" + ], + "weights": [2, 3], + "pairWeight": 4 + } + }, + "network": "1", + "addresses": [ + "0x5F9345FdAd91Bf9757445ADc82e477B33FD3349c", + "0xE052113bd7D7700d623414a0a4585BCaE754E9d5" + ], + "snapshot": 15223064 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/index.ts new file mode 100644 index 00000000..d6b20b2b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-pair-weights/index.ts @@ -0,0 +1,51 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'arpitkarnatak'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const calls: any[] = []; + options.registries.slice(0, 2).forEach((registry) => { + addresses.forEach((address: any) => { + calls.push([registry, 'balanceOf', [address]]); + }); + }); + + const response = await multicall(network, provider, abi, calls, { blockTag }); + + const merged = {}; + response.map((value: any, i: number) => { + const address = calls[i][2][0]; + const registry = calls[i][0]; + merged[registry] = merged[registry] || {}; + merged[registry][address] = merged[registry][address] ?? 0; + merged[registry][address] += parseFloat(formatUnits(value.toString(), 0)); + }); + + const powers = {}; + addresses.forEach((address: any) => { + const balance0 = merged[options.registries[0]][address] ?? 0; + const balance1 = merged[options.registries[1]][address] ?? 0; + const pairCount = Math.min(balance0, balance1); + const votePower = + pairCount * options.pairWeight + + (balance0 - pairCount) * options.weights[0] + + (balance1 - pairCount) * options.weights[1]; + powers[address] = votePower; + }); + return powers; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/README.md new file mode 100644 index 00000000..ff841d0e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/README.md @@ -0,0 +1,55 @@ +# ERC721 with Metadata attribute - Vote power calculated via ownerOf + +This strategy allows you to determine the voting power by reading the metadata attribute of holding NFT. + +Generally `tokenURI(tokenID)` returns the individual metadata URI in ERC721. + +But for performance requirement in snapshot, it needs single endpoint that returns all weight values at once, specifying in `metadataSrc`. + +This strategy is a modification of "erc721-with-metadata". This strategy allows for multiple votes from a single wallet, and different token ids represent different weights. For example, a wallet containing two ERC721 TokenIDs from weight 1, and three ERC721 TokenIDS from weight 4, would receive fourteen votes. (2*1+3*4). The main difference is that this strategy does not require the extension of OpenZeppelin's erc721Enumerable contract, as implemented in "erc721-with-tokenid-range-weights-simple". + +Here is an example of parameters: +```json +{ + "address": "0xedCbF9D4CC3BA9aAA896adADeac1b6DF6326f7D8", + "symbol": "KAP-NFT", + "metadataSrc": "https://6242dbddb6734894c157cfc0.mockapi.io/api/votingWeights" +} +``` + +And this is the required json schema for NFT metadata source. +```json +[ + { + [tokenID]: [value] + } +] +``` + +Example: +(https://6242dbddb6734894c157cfc0.mockapi.io/api/votingWeights) +```json +[ + { + "1": 5 + }, + { + "2": 18 + }, + { + "3": 1 + }, + { + "4": 4 + }, + { + "5": 11 + }, + { + "6": 8 + }, + { + "7": 7 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/examples.json new file mode 100644 index 00000000..97d13133 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-with-metadata-by-ownerof", + "params": { + "address": "0xea1982b712FA2eCf27F12E205b3b31D43FA858E5", + "symbol": "RFM100", + "metadataSrc": "https://6242dbddb6734894c157cfc0.mockapi.io/api/votingWeights" + } + }, + "network": "4", + "addresses": [ + "0x030fBBD3ce096195c8c83bb8BfB70704eed865F9", + "0xb0b085dd0fe6c9632058f9ef088375c16f3aff12", + "0x7f16D5c969380E3420E17B4c3456A3844745A578" + ], + "snapshot": 10825197 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/index.ts new file mode 100644 index 00000000..a434ac69 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata-by-ownerof/index.ts @@ -0,0 +1,128 @@ +import { multicall } from '../../utils'; +import snapshots from '@snapshot-labs/snapshot.js'; + +export const author = 'victor-kyriazakos'; +export const version = '0.1.0'; + +const abi = [ + 'function ownerOf(uint256 tokenId) public view returns (address owner)' +]; + +// flattens the [{ "id": weight }] array into {ids[], weights[]} array +const flattenTokenIdWeightMetadata = ( + tokenIdWeightMetadata: [] +): { ids: number[]; weights: number[] } => { + const tokenData = tokenIdWeightMetadata.map((tokenDato) => { + const ids: number[] = [], + weights: number[] = []; + let datoId; + const keys = Object.keys(tokenDato); + if (keys.length > 0) { + datoId = parseInt(keys[0]); + ids.push(datoId); + weights.push(tokenDato[datoId.toString()]); + } + return { ids, weights }; + }); + + return tokenData.reduce((prev, curr) => ({ + ids: [...prev.ids, ...curr.ids], + weights: [...prev.weights, ...curr.weights] + })); +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const batchSize = 8000; + const maximumAllowedRange = 32000; // batchSize * 4 + let customRangeBalance = {}; + + // 1st, get all metadata values from the source - token weights + const metadata = await snapshots.utils.getJSON(options.metadataSrc); + + if (metadata.length > maximumAllowedRange) + throw new Error( + `Range is too big, the maximum allowed combined range is ${maximumAllowedRange}` + ); + + const getDataFromBlockChain = async ( + contractCalls: [string, string, [number]][] + ) => multicall(network, provider, abi, contractCalls, { blockTag }); + + const filterUnusedAddresses = (addressesToFilter: string[]): string[] => + addressesToFilter.filter((address: string) => + addresses + .map((address: string) => address.toLowerCase()) + .includes(address.toLowerCase()) + ); + + const multiplyOccurrencesByWeights = ( + contractCallResponse: [string, { owner: string }], + weights: number[] + ) => + contractCallResponse + .map((address, index) => Array(weights[index]).fill(address[0])) + .flat(); + + const countAndAccumulateOccurrences = (array: string[]) => + (customRangeBalance = array.reduce( + (prev, curr) => (prev[curr] ? ++prev[curr] : (prev[curr] = 1), prev), + customRangeBalance + )); + + const accumulateCustomRangeBalance = async ({ + ids, + weights + }): Promise<{ + [address: string]: number; + }> => { + // Define contract calls + const contractCalls = ids.map((id: number) => [ + options.address, + 'ownerOf', + [id] + ]) as [string, string, [number]][]; + + // batch-call contract data + const customRangeResponse = await getDataFromBlockChain(contractCalls); + const customRangeResponseWeighted = multiplyOccurrencesByWeights( + customRangeResponse, + weights + ); + + const customRangeResponseWeightedFiltered = filterUnusedAddresses( + customRangeResponseWeighted + ); + + return countAndAccumulateOccurrences(customRangeResponseWeightedFiltered); + }; + + const makeBatch = ({ batchSize }) => { + const { ids, weights } = flattenTokenIdWeightMetadata(metadata); + const batchedIds = [...Array(Math.ceil(ids.length / batchSize))].map(() => + ids.splice(0, batchSize) + ); + const batchedWeights = [ + ...Array(Math.ceil(weights.length / batchSize)) + ].map(() => weights.splice(0, batchSize)); + const batches = batchedIds.map((e, i) => ({ + ids: e, + weights: batchedWeights[i] + })); + return batches; + }; + + const batches = makeBatch({ batchSize: batchSize }); + for (let i = 0; i < batches.length; i++) { + await accumulateCustomRangeBalance({ ...batches[i] }); + } + + return { ...customRangeBalance }; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/README.md new file mode 100644 index 00000000..4b394cc0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/README.md @@ -0,0 +1,53 @@ +# ERC721 with Metadata attribute + +This strategy allows you to determine the voting power by reading the metadata attribute of holding NFT. + +Generally `tokenURI(tokenID)` returns the individual metadata URI in ERC721. + +But for performance requirement in snapshot, it needs single endpoint that returns all weight values at once, specifying in `metadataSrc`. + +Here is an example of parameters: +```json +{ + "address": "0xedCbF9D4CC3BA9aAA896adADeac1b6DF6326f7D8", + "symbol": "KAP-NFT", + "metadataSrc": "https://6242dbddb6734894c157cfc0.mockapi.io/api/votingWeights" +} +``` + +And this is the required json schema for NFT metadata source. +```json +[ + { + [tokenID]: [value] + } +] +``` + +Example: +(https://6242dbddb6734894c157cfc0.mockapi.io/api/votingWeights) +```json +[ + { + "1": 5 + }, + { + "2": 18 + }, + { + "3": 1 + }, + { + "4": 4 + }, + { + "5": 11 + }, + { + "6": 8 + }, + { + "7": 7 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/examples.json new file mode 100644 index 00000000..f983e770 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-with-metadata", + "params": { + "address": "0x986010627A96200C287Cff73007b7b5797C32846", + "symbol": "NFT", + "metadataSrc": "https://6242dbddb6734894c157cfc0.mockapi.io/api/votingWeights" + } + }, + "network": "1", + "addresses": [ + "0x50dd57F50A17d57304e7A4F262Da30bEB31C2E87", + "0x1fbF1E602530fB2e849Ae46B34A29493BCd75EC9", + "0xD130470E8b4B73d74FB723d5cb8A9DfD83462a1f" + ], + "snapshot": 16521706 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/index.ts new file mode 100644 index 00000000..17c56c26 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-metadata/index.ts @@ -0,0 +1,74 @@ +import { Multicaller } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; +import snapshots from '@snapshot-labs/snapshot.js'; + +export const author = 'allmysmarts'; +export const version = '0.1.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // 1st, get all metadata values from the source + const metadata = await snapshots.utils.getJSON(options.metadataSrc); + + // 2nd, get the balance of the token + const callWalletToBalanceOf = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToBalanceOf.call(walletAddress, options.address, 'balanceOf', [ + walletAddress + ]); + } + const walletToBalanceOf: Record = + await callWalletToBalanceOf.execute(); + + // 3rd, get tokenIds for each address, and index + const callWalletIdToTokenID = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletAddress, count] of Object.entries(walletToBalanceOf)) { + if (count.toNumber() > 0) { + for (let index = 0; index < count.toNumber(); index++) { + callWalletIdToTokenID.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + } + const walletIdToTokenID: Record = + await callWalletIdToTokenID.execute(); + + // 4th, sum up metadata value for each address + const walletToAttributeValue = {} as Record; + for (const [walletId, tokenID] of Object.entries(walletIdToTokenID)) { + const walletAddress = walletId.split('-')[0]; + const tokenData = metadata.find((x) => x[tokenID.toString()] > 0); + if (!tokenData) continue; + walletToAttributeValue[walletAddress] = + (walletToAttributeValue[walletAddress] || 0) + + tokenData[tokenID.toString()]; + } + + return Object.fromEntries( + Object.entries(walletToAttributeValue).map(([address, value]) => [ + address, + value + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/README.md new file mode 100644 index 00000000..76155f51 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/README.md @@ -0,0 +1,13 @@ +# erc721-with-multiplier + +This strategy return the balances of the voters for a specific ERC721 NFT with an arbitrary multiplier. + +Here is an example of parameters: + +```json +{ + "address": "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", + "multiplier": 100, + "symbol": "PUNK" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/examples.json new file mode 100644 index 00000000..ac740c1a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-with-multiplier", + "params": { + "address": "0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d", + "symbol": "LAND", + "multiplier": 2000 + } + }, + "network": "1", + "addresses": [ + "0x4eac6325e1dbf1ac90434d39766e164dca71139e", + "0x1b204424563dcfcabd2aee632163b9e6dc8bd4f3" + ], + "snapshot": 12453212 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/index.ts new file mode 100644 index 00000000..b93626db --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-multiplier/index.ts @@ -0,0 +1,34 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = '2fd'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const multipler = options.multiplier || 1; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [options.address, 'balanceOf', [address]]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), 0)) * multipler + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/README.md new file mode 100644 index 00000000..14b52fe0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/README.md @@ -0,0 +1,17 @@ +# erc721-with-tokenid-range-weights-simple + +This strategy is a modification of "erc721-with-tokenid-range-weights". This strategy allows for multiple votes from a single wallet, and different token ids represent different weights. For example, a wallet containing two ERC721 TokenIDs from weight 1, and three ERC721 TokenIDS from weight 4, would receive fourteen votes. (2*1+3*4). It's also possible to set up an optional "defaultWeight" for IDs that don't fall in any declared range. The other difference is that this strategy does not require the extension of OpenZeppelin's erc721Enumerable contract + +Here is an example of parameters: + +```json +{ + "address": "0x696115768bbef67be8bd408d760332a7efbee92d", + "symbol": "LINKSDAO", + "defaultWeight": 2, + "tokenIdWeightRanges": [ + { "start": 1, "end": 100, "weight": 1 }, + { "start": 6364, "end": 6464, "weight": 4 } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/examples.json new file mode 100644 index 00000000..8f8bede0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/examples.json @@ -0,0 +1,67 @@ +[ + { + "name": "Example query with default weight", + "strategy": { + "name": "erc721-with-tokenid-range-weights-simple", + "params": { + "address": "0x427cE6c9E2a504aEB22dc3839FbC4f4B6ebD75bb", + "symbol": "BLUES", + "defaultWeight": 2, + "tokenIdWeightRanges": [ + { "start": 2028, "end": 2029, "weight": 1 }, + { "start": 1, "end": 5, "weight": 7 } + ] + } + }, + "network": "1", + "addresses": [ + "0x285bd3a5c9cae024d70c7e70fcfc59db03637549", + "0xd82af9AE7c547cF81580D550C0460D31A9435312", + "0x226705FdF7C0e118f4DbAf8063026EE2Fc2A17A3", + "0x34db35639EAfe2712aE1F69dfa298b06a5c25053" + ], + "snapshot": 14661926 + }, + { + "name": "Example query", + "strategy": { + "name": "erc721-with-tokenid-range-weights-simple", + "params": { + "address": "0x696115768bbef67be8bd408d760332a7efbee92d", + "symbol": "LINKSDAO", + "tokenIdWeightRanges": [ + { "start": 1, "end": 100, "weight": 1 }, + { "start": 6364, "end": 6464, "weight": 4 } + ] + } + }, + "network": "1", + "addresses": [ + "0x7d863d74917191685616217c8ab1a77e73e79f21", + "0xb111dabb8edd8260b5c1e471945a62be2ee24470", + "0x285bd3a5c9cae024d70c7e70fcfc59db03637549" + ], + "snapshot": 13995858 + }, + { + "name": "Example query with huge ranges", + "strategy": { + "name": "erc721-with-tokenid-range-weights-simple", + "params": { + "address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", + "symbol": "BORED APES", + "tokenIdWeightRanges": [ + { "start": 9020, "end": 9030, "weight": 1 }, + { "start": 1, "end": 9000, "weight": 4 } + ] + } + }, + "network": "1", + "addresses": [ + "0x7d863d74917191685616217c8ab1a77e73e79f21", + "0xb111dabb8edd8260b5c1e471945a62be2ee24470", + "0x285bd3a5c9cae024d70c7e70fcfc59db03637549" + ], + "snapshot": 13995858 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/index.ts new file mode 100644 index 00000000..0d5d4e25 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/index.ts @@ -0,0 +1,144 @@ +import { strategy as erc721WithMultiplier } from '../erc721-with-multiplier'; +import { multicall } from '../../utils'; +import { WeightRange } from './types'; + +export const author = 'FeSens'; +export const version = '0.2.0'; + +const abi = [ + 'function ownerOf(uint256 tokenId) public view returns (address owner)' +]; + +const calculateRangeSize = (tokenIdWeightRanges: WeightRange[]) => { + return tokenIdWeightRanges.reduce((prev, curr) => { + const { start, end } = curr; + if (start > end) + throw new Error( + 'Range start tokenID must always be equal or smaller than the final tokenID' + ); + prev += end - start + 1; + return prev; + }, 0); +}; + +const range = (start, end, step) => + Array.from({ length: (end - start) / step + 1 }, (_, i) => start + i * step); + +const flattenTokenIdWeightRanges = ( + tokenIdWeightRanges: WeightRange[] +): { ids: number[]; weights: number[] } => { + const ranges = tokenIdWeightRanges.map((tokenIdWeightRange) => { + const { start, end, weight } = tokenIdWeightRange; + const ids = range(start, end, 1); + const weights = Array(ids.length).fill(weight); + + return { ids, weights }; + }); + + return ranges.reduce((prev, curr) => ({ + ids: [...prev.ids, ...curr.ids], + weights: [...prev.weights, ...curr.weights] + })); +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const { tokenIdWeightRanges, defaultWeight } = options; + const maximumNumberOfBatches = 4; + const batchSize = 8000; + const maximumAllowedRange = maximumNumberOfBatches * batchSize; + let erc721WeightedBalance = {}; + let customRangeBalance = {}; + + if (calculateRangeSize(tokenIdWeightRanges) > maximumAllowedRange) + throw new Error( + `Range is too big, the maximum allowed combined range is ${maximumAllowedRange}` + ); + + const getDataFromBlockChain = async ( + contractCalls: [string, string, [number]][] + ) => multicall(network, provider, abi, contractCalls, { blockTag }); + + const filterUnusedAddresses = (addressesToFilter: string[]): string[] => + addressesToFilter.filter((address: string) => + addresses + .map((address: string) => address.toLowerCase()) + .includes(address.toLowerCase()) + ); + + const multiplyOccurrencesByWeights = async ( + contractCallResponse: [string, { owner: string }], + weights: number[] + ) => + contractCallResponse + .map((address, index) => Array(weights[index]).fill(address[0])) + .flat(); + + const countAndAccumulateOccurrences = (array: string[]) => + (customRangeBalance = array.reduce( + (prev, curr) => (prev[curr] ? ++prev[curr] : (prev[curr] = 1), prev), + customRangeBalance + )); + + const accumulateCustomRangeBalance = async ({ + ids, + weights + }): Promise<{ + [address: string]: number; + }> => { + const contractCalls = ids.map((id: number) => [ + options.address, + 'ownerOf', + [id] + ]) as [string, string, [number]][]; + const customRangeResponse = await getDataFromBlockChain(contractCalls); + const customRangeResponseWeighted = await multiplyOccurrencesByWeights( + customRangeResponse, + weights + ); + const customRangeResponseWeightedFiltered = filterUnusedAddresses( + customRangeResponseWeighted + ); + + return countAndAccumulateOccurrences(customRangeResponseWeightedFiltered); + }; + + const makeBatch = ({ batchSize }) => { + const { ids, weights } = flattenTokenIdWeightRanges(tokenIdWeightRanges); + const batchedIds = [...Array(Math.ceil(ids.length / batchSize))].map(() => + ids.splice(0, batchSize) + ); + const batchedWeights = [ + ...Array(Math.ceil(weights.length / batchSize)) + ].map(() => weights.splice(0, batchSize)); + const batches = batchedIds.map((e, i) => ({ + ids: e, + weights: batchedWeights[i] + })); + return batches; + }; + + if (defaultWeight) + erc721WeightedBalance = await erc721WithMultiplier( + space, + network, + provider, + addresses, + { ...options, multiplier: defaultWeight }, + snapshot + ); + + const batches = makeBatch({ batchSize: batchSize }); + for (let i = 0; i < batches.length; i++) { + await accumulateCustomRangeBalance({ ...batches[i] }); + } + + return { ...erc721WeightedBalance, ...customRangeBalance }; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/types.d.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/types.d.ts new file mode 100644 index 00000000..9a13ada9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights-simple/types.d.ts @@ -0,0 +1,5 @@ +export interface WeightRange { + start: number; + end: number; + weight: number; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/README.md new file mode 100644 index 00000000..a16d741f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/README.md @@ -0,0 +1,17 @@ +# erc721 with tokenid range weights + +This strategy allows you to weight erc721's with different values by defining ranges of id's. If it does not match a range, it will use the defaultWeight. + +Here is an example of parameters: + +```json +{ + "address": "0x22C1f6050E56d2876009903609a2cC3fEf83B415", + "symbol": "POAP", + "defaultWeight": 1, + "tokenIdWeightRanges": [ + { "start": 0, "end": 3000, "weight": 1 }, + { "start": 3001, "end": 6000, "weight": 2 } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/examples.json new file mode 100644 index 00000000..cb19fac5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-with-tokenid-range-weights", + "params": { + "address": "0x30cDAc3871c41a63767247C8D1a2dE59f5714e78", + "symbol": "Reaper(s)", + "defaultWeight": 1, + "tokenIdWeightRanges": [ + { "start": 0, "end": 3000, "weight": 1 }, + { "start": 3001, "end": 4715, "weight": 2 } + ] + } + }, + "network": "1", + "addresses": [ + "0x863379Ab401d454834E1FE2eCe48F51a29eE9d7A", + "0x4C4E6f13fb5E3f70C3760262a03E317982691d10", + "0xf58195de3af91aff1f8dd559ad41f88f1b6c4aaf" + ], + "snapshot": 13847063 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/index.ts new file mode 100644 index 00000000..f88e8699 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-range-weights/index.ts @@ -0,0 +1,75 @@ +import { Multicaller } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'gregegan'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // First, get the balance of the token + const callWalletToBalanceOf = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToBalanceOf.call(walletAddress, options.address, 'balanceOf', [ + walletAddress + ]); + } + const walletToBalanceOf: Record = + await callWalletToBalanceOf.execute(); + + // Second, get the tokenId's for each token + const callWalletToAddresses = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletAddress, count] of Object.entries(walletToBalanceOf)) { + for (let index = 0; index < count.toNumber(); index++) { + callWalletToAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToAddresses: Record = + await callWalletToAddresses.execute(); + + // Third, sum the weights for each tokenId by finding it's range + const walletToLpBalance = {} as Record; + for (const [walletID, tokenId] of Object.entries(walletIDToAddresses)) { + const address = walletID.split('-')[0]; + + let tokenIdValue = options.defaultWeight; + for (const { start, end, weight } of options.tokenIdWeightRanges) { + if (tokenId.toNumber() >= start && tokenId.toNumber() <= end) { + tokenIdValue = weight; + break; + } + } + + walletToLpBalance[address] = walletToLpBalance[address] + ? walletToLpBalance[address].add(BigNumber.from(tokenIdValue)) + : BigNumber.from(tokenIdValue); + } + + return Object.fromEntries( + Object.entries(walletToLpBalance).map(([address, balance]) => [ + address, + balance.toNumber() + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/README.md new file mode 100644 index 00000000..c94d019d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/README.md @@ -0,0 +1,15 @@ +# erc721-with-tokenid-weighted + +This strategy is a modification of erc721-with-tokenid by dimsome. Instead of a maximum one vote per wallet, this strategy allows for multiple votes from a single wallet. For example, a wallet containing three whitelisted ERC721 TokenIDs would receive three votes. + +In short, this strategy provides one vote per whitelisted ERC721 TokenID- regardless of wallet distribution. + +Here is an example of parameters: + +```json +{ + "address": "0x30cDAc3871c41a63767247C8D1a2dE59f5714e78", + "symbol": "Reaper(s)", + "tokenIds": ["2112", "2871", "3221", "3587"] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/examples.json new file mode 100644 index 00000000..afa05342 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-with-tokenid-weighted", + "params": { + "address": "0x30cDAc3871c41a63767247C8D1a2dE59f5714e78", + "symbol": "Reaper(s)", + "tokenIds": ["2112", "2871", "3221", "3587"] + } + }, + "network": "1", + "addresses": [ + "0x863379Ab401d454834E1FE2eCe48F51a29eE9d7A", + "0x4C4E6f13fb5E3f70C3760262a03E317982691d10", + "0xf58195de3af91aff1f8dd559ad41f88f1b6c4aaf" + ], + "snapshot": 13847063 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/index.ts new file mode 100644 index 00000000..20313d93 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid-weighted/index.ts @@ -0,0 +1,35 @@ +import { multicall } from '../../utils'; + +export const author = 'andrewkingme'; +export const version = '0.1.0'; + +const abi = [ + 'function ownerOf(uint256 tokenId) public view returns (address owner)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + options.tokenIds.map((id: any) => [options.address, 'ownerOf', [id]]), + { blockTag } + ); + + return Object.fromEntries( + addresses.map((address: any) => [ + address, + response.filter( + (element: any) => element.owner.toLowerCase() === address.toLowerCase() + ).length + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/README.md new file mode 100644 index 00000000..c05ff472 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/README.md @@ -0,0 +1,13 @@ +# erc721 with tokenid + +This strategy return the balances of the voters for a specific ERC721 NFT with a given TokenId. + +Here is an example of parameters: + +```json +{ + "address": "0x22C1f6050E56d2876009903609a2cC3fEf83B415", + "symbol": "POAP", + "tokenIds": ["613607", "613237"] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/examples.json new file mode 100644 index 00000000..f887c2f7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721-with-tokenid", + "params": { + "address": "0x22C1f6050E56d2876009903609a2cC3fEf83B415", + "symbol": "POAP", + "tokenIds": ["613607", "613237"] + } + }, + "network": "1", + "addresses": [ + "0x2b85075702b5bc4737d8e1560b7efe8535105b47", + "0x3e17fac953de2cd729b0ace7f6d4353387717e9e", + "0xE76Be9C1e10910d6Bc6b63D8031729747910c2f6", + "0xC5e1569772b2d425Ac9469d39F17341C01e1CF4c" + ], + "snapshot": 12913585 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/index.ts new file mode 100644 index 00000000..8a6d572a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721-with-tokenid/index.ts @@ -0,0 +1,37 @@ +import { multicall } from '../../utils'; + +export const author = 'dimsome'; +export const version = '0.1.0'; + +const abi = [ + 'function ownerOf(uint256 tokenId) public view returns (address owner)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + options.tokenIds.map((id: any) => [options.address, 'ownerOf', [id]]), + { blockTag } + ); + + return Object.fromEntries( + addresses.map((address: any) => [ + address, + response.findIndex( + (res: any) => res.owner.toLowerCase() === address.toLowerCase() + ) > -1 + ? 1 + : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721/README.md b/Implementations/API/backend/utils/snapshot/strategies/erc721/README.md new file mode 100644 index 00000000..f3f08c15 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721/README.md @@ -0,0 +1,12 @@ +# erc721 + +This strategy return the balances of the voters for a specific ERC721 NFT. + +Here is an example of parameters: + +```json +{ + "address": "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", + "symbol": "PUNK" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721/examples.json b/Implementations/API/backend/utils/snapshot/strategies/erc721/examples.json new file mode 100644 index 00000000..689c24f5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc721", + "params": { + "address": "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB", + "symbol": "PUNK" + } + }, + "network": "1", + "addresses": [ + "0x51688cd36c18891167e8036bde2a8fb10ec80c43", + "0x3e17fac953de2cd729b0ace7f6d4353387717e9e", + "0x23f67feb67a3aa1e376d23beaa3f241217e427c9", + "0x54685c62db8e16b1484768db8e0daf3c644d50bf", + "0x766bc61d3150232f6f4e1d81633d68f3a94879e3" + ], + "snapshot": 12413022 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721/index.ts b/Implementations/API/backend/utils/snapshot/strategies/erc721/index.ts new file mode 100644 index 00000000..89dfb9b3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721/index.ts @@ -0,0 +1,33 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [options.address, 'balanceOf', [address]]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), 0)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/erc721/schema.json b/Implementations/API/backend/utils/snapshot/strategies/erc721/schema.json new file mode 100644 index 00000000..a63b9e00 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/erc721/schema.json @@ -0,0 +1,28 @@ +{ + "$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. DOODLE"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["address"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/esd-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/esd-delegation/examples.json new file mode 100644 index 00000000..f45e63fb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/esd-delegation/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Empty Set Dollar", + "strategy": { + "name": "esd-delegation", + "params": { + "uniswap": "0x88ff79eB2Bc5850F27315415da8685282C7610F9", + "rewards": "0x4082D11E506e3250009A991061ACd2176077C88f", + "dao": "0x443d2f2755db5942601fa062cc248aaa153313d3", + "token": "0x36F3FD68E7325a35EB768F1AedaAe9EA0689d723", + "symbol": "ESD", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x37Ed74A0dA66c0392C4c5901c3b3B97675871FE1", + "0x07C867770C43B1c6b715Aa8AC3A55DfD7f835a82" + ], + "snapshot": "latest" + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/esd-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/esd-delegation/index.ts new file mode 100644 index 00000000..d1837c45 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/esd-delegation/index.ts @@ -0,0 +1,37 @@ +import { strategy as esd } from '../esd'; +import { getDelegations } from '../../utils/delegation'; + +export const author = 'l3wi'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const delegations = await getDelegations(space, network, addresses, snapshot); + if (Object.keys(delegations).length === 0) return {}; + + const score = await esd( + space, + network, + provider, + Object.values(delegations).reduce((a: string[], b: string[]) => + a.concat(b) + ), + options, + snapshot + ); + + return Object.fromEntries( + addresses.map((address) => { + const addressScore = delegations[address] + ? delegations[address].reduce((a, b) => a + score[b], 0) + : 0; + return [address, addressScore]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/esd/README.md b/Implementations/API/backend/utils/snapshot/strategies/esd/README.md new file mode 100644 index 00000000..2bc56519 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/esd/README.md @@ -0,0 +1,29 @@ +# ESD Call Strategy + +Allows bonded ESD to be counted towards a valid score. + +## Params + +- `dao` - Address of the DAO proxy address +- `token` - Address of the ESD token +- `Rewards` - Address of the current LP rewards contract +- `uniswap` - Address of the Uniswap pool +- `decimals` - Decimals used by the ESD token + +### Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +"strategies": [ + { + "name": "esd", + "params": { + "uniswap": "0x88ff79eB2Bc5850F27315415da8685282C7610F9", + "rewards": "0x4082D11E506e3250009A991061ACd2176077C88f", + "dao": "0x443d2f2755db5942601fa062cc248aaa153313d3", + "token": "0x36F3FD68E7325a35EB768F1AedaAe9EA0689d723", + "decimals": 18 + } + } +] \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/esd/examples.json b/Implementations/API/backend/utils/snapshot/strategies/esd/examples.json new file mode 100644 index 00000000..be6e0aa9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/esd/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Empty Set Dollar", + "strategy": { + "name": "esd", + "params": { + "uniswap": "0x88ff79eB2Bc5850F27315415da8685282C7610F9", + "rewards": "0x4082D11E506e3250009A991061ACd2176077C88f", + "dao": "0x443d2f2755db5942601fa062cc248aaa153313d3", + "token": "0x36F3FD68E7325a35EB768F1AedaAe9EA0689d723", + "symbol": "ESD", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x0b7376f2a063c771d460210a4fa8787c9a7379f9", + "0x635b230c3fdf6a466bb6dc3b9b51a8ceb0659b67", + "0x22fa8cc33a42320385cbd3690ed60a021891cb32", + "0xcF63E1C31805254b6fB3Ed7829206c2b2505e3a7", + "0xd5d5a7cb1807364cde0bad51d0a7d758943ab114", + "0x7055ef0557dc6ff56cdf0c36d28b346d40a1b8ed", + "0xd1991b6fd521a3f357d96a15956702ce33342fec" + ], + "snapshot": 11350000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/esd/index.ts b/Implementations/API/backend/utils/snapshot/strategies/esd/index.ts new file mode 100644 index 00000000..6ae69757 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/esd/index.ts @@ -0,0 +1,97 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'l3wi'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOfBonded', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const daoQuery = addresses.map((address: any) => [ + options.dao, + 'balanceOfBonded', + [address] + ]); + + const lpQuery = addresses.map((address: any) => [ + options.rewards, + 'balanceOfBonded', + [address] + ]); + + const response = await multicall( + network, + provider, + abi, + [ + [options.token, 'balanceOf', [options.uniswap]], + [options.uniswap, 'totalSupply'], + ...daoQuery, + ...lpQuery + ], + { blockTag } + ); + + const uniswapESD = response[0]; + const uniswapTotalSupply = response[1]; + const daoBalances = response.slice(2, addresses.length + 2); + const lpBalances = response.slice( + addresses.length + 2, + addresses.length * 2 + 2 + ); + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => [ + addresses[i], + parseFloat( + formatUnits( + uniswapESD[0] + .div(uniswapTotalSupply[0]) + .mul(lpBalances[i][0]) + .add(daoBalances[i][0]) + .toString(), + options.decimals + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/eth-balance/examples.json new file mode 100644 index 00000000..29260849 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-balance/examples.json @@ -0,0 +1,33 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "eth-balance", + "params": { + "symbol": "ETH" + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/eth-balance/index.ts new file mode 100644 index 00000000..83289d3a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-balance/index.ts @@ -0,0 +1,40 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import networks from '@snapshot-labs/snapshot.js/src/networks.json'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +const abi = [ + 'function getEthBalance(address addr) public view returns (uint256 balance)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + networks[network].multicall, + 'getEthBalance', + [address] + ]), + { blockTag } + ); + const decimals = options.decimals || 18; + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/README.md b/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/README.md new file mode 100644 index 00000000..f0be0baa --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/README.md @@ -0,0 +1,105 @@ +# Contract call strategy + +Enables addresses to increase their score by donating to charity, adding `params.coeff` ETH to their score for every 1 ETH donated. This helps to level the playing field for less wealthy participants and incentivizes altruistic behavior from all participants. + +Implementation of [eth-received](./../eth-received) + +### Currently Included Charities: +- [GiveDirectly](https://www.givedirectly.org/): 0xc7464dbcA260A8faF033460622B23467Df5AEA42 +- [Unsung.org](http://Unsung.org): 0x02a13ED1805624738Cc129370Fee358ea487B0C6 +- [Heifer.org](http://Heifer.org): 0xD3F81260a44A1df7A7269CF66Abd9c7e4f8CdcD1 +- [GraceAid.org.uk](http://GraceAid.org.uk): 0x236dAA98f115caa9991A3894ae387CDc13eaaD1B +- [SENS.org](http://SENS.org): 0x542EFf118023cfF2821b24156a507a513Fe93539 +- [350.org](http://350.org): 0x50990F09d4f0cb864b8e046e7edC749dE410916b +- [EFF.org](http://EFF.org): 0xb189f76323678E094D4996d182A792E52369c005 +- [WikiLeaks](http://WikiLeaks.org): 0xE96E2181F6166A37EA4C04F6E6E2bD672D72Acc1 +- [GiveWell.org](http://GiveWell.org): 0x7cF2eBb5Ca55A8bd671A020F8BDbAF07f60F26C1 +- [CoolEarth.org](http://CoolEarth.org): 0x3c8cB169281196737c493AfFA8F49a9d823bB9c5 +- [Run2Rescue.org](http://Run2Rescue.org): 0xd17bcbFa6De9E3741aa43Ed32e64696F6a9FA996 +- [Archive.org](http://Archive.org): 0xFA8E3920daF271daB92Be9B87d9998DDd94FEF08 +- [Alt.eco](http://alt.eco): 0x5E7ecc1fC819f937fbEe043b40388C0809361ae9 + +## Params + +- `coeff` - (**Optional**, `number`, Default: `100`) Amount to multiply the sum of a voter's ether sent to charity. When used in conjunction with other strategies, this enables the increase or decrease of leverage given to voters who donate. + + +## Examples + +Can be used instead of, or in conjunction with eth-balance strategy. +In this example, the `params.coeff` of `1000` makes a 1 ETH donation equivalent to a 1000 ETH address balance. Thus, giving voters a massive incentive to donate. + +The space config will look like this: + +```JSON +"strategies": [ + { + "name": "Example ETH-Philanthropy Strategy", + "strategy": { + "name": "eth-philanthropy", + "params": { + "coeff": 1000, + "receivingAddresses": [ + "0xc7464dbcA260A8faF033460622B23467Df5AEA42", + "0x02a13ED1805624738Cc129370Fee358ea487B0C6", + "0xD3F81260a44A1df7A7269CF66Abd9c7e4f8CdcD1", + "0x236dAA98f115caa9991A3894ae387CDc13eaaD1B", + "0x542EFf118023cfF2821b24156a507a513Fe93539", + "0x50990F09d4f0cb864b8e046e7edC749dE410916b", + "0xb189f76323678E094D4996d182A792E52369c005", + "0xE96E2181F6166A37EA4C04F6E6E2bD672D72Acc1", + "0x7cF2eBb5Ca55A8bd671A020F8BDbAF07f60F26C1", + "0x3c8cB169281196737c493AfFA8F49a9d823bB9c5", + "0xd17bcbFa6De9E3741aa43Ed32e64696F6a9FA996", + "0xFA8E3920daF271daB92Be9B87d9998DDd94FEF08", + "0x5E7ecc1fC819f937fbEe043b40388C0809361ae9" + ] + } + }, + "network": "1", + "addresses": [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" + ], + "snapshot": 11414195 + }, + { + "name": "Example ETH-Balance Strategy", + "strategy": { + "name": "eth-balance", + "params": {} + }, + "network": "1", + "addresses": [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" + ], + "snapshot": 11414195 + } +] +``` + +Valid test addresses and snapshot block number: +```typescript +const addresses = [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" +]; + +const snapshot = 11414195; +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/examples.json b/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/examples.json new file mode 100644 index 00000000..1a2e01d0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example ETH Philanthropy Query", + "strategy": { + "name": "eth-philanthropy", + "params": { + "symbol": "ETH", + "coeff": 50 + } + }, + "network": "1", + "addresses": [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x5E7ecc1fC819f937fbEe043b40388C0809361ae9" + ], + "snapshot": 11486650 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/index.ts b/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/index.ts new file mode 100644 index 00000000..920f56a7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-philanthropy/index.ts @@ -0,0 +1,38 @@ +import { Web3Provider } from '@ethersproject/providers'; +import { strategy as ethReceivedStrategy } from '../eth-received'; + +export const author = 'mccallofthewild'; +export const version = '0.1.0'; + +const ethCharities = [ + ['GiveDirectly', '0xc7464dbcA260A8faF033460622B23467Df5AEA42'], + ['Unsung.org', '0x02a13ED1805624738Cc129370Fee358ea487B0C6'], + ['Heifer.org', '0xD3F81260a44A1df7A7269CF66Abd9c7e4f8CdcD1'], + ['GraceAid.org.uk', '0x236dAA98f115caa9991A3894ae387CDc13eaaD1B'], + ['SENS.org', '0x542EFf118023cfF2821b24156a507a513Fe93539'], + ['350.org', '0x50990F09d4f0cb864b8e046e7edC749dE410916b'], + ['EFF.org', '0xb189f76323678E094D4996d182A792E52369c005'], + ['WikiLeaks', '0xE96E2181F6166A37EA4C04F6E6E2bD672D72Acc1'], + ['GiveWell.org', '0x7cF2eBb5Ca55A8bd671A020F8BDbAF07f60F26C1'], + ['CoolEarth.org', '0x3c8cB169281196737c493AfFA8F49a9d823bB9c5'], + ['Run2Rescue.org', '0xd17bcbFa6De9E3741aa43Ed32e64696F6a9FA996'], + ['Archive.org', '0xFA8E3920daF271daB92Be9B87d9998DDd94FEF08'] +]; + +export async function strategy( + ...args: [string, string, Web3Provider, string[], { coeff?: number }, number] +) { + const [space, network, provider, addresses, options, snapshot] = args; + const { coeff = 100 } = options; + return ethReceivedStrategy( + space, + network, + provider, + addresses, + { + receivingAddresses: ethCharities.map(([, address]) => address), + coeff + }, + snapshot + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-received/ElasticSearchTxResult.ts b/Implementations/API/backend/utils/snapshot/strategies/eth-received/ElasticSearchTxResult.ts new file mode 100644 index 00000000..07f0e116 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-received/ElasticSearchTxResult.ts @@ -0,0 +1,81 @@ +export interface ElasticSearchTxResult { + took: number; + timed_out: boolean; + _shards: Shards; + hits: Hits; +} + +export interface Shards { + total: number; + successful: number; + skipped: number; + failed: number; +} + +export interface Hits { + total: Total; + max_score: number; + hits: Hit[]; +} + +export interface Hit { + _index: Index; + _type: Type; + _id: string; + _score: number; + _source: Source; +} + +export enum Index { + The0011EthereumEthereumMainnetTx = '0011-ethereum-ethereum-mainnet-tx' +} + +export interface Source { + blockHash: string; + blockNumber: BlockNumber; + cumulativeGasUsed: BlockNumber; + from: string; + gas: BlockNumber; + gasPrice: BlockNumber; + gasUsed: BlockNumber; + hash: string; + input: string; + logsBloom: string; + nonce: BlockNumber; + r: string; + s: string; + status: boolean; + timestamp: number; + to: string; + publicKey?: string; + transactionIndex: BlockNumber; + standardV?: string; + v: V; + value: Value; +} + +export interface BlockNumber { + raw: string; + num: number; +} + +export enum V { + The0X25 = '0x25', + The0X26 = '0x26' +} + +export interface Value { + raw: string; + padded: string; + eth: number; + num: number; +} + +export enum Type { + Doc = '_doc' +} + +export interface Total { + value: number; + relation: string; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-received/README.md b/Implementations/API/backend/utils/snapshot/strategies/eth-received/README.md new file mode 100644 index 00000000..36e2c521 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-received/README.md @@ -0,0 +1,90 @@ +# Contract call strategy + +Scores addresses by how much ETH they have sent to `params.receivingAddresses`, adding `params.coeff` ETH to their score for every 1 ETH sent. + +This creates a new fundraising opportunity for projects & organizations, levels the playing field for less wealthy participants, and encourages voters to "put their money where their mouth is". + +## Params + +- `receivingAddresses` - (**Required**, `string[]`) Array of addresses to check for ether transactions from voters +- `coeff` - (**Optional**, `number`, Default: `1`) Amount to multiply the sum of a voter's ether sent to `receivingAddresses`. When used in conjunction with other strategies, this enables the increase or decrease of leverage given to voter who send ETH. + + +## Examples + +Can be used instead of, or in conjunction with eth-balance strategy. +In this example, the `params.coeff` of `100` makes a 1 ETH donation equivalent to a 100 ETH address balance. Thus, giving voters a massive incentive to donate. + +The space config will look like this: + +```JSON +"strategies": [ + { + "name": "Example ETH-Received Strategy", + "strategy": { + "name": "eth-received", + "params": { + "coeff": 100, + "receivingAddresses": [ + "0xc7464dbcA260A8faF033460622B23467Df5AEA42", + "0x02a13ED1805624738Cc129370Fee358ea487B0C6", + "0xD3F81260a44A1df7A7269CF66Abd9c7e4f8CdcD1", + "0x236dAA98f115caa9991A3894ae387CDc13eaaD1B", + "0x542EFf118023cfF2821b24156a507a513Fe93539", + "0x50990F09d4f0cb864b8e046e7edC749dE410916b", + "0xb189f76323678E094D4996d182A792E52369c005", + "0xE96E2181F6166A37EA4C04F6E6E2bD672D72Acc1", + "0x7cF2eBb5Ca55A8bd671A020F8BDbAF07f60F26C1", + "0x3c8cB169281196737c493AfFA8F49a9d823bB9c5", + "0xd17bcbFa6De9E3741aa43Ed32e64696F6a9FA996", + "0xFA8E3920daF271daB92Be9B87d9998DDd94FEF08" + ] + } + }, + "network": "1", + "addresses": [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" + ], + "snapshot": 11414195 + }, + { + "name": "Example ETH-Balance Strategy", + "strategy": { + "name": "eth-balance", + "params": {} + }, + "network": "1", + "addresses": [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" + ], + "snapshot": 11414195 + } +] +``` + +Valid test addresses and snapshot block number: +```typescript +const addresses = [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" +]; + +const snapshot = 11414195; +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-received/examples.json b/Implementations/API/backend/utils/snapshot/strategies/eth-received/examples.json new file mode 100644 index 00000000..e78a1eb4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-received/examples.json @@ -0,0 +1,70 @@ +[ + { + "name": "Example ETH Received Strategy", + "strategy": { + "name": "eth-received", + "params": { + "symbol": "ETH", + "receivingAddresses": [ + "0xc7464dbcA260A8faF033460622B23467Df5AEA42", + "0x02a13ED1805624738Cc129370Fee358ea487B0C6", + "0xD3F81260a44A1df7A7269CF66Abd9c7e4f8CdcD1", + "0x236dAA98f115caa9991A3894ae387CDc13eaaD1B", + "0x542EFf118023cfF2821b24156a507a513Fe93539", + "0x50990F09d4f0cb864b8e046e7edC749dE410916b", + "0xb189f76323678E094D4996d182A792E52369c005", + "0xE96E2181F6166A37EA4C04F6E6E2bD672D72Acc1", + "0x7cF2eBb5Ca55A8bd671A020F8BDbAF07f60F26C1", + "0x3c8cB169281196737c493AfFA8F49a9d823bB9c5", + "0xd17bcbFa6De9E3741aa43Ed32e64696F6a9FA996", + "0xFA8E3920daF271daB92Be9B87d9998DDd94FEF08" + ] + } + }, + "network": "1", + "addresses": [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" + ], + "snapshot": 11486650 + }, + { + "name": "Example ETH Received Query", + "strategy": { + "name": "eth-received", + "params": { + "coeff": 100, + "receivingAddresses": [ + "0xc7464dbcA260A8faF033460622B23467Df5AEA42", + "0x02a13ED1805624738Cc129370Fee358ea487B0C6", + "0xD3F81260a44A1df7A7269CF66Abd9c7e4f8CdcD1", + "0x236dAA98f115caa9991A3894ae387CDc13eaaD1B", + "0x542EFf118023cfF2821b24156a507a513Fe93539", + "0x50990F09d4f0cb864b8e046e7edC749dE410916b", + "0xb189f76323678E094D4996d182A792E52369c005", + "0xE96E2181F6166A37EA4C04F6E6E2bD672D72Acc1", + "0x7cF2eBb5Ca55A8bd671A020F8BDbAF07f60F26C1", + "0x3c8cB169281196737c493AfFA8F49a9d823bB9c5", + "0xd17bcbFa6De9E3741aa43Ed32e64696F6a9FA996", + "0xFA8E3920daF271daB92Be9B87d9998DDd94FEF08" + ] + } + }, + "network": "1", + "addresses": [ + "0x100fb703c8b84466f79e838835df6fc180aef740", + "0xbcB5f94590904A64e16Acb08D4Fa4b7baFdC8c3A", + "0x0a0249179f559f60496e23e3907e9a3a54aa6537", + "0x6c65fb326e7734ba5508b5d043718288b43b9ed9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" + ], + "snapshot": 11486650 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-received/index.ts b/Implementations/API/backend/utils/snapshot/strategies/eth-received/index.ts new file mode 100644 index 00000000..76616bc5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-received/index.ts @@ -0,0 +1,83 @@ +import fetch from 'cross-fetch'; +import { Web3Provider } from '@ethersproject/providers'; +import { ElasticSearchTxResult } from './ElasticSearchTxResult'; + +export const author = 'mccallofthewild'; +export const version = '0.1.0'; + +export async function strategy( + ...args: [ + string, + string, + Web3Provider, + string[], + { coeff?: number; receivingAddresses: string[] }, + number + ] +) { + const [, , , addresses, options] = args; + const { coeff = 1, receivingAddresses } = options; + // queries AnyBlock ElasticSearch https://www.anyblockanalytics.com/ + // Account: yidirel126@95ta.com Pass: xU5KKfys76wb633FvGS6 + const charitableTransactions: ElasticSearchTxResult = await fetch( + 'https://api.anyblock.tools/ethereum/ethereum/mainnet/es/tx/search/', + { + method: 'POST', + body: JSON.stringify({ + from: 0, + size: 10000, + query: { + bool: { + must: [ + { + bool: { + should: [ + ...addresses.map((a) => ({ + match: { + from: a + } + })) + ] + } + }, + { + bool: { + should: [ + ...receivingAddresses.map((a) => ({ + match: { + to: a + } + })) + ] + } + } + ] + } + } + }), + headers: { + Authorization: 'Bearer 8c8b3826-afd5-4535-a8be-540562624fbe', + 'Content-Type': 'application/json' + } + } + ) + .then((r) => r.json()) + .catch((e) => { + console.error('Eth-Received AnyBlock ElasticSearch Query Failed:'); + throw e; + }); + + const scores = {}; + for (const address of addresses) { + scores[address] = charitableTransactions.hits.hits + .filter((tx) => { + const validAddress = + tx._source.from.toLowerCase() == address.toLowerCase(); + return validAddress; + }) + .reduce((prev, curr) => { + return prev + curr._source.value.eth * coeff; + }, 0); + } + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/README.md b/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/README.md new file mode 100644 index 00000000..52fa78f6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/README.md @@ -0,0 +1,3 @@ +# ETH Wallet Age strategy + +Allows getting the first transaction date from dfuse api, which are used to calculate voter scores with a number of days from the first transaction. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/examples.json b/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/examples.json new file mode 100644 index 00000000..2ecb1743 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Age in Days", + "strategy": { + "name": "eth-wallet-age", + "params": { + "symbol": "ETH", + "dfuseApiKey": "web_f527db575a38dd11c5b686d7da54d371" + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/index.ts b/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/index.ts new file mode 100644 index 00000000..d71f8958 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-wallet-age/index.ts @@ -0,0 +1,71 @@ +import { EnumType } from 'json-to-graphql-query'; +import fetch from 'cross-fetch'; +import { subgraphRequest } from '../../utils'; + +export const author = 'ChaituVR'; +export const version = '0.1.0'; + +const getJWT = async (dfuseApiKey) => { + const rawResponse = await fetch('https://auth.dfuse.io/v1/auth/issue', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ api_key: dfuseApiKey }) + }); + const content = await rawResponse.json(); + return content.token; +}; + +export async function strategy(space, network, provider, addresses, options) { + let data: any = []; + const query = Object.fromEntries( + addresses.map((address) => [ + `_${address}`, + { + __aliasFor: 'searchTransactions', + __args: { + indexName: new EnumType('CALLS'), + query: `(from:${address} OR to:${address})`, + sort: new EnumType('ASC'), + limit: 1 + }, + edges: { + block: { + header: { + timestamp: true + }, + number: true + }, + node: { + from: true, + to: true + } + } + } + ]) + ); + + const dfuseJWT = await getJWT( + options.dfuseApiKey || 'web_f527db575a38dd11c5b686d7da54d371' + ); + data = await subgraphRequest('https://mainnet.eth.dfuse.io/graphql', query, { + headers: { + Authorization: `Bearer ${dfuseJWT}` + } + }); + return Object.fromEntries( + Object.values(data).map((value: any, i) => [ + addresses[i], + (() => { + const today = new Date().getTime(); + const firstTransaction = + value.edges[0]?.block?.header?.timestamp || today; + const diffInSeconds = Math.abs(firstTransaction - today); + const walletAgeInDays = Math.floor(diffInSeconds / 1000 / 60 / 60 / 24); + return walletAgeInDays; + })() + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/README.md new file mode 100644 index 00000000..514834f8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/README.md @@ -0,0 +1,9 @@ +# ETH minimum balance strategy + +A strategy similar to `erc-20-with-balance`, but for the ETH balance. + +Using a low minimum balance, this strategy can be used as a proxy for "active Ethereum address", based on the assumption that active addresses will always have some ETH on them to pay for fees. + +# Parameters + +`minBalance`: minimum ETH balance required to get voting power of 1. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/examples.json new file mode 100644 index 00000000..ad92a681 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "eth-with-balance", + "params": { + "symbol": "ETH", + "minBalance": 0.5 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/index.ts new file mode 100644 index 00000000..a8042179 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/eth-with-balance/index.ts @@ -0,0 +1,28 @@ +import { strategy as ethBalanceOfStrategy } from '../eth-balance'; + +export const author = 'AronVanAmmers'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await ethBalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address: any) => [ + address[0], + address[1] > (options.minBalance || 0) ? 1 : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/README.md new file mode 100644 index 00000000..9ef4a55a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/README.md @@ -0,0 +1,16 @@ +# ethalend-balance-of + +This is a strategy made for the protocol ETHALend where we have smartWallets and we need to somehow check +directly the balanceOf in the smartWallet instead of the web3 wallet, it returns the balanceOf the specified ERC20 token +as the web3 wallet key and the value as the balanceOf the smartWallet. + +Here is an example of parameters: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18, + "registry": "0x583B965462e11Da63D1d4bC6D2d43d391F79af1f" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/examples.json new file mode 100644 index 00000000..122cfa0d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/examples.json @@ -0,0 +1,32 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ethalend-balance-of", + "params": { + "registry": "0x583B965462e11Da63D1d4bC6D2d43d391F79af1f", + "address": "0x56bA9a2f00A4F581a05bfE9Fd0b16277eD130349", + "symbol": "veETHA", + "decimals": 18 + } + }, + "network": "137", + "addresses": [ + "0xd7539FCdC0aB79a7B688b04387cb128E75cb77Dc", + "0x6E33e22f7aC5A4b58A93C7f6D8Da8b46c50A3E20", + "0xC9dA7343583fA8Bb380A6F04A208C612F86C7701", + "0x4e46cd691b0a159fbe5e2d08a3951a324e2fb4c0", + "0x1de9184f070627a33eab44e329c699b3068c073d", + "0x77c66d0457b6bd390c1824b3e44d6f9c31fa4166", + "0x358271f5578868e8051441677fbaacbd5d80ae39", + "0xae80dcf8109e2774d38884ece6c11191c7a1c583", + "0xe07d2caf3be9447ee71ed721ac2730b8e8e985f0", + "0xf758e816b602feb404948626b2f7f16e948a578c", + "0xb7f5566100751c64b06280b1cb5a9286b08ae161", + "0x647f3d7f8d513bdf67bd407f2f98b90a84e8edb5", + "0xd3670fd0e5bbdc6c04634c66c1b94e9788d71473", + "0x642FC634b8a0809D4d591A9A5367424E52a698C4" + ], + "snapshot": 32843708 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/index.ts new file mode 100644 index 00000000..369ab12f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/index.ts @@ -0,0 +1,56 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'ethalend'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function wallets(address account) external view returns (address)' +]; + +function swapKeys(obj: { [key: string]: string }) { + const newObj = {}; + + for (const key in obj) { + newObj[obj[key]] = key; + } + + return newObj; +} + +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) => + multi.call(address, options.registry, 'wallets', [address]) + ); + + const resultSmartWallets: Record = await multi.execute(); + + addresses.forEach((address) => + multi.call(resultSmartWallets[address], options.address, 'balanceOf', [ + resultSmartWallets[address] + ]) + ); + + const invertedSmartWallets = swapKeys(resultSmartWallets); + + const resultAccounts: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(resultAccounts).map(([address, balance]) => [ + invertedSmartWallets[address], + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/schema.json new file mode 100644 index 00000000..2ee463c5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethalend-balance-of/schema.json @@ -0,0 +1,41 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "registry": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethercats-founder-series/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ethercats-founder-series/examples.json new file mode 100644 index 00000000..9efec74a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethercats-founder-series/examples.json @@ -0,0 +1,14 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ethercats-founder-series", + "params": { + "symbol": "EtherCat" + } + }, + "network": "1", + "addresses": ["0x0e917dcffb792dbfb36b75afcc1a5d6174ec94e2"], + "snapshot": 14121348 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethercats-founder-series/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ethercats-founder-series/index.ts new file mode 100644 index 00000000..2a473659 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethercats-founder-series/index.ts @@ -0,0 +1,60 @@ +import { subgraphRequest } from '../../utils'; + +export const author = 'woodydeck'; +export const version = '1.0.0'; + +// Constants +const url = { + '1': 'https://gateway.thegraph.com/api/656e05ff867c74eeb11bf0199ff5de86/subgraphs/id/0x7859821024e633c5dc8a4fcf86fc52e7720ce525-1' +}; + +const getPower = (id, value) => { + if (value == 0) return 0; + return value * (parseInt(id.slice(2, 4)) * parseInt(id[4])); +}; + +// Strategy +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const requests = addresses.map((a) => ({ + erc1155Balances: { + __args: { + block: { number: snapshot !== 'latest' ? snapshot : undefined }, + where: { + contract: '0xff3559412c4618af7c6e6f166c74252ff6364456', + account: a.toLowerCase() + } + }, + valueExact: true, + token: { + identifier: true + } + } + })); + + const responses = await Promise.all( + requests.map((request) => subgraphRequest(url[network], request)) + ); + + return Object.fromEntries( + responses.map((response, i) => [ + addresses[i], + response.erc1155Balances?.length > 0 + ? response.erc1155Balances + .map((balance) => { + return getPower( + balance?.token?.identifier || '0', + balance?.valueExact || '0' + ); + }) + .reduce((prev, curr) => prev + curr, 0) + : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethercats-founders-series/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ethercats-founders-series/examples.json new file mode 100644 index 00000000..302da04b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethercats-founders-series/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ethercats-founders-series", + "params": { + "symbol": "Meows" + } + }, + "network": "1", + "addresses": [ + "0x03C4dcDbD7Efc4fD32e2C0E92D4E1F8B778e1A56", + "0x0e917dcffb792dbfb36b75afcc1a5d6174ec94e2", + "0x160e5B66f58924736B0a264f25ffd01e6732C39f" + ], + "snapshot": 14101221 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethercats-founders-series/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ethercats-founders-series/index.ts new file mode 100644 index 00000000..06ca6d8c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethercats-founders-series/index.ts @@ -0,0 +1,70 @@ +import { subgraphRequest } from '../../utils'; + +export const author = 'woodydeck'; +export const version = '1.0.0'; + +// Constants +const url = { + '1': 'https://gateway.thegraph.com/api/656e05ff867c74eeb11bf0199ff5de86/subgraphs/id/0x7859821024e633c5dc8a4fcf86fc52e7720ce525-1' +}; + +const getPower = (id, value) => { + if (value == 0) return 0; + return value * (parseInt(id.slice(2, 4)) * parseInt(id[4])); +}; + +// Strategy +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const request: any = { + erc1155Balances: { + __args: { + where: { + contract_in: ['0xff3559412c4618af7c6e6f166c74252ff6364456'], + account_in: addresses.map((address) => address.toLowerCase()) + } + }, + valueExact: true, + account: { + id: true + }, + contract: { + id: true + }, + token: { + identifier: true + } + } + }; + + if (snapshot === 'number') { + request.erc1155Balances.__args.block = { number: snapshot }; + } + + const response = await subgraphRequest(url[network], request); + + const scores = {}; + addresses.forEach((address) => { + const score = + response.erc1155Balances?.length > 0 + ? response.erc1155Balances + .filter((x) => x.account.id == address.toLowerCase()) + .reduce((acc, account) => { + const score = getPower( + account?.token?.identifier || '0', + account?.valueExact || '0' + ); + return acc + score; + }, 0) + : 0; + scores[address] = score; + }); + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/README.md b/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/README.md new file mode 100644 index 00000000..eb0db9ff --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/README.md @@ -0,0 +1,12 @@ +# ethermon-erc721 + +This strategy returns the score of voters dependes upon number of Ethermon NFTs they have and the max tokens it calculate are 200. + +# parameters +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "EMONA", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/examples.json new file mode 100644 index 00000000..17fc08ee --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ethermon-erc721", + "params": { + "EMONA_ADDRESS": "0x9928a8ea82d86290dfd1920e126b3872890525b3", + "tokenWeightIPFS": "abb6cbba-f8e6-4fce-89a8-7360cda5daf9-bucket/weights.json", + "EMON_DATA_ADDRESS": "0x0f05607e51d5224300520a710898a8467d8d1117", + "symbol": "EMONA" + } + }, + "network": "137", + "addresses": ["0x111c26A02ca4050684D4083d72E2A7C1dCbA853f"], + "snapshot": 14101222 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/index.ts new file mode 100644 index 00000000..1fa0bfc1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ethermon-erc721/index.ts @@ -0,0 +1,101 @@ +import fetch from 'cross-fetch'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'syedMohib44'; +export const version = '0.0.2'; + +const abi1 = [ + 'function getMonsterObj(uint64 _objId) external view returns(uint64 objId, uint32 classId, address trainer, uint32 exp, uint32 createIndex, uint32 lastClaimIndex, uint createTime)', + 'function balanceOf(address owner) external view returns (uint256 balance)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi1, { blockTag }); + const clamp = (num: number, min: number, max: number) => + Math.min(Math.max(num, min), max); + + addresses.map((address) => { + multi.call(address, options.EMONA_ADDRESS, 'balanceOf', [address]); + }); + const player_addresses: Record = await multi.execute(); + + const multi1 = new Multicaller(network, provider, abi1, { blockTag }); + + Object.entries(player_addresses).forEach((address) => { + const balance = clamp(+player_addresses[address[0]].toString(), 0, 200); + for (let i = 0; i < balance; i++) { + multi1.call( + address[0].toString() + '-' + i.toString(), + options.EMONA_ADDRESS, + 'tokenOfOwnerByIndex', + [address[0], i] + ); + } + }); + const address_tokens: Record = await multi1.execute(); + const multi2 = new Multicaller(network, provider, abi1, { blockTag }); + + Object.entries(address_tokens).forEach((address_token) => { + const address = address_token[0].split('-')[0].toString(); + const token = +address_token[1].toString(); + multi2.call( + address + '-' + token, + options.EMON_DATA_ADDRESS, + 'getMonsterObj', + [token] + ); + }); + + const monObject: Record = await multi2.execute(); + + const response = await fetch( + 'https://storageapi.fleek.co/' + options.tokenWeightIPFS, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + } + ); + const classIdWeight = await response.json(); + + const result: Record = {}; + for (const [_address, _obj] of Object.entries(monObject)) { + const address = _address.split('-')[0]; + const classId = _obj[1]; + if (!result[address]) { + result[address] = classIdWeight[classId] + ? classIdWeight[classId].weight + : 1; + continue; + } + + result[address] += Number( + +player_addresses[address].toString() > 200 + ? Number( + classIdWeight[classId] + ? (classIdWeight[classId].weight / 200) * + Number(player_addresses[address]) + : 0 + ).toFixed(0) + : classIdWeight[classId] + ? classIdWeight[classId].weight + : 0 + ); + } + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [address, balance]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/README.md b/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/README.md new file mode 100644 index 00000000..1c2a456f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/README.md @@ -0,0 +1 @@ +This is a simple strategy that takes no parameters and returns the count of Orcs owned from the graphql query at: https://open-api.etherorcs.com/api/graphql diff --git a/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/examples.json b/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/examples.json new file mode 100644 index 00000000..08720cec --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/examples.json @@ -0,0 +1,30 @@ +[ + { + "name": "Ether Orcs Balance Of Example", + "strategy": { + "name": "etherorcs-combo-balanceof" + }, + "network": "1", + "addresses": [ + "0x90da518CfC3ceb176eD92eB83f933E8586880B8a", + "0x0d388658633418e8E51A7CF67c7059F863F053d9", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/index.ts b/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/index.ts new file mode 100644 index 00000000..b750b10f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/etherorcs-combo-balanceof/index.ts @@ -0,0 +1,47 @@ +import fetch from 'cross-fetch'; + +export const author = 'tempest-sol'; +export const version = '0.1.1'; + +type OrcType = { + _id: number; + owner: string; +}; + +export async function strategy( + space, + network, + provider, + addresses +): Promise> { + const count: Record = {}; + const res = await fetch('https://open-api.etherorcs.com/api/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: `query( + $orcsFilter: FilterFindManyorcsInput + ) { + orcs(filter: $orcsFilter) { + _id + owner + } + }`, + variables: { + orcsFilter: { + OR: addresses.map((address) => ({ owner: address.toLowerCase() })) + } + } + }) + }); + const response = await res.json(); + if (response && response.data) { + const orcs: OrcType[] = response.data.orcs; + addresses.forEach((address) => { + count[address] = orcs.filter( + (orc) => orc.owner.toLowerCase() === address.toLowerCase() + ).length; + }); + } + return count || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/faraland-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/faraland-staking/examples.json new file mode 100644 index 00000000..0d5eaae8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/faraland-staking/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Faraland stake query", + "strategy": { + "name": "faraland-staking", + "params": { + "address": "0x3057A6EF2aCf803371315380d21D0193471E3EF8", + "symbol": "FARA" + } + }, + "network": "56", + "addresses": ["0x3f105F78359ad80562B4c34296a87b8e66c584C5"], + "snapshot": 10913859 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/faraland-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/faraland-staking/index.ts new file mode 100644 index 00000000..1357adde --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/faraland-staking/index.ts @@ -0,0 +1,51 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const FLASHSTAKE_SUBGRAPH_URL = { + '1': 'https://queries-graphnode.faraland.io/subgraphs/name/edwardevans094/farastore-v12', + '56': 'https://queries-graphnode.faraland.io/subgraphs/name/edwardevans094/farastore-v12' +}; + +export const author = 'edwardEvans094'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + totalStaked: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.users.__args.block = { number: snapshot }; + } + const result = await subgraphRequest( + FLASHSTAKE_SUBGRAPH_URL[network], + params + ); + + const score = {}; + if (result && result.users) { + result.users.map((_data) => { + const address = getAddress(_data.id); + score[address] = Number(_data.totalStaked); + }); + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/fight-club/README.md b/Implementations/API/backend/utils/snapshot/strategies/fight-club/README.md new file mode 100644 index 00000000..a9d4eb28 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/fight-club/README.md @@ -0,0 +1,57 @@ +# fight-club + +This strategy calculates a fight-club member's voting score. + +## Params + +- `gloveAddresses` - (**Required**, `object`) Up to 10 Fight Club Glove NFT + addresses and their associated voting power. +- `weightClassAddress` - (**Required**, `string`) Weight Class Kudo (ERC-1155) + Address +- `weightClassIds` - (**Required**, `object`) Up to 10 Weight Class Kudo IDs and + their associated voting multiplier. +- `symbol` - (**Optional**, `string`) Symbol of the strategy + + +Here is an example of parameters: + +```json +{ + "gloveAddresses": { + "0x25ed58c027921E14D86380eA2646E3a1B5C55A8b": 3 + }, + "weightClassAddress": "0xF1F3ca6268f330fDa08418db12171c3173eE39C9", + "weightClassIds": { + "8": 4, + "26": 5, + "33": 6 + }, + "symbol": "FC" +} +``` + +## Details + +This strategy uses a Multicall to query `balanceOf` fight club glove NFTs and +`balanceOf` weight class kudos (ERC-1155). A voter's total voting score is +calculated via the following equation: + +``` +score = glovePower * weightClassMultiplier +``` + +* If a user has more than 1 fight club glove NFT, only the one with the highest + associated voting power is counted. +* If a user has more than 1 weight class Kudo, only the one with the highest + associated voting multiplier is counted. +* If a user has 0 fight club glove NFTs, then their total vote score is 0, + regardless of their weight class Kudos. +* If a user has a fight club glove NFT, but 0 weight class Kudos, the weight + class multiplier defaults to 1. +* To avoid memory issues, the strategy is limited to 10 distinct glove NFT + addresses and 10 distinct weight class kudo IDs. + +> **Warning**: This strategy uses `ethers.utils.BigNumber.toNumber()` and will + fail if a voter's `glovePower` or `weightClassMultiplier` is is greater than + or equal to `Number.MAX_SAFE_INTEGER` or less than or equal to + `Number.MIN_SAFE_INTEGER` diff --git a/Implementations/API/backend/utils/snapshot/strategies/fight-club/examples.json b/Implementations/API/backend/utils/snapshot/strategies/fight-club/examples.json new file mode 100644 index 00000000..b58d8737 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/fight-club/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "fight-club", + "params": { + "gloveAddresses": { + "0x25ed58c027921E14D86380eA2646E3a1B5C55A8b": 3 + }, + "weightClassAddress": "0xF1F3ca6268f330fDa08418db12171c3173eE39C9", + "weightClassIds": { + "8": 4, + "26": 5, + "33": 6 + }, + "symbol": "FC" + } + }, + "network": "1", + "addresses": [ + "0x1EC1CcEF3e1735bdA3F4BA698e8a524AA7c93274", + "0x3eEFAa9d6e2ab7972C1001D41C82BB4881389257", + "0x7911670881A81F8410d06053d7B3c237cE77b9B4" + ], + "snapshot": 13921307 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/fight-club/index.ts b/Implementations/API/backend/utils/snapshot/strategies/fight-club/index.ts new file mode 100644 index 00000000..8225f8a1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/fight-club/index.ts @@ -0,0 +1,117 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'MantisClone'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function balanceOf(address account, uint256 id) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + // Arbitrary limit to avoid memory issues + const limit = 10; + if (options.gloveAddresses.length > limit) { + throw new Error( + `Number of glove addresses ${options.gloveAddresses.length} exceeds limit ${limit}` + ); + } + if (options.weightClassIds.length > limit) { + throw new Error( + `Number of weight class IDs ${options.weightClassIds.length} exceeds limit ${limit}` + ); + } + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + Object.entries(options.gloveAddresses).forEach(([gloveAddress]) => { + addresses.forEach((address: string) => { + multi.call( + `${address}.gloves.${gloveAddress}`, + gloveAddress, + 'balanceOf(address)', + [address] + ); + }); + }); + + Object.entries(options.weightClassIds).forEach(([weightClassId]) => { + addresses.forEach((address: string) => { + multi.call( + `${address}.weightClasses.${weightClassId}`, + options.weightClassAddress, + 'balanceOf(address, uint256)', + [address, weightClassId] + ); + }); + }); + + const result: Record< + string, + Record> + > = await multi.execute(); + + // console.log('Multicall result'); + // console.dir(result, { depth: null }); + + const weightedResult: Record< + string, + Record> + > = Object.fromEntries( + addresses.map((address: string) => [ + address, + { + gloves: Object.fromEntries( + Object.entries(result[address].gloves).map( + ([gloveAddress, numGloves]) => { + const hasGlove = numGloves.gte(1) ? 1 : 0; + return [ + gloveAddress, + hasGlove * options.gloveAddresses[gloveAddress] + ]; + } + ) + ), + weightClasses: Object.fromEntries( + Object.entries(result[address].weightClasses).map( + ([weightClassId, numKudos]) => { + const hasKudo = numKudos.gte(1) ? 1 : 0; + return [ + weightClassId, + hasKudo * options.weightClassIds[weightClassId] + ]; + } + ) + ) + } + ]) + ); + + // console.log('Weighted result'); + // console.dir(weightedResult, { depth: null }); + + return Object.fromEntries( + addresses.map((address: string) => { + // Only the glove with the highest associated weight is counted. + // Total vote score is 0 if voter has no gloves. + const maxGlove = Math.max( + ...Object.values(weightedResult[address].gloves) + ); + // Only the weight class Kudo with the highest associated weight is counted. + // Weight class multiplier defaults to 1 if voter has no weight class Kudos. + const maxWeightClass = + Math.max(...Object.values(weightedResult[address].weightClasses)) || 1; + return [address, maxGlove * maxWeightClass]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/flashstake/examples.json b/Implementations/API/backend/utils/snapshot/strategies/flashstake/examples.json new file mode 100644 index 00000000..25944bbb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/flashstake/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Flashstake query", + "strategy": { + "name": "flashstake", + "params": { + "address": "0x20398ad62bb2d930646d45a6d4292baa0b860c1f", + "symbol": "FLASH", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x4BF4e3a59add035c479CDE89d98103Cb65DCa941", + "0xd0f50C0431b76FA58Aeefdd68617acb4E0628051", + "0x1e13e5B5ACBB0C3F0fDe50fE7661fdF75df8F932", + "0xa54eEc957386246Ee0Da4FdB3D92c0c31528946E", + "0x3D0Ec0ADbe7c22EE54F1221F5b023471a1c86E51" + ], + "snapshot": 12375794 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/flashstake/index.ts b/Implementations/API/backend/utils/snapshot/strategies/flashstake/index.ts new file mode 100644 index 00000000..e6e0e0fd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/flashstake/index.ts @@ -0,0 +1,105 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const FLASHSTAKE_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/blockzerohello/flash-stake-stats-v2-subgraph' +}; + +export const author = 'anassohail99'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + liquidityPositions: { + liquidityTokenBalance: true, + user: { + id: true + }, + pair: { + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + } + } + }; + + const params2 = { + stakes: { + __args: { + where: { + user_in: addresses.map((address) => address.toLowerCase()), + isActive: true + } + }, + id: true, + amountIn: true, + user: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.users.__args.block = { number: snapshot }; + // @ts-ignore + params2.stakes.__args.block = { number: snapshot }; + } + const result = await subgraphRequest( + FLASHSTAKE_SUBGRAPH_URL[network], + params + ); + + const stakesResult = await subgraphRequest( + FLASHSTAKE_SUBGRAPH_URL[network], + params2 + ); + + const score = {}; + if (stakesResult && stakesResult.stakes) { + stakesResult.stakes.map((_data) => { + const address = getAddress(_data.user); + if (!score[address]) score[address] = 0; + score[address] = Number(score[address]) + Number(_data.amountIn); + }); + } + + if (result && result.users) { + result.users.map((_data) => { + if (_data.liquidityPositions[0]?.pair) { + _data.liquidityPositions.map((__data) => { + const address = getAddress(__data.user.id); + const token0perFlash = + Number(__data.pair.reserve0) / Number(__data.pair.totalSupply); + const userScore = + token0perFlash * Number(__data.liquidityTokenBalance); + if (!score[address]) score[address] = 0; + score[address] = Number(score[address]) + userScore; + }); + } + }); + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/flexa-capacity-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/flexa-capacity-staking/examples.json new file mode 100644 index 00000000..88863959 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/flexa-capacity-staking/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Flexa Capacity staking example query", + "strategy": { + "name": "flexa-capacity-staking", + "params": { + "symbol": "FLEX", + "apiBase": "https://api.capacity.staging.flexa.network" + } + }, + "network": "1", + "addresses": [ + "0x4621E524e6F95A6280b7761BDE3d150101b290F8", + "0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D", + "0xd649bACfF66f1C85618c5376ee4F38e43eE53b63", + "0x726022a9fe1322fA9590FB244b8164936bB00489", + "0xc6665eb39d2106fb1DBE54bf19190F82FD535c19", + "0x6ef2376fa6e12dabb3a3ed0fb44e4ff29847af68" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/flexa-capacity-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/flexa-capacity-staking/index.ts new file mode 100644 index 00000000..1c26520d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/flexa-capacity-staking/index.ts @@ -0,0 +1,40 @@ +import fetch from 'cross-fetch'; +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'amptoken'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const apiUrl = `${options.apiBase}/accounts?addresses=${addresses.join( + ',' + )}&snapshot=${snapshot}`; + + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + Accept: 'application/vnd.flexa.capacity.v1+json' + } + }); + const data = await response.json(); + + return Object.fromEntries( + data.map((value) => { + const { supplyTotal = 0, rewardTotal = 0 } = value; + return [ + getAddress(value.address), + parseFloat( + formatUnits(BigNumber.from(supplyTotal).add(rewardTotal), 18) + ) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/forta-shares/README.md b/Implementations/API/backend/utils/snapshot/strategies/forta-shares/README.md new file mode 100644 index 00000000..bd71ce20 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/forta-shares/README.md @@ -0,0 +1,3 @@ +# forta-shares + +This strategy calculates the voting power of the FORT shares owned by an address. This strategy requires no parameters. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/forta-shares/examples.json b/Implementations/API/backend/utils/snapshot/strategies/forta-shares/examples.json new file mode 100644 index 00000000..26e25c56 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/forta-shares/examples.json @@ -0,0 +1,11 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "forta-shares" + }, + "network": "137", + "addresses": ["0x8eedf056de8d0b0fd282cc0d7333488cc5b5d242"], + "snapshot": 30451309 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/forta-shares/index.ts b/Implementations/API/backend/utils/snapshot/strategies/forta-shares/index.ts new file mode 100644 index 00000000..9bc71f1c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/forta-shares/index.ts @@ -0,0 +1,89 @@ +import fetch from 'cross-fetch'; +import { multicall } from '../../utils'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'emanuel-sol'; +export const version = '0.0.1'; + +const STAKING_CONTRACT = '0xd2863157539b1D11F39ce23fC4834B62082F6874'; +const abi = [ + 'function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) view returns (uint256[] memory)' +]; + +const calculateVotingPower = (userVotingPower, addresses, balances) => { + for (let i = 0; i < addresses.length; i++) { + userVotingPower[addresses[i]] = + userVotingPower[addresses[i]] || BigNumber.from(0); + + const balance = balances[i]; + + userVotingPower[addresses[i]] = userVotingPower[addresses[i]].add(balance); + } + return userVotingPower; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const params = { + addresses: addresses + }; + + const response = await fetch('https://api.forta.network/stats/shares', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params) + }); + + const data = await response.json(); + + const batchAddresses = []; + const batchShareIds = []; + + data.shares.forEach((valuePair) => { + if (valuePair.shares) { + valuePair.shares.forEach((share) => { + batchAddresses.push( + addresses.find( + (addr) => addr.toLowerCase() === share.shareholder.toLowerCase() + ) + ); + batchShareIds.push(share.shareId); + }); + } + }); + + const result = await multicall( + network, + provider, + abi, + [[STAKING_CONTRACT, 'balanceOfBatch', [batchAddresses, batchShareIds]]], + { blockTag } + ); + + let userVotingPower = {}; + + userVotingPower = calculateVotingPower( + userVotingPower, + batchAddresses, + result[0][0] + ); + + return Object.fromEntries( + Object.entries(userVotingPower).map((addressBalancePair) => [ + addressBalancePair[0], + parseFloat(formatUnits(addressBalancePair[1], 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/frax-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/frax-finance/examples.json new file mode 100644 index 00000000..990a8edd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/frax-finance/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "frax-finance", + "params": { + "symbol": "FXS", + "decimals": 18, + "FXS": "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0", + "VEFXS": "0xc8418aF6358FFddA74e09Ca9CC3Fe03Ca6aDC5b0", + "UNI_LP_FRAX_FXS": "0xE1573B9D29e2183B1AF0e743Dc2754979A40D237", + "FARMING_UNI_LP_FRAX_FXS": "0xda2c338350a0E59Ce71CDCED9679A3A590Dd9BEC" + } + }, + "network": "1", + "addresses": [ + "0x36A87d1E3200225f881488E4AEedF25303FebcAe", + "0x234D953a9404Bf9DbC3b526271d440cD2870bCd2", + "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "0x22511ffA88433aB56495947b559dfB62B3d47633" + ], + "snapshot": 12557453 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/frax-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/frax-finance/index.ts new file mode 100644 index 00000000..98903cdd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/frax-finance/index.ts @@ -0,0 +1,229 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall } from '../../utils'; + +const BIG18 = BigNumber.from('1000000000000000000'); + +export const author = 'FraxFinance'; +export const version = '0.0.2'; + +// 0.0.1: FXS Plus FXS in LPs +// 0.0.2: Adds veFXS and removes outdated SushiSwap LPs + +const DECIMALS = 18; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'boostedBalanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'getReserves', + outputs: [ + { + internalType: 'uint112', + name: '_reserve0', + type: 'uint112' + }, + { + internalType: 'uint112', + name: '_reserve1', + type: 'uint112' + }, + { + internalType: 'uint32', + name: '_blockTimestampLast', + type: 'uint32' + } + ], + stateMutability: 'view', + type: 'function', + constant: true + }, + { + inputs: [], + name: 'token0', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Fetch FXS Balance + const fxsQuery = addresses.map((address: any) => [ + options.FXS, + 'balanceOf', + [address] + ]); + + // Fetch veFXS Balance + const vefxsQuery = addresses.map((address: any) => [ + options.VEFXS, + 'balanceOf', + [address] + ]); + + // Fetch FREE_UNI_LP_FRAX_FXS Balance + const freeUniLPFraxFxsQuery = addresses.map((address: any) => [ + options.UNI_LP_FRAX_FXS, + 'balanceOf', + [address] + ]); + + // Fetch FARMING_UNI_LP_FRAX_FXS Balance + const farmingUniLPFraxFxsQuery = addresses.map((address: any) => [ + options.FARMING_UNI_LP_FRAX_FXS, + 'boostedBalanceOf', + [address] + ]); + + const response = await multicall( + network, + provider, + abi, + [ + // Get 1inch LP OPIUM-ETH balance of OPIUM + // [options.OPIUM, 'balanceOf', [options.LP_1INCH_OPIUM_ETH]], + [options.UNI_LP_FRAX_FXS, 'token0'], + [options.UNI_LP_FRAX_FXS, 'getReserves'], + [options.UNI_LP_FRAX_FXS, 'totalSupply'], + ...fxsQuery, + ...vefxsQuery, + ...freeUniLPFraxFxsQuery, + ...farmingUniLPFraxFxsQuery + ], + { blockTag } + ); + + const uniLPFraxFxs_token0 = response[0]; + const uniLPFraxFxs_getReserves = response[1]; + const uniLPFraxFxs_totalSupply = response[2]; + + // Uniswap FRAX/FXS + // ---------------------------------------- + let uni_FraxFxs_reservesFXS_E0; + if (uniLPFraxFxs_token0[0] == options.FXS) + uni_FraxFxs_reservesFXS_E0 = uniLPFraxFxs_getReserves[0]; + else uni_FraxFxs_reservesFXS_E0 = uniLPFraxFxs_getReserves[1]; + const uni_FraxFxs_totalSupply_E0 = uniLPFraxFxs_totalSupply[0]; + const uniLPFraxFxs_fxs_per_LP_E18 = uni_FraxFxs_reservesFXS_E0 + .mul(BIG18) + .div(uni_FraxFxs_totalSupply_E0); + + const responseClean = response.slice(3, response.length); + + const chunks = chunk(responseClean, addresses.length); + const fxsBalances = chunks[0]; + const vefxsBalances = chunks[1]; + const freeUniFraxFxsBalances = chunks[2]; + const farmUniFraxFxsBalances = chunks[3]; + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + const free_fxs = fxsBalances[i][0]; + const vefxs = vefxsBalances[i][0]; + const free_uni_frax_fxs = freeUniFraxFxsBalances[i][0]; + const farm_uni_frax_fxs = farmUniFraxFxsBalances[i][0]; + + // Print statements + // console.log(`==================${addresses[i]}==================`); + // console.log("Free FXS: ", free_fxs.div(BIG18).toString()); + // console.log("veFXS: ", vefxs.div(BIG18).toString()); + // console.log("Free Uni FRAX/FXS LP: ", free_uni_frax_fxs.div(BIG18).toString()); + // console.log("Farmed Uni FRAX/FXS LP [boosted]: ", farm_uni_frax_fxs.div(BIG18).toString()); + // console.log("------"); + // console.log("E18"); + // console.log("FXS per Uni FRAX/FXS LP E18: ", uniLPFraxFxs_fxs_per_LP_E18.toString()); + // console.log("E0"); + // console.log("FXS per Uni FRAX/FXS LP E0: ", uniLPFraxFxs_fxs_per_LP_E18.div(BIG18).toString()); + // console.log(``); + + return [ + addresses[i], + parseFloat( + formatUnits( + free_fxs + .add(vefxs) + .add( + free_uni_frax_fxs.mul(uniLPFraxFxs_fxs_per_LP_E18).div(BIG18) + ) // FXS share in free Uni FRAX/FXS LP + .add( + farm_uni_frax_fxs.mul(uniLPFraxFxs_fxs_per_LP_E18).div(BIG18) + ) // FXS share in farmed Uni FRAX/FXS LP [boosted] + .toString(), + DECIMALS + ) + ) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/galaxy-nft-with-score/examples.json b/Implementations/API/backend/utils/snapshot/strategies/galaxy-nft-with-score/examples.json new file mode 100644 index 00000000..9a8dbfb4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/galaxy-nft-with-score/examples.json @@ -0,0 +1,48 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "galaxy-nft-with-score", + "params": { + "symbol": "SUPA", + "params": { + "NFTCoreAddress": ["0x86835C3B8fA69f8F31C72477C7776A3B61dbAC92"], + "blacklistNFTID": ["4"], + "configs": [ + { + "name": "Basic", + "cumulative": false, + "votingPower": 1 + }, + { + "name": "SuperUMAn", + "cumulative": false, + "votingPower": 10 + }, + { + "name": "UMAster", + "cumulative": false, + "votingPower": 20 + }, + { + "name": "Alumni", + "cumulative": false, + "votingPower": 1 + } + ] + } + } + }, + "network": "137", + "addresses": [ + "0x31A4C53BE2356b09220D348508B97Ed9b26a6c30", + "0x69D0341d380a1229f4751a0A721345dBc716586C", + "0xe5fF2C80759db9408dC9fD22b155b851Cd5aAA94", + "0xBaE38Bf54f2aA0e6159f3d9B656300f61DBd2ED3", + "0x5924EB6A482CeA85e23403E448257FbE5c58A9ed", + "0xD02BAE47fD93325fd0cc3d660B73D8f93C4D7B5E", + "0xfDAc618DDB79f77eb2Ea5c52b7c50f8f728DAFce" + ], + "snapshot": 26049784 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/galaxy-nft-with-score/index.ts b/Implementations/API/backend/utils/snapshot/strategies/galaxy-nft-with-score/index.ts new file mode 100644 index 00000000..2797aba1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/galaxy-nft-with-score/index.ts @@ -0,0 +1,201 @@ +import fetch from 'cross-fetch'; +import { subgraphRequest } from '../../utils'; + +export const author = 'alberthaotan'; +export const version = '0.3.2'; + +const Networks: { + [network: string]: { + name: string; + graphql: string; + subgraph: string; + }; +} = { + '1': { + name: 'ETHEREUM', + graphql: 'https://graphigo.prd.galaxy.eco/query', + subgraph: 'https://api.thegraph.com/subgraphs/name/alberthaotan/nft-eth' + }, + '56': { + name: 'BSC', + graphql: 'https://graphigo.prd.galaxy.eco/query', + subgraph: 'https://api.thegraph.com/subgraphs/name/alberthaotan/nft-bsc' + }, + '137': { + name: 'MATIC', + graphql: 'https://graphigo.prd.galaxy.eco/query', + subgraph: 'https://api.thegraph.com/subgraphs/name/alberthaotan/nft-matic' + } +}; + +interface Config { + name: string; + votingPower: number; + cumulative: boolean; +} + +interface OwnerWithNfts { + [owner: string]: { + [tokenId: string]: string; + }; +} + +interface OwnerToNftCount { + [owner: string]: { + [name: string]: number; + }; +} + +interface OwnerToScore { + [owner: string]: number; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const restoreAddress = addresses.reduce((map, address) => { + map[address.toLowerCase()] = address; + return map; + }, {}); + //addresses.map((a) => a.toLowerCase()) + + const subgraphParams = { + nftContracts: { + __args: { + where: { + id_in: options.params.NFTCoreAddress.map((a) => a.toLowerCase()) + } + }, + id: true, + nfts: { + __args: { + first: 1000 + }, + tokenID: true, + ownership: { + owner: true + } + } + } + }; + if (snapshot !== 'latest') { + subgraphParams.nftContracts.__args['block'] = { number: snapshot }; + } + + const graphqlParams = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + operationName: 'allNFTsByOwnersCoresAndChain', + query: `query allNFTsByOwnersCoresAndChain($option: NFTsOptions!) { + allNFTsByOwnersCoresAndChain(option: $option) { + owner + nfts + { + id + name + nftCore + { + contractAddress + } + } + } + }`, + variables: { + option: { + nftCoreAddresses: options.params.NFTCoreAddress, + chain: Networks[network].name, + owners: addresses + } + } + }) + }; + + const graphqlPromise = fetch(Networks[network].graphql, graphqlParams); + const subgraphPromise = subgraphRequest( + options.params.subgraph + ? options.params.subgraph + : Networks[network].subgraph, + subgraphParams + ); + const promisesRes = await Promise.all([graphqlPromise, subgraphPromise]); + const graphqlData = await promisesRes[0].json(); + const subgraphData = promisesRes[1]; + + // Initialize objects + const configs: Config[] = options.params.configs; + const ownerToNftCount: OwnerToNftCount = Object.fromEntries( + addresses.map((addr) => [addr.toLowerCase(), {}]) + ); + + const ownerToScore: OwnerToScore = {}; + const ownersWithNfts: OwnerWithNfts = + graphqlData.data.allNFTsByOwnersCoresAndChain.reduce((map, item) => { + map[item.owner.toLowerCase()] = item.nfts.reduce((m, i) => { + if (!options.params.blacklistNFTID?.includes(i.id)) { + m[i.nftCore.contractAddress.toLowerCase() + '-' + i.id] = i.name; + } + return m; + }, {}); + return map; + }, {}); + + const subgraphOwnersWithNfts: OwnerWithNfts = {}; + subgraphData.nftContracts.forEach((nftContract) => { + nftContract.nfts.forEach((nft) => { + if (!(nft.ownership[0].owner in subgraphOwnersWithNfts)) { + subgraphOwnersWithNfts[nft.ownership[0].owner] = {}; + } + subgraphOwnersWithNfts[nft.ownership[0].owner][ + nftContract.id + '-' + nft.tokenID + ] = ''; + }); + }); + + // Intersect nft holdings of owners from graphql and subgraph returns + Object.keys(subgraphOwnersWithNfts).forEach((owner) => { + Object.keys(subgraphOwnersWithNfts[owner]).forEach((tokenId) => { + if (owner in ownersWithNfts && tokenId in ownersWithNfts[owner]) { + subgraphOwnersWithNfts[owner][tokenId] = ownersWithNfts[owner][tokenId]; + } + }); + }); + + // Get owners nft counts base on nft name + Object.keys(subgraphOwnersWithNfts).forEach((owner) => { + Object.keys(subgraphOwnersWithNfts[owner]).forEach((tokenId) => { + const nftName = subgraphOwnersWithNfts[owner][tokenId]; + if (nftName != '') { + if (nftName in ownerToNftCount[owner]) { + ownerToNftCount[owner][nftName]++; + } else { + ownerToNftCount[owner][nftName] = 1; + } + } + }); + }); + + // Get owners score base on certain config + Object.keys(ownerToNftCount).forEach((owner) => { + ownerToScore[restoreAddress[owner]] = 0; + configs.forEach((config) => { + if (config.name in ownerToNftCount[owner]) { + if (config.cumulative) { + ownerToScore[restoreAddress[owner]] += + config.votingPower * ownerToNftCount[owner][config.name]; + } else { + ownerToScore[restoreAddress[owner]] += config.votingPower * 1; + } + } + }); + }); + + return ownerToScore; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/README.md b/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/README.md new file mode 100644 index 00000000..488d2873 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/README.md @@ -0,0 +1,27 @@ +# Grant Voting Power with Galxe's Loyalty Points + +With Galxe’s Loyalty Point System, there are a number of strategies you could utilize to reward your community. Our “galxe-loyalty-points” strategy on [Snapshot.org](http://Snapshot.org) is especially designed to accommodate projects that don’t issue ERC-20 tokens or NFTs. By using Galxe Loyalty Points, you can easily reward your contributors with voting power on your proposals. + +Galxe leverages the [Snapshot](https://snapshot.org/#/) platform for Loyalty Points voting, which offers a simple and secure way to vote on-chain. If you’re not familiar with Snapshot, check out their documentation on the [home-snapshot](https://docs.snapshot.org/) to get started. You can seamlessly integrate Galxe Loyalty Points to let your most loyal community members to vote on gated Snapshot proposals. + +For testing purposes, we’ve provided a [playground here](https://snapshot.org/#/playground/galxe-loyalty-points) for you to try things out! + +Here’s how to set it up: + +1. Access your Snapshot Space admin and select the “galxe-loyalty-points” strategy. +2. Since your Galxe Loyalty Points are not tied to any specific chain or snapshot yet, pick whichever chain and snapshot you’d like for testing. +3. Set up the strategy parameters accordingly. + ``` + { + "symbol": "PTS", // Voting power unit shows in proposal + "space_url": "https://galxe.com/Galxe/campaigns", // Galxe space url + "snapshot_id": "chm75g3gm3qfltgb9av0" + } + ``` +4. Input some addresses to test the voting power of each address, and you’re good to go! + +At Galxe, we believe in the power of community, decentralized decision-making, and blockchain technology to drive innovation. With Galxe Loyalty Point System and voting strategy, we’re working to make decentralized decision making more accessible and user-friendly for everyone. + +### About Galxe + +Galxe is the leading platform for building web3 community. With over 30 million users, Galxe has propelled the growth of Optimism, Polygon, Arbitrum, and more than 1800 partners with reward-based loyalty programs. Start your campaign today at [galxe.com](http://galxe.com/)! diff --git a/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/examples.json b/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/examples.json new file mode 100644 index 00000000..77559691 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Galxe users loyalty points", + "strategy": { + "name": "galxe-loyalty-points", + "params": { + "symbol": "PTS", + "space_url": "https://galxe.com/Galxe/campaigns", + "snapshot_id": "chm75g3gm3qfltgb9av0" + } + }, + "network": "137", + "addresses": [ + "0xC229c465E08C4bbE5743B36F321A677D8a398062", + "0x447D24B00937BCc3fE5768c828fb2fae52c3e8C5", + "0x498d038C3F0aFA27D80B4A82e76dcEf5A1dE7840", + "0xfDAc618DDB79f77eb2Ea5c52b7c50f8f728DAFce" + ], + "snapshot": 1 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/index.ts b/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/index.ts new file mode 100644 index 00000000..5df3e24d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/galxe-loyalty-points/index.ts @@ -0,0 +1,60 @@ +import fetch from 'cross-fetch'; +import { error } from 'console'; + +export const author = 'HaynarCool'; +export const version = '0.1.0'; + +const graphqlUrl = 'https://graphigo.prd.galaxy.eco/query'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const url = new URL(options.space_url); + const parts = url.pathname.split('/'); + if (parts.length < 2) { + throw error('invalid galxe space url'); + } + const graphqlParams = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + operationName: 'galxeLoyaltyPoints', + query: `query galxeLoyaltyPoints($alias: String! $addresses: [String!]! $snapshotId: String!) { + space(alias: $alias) { + addressesLoyaltyPoints(addresses: $addresses, snapshotId: $snapshotId) { + address + space + points + } + } + }`, + variables: { + alias: parts[1], + addresses: addresses, + snapshotId: options.snapshot_id + ? options.snapshot_id + : typeof snapshot === 'number' + ? snapshot + : '' + } + }) + }; + const graphqlData = await fetch(graphqlUrl, graphqlParams) + .then((r) => r.json()) + .catch((e) => { + console.error('query galxe user loyalty points failed'); + throw e; + }); + const scores = {}; + graphqlData.data.space.addressesLoyaltyPoints.forEach((item) => { + scores[item.address] = item.points; + }); + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/README.md b/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/README.md new file mode 100644 index 00000000..85938c4c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/README.md @@ -0,0 +1,16 @@ +# gamium-voting + +This is the voting strategy for holders and stakers of Gamium's ERC20 token (GMM). + +Here is an example of parameters: + +```json +{ + "token": "0x5B6bf0c7f989dE824677cFBD507D9635965e9cD3", + "lp_token": "0xEdeec0ED10Abee9b5616bE220540CAb40C9d991E", + "staking_token": "0x8a3FB54dE0df64915FD66B55e1594141C1A880AB", + "staking_pair": "0xaD0916e7Ba7100629EAe9143e035F98ab5EA4ABd", + "symbol": "GMM", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/examples.json new file mode 100644 index 00000000..6be002eb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "GAMIUM Holders & Stakers", + "strategy": { + "name": "gamium-voting", + "params": { + "token": "0x5B6bf0c7f989dE824677cFBD507D9635965e9cD3", + "lp_token": "0xEdeec0ED10Abee9b5616bE220540CAb40C9d991E", + "staking_token": "0x8a3FB54dE0df64915FD66B55e1594141C1A880AB", + "staking_pair": "0xaD0916e7Ba7100629EAe9143e035F98ab5EA4ABd", + "symbol": "GMM", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0xbf9Cd2f0902e1c09A096384b3D673728BcEB5a48", + "0xD662F69daA8acD8A4eBD7E25fcF1cFc054106Ce9", + "0x02622882eDE73f887D5602B2F8285a43A528a120", + "0xcE12c8FdFa479FD9e1B37327ee960ea441E4bc80" + ], + "snapshot": 17342974 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/index.ts new file mode 100644 index 00000000..09e43ff0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gamium-voting/index.ts @@ -0,0 +1,121 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'gamiumworld'; +export const version = '0.1.0'; + +const tokenAbi = [ + 'function balanceOf(address _owner) view returns (uint256 balance)' +]; +const stakingAbi = [ + 'function totalStakeTokenDeposited(address user) view returns (uint256)' +]; +const liquidityPoolAbi = [ + 'function getReserves() view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)', + 'function totalSupply() view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + options.token = options.token || '0x5B6bf0c7f989dE824677cFBD507D9635965e9cD3'; + options.lp_token = + options.lp_token || '0xEdeec0ED10Abee9b5616bE220540CAb40C9d991E'; + options.staking_token = + options.staking_token || '0x8a3FB54dE0df64915FD66B55e1594141C1A880AB'; + options.staking_pair = + options.staking_pair || '0xaD0916e7Ba7100629EAe9143e035F98ab5EA4ABd'; + options.symbol = options.symbol || 'GMM'; + options.decimals = options.decimals || 18; + + const liquidityPoolMulticaller = new Multicaller( + network, + provider, + liquidityPoolAbi, + { blockTag } + ); + + liquidityPoolMulticaller.call( + 'lpTotalSupply', + options.lp_token, + 'totalSupply' + ); + liquidityPoolMulticaller.call('lpReserves', options.lp_token, 'getReserves'); + + const { lpTotalSupply, lpReserves } = + await liquidityPoolMulticaller.execute(); + + const liquidityPoolTokenRatio = + parseFloat(formatUnits(lpReserves[0], options.decimals)) / + parseFloat(formatUnits(lpTotalSupply, options.decimals)); + + const tokenMulticaller = new Multicaller(network, provider, tokenAbi, { + blockTag + }); + const stakingTokenMulticaller = new Multicaller( + network, + provider, + stakingAbi, + { blockTag } + ); + const stakingPairMulticaller = new Multicaller( + network, + provider, + stakingAbi, + { blockTag } + ); + + addresses.forEach((address) => { + stakingPairMulticaller.call( + address, + options.staking_pair, + 'totalStakeTokenDeposited', + [address] + ); + stakingTokenMulticaller.call( + address, + options.staking_token, + 'totalStakeTokenDeposited', + [address] + ); + tokenMulticaller.call(address, options.token, 'balanceOf', [address]); + }); + + const [stakingPairResponse, stakingTokenResponse, tokenResponse]: [ + Record, + Record, + Record + ] = await Promise.all([ + stakingPairMulticaller.execute(), + stakingTokenMulticaller.execute(), + tokenMulticaller.execute() + ]); + + return Object.fromEntries( + addresses.map((address) => { + const tokenBalance = parseFloat( + formatUnits(tokenResponse[address], options.decimals) + ); + const stakingTokenBalance = parseFloat( + formatUnits(stakingTokenResponse[address], options.decimals) + ); + const stakingPairBalance = + parseFloat( + formatUnits(stakingPairResponse[address], options.decimals) + ) * + (1 + liquidityPoolTokenRatio); + return [ + address, + tokenBalance + 2 * stakingTokenBalance + 2 * stakingPairBalance + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/README.md b/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/README.md new file mode 100644 index 00000000..4040b7a2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/README.md @@ -0,0 +1,19 @@ +# GATENet Total Staked Strategy + +A custom GATENet Total Staked Strategy to calculate total staked (voting power) for a wallet's address on the GATENet Staking Platform (https://staking.gatenet.io/). + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "GATE", + "decimals": 18, + "minBalance": 10000 +} +``` +### Definitions +- Address → Smart Contract Address +- Symbol → Symbol of token +- Decimals → Number of decinals of the token +- Minimum Balance → Minimum balance / staked tokens required to vote. +If staked value is <= minBalance it will result 0 (zero) voting power. + diff --git a/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/examples.json b/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/examples.json new file mode 100644 index 00000000..66160389 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/examples.json @@ -0,0 +1,33 @@ +[ + { + "name": "gatenet-total-staked", + "strategy": { + "name": "gatenet-total-staked", + "params": { + "address": "0xB53E4B0F128B231B8728525C616c26E0E59407A1", + "symbol": "GATE", + "decimals": 18, + "subgraph": "https://api.studio.thegraph.com/query/17252/gatenet-cvm/v0.6.1", + "minBalance": 10000 + } + }, + "network": "4", + "addresses": [ + "0xDC70958F76A639922823A8990Cb115B1816E5Ca0", + "0x6646e8428B5029369392B15a6c2e4743bfBdb03a", + "0xCF4a5c2aA6d35F8B1A52E4b336D192b3dEaae416", + "0x3a61B9a71F9dc0182B1910EbB8315a12157Ff293", + "0xa7a7aCC9b9f3eB7136b3F4e33E95fD68bf28c134", + "0xE97D8f38746c2b6114e7AC7ae185750bDE2FF211", + "0x8b273e4042387789bcAeC10dD33D91E7fe9F4179", + "0xc100ADEC02c12fd0A205d589769851213dCc27dE", + "0x96A1003B7FF56a2Df974B6A7e9ddC586f4761f77", + "0x9BA85d16db13D25de2426d5eb42b55794B1efF22", + "0xb60A6e063C15D79afBE1E389e52b3b2f2796aF8B", + "0xbF33c13F3eBC9175ADcC1c4614c84f73288E9101", + "0x66F06B8C449CF2c8ef2D3274738d2185491C6e35", + "0xC48c8F74B4aeC9cC7669700CBfAF83fa7c2469e3" + ], + "snapshot": 11464656 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/index.ts b/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/index.ts new file mode 100644 index 00000000..6b2ceeda --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/index.ts @@ -0,0 +1,241 @@ +import { Multicaller, subgraphRequest } from '../../utils'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'usagar80'; +export const version = '0.0.2'; + +const UNSTAKE = 'Unstake'; +const STAKE = 'Stake'; +const FEE_DISTRIBUTION = 'Fee distribution'; +const REWARD_ADDED = 'Reward'; +const CLAIM = 'Claim'; +const OTHERS = 'others'; + +const abi = ['function LOCK_PERIOD() view returns (uint256)']; + +const getTransactionType = (transaction) => { + switch (transaction.__typename) { + case 'Staking': + if (transaction.txName === 'FeeDistribution') { + return FEE_DISTRIBUTION; + } else if (transaction.txName === 'RewardAdded') { + return REWARD_ADDED; + } else if (transaction.txName === 'Claim') { + return CLAIM; + } + return OTHERS; + case 'CompoundDeposit': + return STAKE; + default: + return UNSTAKE; + } +}; + +//gatenet-total-staked +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 }); + multi.call('lockPeriod', options.address, 'LOCK_PERIOD'); + const multiResult = await multi.execute(); + const lockPeriod = Number(multiResult.lockPeriod); + const result: Record = {}; + const args = { + where: { + sender_in: addresses + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + args.where.time_lte = (await provider.getBlock(snapshot)).timestamp; + } + + const query = { + compoundDeposits: { + __args: args, + __typename: true, + id: true, + sender: true, + amount: true, + shares: true, + time: true + }, + compoundWithdraws: { + __args: args, + __typename: true, + id: true, + sender: true, + amount: true, + shares: true, + time: true + }, + stakings: { + __typename: true, + id: true, + txName: true, + amount: true, + user: true, + time: true, + unStakeIndex: true + } + }; + const transactionsList = await subgraphRequest(options.subgraph, query); + + for (const address of addresses) { + let feePerShare = BigNumber.from(0); + let rewardRate = BigNumber.from(0); + let waitingFees = BigNumber.from(0); + let waitingRewards = BigNumber.from(0); + let currentShares = BigNumber.from(0); + let transactionTypeTemp = ''; + const ether = BigNumber.from(10).pow(18); + + const compoundDeposits = transactionsList.compoundDeposits.filter( + (s) => s.sender.toLowerCase() === address.toLowerCase() + ); + const compoundWithdraws = transactionsList.compoundWithdraws.filter( + (s) => s.sender.toLowerCase() === address.toLowerCase() + ); + + if (compoundDeposits.length > 0) { + let rawTransactions; + // Keeping split condition for the case of Only deposite transaction. + if (compoundWithdraws.length > 0) { + rawTransactions = compoundDeposits + .concat(compoundWithdraws) + .concat(transactionsList.stakings) + .sort(function (a, b) { + return a.time - b.time; + }); + } else { + rawTransactions = compoundDeposits + .concat(transactionsList.stakings) + .sort(function (a, b) { + return a.time - b.time; + }); + } + + const transactions = rawTransactions + .map((transaction) => { + const type = getTransactionType(transaction); + if (type !== OTHERS) { + let amount; + switch (type) { + case STAKE: { + currentShares = BigNumber.from(transaction.shares).add( + currentShares + ); + amount = BigNumber.from(transaction.amount); + break; + } + + case FEE_DISTRIBUTION: { + const totalShares = BigNumber.from(transaction.unStakeIndex); + const transactionAmount = BigNumber.from(transaction.amount); + const previousFeePerShare = feePerShare; + /// 0 + feePerShare = previousFeePerShare.add( + transactionAmount.mul(ether).div(totalShares) + ); + // 0 + amount = currentShares.mul( + feePerShare.sub(previousFeePerShare).div(ether) + ); + waitingFees = waitingFees.add(amount); + break; + } + + case REWARD_ADDED: { + const rewardRateIncrease = BigNumber.from( + transaction.unStakeIndex + ); + const previousRewardRate = rewardRate; + rewardRate = rewardRate + .add(rewardRateIncrease) + .add(previousRewardRate); + + amount = currentShares.mul(rewardRate.sub(previousRewardRate)); + waitingRewards = waitingRewards.add(amount); + break; + } + case UNSTAKE: { + currentShares = currentShares.sub( + BigNumber.from(transaction.shares || 0) + ); + amount = BigNumber.from(transaction.amount); + break; + } + case 'Claim': { + if (transaction.user.toUpperCase() === address.toUpperCase()) { + if (transactionTypeTemp !== UNSTAKE) { + amount = BigNumber.from(transaction.amount); + } + } + break; + } + } + transactionTypeTemp = type; + return amount + ? { + name: type, + timeStamp: transaction.time, + amount + } + : null; + } + }) + .filter((transaction) => transaction) + .reverse(); + // Copied Content Start + const firstUnstakeIndex = transactions.findIndex( + (x) => x.name === UNSTAKE + ); + let filtered = transactions; + if (firstUnstakeIndex >= 0) + filtered = transactions.slice(0, firstUnstakeIndex); + const stake: BigNumberish = filtered + .filter((t) => t.name === STAKE) + .reduce((acc: BigNumberish, transaction) => { + return BigNumber.from(transaction.amount).add(acc); + }, BigNumber.from(0)); + const firstUnstaked = transactions[firstUnstakeIndex]; + let lockedTransaction: BigNumberish = transactions + .slice(firstUnstakeIndex) + .filter( + (t) => + firstUnstaked && + firstUnstaked.timeStamp - t.timeStamp <= lockPeriod && + t.name === STAKE + ) + .reduce((acc: BigNumberish, t) => { + return BigNumber.from(t.amount).add(acc); + }, BigNumber.from(0)); + if (!lockedTransaction) lockedTransaction = 0; + + result[address] = BigNumber.from(0).add(stake).add(lockedTransaction); + } else { + result[address] = BigNumber.from(0); + } + } + const score = Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); + + Object.keys(score).forEach((key) => { + if (score[key] >= (options.minBalance || 0)) score[key] = score[key]; + else score[key] = 0; + }); + + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/schema.json b/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/schema.json new file mode 100644 index 00000000..783a7685 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gatenet-total-staked/schema.json @@ -0,0 +1,43 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + }, + "subgraph": { + "type": "string", + "title": "Subgraph URL", + "pattern": "[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)?" + }, + "minBalance": { + "type": "number", + "title": "Minimum token staked", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/genart/examples.json b/Implementations/API/backend/utils/snapshot/strategies/genart/examples.json new file mode 100644 index 00000000..6241ca12 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genart/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "GEN.ART", + "strategy": { + "name": "genart", + "params": { + "tokenAddress": "0xccdcfced87f8d91028b4fbbb589fb4cdc24d08fa", + "membershipAddress": "0xbadc470f2e159f01396a546fc63d8c0db2697f3b", + "symbol": "GENART", + "decimals": 18 + } + }, + "network": "4", + "addresses": [ + "0x8939a7106957dD14bf3D3aCc9151b96E4bD81bC6", + "0x7C804f539De39Fd3E7d6F2834eebA24d52a30Ca4" + ], + "snapshot": 9787953 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/genart/index.ts b/Implementations/API/backend/utils/snapshot/strategies/genart/index.ts new file mode 100644 index 00000000..d2a832fe --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genart/index.ts @@ -0,0 +1,47 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'stzky'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multiMembership = new Multicaller(network, provider, abi, { + blockTag + }); + const multiToken = new Multicaller(network, provider, abi, { + blockTag + }); + addresses.forEach((address) => + multiMembership.call(address, options.membershipAddress, 'balanceOf', [ + address + ]) + ); + const members: Record = await multiMembership.execute(); + + addresses.forEach((address) => + multiToken.call(address, options.tokenAddress, 'balanceOf', [address]) + ); + const result: Record = await multiToken.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) * + (Number(members[address].toString()) > 0 ? 1 : 0) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/genomesdao/README.md b/Implementations/API/backend/utils/snapshot/strategies/genomesdao/README.md new file mode 100644 index 00000000..d16cf764 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genomesdao/README.md @@ -0,0 +1,16 @@ +# genomesdao + +This strategy returns the balances of the voters and the LP'ed amount for a specific ERC20 token. + +Here is an example of parameters: + +```json +{ + "address": "0x6E8a8726639d12935b3219892155520bdC57366B", + "lpaddress": "0xc1214b61965594b3e08Ea4950747d5A077Cd1886", + "harvaddress": "0x0a0be71cfce12ecb966dc762016c28ef5640b56a", + "veaddress": "0xae059C276904B9E885710c4a68434b4Bc437da6e", + "symbol": "GNOME", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/genomesdao/examples.json b/Implementations/API/backend/utils/snapshot/strategies/genomesdao/examples.json new file mode 100644 index 00000000..5fdd3d26 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genomesdao/examples.json @@ -0,0 +1,41 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "genomesdao", + "params": { + "address": "0x6E8a8726639d12935b3219892155520bdC57366B", + "lpaddress": "0xc1214b61965594b3e08Ea4950747d5A077Cd1886", + "harvaddress": "0x0a0be71cfce12ecb966dc762016c28ef5640b56a", + "veaddress": "0xae059C276904B9E885710c4a68434b4Bc437da6e", + "symbol": "GNOME", + "decimals": 18 + } + }, + "network": "137", + "addresses": [ + "0xd53c79ff8c473bbfe4e40e5525d4d24fd4b8534c", + "0xA4e2E09eDEDC19342801243CcFcB834F582616aE", + "0x326a9c20f220042c9e2ee3d7969e2a3963a67606", + "0xc1214b61965594b3e08ea4950747d5a077cd1886", + "0x7641d6f27dfd771daa164b9dc9aa13a8ebe41ce1", + "0x39cc360806b385c96969ce9ff26c23476017f652", + "0xb2fe752658b69bb6121cc20fc44453cb64a41c84", + "0x7566f600d85c1d4e8adbd7ec320cf0de90c13199", + "0x7f30ef435c125e42cd8ec68c3f2e9f79c5923901", + "0xc292eafd5422a9c961cc2630cbddf04c93db33bd", + "0x85a31f476428284c2eba10c1d18b4b66a8503d1f", + "0x5771605020287556c6f24341879437d38a651250", + "0xc2ddec8a16c757d7fc683b4ad4634ef87be6f26f", + "0x452cbcc3ff392ae6e42f0aff8113f21f0a144846", + "0xa4e2e09ededc19342801243ccfcb834f582616ae", + "0xeb8dfecd1d9447f31122cae7f1bada00dda9746c", + "0xfaf6a40bc40cef05fafdc3e0036611c8f8c9d3fa", + "0x374615cb8cf32360eb592f3f35163ed4c2fdcc7a", + "0xd49c91a75fbf8943d12c65b8d1a5da4173ed1d21", + "0x74ddd85389f4fd0069c922273498b4e28cdc9ccc", + "0x0057ff99a06f82cd876c4f7f1718bd9a4f2e74b6" + ], + "snapshot": 26201867 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/genomesdao/index.ts b/Implementations/API/backend/utils/snapshot/strategies/genomesdao/index.ts new file mode 100644 index 00000000..9efab03c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genomesdao/index.ts @@ -0,0 +1,69 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'Charles-repo'; +export const version = '0.1.2'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)', + 'function token0() external view returns (address)', + 'function getReserves() external view returns (uint112, uint112, uint32)' +]; + +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: any) => + multi.call(address, options.address, 'balanceOf', [address]) + ); + const result: Record = await multi.execute(); + + addresses.forEach((address: any) => + multi.call(address, options.veaddress, 'balanceOf', [address]) + ); + const veresult: Record = await multi.execute(); + + addresses.forEach((address: any) => + multi.call(address, options.lpaddress, 'balanceOf', [address]) + ); + const resultLP1: Record = await multi.execute(); + + addresses.forEach((address: any) => + multi.call(address, options.harvaddress, 'balanceOf', [address]) + ); + const resultLP2: Record = await multi.execute(); + + multi.call('token0', options.lpaddress, 'token0', []); + multi.call('getReserves', options.lpaddress, 'getReserves', []); + multi.call('totalSupply', options.lpaddress, 'totalSupply', []); + const { token0, getReserves, totalSupply } = await multi.execute(); + let totalGnomeAmount: BigNumberish; + if (token0.toLowerCase() === options.address.toLowerCase()) { + totalGnomeAmount = getReserves[0]; + } else { + totalGnomeAmount = getReserves[1]; + } + + return Object.fromEntries( + Object.entries(resultLP1).map(([address, balance]) => { + let bal: BigNumber = BigNumber.from(balance) + .add(resultLP2[address]) + .mul(totalGnomeAmount) + .div(totalSupply); + bal = bal + .add(result[address]) + .add(BigNumber.from(veresult[address]).mul(5)); + return [address, parseFloat(formatUnits(bal, options.decimals))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/genomesdao/schema.json b/Implementations/API/backend/utils/snapshot/strategies/genomesdao/schema.json new file mode 100644 index 00000000..51b7369d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genomesdao/schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "address": { + "type": "string", + "title": "Governance Token Contract address", + "examples": ["e.g. 0x6E8a8726639d12935b3219892155520bdC57366B"] + }, + "lpaddress": { + "type": "string", + "title": "LP Token Contract address on Sushi", + "examples": ["e.g. 0xc1214b61965594b3e08Ea4950747d5A077Cd1886"] + }, + "harvaddress": { + "type": "string", + "title": "Harvest Token Contract Address", + "examples": ["e.g. 0x0a0be71cfce12ecb966dc762016c28ef5640b56a"] + }, + "veaddress": { + "type": "string", + "title": "veGNOME Token Contract Address", + "examples": ["e.g. 0xae059C276904B9E885710c4a68434b4Bc437da6e"] + }, + "symbol": { + "type": "string", + "title": "Symbol", + "examples": ["e.g. GNOME"], + "maxLength": 16 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "lpaddress", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/README.md b/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/README.md new file mode 100644 index 00000000..1d7e4f75 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/README.md @@ -0,0 +1,21 @@ +This is simple strategy to use balance from graph instead of archive node. + +The configuration would be like this: + +```json +{ + "symbol": "GZ", + "graph": "the api address of your graph", +} +``` + +The schema of the graph project is: + +``` +User { + id: ID! + genzeeBalance: Int! +} +``` + +As a example, here is a the graph project for Genzee NFTs: [genzee](https://thegraph.com/hosted-service/subgraph/alephao/genzee). diff --git a/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/examples.json b/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/examples.json new file mode 100644 index 00000000..d2eb5b37 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "genzees-from-subgraph", + "params": { + "symbol": "GZ", + "graph": "https://api.thegraph.com/subgraphs/name/alephao/genzee" + } + }, + "network": "1", + "addresses": [ + "0x000000070f91b6c56fa08d4f3a26c7fc992b38f4", + "0x0007796d3b5bae6e3960ecb639bc159f6c1dcdf0", + "0x000d65681e8df8a6ad5bc864833c38c9ecdac0ff", + "0x0010e29271bbca7abfbbbda1bdec668720cca795", + "0x002c609dc34918269e2174d82fcb6ecb4f6cf386" + ], + "snapshot": 14893355 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/index.ts b/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/index.ts new file mode 100644 index 00000000..2fa4b5a3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/genzees-from-subgraph/index.ts @@ -0,0 +1,59 @@ +import { subgraphRequest } from '../../utils'; + +export const author = 'alephao'; +export const version = '0.1.0'; + +const LIMIT = 500; + +function makeQuery(snapshot, addressSet) { + const query = { + users: { + __args: { + where: { + id_in: addressSet + }, + first: LIMIT + }, + id: true, + genzeeBalance: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + query.users.__args.block = { number: snapshot }; + } + return query; +} + +export async function strategy( + _space, + _network, + _provider, + addresses, + options, + snapshot +) { + const _addresses = addresses.map((x) => x.toLowerCase()); + const addressSubsets = Array.apply( + null, + Array(Math.ceil(_addresses.length / LIMIT)) + ).map((_e, i) => _addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + + const returnedFromSubgraph = await Promise.all( + addressSubsets.map((subset) => + subgraphRequest(options.graph, makeQuery(snapshot, subset)) + ) + ); + + const result = returnedFromSubgraph.map((x) => x.users).flat(); + const scores = {}; + addresses.forEach((address) => { + const account = result.filter((x) => x.id == address.toLowerCase())[0]; + let score = 0; + if (account) { + score = parseFloat(account.genzeeBalance); + } + scores[address] = score; + }); + return scores || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gin-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/gin-finance/examples.json new file mode 100644 index 00000000..ff70d892 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gin-finance/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "gin-finance", + "params": { + "address": "0x66a2a913e447d6b4bf33efbec43aaef87890fbbc", + "symbol": "USDC" + } + }, + "network": "288", + "addresses": ["0xb50edc1F33b0905A9D70A8733712244E8C6E035b"], + "snapshot": 649260 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/gin-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/gin-finance/index.ts new file mode 100644 index 00000000..168cb350 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gin-finance/index.ts @@ -0,0 +1,88 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const GIN_FINANCE_SUBGRAPH_URL = { + '288': 'https://api.thegraph.com/subgraphs/name/gindev2/gin-subgraph' +}; + +export const author = 'perpetuum7'; +export const version = '1.0.0'; + +export async function strategy( + _space: string, + network: string, + _provider, + addresses: string[], + options: { address: string }, + snapshot?: number | string +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + liquidityPositions: { + __args: { + where: { + liquidityTokenBalance_gt: 0 + } + }, + liquidityTokenBalance: true, + pair: { + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.users.liquidityPositions.__args.block = { number: snapshot }; + } + + const tokenAddress = options.address.toLowerCase(); + const result = await subgraphRequest( + GIN_FINANCE_SUBGRAPH_URL[network], + params + ); + + const score = {}; + + if (result && result.users) { + result.users.forEach((u) => { + u.liquidityPositions + .filter( + (lp) => + lp.pair.token0.id == tokenAddress || + lp.pair.token1.id == tokenAddress + ) + .forEach((lp) => { + const token0perUni = lp.pair.reserve0 / lp.pair.totalSupply; + const token1perUni = lp.pair.reserve1 / lp.pair.totalSupply; + const userScore = + lp.pair.token0.id == tokenAddress + ? token0perUni * lp.liquidityTokenBalance + : token1perUni * lp.liquidityTokenBalance; + + const userAddress = getAddress(u.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScore; + }); + }); + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/README.md new file mode 100644 index 00000000..f3222f01 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/README.md @@ -0,0 +1,12 @@ +# Giveth Balancer balance + +This strategy sums up all the GIV on Balancer LP pools. + +Here is an example of parameters: + +```json +{ + "symbol": "GIV", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/examples.json new file mode 100644 index 00000000..59c747cc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "giveth-balancer-balance", + "params": { + "symbol": "GIV", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x839395e20bbb182fa440d08f850e6c7a8f6f0780", + "0x960a16c9070a9bbbb03e1bfd418982636d56d77d", + "0x00d18ca9782be1caef611017c2fbc1a39779a57c" + ], + "snapshot": 14036425 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/index.ts new file mode 100644 index 00000000..3e15cbfd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-balancer-balance/index.ts @@ -0,0 +1,101 @@ +import { subgraphRequest } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; +import { getAddress } from '@ethersproject/address'; +import { parseUnits, formatUnits } from '@ethersproject/units'; + +export const author = 'pkretzschmar'; +export const version = '0.1.0'; + +const GIVETH_SUBGRAPH_API = + 'https://api.thegraph.com/subgraphs/name/giveth/giveth-economy-mainnet'; + +const BALANCER_SUBGRAPH_API = + 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2'; + +const poolParams = { + pool: { + __args: { + id: '0x7819f1532c49388106f7762328c51ee70edd134c000200000000000000000109' + }, + tokens: { + balance: true, + symbol: true + }, + totalShares: true + } +}; + +const params = { + balances: { + __args: { + orderBy: 'id', + orderDirection: 'asc', + where: { + id_in: [] + } + }, + id: true, + balance: true, + givStaked: true, + balancerLp: true, + balancerLpStaked: true + } +}; + +const formatReserveBalance = (data, decimals) => { + const givToken = data.pool.tokens.find((elem) => elem.symbol === 'GIV'); + const balance = parseUnits(givToken.balance, decimals); + const totalShares = parseUnits(data.pool.totalShares, decimals); + return { balance, totalShares }; +}; + +const calcGivAmount = ( + amountLP: BigNumber, + totalLP: BigNumber, + givBalance: BigNumber +): BigNumber => { + return amountLP.mul(givBalance).div(totalLP); +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + if (snapshot !== 'latest') { + // @ts-ignore + poolParams.pool.__args.block = { number: snapshot }; + // @ts-ignore + params.balances.__args.block = { number: snapshot }; + } + const balData = await subgraphRequest(BALANCER_SUBGRAPH_API, poolParams); + const balFormatedData = formatReserveBalance(balData, options.decimals); + + params.balances.__args.where.id_in = addresses.map((address) => + address.toLowerCase() + ); + + const data = await subgraphRequest(GIVETH_SUBGRAPH_API, params); + const dataBalances = data.balances; + + const score = {}; + dataBalances.map((addressBalance) => { + const { id, balance, givStaked, balancerLp, balancerLpStaked } = + addressBalance; + const totalGIV = BigNumber.from(balance).add(givStaked); + + const balGIV = calcGivAmount( + BigNumber.from(balancerLp).add(balancerLpStaked), + balFormatedData.totalShares, + balFormatedData.balance + ); + score[getAddress(id)] = parseFloat( + formatUnits(totalGIV.add(balGIV), options.decimals) + ); + }); + + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/README.md b/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/README.md new file mode 100644 index 00000000..34bbc626 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/README.md @@ -0,0 +1,12 @@ +# Giveth Gnosis balance + +This strategy sums up all the GIV on xDai, including the ones that are staked in Honeyswap pools, Sushiswap pools, and single staked on GIV pool. This version considers GIVpower + +Here is an example of parameters: + +```json +{ + "symbol": "GIV", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/examples.json new file mode 100644 index 00000000..ce772e79 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "giveth-gnosis-balance-v2", + "params": { + "symbol": "GIV", + "decimals": 18 + } + }, + "network": "100", + "addresses": [ + "0x839395e20bbb182fa440d08f850e6c7a8f6f0780", + "0x960a16c9070a9bbbb03e1bfd418982636d56d77d", + "0x00d18ca9782be1caef611017c2fbc1a39779a57c" + ], + "snapshot": 24831608 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/index.ts new file mode 100644 index 00000000..ca3efc5c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-gnosis-balance-v2/index.ts @@ -0,0 +1,87 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'mateodaza'; +export const version = '0.1.0'; + +const GIVETH_SUBGRAPH_API = + 'https://api.thegraph.com/subgraphs/name/giveth/giveth-economy-second-xdai'; +const XDAI_BLOCKS_API = + 'https://api.thegraph.com/subgraphs/name/elkfinance/xdai-blocks'; + +const blockParams = { + blocks: { + __args: { + first: 1, + orderBy: 'timestamp', + orderDirection: 'desc', + where: { + timestamp_lte: '' + } + }, + number: true + } +}; + +const pairParams = { + pair: { + __args: { + id: '' + }, + reserve0: true, + totalSupply: true + } +}; + +const params = { + tokenBalances: { + __args: { + orderBy: 'id', + orderDirection: 'asc', + where: { + user_in: [] + } + }, + id: true, + balance: true, + token: true + } +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const block = await provider.getBlock(blockTag); + blockParams.blocks.__args.where.timestamp_lte = block.timestamp; + const xDaiBlock = await subgraphRequest(XDAI_BLOCKS_API, blockParams); + const blockNumber = Number(xDaiBlock.blocks[0].number); + // @ts-ignore + params.tokenBalances.__args.block = { number: blockNumber }; + if (snapshot !== 'latest') { + // @ts-ignore + pairParams.pair.__args.block = { number: blockNumber }; + } + + params.tokenBalances.__args.where.user_in = addresses.map((address) => + address.toLowerCase() + ); + + const data = await subgraphRequest(GIVETH_SUBGRAPH_API, params); + + const score = {}; + data.tokenBalances.map((addressBalance) => { + const id = addressBalance.id.split('-')[1]; + const prevScore = score[getAddress(id)] ? score[getAddress(id)] : 0; + score[getAddress(id)] = + prevScore + + parseFloat(formatUnits(addressBalance.balance, options.decimals)); + }); + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/README.md new file mode 100644 index 00000000..6bc76c77 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/README.md @@ -0,0 +1,12 @@ +# Giveth xDai balance + +This strategy sums up all the GIV on xDai, including the ones that are staked in Honeyswap pools, Sushiswap pools, and single staked on GIV pool. + +Here is an example of parameters: + +```json +{ + "symbol": "GIV", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/examples.json new file mode 100644 index 00000000..8bcc9307 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "giveth-xdai-balance", + "params": { + "symbol": "GIV", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x839395e20bbb182fa440d08f850e6c7a8f6f0780", + "0x960a16c9070a9bbbb03e1bfd418982636d56d77d", + "0x00d18ca9782be1caef611017c2fbc1a39779a57c" + ], + "snapshot": 14036425 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/index.ts new file mode 100644 index 00000000..0f8ece58 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/giveth-xdai-balance/index.ts @@ -0,0 +1,141 @@ +import { subgraphRequest } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; +import { getAddress } from '@ethersproject/address'; +import { parseUnits, formatUnits } from '@ethersproject/units'; + +export const author = 'pkretzschmar'; +export const version = '0.1.0'; + +const GIVETH_SUBGRAPH_API = + 'https://api.thegraph.com/subgraphs/name/giveth/giveth-economy-xdai'; +const XDAI_BLOCKS_API = + 'https://api.thegraph.com/subgraphs/name/elkfinance/xdai-blocks'; +const PAIR_IDS = [ + '0x08ea9f608656a4a775ef73f5b187a2f1ae2ae10e', + '0x55ff0cef43f0df88226e9d87d09fa036017f5586' +]; +const PAIR_APIS = [ + 'https://api.thegraph.com/subgraphs/name/1hive/honeyswap-xdai', + 'https://api.thegraph.com/subgraphs/name/sushiswap/xdai-exchange' +]; + +const blockParams = { + blocks: { + __args: { + first: 1, + orderBy: 'timestamp', + orderDirection: 'desc', + where: { + timestamp_lte: '' + } + }, + number: true + } +}; + +const pairParams = { + pair: { + __args: { + id: '' + }, + reserve0: true, + totalSupply: true + } +}; + +const params = { + balances: { + __args: { + orderBy: 'id', + orderDirection: 'asc', + where: { + id_in: [] + } + }, + id: true, + balance: true, + givStaked: true, + honeyswapLp: true, + honeyswapLpStaked: true, + sushiswapLp: true, + sushiSwapLpStaked: true + } +}; + +const formatReserveBalance = (data, decimals) => { + const reserve = parseUnits(data.pair.reserve0, decimals); + const totalSupply = parseUnits(data.pair.totalSupply, decimals); + return { reserve, totalSupply }; +}; + +const calcGivAmount = ( + amountLP: BigNumber, + totalLP: BigNumber, + givBalance: BigNumber +): BigNumber => { + return amountLP.mul(givBalance).div(totalLP); +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const block = await provider.getBlock(blockTag); + blockParams.blocks.__args.where.timestamp_lte = block.timestamp; + const xDaiBlock = await subgraphRequest(XDAI_BLOCKS_API, blockParams); + const blockNumber = Number(xDaiBlock.blocks[0].number); + // @ts-ignore + params.balances.__args.block = { number: blockNumber }; + if (snapshot !== 'latest') { + // @ts-ignore + pairParams.pair.__args.block = { number: blockNumber }; + } + + params.balances.__args.where.id_in = addresses.map((address) => + address.toLowerCase() + ); + + const requests = PAIR_APIS.map((API, index) => { + pairParams.pair.__args.id = PAIR_IDS[index]; + return subgraphRequest(API, pairParams); + }); + requests.push(subgraphRequest(GIVETH_SUBGRAPH_API, params)); + const [hnyData, sushiData, data] = await Promise.all(requests); + + const hnyFormatedData = formatReserveBalance(hnyData, options.decimals); + const sushiFormatedData = formatReserveBalance(sushiData, options.decimals); + const dataBalances = data.balances; + + const score = {}; + dataBalances.map((addressBalance) => { + const { + id, + balance, + givStaked, + honeyswapLp, + honeyswapLpStaked, + sushiswapLp, + sushiSwapLpStaked + } = addressBalance; + const totalGIV = BigNumber.from(balance).add(givStaked); + const hnyGIV = calcGivAmount( + BigNumber.from(honeyswapLp).add(honeyswapLpStaked), + hnyFormatedData.totalSupply, + hnyFormatedData.reserve + ); + const sushiGIV = calcGivAmount( + BigNumber.from(sushiswapLp).add(sushiSwapLpStaked), + sushiFormatedData.totalSupply, + sushiFormatedData.reserve + ); + score[getAddress(id)] = parseFloat( + formatUnits(totalGIV.add(hnyGIV).add(sushiGIV), options.decimals) + ); + }); + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/glide/examples.json b/Implementations/API/backend/utils/snapshot/strategies/glide/examples.json new file mode 100644 index 00000000..fc61eff6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/glide/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Glide Finance Query", + "strategy": { + "name": "glide", + "params": { + "symbol": "GLIDE", + "glide": { + "numerator": 1, + "denominator": 1 + }, + "lp": { + "numerator": 1, + "denominator": 1 + } + } + }, + "network": "20", + "addresses": [ + "0x3Ac3935326cCA3146365a39458b5905bCdAe476b", + "0x9e33Cc5b5835518609f1C432C0B75f6e28524e7f", + "0x61ffC37eFB973561d5fe91B11c14EbAd603F7d67" + ], + "snapshot": 10018666 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/glide/index.ts b/Implementations/API/backend/utils/snapshot/strategies/glide/index.ts new file mode 100644 index 00000000..2d289c8f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/glide/index.ts @@ -0,0 +1,135 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'racollette'; +export const version = '0.1.0'; + +const MASTERCHEF = '0x7F5489f77Bb8515DE4e0582B60Eb63A7D9959821'; +const LP1_TOKEN = '0xbeeAAb15628329C2C89Bc9F403d34b31fbCb3085'; // GLIDE-ELA +const LP2_TOKEN = '0x26aCE9c9da938fb2Db91B0d0E7703861c249bf08'; // GLIDE-USDC +const GLIDE_VAULT = '0xBe224bb2EFe1aE7437Ab428557d3054E63033dA9'; +const DIVIDEND_POOL = '0x80f2cF7059336b44a75F00451B81f8d742DD2b94'; +const GLIDE_TOKEN = '0xd39eC832FF1CaaFAb2729c76dDeac967ABcA8F27'; + +const abi = [ + 'function balanceOf(address) view returns (uint256 amount)', + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)', + 'function totalSupply() view returns (uint256)' +]; + +const vault_abi = [ + 'function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime, uint256 glideAtLastUserAction, uint256 lastUserActionTime)', + 'function getPricePerFullShare() view returns (uint256)' +]; + +const dividend_abi = [ + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const multi1 = new Multicaller(network, provider, abi, { blockTag }); + const multi2 = new Multicaller(network, provider, vault_abi, { blockTag }); + const multi3 = new Multicaller(network, provider, dividend_abi, { blockTag }); + const precision = BigNumber.from(10).pow(18); + + addresses.forEach((address: any) => { + multi1.call(`glide.${address}`, GLIDE_TOKEN, 'balanceOf', [address]); + multi1.call(`manualStaking.${address}`, MASTERCHEF, 'userInfo', [ + '0', + address + ]); + multi1.call(`lp1InFarm.${address}`, MASTERCHEF, 'userInfo', ['1', address]); + multi1.call(`lp2InFarm.${address}`, MASTERCHEF, 'userInfo', ['7', address]); + multi1.call(`lp1.${address}`, LP1_TOKEN, 'balanceOf', [address]); + multi1.call(`lp2.${address}`, LP2_TOKEN, 'balanceOf', [address]); + }); + addresses.forEach((address: any) => { + multi2.call(`autoStaking.${address}`, GLIDE_VAULT, 'userInfo', [address]); + }); + addresses.forEach((address: any) => { + multi3.call(`feeStaking.${address}`, DIVIDEND_POOL, 'userInfo', [address]); + }); + + multi1.call(`lp1.totalSupply`, LP1_TOKEN, 'totalSupply', []); + multi1.call(`lp2.totalSupply`, LP2_TOKEN, 'totalSupply', []); + multi1.call(`lp1.glide`, GLIDE_TOKEN, 'balanceOf', [LP1_TOKEN]); + multi1.call(`lp2.glide`, GLIDE_TOKEN, 'balanceOf', [LP2_TOKEN]); + multi2.call(`autoStaking.shares`, GLIDE_VAULT, 'getPricePerFullShare', []); + + const result1 = await multi1.execute(); + const result2 = await multi2.execute(); + const result3 = await multi3.execute(); + + return Object.fromEntries( + addresses.map((address) => [ + address, + // GLIDE in wallet + parseFloat(formatUnits(result1.glide[address], 18)) + + // GLIDE-ELA LP in farm + parseFloat( + formatUnits( + result1.lp1InFarm[address][0] + .mul(result1.lp1.glide) + .div(result1.lp1.totalSupply) + .mul(options.lp.numerator) + .div(options.lp.denominator), + 18 + ) + ) + + // GLIDE-USDC LP in farm + parseFloat( + formatUnits( + result1.lp2InFarm[address][0] + .mul(result1.lp2.glide) + .div(result1.lp2.totalSupply) + .mul(options.lp.numerator) + .div(options.lp.denominator), + 18 + ) + ) + + // GLIDE-ELA LP in wallet + parseFloat( + formatUnits( + result1.lp1[address] + .mul(result1.lp1.glide) + .div(result1.lp1.totalSupply) + .mul(options.lp.numerator) + .div(options.lp.denominator), + 18 + ) + ) + + // GLIDE-USDC LP in wallet + parseFloat( + formatUnits( + result1.lp2[address] + .mul(result1.lp2.glide) + .div(result1.lp2.totalSupply) + .mul(options.lp.numerator) + .div(options.lp.denominator), + 18 + ) + ) + + // GLIDE in manual staking + parseFloat(formatUnits(result1.manualStaking[address][0], 18)) + + // GLIDE in auto staking + parseFloat( + formatUnits( + result2.autoStaking[address][0] + .mul(result2.autoStaking.shares) + .div(precision) + ) + ) + + // GLIDE in fee staking + parseFloat(formatUnits(result3.feeStaking[address][0], 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gno/examples.json b/Implementations/API/backend/utils/snapshot/strategies/gno/examples.json new file mode 100644 index 00000000..c79024dd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gno/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "GNO Token Votes", + "strategy": { + "name": "gno", + "network": "1", + "params": { + "symbol": "GNO", + "decimals": 18, + "SUBGRAPH_URL": "https://api.thegraph.com/subgraphs/id/QmfZ5cepEwspcrLLwtA3i5M6qEnv3WKDkpAtC65t8VpY6M" + } + }, + "network": "100", + "addresses": [ + "0x53bcFaEd43441C7bB6149563eC11f756739C9f6A", + "0x000000000000541e251335090ac5b47176af4f7e", + "0x0000000000007f150bd6f54c40a34d7c3d5e9f56", + "0x0000000000051666bbfbb42925c3ee5d50cf6b10", + "0x00000000003b3cc22af3ae1eac0440bcee416b40", + "0x000000000dfde7deaf24138722987c9a6991e2d4" + ], + "snapshot": 14476000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/gno/index.ts b/Implementations/API/backend/utils/snapshot/strategies/gno/index.ts new file mode 100644 index 00000000..15618e7d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gno/index.ts @@ -0,0 +1,40 @@ +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'nginnever'; +export const version = '0.1.1'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const adds = addresses.map((element) => { + return element.toLowerCase(); + }); + + const params = { + users: { + __args: { + where: { id_in: adds }, + first: 1000 + }, + id: true, + voteWeight: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.__args.block = { number: snapshot }; + } + const result = await subgraphRequest(options.SUBGRAPH_URL, params); + const score = {}; + result.users.map((user) => { + score[getAddress(user.id)] = parseFloat(formatUnits(user.voteWeight, 18)); + }); + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/goldfinch-membership/examples.json b/Implementations/API/backend/utils/snapshot/strategies/goldfinch-membership/examples.json new file mode 100644 index 00000000..9eef26be --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/goldfinch-membership/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Goldfinch Membership", + "strategy": { + "name": "goldfinch-membership", + "params": { "symbol": "UID" } + }, + "network": "1", + "addresses": [ + "0x526C7665C5dd9cD7102C6d42D407a0d9DC1e431d", + "0x721931508df2764fd4f70c53da646cb8aed16ace", + "0x4902b20bB3B8e7776CBcDCB6e3397E7F6b4e449E" + ], + "snapshot": 14291193 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/goldfinch-membership/index.ts b/Implementations/API/backend/utils/snapshot/strategies/goldfinch-membership/index.ts new file mode 100644 index 00000000..52fe8a2f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/goldfinch-membership/index.ts @@ -0,0 +1,54 @@ +import { multicall } from '../../utils'; +import { strategy as erc1155AllBalancesOf } from '../erc1155-all-balances-of'; + +export const author = 'sanjayprabhu'; +export const version = '0.1.0'; + +const goListAbi = ['function goList(address) view returns (bool)']; + +const LEGACY_GOLDFINCH_CONFIG = '0x4eb844Ff521B4A964011ac8ecd42d500725C95CC'; +const UID = '0xba0439088dc1e75F58e0A7C107627942C15cbb41'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const uidResult: { [address: string]: number } = await erc1155AllBalancesOf( + space, + network, + provider, + addresses, + { + address: UID, + symbol: 'UID' + }, + snapshot + ); + + const goListResult: [[boolean]] = await multicall( + network, + provider, + goListAbi, + addresses.map((address: any) => [ + LEGACY_GOLDFINCH_CONFIG, + 'goList', + [address] + ]), + { blockTag } + ); + + // If you don't have a UID, but are on the goList, that's OK. + addresses.forEach((address, index) => { + if (!uidResult[address] && goListResult[index][0]) { + uidResult[address] = 1; + } + }); + + return uidResult; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/goldfinch-voting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/goldfinch-voting-power/examples.json new file mode 100644 index 00000000..0a9f450c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/goldfinch-voting-power/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Goldfinch Voting Power", + "strategy": { + "name": "goldfinch-voting-power", + "params": { "symbol": "GFI", "decimals": 18 } + }, + "network": "1", + "addresses": [ + "0x526C7665C5dd9cD7102C6d42D407a0d9DC1e431d", + "0x721931508df2764fd4f70c53da646cb8aed16ace", + "0x4902b20bB3B8e7776CBcDCB6e3397E7F6b4e449E", + "0x8F4fc35cF5ad925bCC1cAe81E5da62fCA1c2Dd2a", + "0x6C3FB1A2A55fdcFb620Ae8f19b6fc332622050DD" + ], + "snapshot": 16080167 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/goldfinch-voting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/goldfinch-voting-power/index.ts new file mode 100644 index 00000000..210c8300 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/goldfinch-voting-power/index.ts @@ -0,0 +1,101 @@ +import { multicall } from '../../utils'; +import { strategy as erc20BalanceOf } from '../erc20-balance-of'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'sanjayprabhu'; +export const version = '0.1.0'; + +const COMMUNITY_REWARDS = '0x0Cd73c18C085dEB287257ED2307eC713e9Af3460'; +const STAKING_REWARDS = '0xFD6FF39DA508d281C2d255e9bBBfAb34B6be60c3'; +const GFI = '0xdab396cCF3d84Cf2D07C4454e10C8A6F5b008D2b'; +const MEMBERSHIP_REWARDS = '0x4e5d9b093986d864331d88e0a13a616e1d508838'; + +const COMMUNITY_REWARDS_ABI = [ + 'function totalUnclaimed(address owner) view returns (uint256)' +]; +const STAKING_REWARDS_ABI = [ + 'function totalOptimisticClaimable(address owner) view returns (uint256)' +]; +const MEMBERSHIP_REWARDS_ABI = [ + 'function votingPower(address addr) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Held GFI + const gfiResult: { [address: string]: number } = await erc20BalanceOf( + space, + network, + provider, + addresses, + { + address: GFI, + symbol: 'GFI' + }, + snapshot + ); + + // Locked amount in Community Rewards + const unclaimedCommunityRewards = await multicall( + network, + provider, + COMMUNITY_REWARDS_ABI, + addresses.map((address: any) => [ + COMMUNITY_REWARDS, + 'totalUnclaimed', + [address] + ]), + { blockTag } + ); + + const unclaimedStakingRewards = await multicall( + network, + provider, + STAKING_REWARDS_ABI, + addresses.map((address: any) => [ + STAKING_REWARDS, + 'totalOptimisticClaimable', + [address] + ]), + { blockTag } + ); + + const membershipRewards = await multicall( + network, + provider, + MEMBERSHIP_REWARDS_ABI, + addresses.map((address: any) => [ + MEMBERSHIP_REWARDS, + 'votingPower', + [address] + ]), + { blockTag } + ); + + addresses.forEach((address, index) => { + const parsedCommunityRewards = parseFloat( + formatUnits(unclaimedCommunityRewards[index][0], options.decimals) + ); + const parsedStakingRewards = parseFloat( + formatUnits(unclaimedStakingRewards[index][0], options.decimals) + ); + const parsedMembershipRewards = parseFloat( + formatUnits(membershipRewards[index][0], options.decimals) + ); + gfiResult[address] = + gfiResult[address] + + parsedCommunityRewards + + parsedStakingRewards + + parsedMembershipRewards; + }); + + return gfiResult; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/README.md b/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/README.md new file mode 100644 index 00000000..47658c50 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/README.md @@ -0,0 +1,73 @@ +# max eth fuse + +** This strategy is limited to networks 1 and 122 and allows at most 2 sub strategies** + +This is similar to the multichain strategy only that it chooses the chain with max voting power. + +Here is an example of parameters: + +In the below example, the tokens on etherem and fuse are queried and the max value denotes the voting power + +```json +{ + "symbol": "GOOD", + "strategies": [ + { + "name": "contract-call", + "network": "122", + "params": { + "symbol": "GOOD", + "address": "0x603b8c0f110e037b51a381cbcacabb8d6c6e4543", + "decimals": 18, + "methodABI": { + "name": "getVotes", + "type": "function", + "inputs": [ + { + "name": "voter", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + } + } + }, + { + "name": "contract-call", + "network": "1", + "params": { + "symbol": "GOOD", + "address": "0x603b8c0f110e037b51a381cbcacabb8d6c6e4543", + "decimals": 18, + "methodABI": { + "name": "getVotes", + "type": "function", + "inputs": [ + { + "name": "voter", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + } + } + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/examples.json b/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/examples.json new file mode 100644 index 00000000..d24cd138 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/examples.json @@ -0,0 +1,75 @@ +[ + { + "name": "GoodDolalr Multichain", + "strategy": { + "name": "gooddollar-multichain", + "params": { + "symbol": "GOOD", + "strategies": [ + { + "name": "contract-call", + "network": "122", + "params": { + "symbol": "GOOD", + "address": "0x603b8c0f110e037b51a381cbcacabb8d6c6e4543", + "decimals": 18, + "methodABI": { + "name": "getVotes", + "type": "function", + "inputs": [ + { + "name": "voter", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + } + } + }, + { + "name": "contract-call", + "network": "1", + "params": { + "symbol": "GOOD", + "address": "0x603b8c0f110e037b51a381cbcacabb8d6c6e4543", + "decimals": 18, + "methodABI": { + "name": "getVotes", + "type": "function", + "inputs": [ + { + "name": "voter", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + } + } + } + ] + } + }, + "network": "1", + "addresses": [ + "0x66582D24FEaD72555adaC681Cc621caCbB208324", + "0xA48840D89a761502A4a7d995c74f3864D651A87F" + ], + "snapshot": 15000000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/index.ts b/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/index.ts new file mode 100644 index 00000000..649642f4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gooddollar-multichain/index.ts @@ -0,0 +1,57 @@ +import { getProvider, getSnapshots } from '../../utils'; +import strategies from '..'; + +export const author = 'sirpy'; +export const version = '1.0.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const promises: any = []; + const validStrategies = options.strategies + .filter((s) => s.network === '122' || s.network === '1') + .slice(0, 2); + const blocks = await getSnapshots( + network, + snapshot, + provider, + validStrategies.map((s) => s.network || network) + ); + + for (const strategy of validStrategies) { + // If snapshot is taken before a network is activated then ignore its strategies + if ( + options.startBlocks && + blocks[strategy.network] < options.startBlocks[strategy.network] + ) { + continue; + } + + promises.push( + strategies[strategy.name].strategy( + space, + strategy.network, + getProvider(strategy.network), + addresses, + strategy.params, + blocks[strategy.network] + ) + ); + } + + const results = await Promise.all(promises); + return results.reduce((finalResults: any, strategyResult: any) => { + for (const [address, value] of Object.entries(strategyResult)) { + if (!finalResults[address]) { + finalResults[address] = 0; + } + finalResults[address] = Math.max(finalResults[address], value as number); + } + return finalResults; + }, {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/governor-alpha/examples.json b/Implementations/API/backend/utils/snapshot/strategies/governor-alpha/examples.json new file mode 100644 index 00000000..9a9dd170 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/governor-alpha/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example governor-alpha strategy to get delegated coins", + "strategy": { + "name": "governor-alpha", + "params": { + "address": "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + "symbol": "GTC", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x3c7d43de93Eb6598172f20Feb8ecFE8DEc70FE41", + "0x839395e20bbB182fa440d08F850E6c7A8f6F0780", + "0x44aa9c5a034c1499ec27906e2d427b704b567ffe" + ], + "snapshot": 12582006 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/governor-alpha/index.ts b/Implementations/API/backend/utils/snapshot/strategies/governor-alpha/index.ts new file mode 100644 index 00000000..8407f51d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/governor-alpha/index.ts @@ -0,0 +1,37 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'snapshot-labs'; +export const version = '0.0.1'; + +const abi = [ + 'function getCurrentVotes(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'getCurrentVotes', + [address.toLowerCase()] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/README.md b/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/README.md new file mode 100644 index 00000000..3405ce85 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/README.md @@ -0,0 +1,14 @@ +# governor-delegator + +Allows users to vote with their token balance, if the user delegated their voting power to a particular delegate address + +## Params + +```JSON +{ + "delegate": "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5", + "address": "0xc18360217d8f7ab5e7c516566761ea12ce7f9d72", + "symbol": "ENS", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/examples.json b/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/examples.json new file mode 100644 index 00000000..555ea961 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "governor-delegator", + "params": { + "delegate": "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5", + "address": "0xc18360217d8f7ab5e7c516566761ea12ce7f9d72", + "symbol": "ENS", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xf6b6f07862a02c85628b3a9688beae07fea9c863", + "0x0d24f692c05036602076b3f51242b5a34c55ee38", + "0xcafebabe86cbb8534134d96f746dccefcfe3babe", + "0xb8c2c29ee19d8307cb7255e1cd9cbde883a267d5", + "0x27e5343620b95645448de3710ca4162055670109" + ], + "snapshot": 13613058 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/index.ts b/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/index.ts new file mode 100644 index 00000000..ee35dd0f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/governor-delegator/index.ts @@ -0,0 +1,47 @@ +import { getAddress } from '@ethersproject/address'; +import { multicall } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'snapshot-labs'; +export const version = '0.1.0'; + +const abi = ['function delegates(address) view returns (address)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const delegates = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'delegates', + [address.toLowerCase()] + ]), + { blockTag } + ); + + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address: any, i) => [ + getAddress(address[0]), + delegates[i].toString().toLowerCase() === options.delegate.toLowerCase() + ? address[1] + : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/README.md new file mode 100644 index 00000000..e55ca50d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/README.md @@ -0,0 +1,14 @@ +# gysr-lp-staking-balance + +This strategy returns the LP staking balance for each user in a specified GYSR pool with a given token. + +Here is an example of the parameters: + +```json +{ + "pool": "0x05dff8d71cd222e09bb71e44e0be7cc8f03a07c9", + "tokenAddress": "0xa92e7c82b11d10716ab534051b271d2f6aef7df5", + "symbol": "ARA", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/examples.json new file mode 100644 index 00000000..807040a2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example GYSR LP Staking Query", + "strategy": { + "name": "gysr-lp-staking-balance", + "params": { + "pool": "0x05dff8d71cd222e09bb71e44e0be7cc8f03a07c9", + "tokenAddress": "0xa92e7c82b11d10716ab534051b271d2f6aef7df5", + "symbol": "ARA", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x29c0b49931A18CB8F343b8474D2fb103DBa1e294", + "0xecfecf375160d2e3d1ab27a31a161d955c7650cb", + "0x75f1771f278b064a6f09242ad8a8a5d1da1cc9bd", + "0x001292f42f0ec85636f652f618fefd94bcc3ac6e" + ], + "snapshot": 15364930 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/index.ts new file mode 100644 index 00000000..9cf9e736 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/index.ts @@ -0,0 +1,72 @@ +import { multicall } from '../../utils'; + +export const author = 'mitesh-mutha'; +export const version = '0.0.1'; + +const tokenABI = [ + 'function balanceOf(address account) view returns (uint256)', + 'function totalSupply() view returns (uint256)' +]; + +const poolABI = [ + 'function stakingBalances(address user) external view returns (uint256[])', + 'function stakingTokens() external view returns (address[])' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Fetch pool -> get lp token address + const poolCallResult = await multicall( + network, + provider, + poolABI, + [[options.pool, 'stakingTokens', []]], + { blockTag } + ); + const lpTokenAddress = poolCallResult[0][0][0]; + + // Fetch balances from lp token + const callResult = await multicall( + network, + provider, + tokenABI, + [ + [lpTokenAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [lpTokenAddress]] + ], + { blockTag } + ); + const totalSupply = callResult[0]; + const rewardTokenBalance = callResult[1]; + const rewardTokensPerLP = + rewardTokenBalance / 10 ** options.decimals / (totalSupply / 1e18); + + // Fetch balances + const balanceResult = await multicall( + network, + provider, + poolABI, + addresses.map((address: any) => [ + options.pool, + 'stakingBalances', + [address] + ]), + { blockTag } + ); + + // Final result + return Object.fromEntries( + balanceResult.map((value, i) => [ + addresses[i], + (value / 10 ** options.decimals) * rewardTokensPerLP + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/schema.json b/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/schema.json new file mode 100644 index 00000000..4aef4cbb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-lp-staking-balance/schema.json @@ -0,0 +1,41 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "pool": { + "type": "string", + "title": "Pool Address", + "examples": ["e.g. 0x05dff8d71cd222e09bb71e44e0be7cc8f03a07c9"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "tokenAddress": { + "type": "string", + "title": "Token Address", + "examples": ["e.g. 0xa92e7c82b11d10716ab534051b271d2f6aef7df5"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["pool", "tokenAddress", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/README.md b/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/README.md new file mode 100644 index 00000000..ce3b200b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/README.md @@ -0,0 +1,16 @@ +# GYSR Pending Rewards + +This strategy returns the pending rewards for each user in a specified GYSR pool. It supports the networks the GYSR Pool Info is available. + +Here is an example of parameters: + +```json +{ + // Required + "pool": "0xE48eddbBcA614be5416f20dE57D858562b72479d", + + // Optional + "rewardToken": "0xb521022EeaD7E7eEe95D30BA1A1f0aB657F83a61", + "symbol": "REX" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/examples.json b/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/examples.json new file mode 100644 index 00000000..6d42e5db --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example GYSR Pending Rewards query", + "strategy": { + "name": "gysr-pending-rewards", + "params": { + "pool": "0x30c0f65d9b27ebe2cc2a49cbcb4133230b3fb381", + "rewardToken": "0xbEa98c05eEAe2f3bC8c3565Db7551Eb738c8CCAb", + "symbol": "GYSR" + } + }, + "network": 1, + "addresses": [ + "0xc56e357599de4418c3a33ad611130d2b2b36b19a", + "0x3a5a5ed68b3eea83df82fbd3d16b16562e205ffb", + "0x6c2448c22a7947bd4fd886b719618b913cd09538", + "0x30c0f65d9b27ebe2cc2a49cbcb4133230b3fb381" + ], + "snapshot": 15410000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/index.ts b/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/index.ts new file mode 100644 index 00000000..9b4d59b1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/index.ts @@ -0,0 +1,85 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { multicall, Multicaller } from '../../utils'; + +export const author = 'mitesh-mutha'; +export const version = '0.1.0'; + +const poolInfoAddressForNetwork = { + 1: '0x01356d78c770840166C1654691D19Bd33C52EaAd', + 10: '0x3cAA041d1a0a78d141703C0E95408c1801Ed74dd', + 42: '0x91EB59690526b748FE1046D27BdB1B3dadeaf958', + 137: '0x53590f017d73bAb31A6CbCBF6500A66D92fecFbE' +}; + +const poolInfoABI = [ + 'function rewards(address pool, address addr) external view returns (uint256[])' +]; + +const poolABI = ['function rewardTokens() external view returns (address[])']; + +const tokenABI = ['function decimals() external view returns (uint8)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Network specific pool info contract address + const poolInfoContractAddress = poolInfoAddressForNetwork[network]; + + // Determine which token rewards to count for voting power + let tokenIndex = 0; + + const tokenCallResult = await multicall( + network, + provider, + poolABI, + [[options.pool, 'rewardTokens', []]], + { blockTag } + ); + + const rewardTokens = tokenCallResult[0].map((tokenAddress) => [ + tokenAddress.toString().toLowerCase() + ]); + + if (!(options.rewardToken == null)) { + tokenIndex = Math.max( + rewardTokens.indexOf(options.rewardToken.toLowerCase()), + 0 + ); + } + + // Determine decimals + const rewardTokenAddress = tokenCallResult[0][tokenIndex].toString(); + const decimalCallResult = await multicall( + network, + provider, + tokenABI, + [[rewardTokenAddress, 'decimals', []]], + { blockTag } + ); + const decimals = decimalCallResult[0]; + + // Get the pending rewards for addresses + const multi = new Multicaller(network, provider, poolInfoABI, { blockTag }); + addresses.forEach((address) => + multi.call(address, poolInfoContractAddress, 'rewards', [ + options.pool, + address + ]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balances]) => [ + address, + parseFloat(formatUnits(balances[tokenIndex], decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/schema.json b/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/schema.json new file mode 100644 index 00000000..468a4eb7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-pending-rewards/schema.json @@ -0,0 +1,36 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "pool": { + "type": "string", + "title": "Pool Address", + "examples": ["e.g. 0xE48eddbBcA614be5416f20dE57D858562b72479d"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "rewardToken": { + "type": "string", + "title": "Reward Token Address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["pool"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/README.md new file mode 100644 index 00000000..b322e010 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/README.md @@ -0,0 +1,13 @@ +# GYSR staking balance + +This strategy returns the staking balance for each user in a specified GYSR pool. + +Here is an example of parameters: + +```json +{ + "pool": "0x30C0f65D9b27EBE2CC2A49Cbcb4133230b3fb381", + "symbol": "GYSR", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/examples.json new file mode 100644 index 00000000..9f113642 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/examples.json @@ -0,0 +1,68 @@ +[ + { + "name": "Example GYSR staking balance query", + "strategy": { + "name": "gysr-staking-balance", + "params": { + "pool": "0x30C0f65D9b27EBE2CC2A49Cbcb4133230b3fb381", + "symbol": "GYSR", + "decimals": 18 + } + }, + "network": 1, + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0xa0ffbF245095dBFbB3A76d86C18Cda009b96aD8B", + "0x126bC064dBd1d0205fC608C3178a60C9706b482C", + "0xC643B5B1d3912C52f9498E8C6E8BF321C94F28E9", + "0x39cf6E97046cc8454fE0aaD04B05524Ae34025Cb", + "0xA80481E3f9098602954B2E5cf306e6dEE053EF3E" + ], + "snapshot": 14200000 + }, + { + "name": "Example GYSR NFT staking balance query on Polygon", + "strategy": { + "name": "gysr-staking-balance", + "params": { + "pool": "0xc8D2A357178b9466Ad0f153CD2c950226F612934", + "symbol": "NFT", + "decimals": 0 + } + }, + "network": 137, + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x1Fd05325d3236D4e07963195538b75636ABDE75C", + "0xa15226cAB1A266958f881Ec18035741D047aA47F", + "0x97A8C2e0E1f17296e8C109eD136Fe9C75d745092", + "0xdab44c6D2e0691856016B4412D70ecDE86b0d0b1", + "0xA80481E3f9098602954B2E5cf306e6dEE053EF3E" + ], + "snapshot": 24976000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/index.ts new file mode 100644 index 00000000..e5d2c0ed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/gysr-staking-balance/index.ts @@ -0,0 +1,34 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'devinaconley'; +export const version = '0.0.1'; + +const abi = [ + 'function stakingBalances(address user) external view returns (uint256[])' +]; + +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) => + multi.call(address, options.pool, 'stakingBalances', [address]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balances]) => [ + address, + parseFloat(formatUnits(balances[0], options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/h2o/README.md b/Implementations/API/backend/utils/snapshot/strategies/h2o/README.md new file mode 100644 index 00000000..7fa05205 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/h2o/README.md @@ -0,0 +1,17 @@ +# h2o + +Returns the underlying collateral balance for each voter's H2O vault(s). + +## Params + +- `symbol` - (**Optional**, `string`) Symbol of collateral ERC20 token +- `collateralTypeId` - (**Required**, `string`) Collateral type ID + +Here is an example of parameters: + +```json +{ + "symbol": "OCEAN", + "collateralTypeId": "OCEAN-A", +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/h2o/examples.json b/Implementations/API/backend/utils/snapshot/strategies/h2o/examples.json new file mode 100644 index 00000000..220727b4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/h2o/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "h2o", + "params": { + "symbol": "OCEAN", + "collateralTypeId": "OCEAN-A" + } + }, + "network": "1", + "addresses": [ + "0x1EC1CcEF3e1735bdA3F4BA698e8a524AA7c93274", + "0x0ac0240f4656dc80684d0df1208bb91c0220725d", + "0x24a9689d209622fd6def27a21174c1cdfb479483", + "0x650235ffc650d3b3d95c904be345aafb97c586cf", + "0x71d5b84d22b6df54fa6cc6c325ff36c86b9f356c", + "0x933b7dfefae9e2e8385054d1c9b1143d9aeeb069", + "0x99665a5252c15ca3e24fcebab56d0721fb9d1158" + ], + "snapshot": 14678934 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/h2o/index.ts b/Implementations/API/backend/utils/snapshot/strategies/h2o/index.ts new file mode 100644 index 00000000..07e5122d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/h2o/index.ts @@ -0,0 +1,50 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'MantisClone'; +export const version = '0.1.0'; + +const SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/h2odata/h2o-mainnet' +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const params = { + users: { + id: true, + safes: { + collateralType: { + __args: { + where: { + id: options.collateralTypeId + } + } + }, + collateral: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.__args = { block: { number: snapshot } }; + } + + const result = await subgraphRequest(SUBGRAPH_URL[network], params); + + return Object.fromEntries( + result.users.map((user) => [ + getAddress(user.id), + user.safes.reduce( + (partialSum, safe) => partialSum + parseFloat(safe.collateral), + 0 + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/h2o/schema.json b/Implementations/API/backend/utils/snapshot/strategies/h2o/schema.json new file mode 100644 index 00000000..9b658208 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/h2o/schema.json @@ -0,0 +1,26 @@ +{ + "$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. OCEAN"], + "maxLength": 16 + }, + "collateralTypeId": { + "type": "string", + "title": "collateralTypeId", + "examples": ["e.g. OCEAN-A"], + "maxLength": 16 + } + }, + "required": ["collateralTypeId"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/harmony-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/harmony-staking/examples.json new file mode 100644 index 00000000..b1d6be8a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/harmony-staking/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "harmony-staking", + "params": { + "symbol": "ONE" + } + }, + "network": "1666600000", + "addresses": [ + "0xF677b8EF72C34f63c43f47C30612B1A3Ec1b622F", + "0xd143988234dF9117f4Baa00b5f8D4A56d64e56eA", + "0xA5241513DA9F4463F1d4874b548dFBAC29D91f34" + ], + "snapshot": 10937992 + }, + { + "name": "Example query", + "strategy": { + "name": "harmony-staking", + "params": { + "symbol": "ONE" + } + }, + "network": "1666700000", + "addresses": [ + "0x29c2eC57803f8b695f02613E5FA1749c165c5057", + "0xd143988234dF9117f4Baa00b5f8D4A56d64e56eA", + "0xA5241513DA9F4463F1d4874b548dFBAC29D91f34" + ], + "snapshot": 192690 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/harmony-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/harmony-staking/index.ts new file mode 100644 index 00000000..113a840c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/harmony-staking/index.ts @@ -0,0 +1,45 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'harmony-one'; +export const version = '0.0.1'; + +type Params = { + symbol: string; + decimals: number; +}; + +export async function strategy( + _space: string, + _network: string, + provider: StaticJsonRpcProvider, + // adding a 0 value for addresses not in the result is not needed + // since they are dropped anyway in utils.ts + // https://github.com/snapshot-labs/snapshot-strategies/blob/02439eb120ed7c4cc0c493924b78d92d22006b40/src/utils.ts#L26 + _addresses: Array, + options: Params, + snapshot: number | string +) { + // provider = new StaticJsonRpcProvider({ + // url: "http://127.0.0.1:9500", + // timeout: 25000, + // }); + const blockTag: number | string = + typeof snapshot === 'number' ? snapshot : 'latest'; + const response: Record = await provider.send( + 'hmyv2_getValidatorsStakeByBlockNumber', + [blockTag] + ); + return Object.fromEntries( + Object.entries(response).map(([address, balance]) => [ + address, + parseFloat( + formatUnits( + BigNumber.from('0x' + balance.toString(16)), + options && options.decimals ? options.decimals : 18 + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/has-rock/README.md b/Implementations/API/backend/utils/snapshot/strategies/has-rock/README.md new file mode 100644 index 00000000..3efe316b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/has-rock/README.md @@ -0,0 +1,3 @@ +# has-rocks + +This is a strategy for addresses with rocks. diff --git a/Implementations/API/backend/utils/snapshot/strategies/has-rock/examples.json b/Implementations/API/backend/utils/snapshot/strategies/has-rock/examples.json new file mode 100644 index 00000000..e4d19b7c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/has-rock/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "has-rock", + "params": { + "address": "0x37504AE0282f5f334ED29b4548646f887977b7cC", + "symbol": "ROCK" + } + }, + "network": "1", + "addresses": ["0x66C25a700F9c762891b4d6fbf5aF9Dc2eb04266A"], + "snapshot": 13118882 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/has-rock/index.ts b/Implementations/API/backend/utils/snapshot/strategies/has-rock/index.ts new file mode 100644 index 00000000..f05c0bbe --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/has-rock/index.ts @@ -0,0 +1,40 @@ +import { multicall } from '../../utils'; + +export const author = 'AngelDAO'; +export const version = '0.1.0'; + +const abi = [ + 'function rocks(uint256) view returns (address owner, bool currentlyForSale, uint256 price, uint256 timesSold)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const calls = [] as any; + for (let i = 0; i < 100; i++) { + calls.push([options.address, 'rocks', [i]]); + } + + const response = await multicall(network, provider, abi, calls, { blockTag }); + + const result = {} as any; + + addresses.forEach((address) => { + let addressRocks = 0; + response.forEach((rockObject) => { + if (rockObject.owner == address) { + addressRocks++; + } + }); + result[address] = addressRocks; + }); + + return result; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashes-voting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hashes-voting/examples.json new file mode 100644 index 00000000..e0441341 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashes-voting/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example Hashes votes query", + "strategy": { + "name": "hashes-voting", + "params": { + "address": "0xD07e72b00431af84AD438CA995Fd9a7F0207542d", + "symbol": "HASH" + } + }, + "network": "1", + "addresses": [ + "0xecece69f68bde56e7d53bd77d84da21a386bae0f", + "0x6dfc724df608c15027772f770f00b2b7a4040976" + ], + "snapshot": 13366241 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashes-voting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hashes-voting/index.ts new file mode 100644 index 00000000..63251e9f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashes-voting/index.ts @@ -0,0 +1,37 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'brennanfife'; +export const version = '0.1.0'; + +const abi = [ + 'function getCurrentVotes(address _account) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'getCurrentVotes', + [address.toLowerCase()] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/README.md new file mode 100644 index 00000000..6ff23c17 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/README.md @@ -0,0 +1,19 @@ +# hashflow-governance-power + +This strategy is used for the Hashflow DAO to consider both held HFT and LP'd HFT equally. + +It was implemented as a result of community proposal [#272](https://gov.hashflow.com/t/allow-hashgang-to-use-their-hft-lp-tokens-to-vote/272/20), which was passed via a [snapshot vote](https://snapshot.org/#/hashflowdao.eth/proposal/0x0fe602a533b10fab93a66b155770ba948ff23209412487ec1ba3c5a4d75357ef). + +It allows users who are LP'ing HFT in public HFT pools to use those for voting. This eliminates the need to withdraw from pools to participate in voting. + +The exact implementation uses the fact that the `payout` field of a pool's `function assetDetails()` call shares how many HFT are claimable in the pool. This can then be combined with the totalSupply of each pool's LP token to determine the HFT value of funds deposited by any given address. + +Here is an example of parameters: + +```json +{ + "hftContract": "0xb3999f658c0391d94a37f7ff328f3fec942bcadc", + "hftPool": "0x5afe266ab4e43c32bad5459fe8df116dd5541222", + "hToken": "0x8a96b94bee6636042f2019c60c43b4f1c8c177a9" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/examples.json new file mode 100644 index 00000000..9220e982 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/examples.json @@ -0,0 +1,68 @@ +[ + { + "name": "Mainnet query", + "strategy": { + "name": "hashflow-governance-power", + "params": { + "hftContract": "0xb3999f658c0391d94a37f7ff328f3fec942bcadc", + "hftPool": "0x5afe266ab4e43c32bad5459fe8df116dd5541222", + "hToken": "0x8a96b94bee6636042f2019c60c43b4f1c8c177a9" + } + }, + "network": "1", + "addresses": [ + "0x5caf573a688f21bb7a73a5f1cbeac1dfd6b505ca", + "0x5f09807819ca23d2b55d418a1cda9758725b35c1", + "0xb719e6279c5a9b5aa2b7a47f9e0e6453fa405808", + "0xaac0643b2a9f8351113388d935ff0718dae07e56", + "0x5409630192a9adaaa9ffbd46a3c695386be04e9c", + "0xd36ebe8e232955a444b4ab9a9297549c99ff09a9", + "0xee6d0298496153a026678488673a39c6153ab7f6", + "0xcabe8da7ce365392970437c9f4edfb4c89560257", + "0x910c60c049d833a7fd2f5bda310bbea1c5fa3598", + "0x8b5ce16637225516e2af14962407ec5afb8b9b73", + "0x720d2dad9cf67988e5b09d1d0dc4ebe5843cd611", + "0x1bbae2d283f2c14f43147e53d4f1b69601a1eac3", + "0x1e377d026dbb28dff1662927b2cb836b7c9f06d3", + "0xd340c943ae137ea0bab682a286d1af65e4e39a6b", + "0x71311a0e6167efcf28eeab78f87c008d05760b0b", + "0xc4b86bd3d432bd0b55dcd0e4bd3db91e05de2c26" + ], + "snapshot": 16093501 + }, + { + "name": "BSC query", + "strategy": { + "name": "hashflow-governance-power", + "params": { + "hftContract": "0x44ec807ce2f4a6f2737a92e985f318d035883e47", + "hftPool": "0x7286f5be4e531f6ea7660c739108d95aa2fef959", + "hToken": "0x1ddc58822f0071bf6ab973c94a185a97d000d036" + } + }, + "network": "56", + "addresses": [ + "0x884d6fa3a4b349880486ad4d7c833ca968c785d8", + "0xc0b570dc0363b1d3c9714f1060e716696acd4034", + "0xf4a290d914592a7f482fccce09ad73762a5998ca", + "0xdb4d32ca46310b4079fef1d135c88c1d1def32d7", + "0xc479328e6254bf692a3b22f051245069b307647a", + "0x66633cc6b84cd127a0bb84864c1a4ea0172469a6", + "0x1e46348fba3e75fd3b609351adfc22612c20be54", + "0xf41939922947f7c93cfa77364b35c6e85e53aef3", + "0x34f0431bec239c0828fbcf96c32092e5c926df21", + "0x67bcd41adbf12d1ecffd86e17af2b14179b556a3", + "0xf6e1acdb0a18cb397dde6ec0cca024d875e3e6e2", + "0x4d37f2a705377ac4c0827b79a8a4b84267b03ea1", + "0x4e97283bac00e1be24474e0671a1457e55991d11", + "0x9448e95d67fbe5ee576eeddb2adc637bae141e10", + "0xbe7d87e410a5c880f7a8ee83d4ae9d3db26b0d84", + "0x9f9d3b1c793a367310ebe5d159c4599914abdbd5", + "0x7a43dc785ced8554478b2ba2e491347c0826adde", + "0x04d29d747aa0abda9144fc15d1546a1bb959c40a", + "0xbe18f84532d8f7fb6d7919401c0096f3e257db8b", + "0x672c3b982325be86e221fe5d0eb5391ffa9cc8fc" + ], + "snapshot": 23545695 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/index.ts new file mode 100644 index 00000000..cc53c4be --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/index.ts @@ -0,0 +1,62 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { multicall, Multicaller } from '../../utils'; + +export const author = 'mib-hashflow'; +export const version = '0.1.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function assetDetails(address token) external view returns (uint128 withdrawalLimit, uint128 cap, uint128 netPayout, uint128 timestamp, address hToken, address hTokenXChain, bool listed)', + 'function totalSupply() external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const contractData = await multicall( + network, + provider, + abi, + [ + [options.hftPool, 'assetDetails', [options.hftContract]], + [options.hToken, 'totalSupply', []] + ], + { blockTag } + ); + + const netPayout: BigNumberish = contractData[0].netPayout; + const totalSupply: BigNumberish = contractData[1][0]; + + const lpTokenWeight = + parseFloat(formatUnits(netPayout, 18)) / + parseFloat(formatUnits(totalSupply, 18)); + + const hftMulti = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + hftMulti.call(address, options.hftContract, 'balanceOf', [address]) + ); + const hftBalances: Record = await hftMulti.execute(); + + const lpMulti = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + lpMulti.call(address, options.hToken, 'balanceOf', [address]) + ); + const lpBalances: Record = await lpMulti.execute(); + + return Object.fromEntries( + Object.entries(hftBalances).map(([address, balance]) => [ + getAddress(address), + parseFloat(formatUnits(balance, 18)) + + lpTokenWeight * parseFloat(formatUnits(lpBalances[address], 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/schema.json b/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/schema.json new file mode 100644 index 00000000..6e5b10b5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashflow-governance-power/schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "hftContract": { + "type": "string", + "title": "HFT Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "hftPool": { + "type": "string", + "title": "HFT Pool address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "hToken": { + "type": "string", + "title": "HFT Pool HFT-hToken address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["hftContract", "hftPool", "hToken"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/README.md b/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/README.md new file mode 100644 index 00000000..f3cea7d3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/README.md @@ -0,0 +1,23 @@ +# hashflow-vehft-v1 + +This strategy is used to account for the veHFT power that the Hashflow +staking vaults yield to the user. + +The voting power in the Hashflow DAO is a function of three parameters: + +- the amount of HFT staked +- the lock duration (normalized to 4 years between 0 and 1) +- ownership of a Creation's Coffer NFT + +The formula is as follows: + +[HFT amount] x [normalized time lock] x [1.1 if an NFT is owned] + +Here is an example of parameters: + +```json +{ + "hftVault": "0x15725391A37A5fFeB04F79cf25DA8460A3f068F6", + "nftContract": "0xb99E4E9b8Fd99c2C90aD5382dBC6ADfDfE3A33f3" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/examples.json new file mode 100644 index 00000000..ad36c858 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Ethereum Query", + "strategy": { + "name": "hashflow-vehft", + "params": { + "hftVault": "0x15725391A37A5fFeB04F79cf25DA8460A3f068F6", + "nftContract": "0xb99E4E9b8Fd99c2C90aD5382dBC6ADfDfE3A33f3" + } + }, + "network": "1", + "addresses": [ + "0x602bcC6BF7cfd77048AB7cA4113382ef269362f2", + "0x2EE3300306c156948947a9c63959e89d9d60824F", + "0xAF8324Ab53229651E4C8Dea252a554AacA6f9577", + "0x6D2c2D57d49B015847a4A48ebB143138a5d7A455", + "0x41982604fDDce37bf04624A6F4A511f94043DEE0", + "0x34E1AE9a9b3F8e6B648A0e8952cf6C553d65fd62" + ], + "snapshot": 16402454 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/index.ts new file mode 100644 index 00000000..e3c0029d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/index.ts @@ -0,0 +1,65 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'gxmxni-hashflow'; +export const version = '0.1.0'; + +const STAKES_ABI = + 'function stakes(address user) external returns (uint128 amount, uint64 lockExpiry)'; +const BALANCE_OF_ABI = + 'function balanceOf(address owner) external view returns (uint256 balance)'; + +const FOUR_YEARS_IN_SECONDS = 4 * 365 * 24 * 3_600; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const { timestamp } = await provider.getBlock(blockTag); + + const stakes = await multicall( + network, + provider, + [STAKES_ABI], + addresses.map((a) => [options.hftVault, 'stakes', [a]]), + { + blockTag + } + ); + + const rawVotingPower = stakes.map((s) => { + const timestampBN = BigNumber.from(timestamp); + const timeUntilExpiry = (s[1].gt(timestampBN) ? s[1] : timestampBN).sub( + timestampBN + ); + return timeUntilExpiry.mul(s[0]).div(FOUR_YEARS_IN_SECONDS); + }); + + const nftBalances = await multicall( + network, + provider, + [BALANCE_OF_ABI], + addresses.map((a) => [options.nftContract, 'balanceOf', [a]]), + { + blockTag + } + ); + + const votingPower = rawVotingPower.map((rvp, idx) => + parseFloat( + formatUnits(rvp.mul(nftBalances[idx][0].gt(0) ? 110 : 100).div(100), 18) + ) + ); + + return Object.fromEntries( + addresses.map((a, idx) => [getAddress(a), votingPower[idx]]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/schema.json b/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/schema.json new file mode 100644 index 00000000..82adaefd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hashflow-vehft/schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "hftVault": { + "type": "string", + "title": "HFT Staking Vault Address", + "examples": ["e.g. 0x15725391A37A5fFeB04F79cf25DA8460A3f068F6"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "nftContract": { + "type": "string", + "title": "NFT Contract Address", + "examples": ["e.g. 0xb99E4E9b8Fd99c2C90aD5382dBC6ADfDfE3A33f3"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["hftVault", "nftContract"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/README.md b/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/README.md new file mode 100644 index 00000000..c0095086 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/README.md @@ -0,0 +1,17 @@ +# hedgey-delegate + +This strategy calls the delegatedBalances function on Hedgey Vesting contracts + +It can also apply a multiplier to the value returned + +Here is an example of parameters: + +```json +{ + "contract": "0xce7ac66e78aae01d899eb90b63d1f20be2e9c4b1", + "token": "0xE13FB676E9bdd7AFda91495eC4e027FC63212FC3", + "symbol": "TACO", + "decimals": 18, + "multiplier": 10 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/examples.json new file mode 100644 index 00000000..aae572d3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example voting based on hedgey vesting token with delegation and multiplier", + "strategy": { + "name": "hedgey-delegate", + "params": { + "contracts": [ + "0xce7ac66e78aae01d899eb90b63d1f20be2e9c4b1", + "0x24f4BC74C00412422C9D2A7c78033fc8Aea8Da18" + ], + "token": "0xE13FB676E9bdd7AFda91495eC4e027FC63212FC3", + "symbol": "TACO", + "decimals": 18, + "multiplier": 10 + } + }, + "network": "5", + "addresses": [ + "0xAa9c3a194C9E78085260dfC7EAcFef51653412Bf", + "0x92d9802eFcD0485876DDC13c16cEA67e6aD5EB35", + "0xF0138A76223d93192C9c903520f1cf95c9094065", + "0xDC13Ab880e2AB7b5544a7b927769B5CEc6d62a0b", + "0xe31D847B47465cC2745319dAc9E0c6ac711cA10b" + ], + "snapshot": 8974862 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/index.ts new file mode 100644 index 00000000..377ad96a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/index.ts @@ -0,0 +1,40 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'hedgey-finance'; +export const version = '1.0.0'; + +const abi = [ + 'function delegatedBalances(address delegate, address token) view returns (uint256 delegatedBalance)' +]; + +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 }); + options.contracts.forEach((contract) => { + addresses.forEach((address) => + multi.call(address, contract, 'delegatedBalances', [ + address, + options.token + ]) + ); + }); + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) * options.multiplier + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/schema.json b/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/schema.json new file mode 100644 index 00000000..c68efcf7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey-delegate/schema.json @@ -0,0 +1,49 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "contracts": { + "type": "array", + "title": "Hedgey Contract addresses", + "items": { + "type": "string", + "examples": ["e.g. 0xCe7Ac66E78aAE01d899eb90b63D1f20bE2E9c4B1"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "token": { + "type": "string", + "title": "Token Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + }, + "multiplier": { + "type": "number", + "title": "Multiplier", + "examples": ["e.g. 1"] + } + }, + "required": ["contracts", "token", "decimals", "multiplier"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/README.md b/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/README.md new file mode 100644 index 00000000..42f2440e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/README.md @@ -0,0 +1,16 @@ +# Hedgey strategy + +Calculates voting rights based on the underlying tokens locked in the Hedgey protocol from multiple contracts with a multiplyer + +## Examples + +### Input parameters + +An array of ContractDetails + + - address: The address of the contract + - token: The address of the token that is taken into account for the score + - decimal: The decimal value the token uses + - contractType: Can be NFT for the standard NFT contract or TokenInfusedNFT for the token infused NFT contract + - lockedTokenMultiplier: a simple multiplyer for locked tokens + - lockedTokenMonthlyMultiplier: a multiplier based on the amount of time the tokens will be unlocked for diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/examples.json new file mode 100644 index 00000000..fca088df --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example voting based on hedgey token underlying balance from multiple contracts with a multiplyer", + "strategy": { + "name": "hedgey-multi", + "params": { + "contracts": [ + { + "address": "0x4bc8ea84bdc3ebb01d495e5d1605d4f082aeb5d7", + "token": "0xccDE1F2B71573a865d3896eb019C41Bf4BF92e65", + "decimal": 18, + "contractType": "NFT", + "lockedTokenMultiplier": 2, + "lockedTokenMonthlyMultiplier": { + "default": 1, + "1": 1.5, + "2": 2 + } + } + ] + } + }, + "network": "4", + "snapshot": 11452455, + "addresses": ["0x92d9802eFcD0485876DDC13c16cEA67e6aD5EB35"] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/index.ts new file mode 100644 index 00000000..5e6867ba --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey-multi/index.ts @@ -0,0 +1,217 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'bark4mark'; +export const version = '0.1.0'; + +const MAX_CONTRACTS = 8; + +enum ContractType { + NFT = 'NFT', + TokenInfusedNFT = 'TokenInfusedNFT' +} + +type ContractDetails = { + address: string; + token: string; + decimal: number; + contractType: ContractType; + lockedTokenMultiplier: number; + lockedTokenMonthlyMultiplier: any; +}; + +const abis = { + NFT: [ + 'function balanceOf(address owner) external view returns (uint256 balance)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId)', + 'function futures(uint256 index) external view returns (uint256 amount, address token, uint256 unlockDate)', + 'function token() external view returns (address token)' + ], + TokenInfusedNFT: [ + 'function futures(uint256 index) external view returns (uint256 amount, uint256 unlockDate)' + ] +}; + +const compareAddresses = (address1: string, address2: string): boolean => { + if (!address1 || !address2) return false; + return address1.toLowerCase() === address2.toLowerCase(); +}; + +const getMonthDifference = (start: Date, end: Date): number => { + return ( + end.getMonth() - + start.getMonth() + + 12 * (end.getFullYear() - start.getFullYear()) + ); +}; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const contractDetails: ContractDetails[] = options.contracts; + if (contractDetails.length > MAX_CONTRACTS) { + throw new Error(`Max number (${MAX_CONTRACTS}) of contracts exceeded`); + } + const balanceOfMulti = new Multicaller(network, provider, abis.NFT, { + blockTag + }); + + contractDetails.forEach((contractDetail) => { + addresses.forEach((address: string) => { + balanceOfMulti.call( + `${contractDetail.address}/${address}`, + contractDetail.address, + 'balanceOf', + [address] + ); + }); + if (contractDetail.contractType === ContractType.TokenInfusedNFT) { + balanceOfMulti.call( + `${contractDetail.address}/token`, + contractDetail.address, + 'token' + ); + } + }); + + const balanceOfResult = await balanceOfMulti.execute(); + const nftHolderMulti = new Multicaller(network, provider, abis.NFT, { + blockTag + }); + + contractDetails.forEach((contractDetail) => { + addresses.forEach((address: string) => { + const balance = balanceOfResult[`${contractDetail.address}/${address}`]; + for (let index = 0; index < balance; index++) { + nftHolderMulti.call( + `${contractDetail.contractType}/${contractDetail.address}/${address}/${index}`, + contractDetail.address, + 'tokenOfOwnerByIndex', + [address, index] + ); + } + }); + }); + + const nftHolders = await nftHolderMulti.execute(); + + const nftDealsMulti = new Multicaller(network, provider, abis.NFT, { + blockTag + }); + + const tiNFTDealsMulti = new Multicaller( + network, + provider, + abis.TokenInfusedNFT, + { blockTag } + ); + + for (const [path, nftId] of Object.entries(nftHolders)) { + const [contractType, contractAddress, address] = path.split('/'); + switch (contractType) { + case ContractType.NFT: + nftDealsMulti.call( + `${contractAddress}/${address}`, + contractAddress, + 'futures', + [nftId] + ); + break; + case ContractType.TokenInfusedNFT: + tiNFTDealsMulti.call( + `${contractAddress}/${address}`, + contractAddress, + 'futures', + [nftId] + ); + break; + } + } + + const nftDeals = await nftDealsMulti.execute(); + const tiNFTDeals = await tiNFTDealsMulti.execute(); + + const votes = {}; + + for (const [path, deal] of Object.entries(nftDeals)) { + const [contractAddress, address] = path.split('/'); + const contractDetail = contractDetails.find( + (element) => element.address === contractAddress + ); + + if (!contractDetail) continue; + if (!compareAddresses(deal.token, contractDetail.token)) continue; + + const amount = BigNumber.from(deal.amount).div( + BigNumber.from(10).pow(contractDetail.decimal) + ); + + let score = amount.toNumber(); + score = score * contractDetail.lockedTokenMultiplier; + const durationStart = new Date(); + const unlockDate = new Date(deal.unlockDate * 1000); + + const months = getMonthDifference(durationStart, unlockDate); + let monthlyMultiplier = contractDetail.lockedTokenMonthlyMultiplier[months]; + + if (!monthlyMultiplier) + monthlyMultiplier = + contractDetail.lockedTokenMonthlyMultiplier['default']; + + score = score * monthlyMultiplier; + + let existingAmount = votes[address]; + if (existingAmount) { + existingAmount = existingAmount + score; + } else { + votes[address] = score; + } + } + + for (const [path, deal] of Object.entries(tiNFTDeals)) { + const [contractAddress, address] = path.split('/'); + const contractDetail = contractDetails.find( + (element) => element.address === contractAddress + ); + + if (!contractDetail) continue; + + const contractToken = balanceOfResult[`${contractDetail.address}/token`]; + if (!compareAddresses(contractToken, contractDetail.token)) continue; + + const amount = BigNumber.from(deal.amount).div( + BigNumber.from(10).pow(contractDetail.decimal) + ); + + let score = amount.toNumber(); + score = score * contractDetail.lockedTokenMultiplier; + const durationStart = new Date(); + const unlockDate = new Date(deal.unlockDate * 1000); + + const months = getMonthDifference(durationStart, unlockDate); + if (months > 1) { + let monthlyMultiplier = + contractDetail.lockedTokenMonthlyMultiplier[months]; + + if (!monthlyMultiplier) + monthlyMultiplier = + contractDetail.lockedTokenMonthlyMultiplier['default']; + + score = score * monthlyMultiplier; + } + let existingAmount = votes[address]; + if (existingAmount) { + existingAmount = existingAmount + score; + } else { + votes[address] = score; + } + } + + return votes; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey/README.md b/Implementations/API/backend/utils/snapshot/strategies/hedgey/README.md new file mode 100644 index 00000000..ee7d56fc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey/README.md @@ -0,0 +1,19 @@ +# Hedgey strategy + +Calculates voting rights based on the underlying tokens locked in the Hedgey protocol + +## Examples + +Requires 3 input parameters: + +contractAddress + +The address of the Hedgey NFT contract deployed on the selected network. + +token + +The token address that contains the token balance for voting rights + +decimals + +The number of decimals used by the token (usually 18) diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hedgey/examples.json new file mode 100644 index 00000000..1733e216 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example voting based on hedgey token underlying balance", + "strategy": { + "name": "hedgey", + "params": { + "contractAddress": "0x8d53fd121198d0982f8e19286e924c78ed5a6a7a", + "token": "0xb0C5f3100A4d9d9532a4CfD68c55F1AE8da987Eb", + "decimals": 18 + } + }, + "network": "100", + "snapshot": 22423721, + "addresses": ["0x92d9802eFcD0485876DDC13c16cEA67e6aD5EB35"] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hedgey/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hedgey/index.ts new file mode 100644 index 00000000..14c28f3b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hedgey/index.ts @@ -0,0 +1,78 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'Hedgey'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner) external view returns (uint256 balance)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId)', + 'function futures(uint256 index) external view returns (uint256 amount, address token, uint256 unlockDate)' +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const balanceOfMulti = new Multicaller(network, provider, abi, { + blockTag + }); + + addresses.forEach((address: string) => { + balanceOfMulti.call(address, options.contractAddress, 'balanceOf', [ + address + ]); + }); + + const balanceOfResult = await balanceOfMulti.execute(); + + const nftHolderMulti = new Multicaller(network, provider, abi, { + blockTag + }); + + for (const [address, balance] of Object.entries(balanceOfResult)) { + for (let index = 0; index < balance; index++) { + nftHolderMulti.call( + `${address}-${index}`, + options.contractAddress, + 'tokenOfOwnerByIndex', + [address, String(index)] + ); + } + } + + const nftHolders = await nftHolderMulti.execute(); + + const dealsMulti = new Multicaller(network, provider, abi, { blockTag }); + + for (const [address, nftId] of Object.entries(nftHolders)) { + dealsMulti.call(address, options.contractAddress, 'futures', [nftId]); + } + const ownerToDeal = await dealsMulti.execute(); + + const votes = {}; + for (const [index, deal] of Object.entries(ownerToDeal)) { + const address = index.split('-')[0]; + if (!votes[address]) { + votes[address] = BigNumber.from(0); + } + + if (deal.token.toLowerCase() === options.token.toLowerCase()) { + votes[address] = votes[address].add(deal.amount); + } + } + + const result = []; + + Object.keys(votes).forEach((address) => { + const score = votes[address].div(BigNumber.from(10).pow(options.decimals)); + result[address] = score.toNumber(); + }); + + return result; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/helix/README.md b/Implementations/API/backend/utils/snapshot/strategies/helix/README.md new file mode 100644 index 00000000..19de060c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/helix/README.md @@ -0,0 +1,41 @@ +# Helix + +This is the strategy, it returns the balances of the voters for HELIX token balances +in Helix project(pools, farms, vaults, token). + +Here is an example of parameters: + +```json +[ + { + "name": "Example query Helix", + "strategy": { + "name": "helix", + "params": { + "address": "0x79DD2dad8D04F9279F94580DBEd2306A0aE118Bd", + "masterChef": "0x15B400a434E0d94e0F1e1A0AA4a08E98A2d04128", + "autoHelix": "0xF34316463B29B0D7A387a8443b2E17B3629e99b2", + "vault": "0x370650b477D550E4611CCe63eE8b7eCa64F207C0", + "helixLPs": [ + { + "address": "0xfbfe53025c54b70b48070904f8765703D2aD749D", + "pid": 1 + }, + { + "address": "0x161962Aec8f3c61D865cd5d53A334780763364e6", + "pid": 2 + } + ], + "symbol": "HELIX", + "decimals": 18 + } + }, + "network": "4", + "addresses": [ + "0x2155BCeA4f362D5D9CE67817b826A8F31b61D0BF", + "0xb1F7D313Ce45fe62EdE9CE4cfb46833051d38e57" + ], + "snapshot": 10819269 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/helix/examples.json b/Implementations/API/backend/utils/snapshot/strategies/helix/examples.json new file mode 100644 index 00000000..d5a6345b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/helix/examples.json @@ -0,0 +1,32 @@ +[ + { + "name": "Example query Helix", + "strategy": { + "name": "helix", + "params": { + "address": "0x79DD2dad8D04F9279F94580DBEd2306A0aE118Bd", + "masterChef": "0x15B400a434E0d94e0F1e1A0AA4a08E98A2d04128", + "autoHelix": "0xF34316463B29B0D7A387a8443b2E17B3629e99b2", + "vault": "0x370650b477D550E4611CCe63eE8b7eCa64F207C0", + "helixLPs": [ + { + "address": "0xfbfe53025c54b70b48070904f8765703D2aD749D", + "pid": 1 + }, + { + "address": "0x161962Aec8f3c61D865cd5d53A334780763364e6", + "pid": 2 + } + ], + "symbol": "HELIX", + "decimals": 18 + } + }, + "network": "4", + "addresses": [ + "0x2155BCeA4f362D5D9CE67817b826A8F31b61D0BF", + "0xb1F7D313Ce45fe62EdE9CE4cfb46833051d38e57" + ], + "snapshot": 10819269 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/helix/index.ts b/Implementations/API/backend/utils/snapshot/strategies/helix/index.ts new file mode 100644 index 00000000..ec0b8eb0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/helix/index.ts @@ -0,0 +1,138 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import examplesFile from './examples.json'; + +export const author = 'helixgeometry'; +export const version = '0.0.1'; +export const examples = examplesFile; + +const abi = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address _owner) view returns (uint256 balance)', + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +const autoHelixAbi = [ + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)', + 'function getPricePerFullShare() view returns (uint256)' +]; + +const vaultAbi = ['function getDepositAmount(address) view returns (uint256)']; + +const bn = (num: any): BigNumber => { + return BigNumber.from(num.toString()); +}; + +const addUserBalance = (userBalances, user: string, balance) => { + if (userBalances[user]) { + return (userBalances[user] = userBalances[user].add(balance)); + } else { + return (userBalances[user] = balance); + } +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multicall = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address: any) => { + multicall.call(`token.${address}`, options.address, 'balanceOf', [address]); + multicall.call(`masterChef.${address}`, options.masterChef, 'userInfo', [ + '0', + address + ]); + }); + options.helixLPs.forEach((lp: { address: string; pid: number }) => { + multicall.call(`lp.${lp.pid}.totalSupply`, lp.address, 'totalSupply'); + multicall.call(`lp.${lp.pid}.balanceOf`, options.address, 'balanceOf', [ + lp.address + ]); + addresses.forEach((address: any) => { + multicall.call( + `lpUsers.${address}.${lp.pid}`, + options.masterChef, + 'userInfo', + [lp.pid, address] + ); + }); + }); + + const multicallAutoCompound = new Multicaller( + network, + provider, + autoHelixAbi, + { + blockTag + } + ); + multicallAutoCompound.call( + 'priceShare', + options.autoHelix, + 'getPricePerFullShare' + ); + addresses.forEach((address) => { + multicallAutoCompound.call( + `autoPool.${address}`, + options.autoHelix, + 'userInfo', + [address] + ); + }); + + const multicallVault = new Multicaller(network, provider, vaultAbi, { + blockTag + }); + addresses.forEach((address: any) => { + multicallVault.call(`vault.${address}`, options.vault, 'getDepositAmount', [ + address + ]); + }); + + const [resultVault, resultAutoHelix, result] = await Promise.all([ + multicallVault.execute(), + multicallAutoCompound.execute(), + multicall.execute() + ]); + + const userBalances: any = []; + for (let i = 0; i < addresses.length - 1; i++) { + userBalances[addresses[i]] = bn(0); + } + + addresses.forEach((address: any) => { + addUserBalance(userBalances, address, result.token[address]); + addUserBalance(userBalances, address, result.masterChef[address][0]); + addUserBalance( + userBalances, + address, + resultAutoHelix.autoPool[address][0] + .mul(resultAutoHelix.priceShare) + .div(bn(parseUnits('1', options.decimals))) + ); + options.helixLPs.forEach((lp: { address: string; pid: number }) => { + addUserBalance( + userBalances, + address, + result.lpUsers[address][lp.pid][0] + .mul(result.lp[lp.pid].balanceOf) + .div(result.lp[lp.pid].totalSupply) + ); + }); + addUserBalance(userBalances, address, resultVault.vault[address]); + }); + + return Object.fromEntries( + Object.entries(userBalances).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/README.md b/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/README.md new file mode 100644 index 00000000..0b1833bc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/README.md @@ -0,0 +1,30 @@ +# holds-tokens + +This strategy return the balances of the voters for a specific ERC20 or ERC721 and maps them to the number of votes that voter gets based on holding a set of tokens. on multiple networks + +## Parameters + +Same as [erc20-with-balance](https://snapshot.org/#/strategy/erc20-with-balance) strategy. except that we have to pass the `network` parameter. This is the network that the contract is deployed on. also can pass multiple params into `tokenAddresses` array (Check the example below). + +> Note: minBalance is exclusive. For example if you pass `1`, balance should be more than `1` + +Here is an example of parameters: + +```json +{ + "tokenAddresses": [ + { + "address": "0x7Bd29408f11D2bFC23c34f18275bBf23bB716Bc7", + "network": "1", + "decimals": 0, + "minBalance": 1 + }, + { + "address": "0xc34cbca32e355636c7f52dd8beab0af2396ebd79", + "network": "137", + "decimals": 0, + "minBalance": 1 + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/examples.json b/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/examples.json new file mode 100644 index 00000000..e74fe682 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "holds-tokens", + "params": { + "symbol": "XYZ", + "tokenAddresses": [ + { + "address": "0x7Bd29408f11D2bFC23c34f18275bBf23bB716Bc7", + "network": "1", + "decimals": 0, + "minBalance": 0 + }, + { + "address": "0xc34cbca32e355636c7f52dd8beab0af2396ebd79", + "network": "137", + "decimals": 0, + "minBalance": 0 + } + ] + } + }, + "network": "1", + "addresses": [ + "0xC5e38233Cc0D7CFf9340e6139367aBA498EC9b18", + "0x2009a752a50D3CDe486d7b5921944377B729E747", + "0x7b15e6c439b27a553b65a9904ce571da6691a0fb", + "0x8d2f3a76a76f055d62a931678ab16b042e7badeb", + "0xEDe64a571CFe98B936271B935a955620f387E05A" + ], + "snapshot": 13317369 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/index.ts b/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/index.ts new file mode 100644 index 00000000..c1b8c8e5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/holds-tokens/index.ts @@ -0,0 +1,41 @@ +import { strategy as multichainStrategy } from '../multichain'; + +export const author = 'lightninglu10'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const tokens = options.tokenAddresses || []; + + options.strategies = tokens.map((token) => ({ + name: 'erc20-with-balance', + network: token.network, + params: { + address: token.address, + decimals: token.decimals, + minBalance: token.minBalance + } + })); + + const scores: any = await multichainStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + return Object.fromEntries( + Object.entries(scores).map((address: any) => [ + address[0], + address[1] === tokens.length ? 1 : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/honeyswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/honeyswap/README.md new file mode 100644 index 00000000..56d9c5b1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/honeyswap/README.md @@ -0,0 +1,13 @@ +# honeyswap + +Here is an example of parameters: + +```json +{ + "address": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", + "useStakedBalances": "true" +} +``` + +- *address* - the underlying token +- *useStakedBalances* - if **true** it will also return the token balances from the HoneyFarm pools diff --git a/Implementations/API/backend/utils/snapshot/strategies/honeyswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/honeyswap/examples.json new file mode 100644 index 00000000..f4271906 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/honeyswap/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "honeyswap", + "params": { + "symbol": "WXDAI", + "address": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", + "useStakedBalances": "true" + } + }, + "network": "100", + "addresses": [ + "0x60a9372862bD752CD02D9AE482F94Cd2fe92A0Bf", + "0x8520Fc4C282342f8e746b881b9B60c14F96A0fAB", + "0x28DDA781160928D6BeF89833cD646A145a7899F3", + "0xEB8Db0f667B4f96c6819c7E3f14Bd3a2bd89B062", + "0x5EEDb07269278609D396210A4CB2D3A66f696daf", + "0xc88db189Dc19753908534411794deD880C10B5cb", + "0xC6f1a9D4Fb5681f986d3Dc6EC116f66D95CC2F03" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/honeyswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/honeyswap/index.ts new file mode 100644 index 00000000..f77387f7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/honeyswap/index.ts @@ -0,0 +1,224 @@ +import { getAddress } from '@ethersproject/address'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; + +export const author = 'Kittyslasher'; +export const version = '0.2.0'; + +const HONEYSWAP_SUBGRAPH_URL = { + '100': { + exchange: 'https://api.thegraph.com/subgraphs/name/1hive/uniswap-v2', + farm: 'https://api.thegraph.com/subgraphs/name/1hive/honeyfarm-xdai' + } +}; + +async function getPairs(network, snapshot, token) { + const getParams = (prop) => { + const params = { + pairs: { + __args: { + where: { + [prop]: token.toLowerCase() + }, + first: 1000 + }, + id: true, + totalSupply: true, + reserve0: true, + reserve1: true, + token0: { + id: true, + decimals: true + }, + token1: { + id: true, + decimals: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.pairs.__args.block = { number: snapshot }; + } + return params; + }; + const [result1, result2] = await Promise.all( + ['token0', 'token1'].map((prop) => + subgraphRequest(HONEYSWAP_SUBGRAPH_URL[network].exchange, getParams(prop)) + ) + ); + return Object.fromEntries( + result1.pairs.concat(result2.pairs).map((pair) => { + const isToken0 = pair.token0.id == token.toLowerCase(); + const rate = isToken0 + ? +pair.reserve0 / +pair.totalSupply + : +pair.reserve1 / +pair.totalSupply; + const decimals = isToken0 ? pair.token0.decimals : pair.token1.decimals; + return [pair.id, { rate, decimals }]; + }) + ); +} + +async function getPools(network, snapshot, token) { + const pairs = await getPairs(network, snapshot, token); + const params = { + pools: { + __args: { + where: { + id_in: Object.keys(pairs) + }, + first: 1000 + }, + id: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.pools.__args.block = { number: snapshot }; + } + const result = await subgraphRequest( + HONEYSWAP_SUBGRAPH_URL[network].farm, + params + ); + return Object.fromEntries( + result.pools.map((pool) => [pool.id, pairs[pool.id]]) + ); +} + +async function getStakedBalances(network, snapshot, token, addresses) { + const pools = await getPools(network, snapshot, token); + const PAGE_SIZE = 1000; + const params = { + deposits: { + __args: { + where: { + pool_in: Object.keys(pools), + user_in: addresses.map((addr) => addr.toLowerCase()), + status: 'Open' + }, + first: PAGE_SIZE + }, + user: { + id: true + }, + amount: true, + pool: { + id: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.deposits.__args.block = { number: snapshot }; + } + let deposits = []; + let page = 0; + while (true) { + // @ts-ignore + params.deposits.__args.skip = page * PAGE_SIZE; + const result = await subgraphRequest( + HONEYSWAP_SUBGRAPH_URL[network].farm, + params + ); + deposits = deposits.concat(result.deposits); + page++; + if (result.deposits.length < PAGE_SIZE) break; + } + + return deposits.map((deposit: any) => { + const pool = pools[deposit.pool.id]; + const amount = + parseFloat(formatUnits(deposit.amount, pool.decimals)) * pool.rate; + return { + address: deposit.user.id, + amount + }; + }); +} + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + liquidityPositions: { + __args: { + where: { + liquidityTokenBalance_gt: 0 + } + }, + liquidityTokenBalance: true, + pair: { + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.liquidityPositions.__args.block = { number: snapshot }; + } + const tokenAddress = options.address.toLowerCase(); + const result = await subgraphRequest( + HONEYSWAP_SUBGRAPH_URL[network].exchange, + params + ); + const score = {}; + if (result && result.users) { + result.users.forEach((u) => { + u.liquidityPositions + .filter( + (lp) => + lp.pair.token0.id == tokenAddress || + lp.pair.token1.id == tokenAddress + ) + .forEach((lp) => { + const token0perUni = lp.pair.reserve0 / lp.pair.totalSupply; + const token1perUni = lp.pair.reserve1 / lp.pair.totalSupply; + const userScore = + lp.pair.token0.id == tokenAddress + ? token0perUni * lp.liquidityTokenBalance + : token1perUni * lp.liquidityTokenBalance; + + const userAddress = getAddress(u.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScore; + }); + }); + } + if (options.useStakedBalances === 'true') { + const stakedBalances = await getStakedBalances( + network, + snapshot, + options.address, + addresses + ); + stakedBalances.forEach((balance) => { + const userAddress = getAddress(balance.address); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] += balance.amount; + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-bridged-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hopr-bridged-balance/examples.json new file mode 100644 index 00000000..33516c30 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-bridged-balance/examples.json @@ -0,0 +1,65 @@ +[ + { + "name": "Get bridged HOPR token balance", + "strategy": { + "name": "hopr-bridged-balance", + "params": { + "symbol": "HOPR", + "xHopr": "0xd057604a14982fe8d88c5fc25aac3267ea142a08", + "wxHopr": "0xD4fdec44DB9D44B8f2b6d529620f9C0C7066A2c1", + "hopr": "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da" + } + }, + "network": "100", + "addresses": [ + "0x04BBB7eA18EA570aE47d4489991645E4E49bBf37", + "0x2aF80738aC01e7883d11c912dFe8322C129ae5C5", + "0x0bb43EFc1a613658177D8f67CcF9CFFD8B25b906", + "0x53e85186ebF5A7d4BD06324F7b9D8B3623EF0307", + "0x2DCDB99930E279f1e9Ad11F491163051432542A0", + "0x4326990033eCd87A5444383Cf8c715E696301910", + "0xEd6a59A7C1D5a88b7cb5eb877A7A6078A7e801C7", + "0xeFC05B0D0C8bE8D4Cb3a220ef582E9f7E6FBCd00", + "0xC7B169b108c5e75991C520AEA97140534291C81D", + "0x04Be52434EB64aDdF373137310551ac42013677c", + "0xBE8C93a8C18AF63aAB449994AFAc13E71240ccC4", + "0xf813773eBDD4759c1B780d745081f046A5B776fB", + "0x7F26C34Ed10bF66602009231bBFF24f2f84e9270", + "0x4abd7276C53279b3aBFFF2B5D8A47c0AFc84833B", + "0x3e1A12a6019ee26418F22B656926fE38F5e58C5f", + "0x7A27A4D91231aCB3282b410Cc784517B417FA0DA" + ], + "snapshot": 18200908 + }, + { + "name": "Get bridged HOPR token balance", + "strategy": { + "name": "hopr-bridged-balance", + "params": { + "xHopr": "0xd057604a14982fe8d88c5fc25aac3267ea142a08", + "wxHopr": "0xD4fdec44DB9D44B8f2b6d529620f9C0C7066A2c1", + "hopr": "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da" + } + }, + "network": "1", + "addresses": [ + "0x04BBB7eA18EA570aE47d4489991645E4E49bBf37", + "0x2aF80738aC01e7883d11c912dFe8322C129ae5C5", + "0x0bb43EFc1a613658177D8f67CcF9CFFD8B25b906", + "0x53e85186ebF5A7d4BD06324F7b9D8B3623EF0307", + "0x2DCDB99930E279f1e9Ad11F491163051432542A0", + "0x4326990033eCd87A5444383Cf8c715E696301910", + "0xEd6a59A7C1D5a88b7cb5eb877A7A6078A7e801C7", + "0xeFC05B0D0C8bE8D4Cb3a220ef582E9f7E6FBCd00", + "0xC7B169b108c5e75991C520AEA97140534291C81D", + "0x04Be52434EB64aDdF373137310551ac42013677c", + "0xBE8C93a8C18AF63aAB449994AFAc13E71240ccC4", + "0xf813773eBDD4759c1B780d745081f046A5B776fB", + "0x7F26C34Ed10bF66602009231bBFF24f2f84e9270", + "0x4abd7276C53279b3aBFFF2B5D8A47c0AFc84833B", + "0x3e1A12a6019ee26418F22B656926fE38F5e58C5f", + "0x7A27A4D91231aCB3282b410Cc784517B417FA0DA" + ], + "snapshot": 13269966 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-bridged-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hopr-bridged-balance/index.ts new file mode 100644 index 00000000..e9e2f3f6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-bridged-balance/index.ts @@ -0,0 +1,180 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall, subgraphRequest } from '../../utils'; + +export const author = 'QYuQianchen'; +export const version = '0.1.0'; + +const tokenAbi = ['function balanceOf(address) view returns (uint256)']; + +const XDAI_BLOCK_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/1hive/xdai-blocks'; +const MAINNET_BLOCK_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/blocklytics/ethereum-blocks'; +const HOPR_XDAI_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/hoprnet/hopr-on-xdai'; +const HOPR_MAINNET_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/hoprnet/hopr-on-mainnet'; +const LIMIT = 1000; // 1000 addresses per query in Subgraph + +async function getXdaiBlockNumber(timestamp: number): Promise { + const query = { + blocks: { + __args: { + first: 1, + orderBy: 'number', + orderDirection: 'desc', + where: { + timestamp_lte: timestamp + } + }, + number: true, + timestamp: true + } + }; + const data = await subgraphRequest(XDAI_BLOCK_SUBGRAPH_URL, query); + return Number(data.blocks[0].number); +} + +async function getMainnetBlockNumber(timestamp: number): Promise { + const query = { + blocks: { + __args: { + first: 1, + orderBy: 'number', + orderDirection: 'desc', + where: { + timestamp_lte: timestamp + } + }, + number: true, + timestamp: true + } + }; + const data = await subgraphRequest(MAINNET_BLOCK_SUBGRAPH_URL, query); + return Number(data.blocks[0].number); +} + +async function xHoprSubgraphQuery( + addresses: string[], + blockNumber: number +): Promise<{ [propName: string]: number }> { + const query = { + accounts: { + __args: { + first: LIMIT, + block: { + number: blockNumber + }, + where: { + id_in: addresses.map((adr) => adr.toLowerCase()) + } + }, + id: true, + totalBalance: true + } + }; + const data = await subgraphRequest(HOPR_XDAI_SUBGRAPH_URL, query); + // map result (data.accounts) to addresses + const entries = data.accounts.map((d) => [d.id, Number(d.totalBalance)]); + return Object.fromEntries(entries); +} + +async function hoprSubgraphQuery( + addresses: string[], + blockNumber: number +): Promise<{ [propName: string]: number }> { + const query = { + accounts: { + __args: { + first: LIMIT, + block: { + number: blockNumber + }, + where: { + id_in: addresses.map((adr) => adr.toLowerCase()) + } + }, + id: true, + amount: true + } + }; + const data = await subgraphRequest(HOPR_MAINNET_SUBGRAPH_URL, query); + // map result (data.accounts) to addresses + const entries = data.accounts.map((d) => [d.id, Number(d.amount)]); + return Object.fromEntries(entries); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const isEth = network === '1'; // either ETH mainnet or xDAI + + const [res, block] = await Promise.all([ + multicall( + network, + provider, + tokenAbi, + addresses + .map((address: any) => [ + isEth ? options.hopr : options.xHopr, + 'balanceOf', + [address] + ]) + .concat( + isEth + ? [] + : addresses.map((address: any) => [ + options.wxHopr, + 'balanceOf', + [address] + ]) + ), + { blockTag } + ), + provider.getBlock(blockTag) + ]); + + const currentNetwork: BigNumber[] = isEth + ? res.map((r) => r[0] as BigNumber) + : addresses.map((r, i) => + (res[i][0] as BigNumber).add(res[i + addresses.length][0] as BigNumber) + ); + const subgraphBlock = isEth + ? await getXdaiBlockNumber(block.timestamp) + : await getMainnetBlockNumber(block.timestamp); + + // trim addresses to sub of "LIMIT" addresses. + const addressSubsets = Array.apply( + null, + Array(Math.ceil(addresses.length / LIMIT)) + ).map((_e, i) => addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + const returnedFromSubgraph = isEth + ? await Promise.all( + addressSubsets.map((subset) => + xHoprSubgraphQuery(subset, subgraphBlock) + ) + ) + : await Promise.all( + addressSubsets.map((subset) => hoprSubgraphQuery(subset, subgraphBlock)) + ); + + // get and parse balance from subgraph + const subgraphBalance = Object.assign({}, ...returnedFromSubgraph); + const subgraphScore = addresses.map( + (address) => subgraphBalance[address.toLowerCase()] ?? 0 + ); + + return Object.fromEntries( + currentNetwork.map((value, i) => [ + addresses[i], // current network balance + parseFloat(formatUnits(value, 18)) + subgraphScore[i] // subgraph balance + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/README.md b/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/README.md new file mode 100644 index 00000000..8d50dd51 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/README.md @@ -0,0 +1,31 @@ +# HOPR Stake and Balance QV + +This `hopr-stake-and-balance-qv` strategy calculates voting power with: +`(B1 + B2 + B3 + S1 + S2)^0.5` + +where: +- B1: balance of HOPR token on mainnet +- B2: balance of HOPR token on Gnosis chain (xHOPR) +- B3: balance of wrapped HOPR token on Gnosis chain (wxHOPR) +- S1: amount of xHOPR token staked into the latest staking season +- S2: amount of wxHOPR token unclaimed from the latest staking season + +## Parameters +- "tokenAddress": Contract address of HOPR token on mainnet. Value should be `"0xf5581dfefd8fb0e4aec526be659cfab1f8c781da"` +- "symbol": Token Symbol. Value should be `"HOPR"`. +- "season": Number of the on-going season. E.g. `7`. +- "fallbackGnosisBlock": Fallback block number on Gnosis chain, in case Gnosis block number cannot be translated from Ethereum mainnet due to subgraph issues. E.g. `27852687`, +- "subgraphStudioProdQueryApiKey": Production decetralized subgraph studio query API key. If no key can be provided, use `null`. +- "subgraphStudioDevAccountId": Development subgraph studio account ID. Note that this ID should not be exposed normally. If unknown, use `null`. +- "subgraphHostedAccountName": Legacy hosted subgraph account name. Vallue is `"hoprnet"`. +- "useStake": If the staking program should be consided. If `false`, `S1 + S2 === 0`. Value should be set to `true`. +- "useHoprOnGnosis": If tokens on Gnosis chain should be consided. If `false`, `B2 + B3 === 0`. Value should be set to `true`. +- "useHoprOnMainnet": If tokens on Ethereum mainnet should be consided. If `false`, `B1 === 0`. Value should be set to `true`. +- "subgraphStudioProdAllSeasonQueryId": Production stake all season subgraph ID .Value is `"DrkbaCvNGVcNH1RghepLRy6NSHFi8Dmwp4T2LN3LqcjY"`. +- "subgraphStudioDevAllSeasonVersion": Latest development version of the stake all season subgraph. E.g. `"v0.0.9"` +- "subgraphStudioDevAllSeasonSubgraphName": Name of the staking subgraph in Graph Studio. Value should be `"hopr-stake-all-seasons"`. +- "subgraphHostedAllSeasonSubgraphName": Name of the staking subgraph in Graph Hosted service. Value should be `"hopr-stake-all-seasons"`. +- "subgraphStudioProdHoprOnGnosisQueryId": Latest development version of the HOPR token balances on Gnosis subgraph. Value should be `"njToE7kpetd3P9sJdYQPSq6yQjBs7w9DahQpBj6WAoD"`. +- "subgraphStudioDevHoprOnGnosisSubgraphName": Name of the HOPR token balances on Gnosis subgraph in Graph Studio. Value should be "hopr-on-gnosis"` +- "subgraphStudioDevHoprOnGnosisVersion": Latest development version of the HOPR token balances on Gnosis subgraph. E.g. "v0.0.2"` +- "subgraphHostedHoprOnGnosisSubgraphName": Name of the the HOPR token balances on Gnosis in Graph Hosted service. Value should be `"hopr-on-xdai"` diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/examples.json new file mode 100644 index 00000000..d56e72a4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/examples.json @@ -0,0 +1,48 @@ +[ + { + "name": "Calculate score based on HOPR stake season x and token balances across chains", + "strategy": { + "name": "hopr-stake-and-balance-qv", + "params": { + "tokenAddress": "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da", + "symbol": "HOPR", + "season": 7, + "fallbackGnosisBlock": 27852687, + "subgraphStudioProdQueryApiKey": null, + "subgraphStudioDevAccountId": null, + "subgraphHostedAccountName": "hoprnet", + "useStake": true, + "useHoprOnGnosis": true, + "useHoprOnMainnet": true, + "subgraphStudioProdAllSeasonQueryId": "DrkbaCvNGVcNH1RghepLRy6NSHFi8Dmwp4T2LN3LqcjY", + "subgraphStudioDevAllSeasonVersion": "v0.0.9", + "subgraphStudioDevAllSeasonSubgraphName": "hopr-stake-all-seasons", + "subgraphHostedAllSeasonSubgraphName": "hopr-stake-all-seasons", + "subgraphStudioProdHoprOnGnosisQueryId": "njToE7kpetd3P9sJdYQPSq6yQjBs7w9DahQpBj6WAoD", + "subgraphStudioDevHoprOnGnosisSubgraphName": "hopr-on-gnosis", + "subgraphStudioDevHoprOnGnosisVersion": "v0.0.2", + "subgraphHostedHoprOnGnosisSubgraphName": "hopr-on-xdai" + } + }, + "network": "1", + "addresses": [ + "0x04BBB7eA18EA570aE47d4489991645E4E49bBf37", + "0x2aF80738aC01e7883d11c912dFe8322C129ae5C5", + "0x0bb43EFc1a613658177D8f67CcF9CFFD8B25b906", + "0x53e85186ebF5A7d4BD06324F7b9D8B3623EF0307", + "0x2DCDB99930E279f1e9Ad11F491163051432542A0", + "0x4326990033eCd87A5444383Cf8c715E696301910", + "0xEd6a59A7C1D5a88b7cb5eb877A7A6078A7e801C7", + "0xeFC05B0D0C8bE8D4Cb3a220ef582E9f7E6FBCd00", + "0xC7B169b108c5e75991C520AEA97140534291C81D", + "0x04Be52434EB64aDdF373137310551ac42013677c", + "0xBE8C93a8C18AF63aAB449994AFAc13E71240ccC4", + "0xf813773eBDD4759c1B780d745081f046A5B776fB", + "0x7F26C34Ed10bF66602009231bBFF24f2f84e9270", + "0x4abd7276C53279b3aBFFF2B5D8A47c0AFc84833B", + "0x3e1A12a6019ee26418F22B656926fE38F5e58C5f", + "0x7A27A4D91231aCB3282b410Cc784517B417FA0DA" + ], + "snapshot": 17220270 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/index.ts new file mode 100644 index 00000000..d5e46b60 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-stake-and-balance-qv/index.ts @@ -0,0 +1,396 @@ +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall, subgraphRequest } from '../../utils'; + +/** + * @dev Calculate score based on Quadratic Voting system.Token balance comes from + * - Mainnet HOPR token balance, read from multicall + * - Gnosis chain, HOPR token balance, read from subgraph (xHOPR balance and wxHOPR balance) and multicall (mainnet HOPR balance) + * - Gnosis chain. HOPR token staked into the most recent stake season, read from subgraph. + */ +export const author = 'QYuQianchen'; +export const version = '0.1.0'; + +const XDAI_BLOCK_HOSTED_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/1hive/xdai-blocks'; +const QUERY_LIMIT = 1000; // 1000 addresses per query in Subgraph +const tokenAbi = ['function balanceOf(address) view returns (uint256)']; // get mainnet HOPR token balance +// const DEFAULT_HOPR_STAKING_ALL_SEASONS_PROD_SUBGRAPH_ID = 'DrkbaCvNGVcNH1RghepLRy6NSHFi8Dmwp4T2LN3LqcjY'; +// const DEFAULT_HOPR_ON_GNOSIS_PROD_SUBGRAPH_ID = 'njToE7kpetd3P9sJdYQPSq6yQjBs7w9DahQpBj6WAoD'; +const DEFAULT_HOPR_HOSTED_ACCOUNT_NAME = 'hoprnet'; +const DEFAULT_HOPR_STAKING_ALL_SEASONS_HOSTED_SUBGRAPH_NAME = + 'hopr-stake-all-seasons'; +const DEFAULT_HOPR_BALANCE_ON_GNOSIS_HOSTED_SUBGRAPH_NAME = 'hopr-on-xdai'; + +function getStudioProdSubgraphUrl( + apiKey: string | null | undefined, + subgraphId: string +): string | null { + return !apiKey + ? null + : `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/${subgraphId}`; +} +function getStudioDevSubgraphUrl( + accountStudioId: string | null | undefined, + subgraphName: string, + version: string +): string | null { + return !accountStudioId + ? null + : `https://api.studio.thegraph.com/query/${accountStudioId}/${subgraphName}/${version}`; +} +function getHostedSubgraphUrl( + accountName: string, + subgraphName: string +): string { + return `https://api.thegraph.com/subgraphs/name/${accountName}/${subgraphName}`; +} + +/** + * Try to query subgraphs from three differnt endpoints (hosted service, studio for development, studio in production), if applicable + * @param hostedSubgraphUrl hosted subgrpah url + * @param stuidoDevSubgraphUrl development url foro studio subgraph + * @param studioProdSubgraphUrl production url foro studio subgraph + * @param builtQuery query object + * @returns null or an object of summed token balance per address + */ +async function subgraphRequestsToVariousServices( + hostedSubgraphUrl: string, + stuidoDevSubgraphUrl: string | null, + studioProdSubgraphUrl: string | null, + builtQuery: any +): Promise { + try { + // first try with hosted service + return subgraphRequest(hostedSubgraphUrl, builtQuery); + } catch (error) { + // console.log('Failed to get data from hostedSubgraphUrl'); + } + + // then try with studio dev service + if (stuidoDevSubgraphUrl) { + try { + return subgraphRequest(stuidoDevSubgraphUrl, builtQuery); + } catch (error) { + // console.log('Failed to get data from stuidoDevSubgraphUrl'); + } + } + + // then try with studio prod service + if (studioProdSubgraphUrl) { + try { + return subgraphRequest(studioProdSubgraphUrl, builtQuery); + } catch (error) { + // console.log('Failed to get data from studioProdSubgraphUrl'); + } + } + return null; +} + +/** + * Get block number from Gnosis chain at a given timestamp. + * The timestamp of the returned block should be no-bigger than the desired timestamp + * @param timestamp number of timestamp + * @param fallbackBlockNumber fallback block number on Gnosis chain, in case no result gets returned. + * @returns a number + */ +async function getGnosisBlockNumber( + timestamp: number, + fallbackBlockNumber: number +): Promise { + const query = { + blocks: { + __args: { + first: 1, + orderBy: 'number', + orderDirection: 'desc', + where: { + timestamp_lte: timestamp + } + }, + number: true, + timestamp: true + } + }; + + // query from subgraph + const data = await subgraphRequestsToVariousServices( + XDAI_BLOCK_HOSTED_SUBGRAPH_URL, + null, + null, + query + ); + return !data ? fallbackBlockNumber : Number(data.blocks[0].number); +} + +async function stakingSubgraphQuery( + hostedSubgraphUrl: string, + stuidoDevSubgraphUrl: string | null, + studioProdSubgraphUrl: string | null, + seasonNumber: string, + addresses: string[], + blockNumber: number, + snapshot: number | string +): Promise<{ [propName: string]: BigNumber }> { + const query = { + stakingParticipations: { + __args: { + first: QUERY_LIMIT, + where: { + account_: { + id_in: addresses.map((adr) => adr.toLowerCase()) + }, + stakingSeason_: { + seasonNumber + } + } + }, + account: { + id: true + }, + actualLockedTokenAmount: true, + airdropLockedTokenAmount: true, + unclaimedRewards: true, + virtualLockedTokenAmount: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + query.stakingParticipations.__args.block = { number: blockNumber }; + } + + // query from subgraph + const data = await subgraphRequestsToVariousServices( + hostedSubgraphUrl, + stuidoDevSubgraphUrl, + studioProdSubgraphUrl, + query + ); + + // map result (data.accounts) to addresses + const entries = !data + ? addresses.map((addr) => [addr, BigNumber.from('0')]) + : data.stakingParticipations.map((d) => [ + d.account.id, + BigNumber.from(d.actualLockedTokenAmount) + .add(BigNumber.from(d.airdropLockedTokenAmount)) + .add(BigNumber.from(d.virtualLockedTokenAmount)) + .add(BigNumber.from(d.unclaimedRewards)) + ]); + return Object.fromEntries(entries); +} + +async function hoprTotalOnGnosisSubgraphQuery( + hostedSubgraphUrl: string, + stuidoDevSubgraphUrl: string | null, + studioProdSubgraphUrl: string | null, + addresses: string[], + blockNumber: number, + snapshot: number | string +): Promise<{ [propName: string]: BigNumber }> { + const query = { + accounts: { + __args: { + first: QUERY_LIMIT, + where: { + id_in: addresses.map((adr) => adr.toLowerCase()) + } + }, + id: true, + totalBalance: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + query.accounts.__args.block = { number: blockNumber }; + } + + // query from subgraph + const data = await subgraphRequestsToVariousServices( + hostedSubgraphUrl, + stuidoDevSubgraphUrl, + studioProdSubgraphUrl, + query + ); + + // map result (data.accounts) to addresses + const entries = !data + ? addresses.map((addr) => [addr, BigNumber.from('0')]) + : data.accounts.map((d) => [ + d.id, + parseUnits(d.totalBalance.toString(), 18) + ]); + return Object.fromEntries(entries); +} + +/** + * Calculate the final score + * @param shouldIncludeMainnetValue if the mainnet token balance should be taken into account + * @param subgraphScore Sum of score from two subgraphs + * @param mainnetTokenResults Multicall returned result, this should contain token balances in an array + * @param index index of the current address + * @returns squared root of the sum of subgraph scores and token amounts if the sum is above 1, if not, returns 0. + */ +function calculateScore( + shouldIncludeMainnetValue: boolean, + subgraphScore: BigNumber, + mainnetTokenResults: BigNumber[], + index: number +) { + const summedAmount = shouldIncludeMainnetValue + ? subgraphScore.add(BigNumber.from(mainnetTokenResults[index].toString())) + : subgraphScore; + const summedAmountInEth = parseFloat(formatUnits(summedAmount, 18)); + if (summedAmountInEth > 1) { + return Math.sqrt(summedAmountInEth); + } else { + return 0; + } +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + // Network must be Ethereum Mainnet + if (network !== '1') { + throw new Error('Wrong network! Please use mainnet.'); + } + + // Get the block on mainnet and find the corresponding time on Gnosis chain) + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + // get token balance (if applicable) and block + const [resHoprOnMainnet, block] = await Promise.all([ + options.useHoprOnMainnet + ? multicall( + network, + provider, + tokenAbi, + addresses.map((address: any) => [ + options.tokenAddress, + 'balanceOf', + [address] + ]), + { blockTag } + ) + : [], + provider.getBlock(blockTag) + ]); + + // get the block number for subgraph query + const subgraphBlock = await getGnosisBlockNumber( + block.timestamp, + options.fallbackGnosisBlock + ); + // console.log( + // `Block on mainnet: ${block.number} and on Gnosis ${subgraphBlock}` + // ); + + // trim addresses to sub of "QUERY_LIMIT" addresses. + const addressSubsets = Array.apply( + null, + Array(Math.ceil(addresses.length / QUERY_LIMIT)) + ).map((_e, i) => addresses.slice(i * QUERY_LIMIT, (i + 1) * QUERY_LIMIT)); + + let returnedFromSubgraphStake; + if (options.useStake) { + // construct URLs for stake season + const hostedAllSeasonSubgraphUrl: string = getHostedSubgraphUrl( + options.subgraphHostedAccountName ?? DEFAULT_HOPR_HOSTED_ACCOUNT_NAME, + options.subgraphHostedAllSeasonSubgraphName ?? + DEFAULT_HOPR_STAKING_ALL_SEASONS_HOSTED_SUBGRAPH_NAME + ); + const stuidoDevAllSeasonSubgraphUrl = getStudioDevSubgraphUrl( + options.subgraphStudioDevAccountId, + options.subgraphStudioDevAllSeasonSubgraphName, + options.subgraphStudioDevAllSeasonVersion + ); + const studioProdAllSeasonSubgraphUrl = getStudioProdSubgraphUrl( + options.subgraphStudioProdQueryApiKey, + options.subgraphStudioProdAllSeasonQueryId + ); + // get subgraph result for stake season + returnedFromSubgraphStake = await Promise.all( + addressSubsets.map((subset) => + stakingSubgraphQuery( + hostedAllSeasonSubgraphUrl, + stuidoDevAllSeasonSubgraphUrl, + studioProdAllSeasonSubgraphUrl, + options.season.toString(), + subset, + subgraphBlock, + snapshot + ) + ) + ); + } + + let returnedFromSubgraphOnGnosis; + if (options.useHoprOnGnosis) { + // construct URLs for HOPR on Gnosis + const hostedHoprOnGnosisSubgraphUrl: string = getHostedSubgraphUrl( + options.subgraphHostedAccountName ?? DEFAULT_HOPR_HOSTED_ACCOUNT_NAME, + options.subgraphHostedTokenOnGnosisSubgraphName ?? + DEFAULT_HOPR_BALANCE_ON_GNOSIS_HOSTED_SUBGRAPH_NAME + ); + const stuidoDevHoprOnGnosisSubgraphUrl = getStudioDevSubgraphUrl( + options.subgraphStudioDevAccountId, + options.subgraphStudioDevHoprOnGnosisSubgraphName, + options.subgraphStudioDevHoprOnGnosisVersion + ); + const studioProdHoprOnGnosisSubgraphUrl = getStudioProdSubgraphUrl( + options.subgraphStudioProdQueryApiKey, + options.subgraphStudioProdHoprOnGnosisQueryId + ); + // get subgraph result for hopr on gnosis + returnedFromSubgraphOnGnosis = await Promise.all( + addressSubsets.map((subset) => + hoprTotalOnGnosisSubgraphQuery( + hostedHoprOnGnosisSubgraphUrl, + stuidoDevHoprOnGnosisSubgraphUrl, + studioProdHoprOnGnosisSubgraphUrl, + subset, + subgraphBlock, + snapshot + ) + ) + ); + } + + // get and parse balance from subgraph + const subgraphStakeBalanceStake = Object.assign( + {}, + ...returnedFromSubgraphStake + ); + const subgraphStakeBalanceOnGnosis = Object.assign( + {}, + ...returnedFromSubgraphOnGnosis + ); + + const subgraphScore: BigNumber[] = addresses.map((address) => + ( + subgraphStakeBalanceStake[address.toLowerCase()] ?? BigNumber.from('0') + ).add( + subgraphStakeBalanceOnGnosis[address.toLowerCase()] ?? BigNumber.from('0') + ) + ); + + // return sqrt(subgraph score + hopr on mainet score) + return Object.fromEntries( + addresses.map((adr, i) => [ + adr, + calculateScore( + options.useHoprOnMainnet, + subgraphScore[i], + resHoprOnMainnet, + i + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-by-season/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-by-season/examples.json new file mode 100644 index 00000000..73b30b8d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-by-season/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Stakes and unclaimed rewards from HOPR Stake program Season x", + "strategy": { + "name": "hopr-staking-by-season", + "params": { + "tokenAddress": "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da", + "symbol": "HOPR", + "variant": "staking", + "season": 3 + } + }, + "network": "100", + "addresses": [ + "0x04BBB7eA18EA570aE47d4489991645E4E49bBf37", + "0x2aF80738aC01e7883d11c912dFe8322C129ae5C5", + "0x0bb43EFc1a613658177D8f67CcF9CFFD8B25b906", + "0x53e85186ebF5A7d4BD06324F7b9D8B3623EF0307", + "0x2DCDB99930E279f1e9Ad11F491163051432542A0", + "0x4326990033eCd87A5444383Cf8c715E696301910", + "0xEd6a59A7C1D5a88b7cb5eb877A7A6078A7e801C7", + "0xeFC05B0D0C8bE8D4Cb3a220ef582E9f7E6FBCd00", + "0xC7B169b108c5e75991C520AEA97140534291C81D", + "0x04Be52434EB64aDdF373137310551ac42013677c", + "0xBE8C93a8C18AF63aAB449994AFAc13E71240ccC4", + "0xf813773eBDD4759c1B780d745081f046A5B776fB", + "0x7F26C34Ed10bF66602009231bBFF24f2f84e9270", + "0x4abd7276C53279b3aBFFF2B5D8A47c0AFc84833B", + "0x3e1A12a6019ee26418F22B656926fE38F5e58C5f", + "0x7A27A4D91231aCB3282b410Cc784517B417FA0DA" + ], + "snapshot": 21953000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-by-season/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-by-season/index.ts new file mode 100644 index 00000000..11770ede --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-by-season/index.ts @@ -0,0 +1,124 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest } from '../../utils'; + +export const author = 'QYuQianchen'; +export const version = '0.1.0'; + +const XDAI_BLOCK_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/1hive/xdai-blocks'; +const HOPR_STAKING_SUBGRAPH_ROOT = + 'https://api.thegraph.com/subgraphs/name/hoprnet/'; +const DEFAULT_HOPR_STAKING_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/hoprnet/staking-season3'; +const LIMIT = 1000; // 1000 addresses per query in Subgraph + +function buildHoprStakeSubgraphUrl(variant: string, seasonNumber: number) { + if (variant === 'staking' || variant === 'stake') { + return `${HOPR_STAKING_SUBGRAPH_ROOT}${variant}-season${seasonNumber}`; + } + // element of subgraph url does not respect the + return DEFAULT_HOPR_STAKING_SUBGRAPH_URL; +} + +async function getXdaiBlockNumber(timestamp: number): Promise { + const query = { + blocks: { + __args: { + first: 1, + orderBy: 'number', + orderDirection: 'desc', + where: { + timestamp_lte: timestamp + } + }, + number: true, + timestamp: true + } + }; + const data = await subgraphRequest(XDAI_BLOCK_SUBGRAPH_URL, query); + return Number(data.blocks[0].number); +} + +async function stakingSubgraphQuery( + subgraphUrl: string, + addresses: string[], + blockNumber: number, + snapshot: number | string +): Promise<{ [propName: string]: BigNumber }> { + const query = { + accounts: { + __args: { + first: LIMIT, + where: { + id_in: addresses.map((adr) => adr.toLowerCase()) + } + }, + id: true, + actualStake: true, + unclaimedRewards: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + query.accounts.__args.block = { number: blockNumber }; + } + + const data = await subgraphRequest(subgraphUrl, query); + // map result (data.accounts) to addresses + const entries = data.accounts.map((d) => [ + d.id, + BigNumber.from(d.actualStake).add(BigNumber.from(d.unclaimedRewards)) + ]); + return Object.fromEntries(entries); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const isXdai = network === '100'; // either xDAI or ETH mainnet + const block = await provider.getBlock(blockTag); + console.log(block.number); + + // get the block number for subgraph query + const subgraphBlock = isXdai + ? block.number + : await getXdaiBlockNumber(block.timestamp); + + const stakingSubgraphUrl = buildHoprStakeSubgraphUrl( + options.variant ?? 'staking', + options.season ?? 3 + ); + + // trim addresses to sub of "LIMIT" addresses. + const addressSubsets = Array.apply( + null, + Array(Math.ceil(addresses.length / LIMIT)) + ).map((_e, i) => addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + + const returnedFromSubgraph = await Promise.all( + addressSubsets.map((subset) => + stakingSubgraphQuery(stakingSubgraphUrl, subset, subgraphBlock, snapshot) + ) + ); + + // get and parse balance from subgraph + const subgraphBalance = Object.assign({}, ...returnedFromSubgraph); + const subgraphScore = addresses.map( + (address) => subgraphBalance[address.toLowerCase()] ?? 0 + ); + + return Object.fromEntries( + addresses.map((adr, i) => [ + adr, + parseFloat(formatUnits(subgraphScore[i], 18)) // subgraph balance + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-s2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-s2/examples.json new file mode 100644 index 00000000..996f9a1a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-s2/examples.json @@ -0,0 +1,32 @@ +[ + { + "name": "Stakes and unclaimed rewards from HOPR Stake program Season 2", + "strategy": { + "name": "hopr-staking-s2", + "params": { + "tokenAddress": "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da", + "symbol": "HOPR" + } + }, + "network": "100", + "addresses": [ + "0x04BBB7eA18EA570aE47d4489991645E4E49bBf37", + "0x2aF80738aC01e7883d11c912dFe8322C129ae5C5", + "0x0bb43EFc1a613658177D8f67CcF9CFFD8B25b906", + "0x53e85186ebF5A7d4BD06324F7b9D8B3623EF0307", + "0x2DCDB99930E279f1e9Ad11F491163051432542A0", + "0x4326990033eCd87A5444383Cf8c715E696301910", + "0xEd6a59A7C1D5a88b7cb5eb877A7A6078A7e801C7", + "0xeFC05B0D0C8bE8D4Cb3a220ef582E9f7E6FBCd00", + "0xC7B169b108c5e75991C520AEA97140534291C81D", + "0x04Be52434EB64aDdF373137310551ac42013677c", + "0xBE8C93a8C18AF63aAB449994AFAc13E71240ccC4", + "0xf813773eBDD4759c1B780d745081f046A5B776fB", + "0x7F26C34Ed10bF66602009231bBFF24f2f84e9270", + "0x4abd7276C53279b3aBFFF2B5D8A47c0AFc84833B", + "0x3e1A12a6019ee26418F22B656926fE38F5e58C5f", + "0x7A27A4D91231aCB3282b410Cc784517B417FA0DA" + ], + "snapshot": 21138069 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-s2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-s2/index.ts new file mode 100644 index 00000000..c89576f5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking-s2/index.ts @@ -0,0 +1,107 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest } from '../../utils'; + +export const author = 'QYuQianchen'; +export const version = '0.1.0'; + +const XDAI_BLOCK_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/1hive/xdai-blocks'; +const HOPR_STAKING_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/hoprnet/staking-season2'; +const LIMIT = 1000; // 1000 addresses per query in Subgraph + +async function getXdaiBlockNumber(timestamp: number): Promise { + const query = { + blocks: { + __args: { + first: 1, + orderBy: 'number', + orderDirection: 'desc', + where: { + timestamp_lte: timestamp + } + }, + number: true, + timestamp: true + } + }; + const data = await subgraphRequest(XDAI_BLOCK_SUBGRAPH_URL, query); + return Number(data.blocks[0].number); +} + +async function stakingSubgraphQuery( + addresses: string[], + blockNumber: number, + snapshot: number | string +): Promise<{ [propName: string]: BigNumber }> { + const query = { + accounts: { + __args: { + first: LIMIT, + where: { + id_in: addresses.map((adr) => adr.toLowerCase()) + } + }, + id: true, + actualStake: true, + unclaimedRewards: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + query.accounts.__args.block = { number: blockNumber }; + } + const data = await subgraphRequest(HOPR_STAKING_SUBGRAPH_URL, query); + // map result (data.accounts) to addresses + const entries = data.accounts.map((d) => [ + d.id, + BigNumber.from(d.actualStake).add(BigNumber.from(d.unclaimedRewards)) + ]); + return Object.fromEntries(entries); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const isXdai = network === '100'; // either xDAI or ETH mainnet + const block = await provider.getBlock(blockTag); + console.log(block.number); + + // get the block number for subgraph query + const subgraphBlock = isXdai + ? block.number + : await getXdaiBlockNumber(block.timestamp); + + // trim addresses to sub of "LIMIT" addresses. + const addressSubsets = Array.apply( + null, + Array(Math.ceil(addresses.length / LIMIT)) + ).map((_e, i) => addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + + const returnedFromSubgraph = await Promise.all( + addressSubsets.map((subset) => + stakingSubgraphQuery(subset, subgraphBlock, snapshot) + ) + ); + + // get and parse balance from subgraph + const subgraphBalance = Object.assign({}, ...returnedFromSubgraph); + const subgraphScore = addresses.map( + (address) => subgraphBalance[address.toLowerCase()] ?? 0 + ); + + return Object.fromEntries( + addresses.map((adr, i) => [ + adr, + parseFloat(formatUnits(subgraphScore[i], 18)) // subgraph balance + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking/examples.json new file mode 100644 index 00000000..2a88e025 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking/examples.json @@ -0,0 +1,61 @@ +[ + { + "name": "Stakes and unclaimed rewards from HOPR Stake program", + "strategy": { + "name": "hopr-staking", + "params": { + "tokenAddress": "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da", + "symbol": "HOPR" + } + }, + "network": "1", + "addresses": [ + "0x04BBB7eA18EA570aE47d4489991645E4E49bBf37", + "0x2aF80738aC01e7883d11c912dFe8322C129ae5C5", + "0x0bb43EFc1a613658177D8f67CcF9CFFD8B25b906", + "0x53e85186ebF5A7d4BD06324F7b9D8B3623EF0307", + "0x2DCDB99930E279f1e9Ad11F491163051432542A0", + "0x4326990033eCd87A5444383Cf8c715E696301910", + "0xEd6a59A7C1D5a88b7cb5eb877A7A6078A7e801C7", + "0xeFC05B0D0C8bE8D4Cb3a220ef582E9f7E6FBCd00", + "0xC7B169b108c5e75991C520AEA97140534291C81D", + "0x04Be52434EB64aDdF373137310551ac42013677c", + "0xBE8C93a8C18AF63aAB449994AFAc13E71240ccC4", + "0xf813773eBDD4759c1B780d745081f046A5B776fB", + "0x7F26C34Ed10bF66602009231bBFF24f2f84e9270", + "0x4abd7276C53279b3aBFFF2B5D8A47c0AFc84833B", + "0x3e1A12a6019ee26418F22B656926fE38F5e58C5f", + "0x7A27A4D91231aCB3282b410Cc784517B417FA0DA" + ], + "snapshot": 13269966 + }, + { + "name": "Stakes and unclaimed rewards from HOPR Stake program", + "strategy": { + "name": "hopr-staking", + "params": { + "tokenAddress": "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da" + } + }, + "network": "100", + "addresses": [ + "0x04BBB7eA18EA570aE47d4489991645E4E49bBf37", + "0x2aF80738aC01e7883d11c912dFe8322C129ae5C5", + "0x0bb43EFc1a613658177D8f67CcF9CFFD8B25b906", + "0x53e85186ebF5A7d4BD06324F7b9D8B3623EF0307", + "0x2DCDB99930E279f1e9Ad11F491163051432542A0", + "0x4326990033eCd87A5444383Cf8c715E696301910", + "0xEd6a59A7C1D5a88b7cb5eb877A7A6078A7e801C7", + "0xeFC05B0D0C8bE8D4Cb3a220ef582E9f7E6FBCd00", + "0xC7B169b108c5e75991C520AEA97140534291C81D", + "0x04Be52434EB64aDdF373137310551ac42013677c", + "0xBE8C93a8C18AF63aAB449994AFAc13E71240ccC4", + "0xf813773eBDD4759c1B780d745081f046A5B776fB", + "0x7F26C34Ed10bF66602009231bBFF24f2f84e9270", + "0x4abd7276C53279b3aBFFF2B5D8A47c0AFc84833B", + "0x3e1A12a6019ee26418F22B656926fE38F5e58C5f", + "0x7A27A4D91231aCB3282b410Cc784517B417FA0DA" + ], + "snapshot": 18200908 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking/index.ts new file mode 100644 index 00000000..33f63055 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-staking/index.ts @@ -0,0 +1,104 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest } from '../../utils'; + +export const author = 'QYuQianchen'; +export const version = '0.1.0'; + +const XDAI_BLOCK_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/1hive/xdai-blocks'; +const HOPR_STAKING_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/hoprnet/hopr-staking-program'; +const LIMIT = 1000; // 1000 addresses per query in Subgraph + +async function getXdaiBlockNumber(timestamp: number): Promise { + const query = { + blocks: { + __args: { + first: 1, + orderBy: 'number', + orderDirection: 'desc', + where: { + timestamp_lte: timestamp + } + }, + number: true, + timestamp: true + } + }; + const data = await subgraphRequest(XDAI_BLOCK_SUBGRAPH_URL, query); + return Number(data.blocks[0].number); +} + +async function stakingSubgraphQuery( + addresses: string[], + blockNumber: number +): Promise<{ [propName: string]: BigNumber }> { + const query = { + accounts: { + __args: { + first: LIMIT, + block: { + number: blockNumber + }, + where: { + id_in: addresses.map((adr) => adr.toLowerCase()) + } + }, + id: true, + actualStake: true, + virtualStake: true, + unclaimedRewards: true + } + }; + const data = await subgraphRequest(HOPR_STAKING_SUBGRAPH_URL, query); + // map result (data.accounts) to addresses + const entries = data.accounts.map((d) => [ + d.id, + BigNumber.from(d.actualStake).add( + BigNumber.from(d.virtualStake).add(BigNumber.from(d.unclaimedRewards)) + ) + ]); + return Object.fromEntries(entries); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const isXdai = network === '100'; // either xDAI or ETH mainnet + const [block] = await Promise.all([provider.getBlock(blockTag)]); + + // get the block number for subgraph query + const subgraphBlock = isXdai + ? block.number + : await getXdaiBlockNumber(block.timestamp); + + // trim addresses to sub of "LIMIT" addresses. + const addressSubsets = Array.apply( + null, + Array(Math.ceil(addresses.length / LIMIT)) + ).map((_e, i) => addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + + const returnedFromSubgraph = await Promise.all( + addressSubsets.map((subset) => stakingSubgraphQuery(subset, subgraphBlock)) + ); + + // get and parse balance from subgraph + const subgraphBalance = Object.assign({}, ...returnedFromSubgraph); + const subgraphScore = addresses.map( + (address) => subgraphBalance[address.toLowerCase()] ?? 0 + ); + + return Object.fromEntries( + addresses.map((adr, i) => [ + adr, + parseFloat(formatUnits(subgraphScore[i], 18)) // subgraph balance + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-uni-lp-farm/examples.json b/Implementations/API/backend/utils/snapshot/strategies/hopr-uni-lp-farm/examples.json new file mode 100644 index 00000000..b15c5a66 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-uni-lp-farm/examples.json @@ -0,0 +1,38 @@ +[ + { + "name": "Get nominal HOPR tokens from HOPR LP token, incl. those staked in the HoprFarm", + "strategy": { + "name": "hopr-uni-lp-farm", + "params": { + "symbol": "HOPR", + "farmAddress": "0x2fc0e2cfe5d6ea300d555e5907319a5f7e09884f", + "uniPoolAddress": "0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d", + "hoprAddress": "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da", + "daiAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", + "farmDeployBlock": 12094980, + "xHoprAddress": "0xd057604a14982fe8d88c5fc25aac3267ea142a08", + "wxHoprAddress": "0xd4fdec44db9d44b8f2b6d529620f9c0c7066a2c1" + } + }, + "network": "1", + "addresses": [ + "0x04BBB7eA18EA570aE47d4489991645E4E49bBf37", + "0x2aF80738aC01e7883d11c912dFe8322C129ae5C5", + "0x0bb43EFc1a613658177D8f67CcF9CFFD8B25b906", + "0x53e85186ebF5A7d4BD06324F7b9D8B3623EF0307", + "0x2DCDB99930E279f1e9Ad11F491163051432542A0", + "0x4326990033eCd87A5444383Cf8c715E696301910", + "0xEd6a59A7C1D5a88b7cb5eb877A7A6078A7e801C7", + "0xeFC05B0D0C8bE8D4Cb3a220ef582E9f7E6FBCd00", + "0xC7B169b108c5e75991C520AEA97140534291C81D", + "0x04Be52434EB64aDdF373137310551ac42013677c", + "0xBE8C93a8C18AF63aAB449994AFAc13E71240ccC4", + "0xf813773eBDD4759c1B780d745081f046A5B776fB", + "0x7F26C34Ed10bF66602009231bBFF24f2f84e9270", + "0x4abd7276C53279b3aBFFF2B5D8A47c0AFc84833B", + "0x3e1A12a6019ee26418F22B656926fE38F5e58C5f", + "0x7A27A4D91231aCB3282b410Cc784517B417FA0DA" + ], + "snapshot": 12509840 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/hopr-uni-lp-farm/index.ts b/Implementations/API/backend/utils/snapshot/strategies/hopr-uni-lp-farm/index.ts new file mode 100644 index 00000000..fd334a1c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/hopr-uni-lp-farm/index.ts @@ -0,0 +1,199 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall, subgraphRequest } from '../../utils'; + +export const author = 'hoprnet'; +export const version = '0.1.0'; + +const tokenAndPoolAbi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'liquidityProviders', + outputs: [ + { + internalType: 'uint256', + name: 'claimedUntil', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'currentBalance', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +const XDAI_BLOCK_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/1hive/xdai-blocks'; +const HOPR_XDAI_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/hoprnet/hopr-on-xdai'; +const LIMIT = 1000; // 1000 addresses per query in Subgraph + +async function getXdaiBlockNumber(timestamp: number): Promise { + const query = { + blocks: { + __args: { + first: 1, + orderBy: 'number', + orderDirection: 'desc', + where: { + timestamp_lte: timestamp + } + }, + number: true, + timestamp: true + } + }; + const data = await subgraphRequest(XDAI_BLOCK_SUBGRAPH_URL, query); + return Number(data.blocks[0].number); +} + +async function xHoprSubgraphQuery( + addresses: string[], + blockNumber: number +): Promise<{ [propName: string]: number }> { + const query = { + accounts: { + __args: { + first: LIMIT, + block: { + number: blockNumber + }, + where: { + id_in: addresses.map((adr) => adr.toLowerCase()) + } + }, + id: true, + totalBalance: true + } + }; + const data = await subgraphRequest(HOPR_XDAI_SUBGRAPH_URL, query); + // map result (data.accounts) to addresses + const entries = data.accounts.map((d) => [d.id, Number(d.totalBalance)]); + return Object.fromEntries(entries); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const [res, block] = await Promise.all([ + multicall( + network, + provider, + tokenAndPoolAbi, + [ + [options.hoprAddress, 'balanceOf', [options.uniPoolAddress]], + [options.uniPoolAddress, 'totalSupply', []] + ] + .concat( + addresses.map((address: any) => [ + options.uniPoolAddress, + 'balanceOf', + [address] + ]) + ) + .concat( + blockTag >= options.farmDeployBlock || blockTag === 'latest' + ? addresses.map((address: any) => [ + options.farmAddress, + 'liquidityProviders', + [address] + ]) + : [] + ), + { blockTag } + ), + provider.getBlock(blockTag) + ]); + + const hoprBalanceOfPool = res[0]; + const poolTotalSupply = res[1]; + const response: BigNumber[] = + blockTag >= options.farmDeployBlock || blockTag === 'latest' + ? res + .slice(2, 2 + addresses.length) + .map((r, i) => + (r[0] as BigNumber).add( + res[2 + i + addresses.length][1] as BigNumber + ) + ) + : res.slice(2).map((r) => r[0] as BigNumber); + + // get block timestamp to search on xDai subgraph + const snapshotXdaiBlock = await getXdaiBlockNumber(block.timestamp); + // trim addresses to sub of "LIMIT" addresses. + const addressSubsets = Array.apply( + null, + Array(Math.ceil(addresses.length / LIMIT)) + ).map((_e, i) => addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + const returnedFromSubgraph = await Promise.all( + addressSubsets.map((subset) => + xHoprSubgraphQuery(subset, snapshotXdaiBlock) + ) + ); + // get and parse xHOPR and wxHOPR balance + const hoprOnXdaiBalance = Object.assign({}, ...returnedFromSubgraph); + const hoprOnXdaiScore = addresses.map( + (address) => hoprOnXdaiBalance[address.toLowerCase()] ?? 0 + ); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], // LP token amount * pool's HOPR token balnce / total pool supply + parseFloat( + formatUnits(value.mul(hoprBalanceOfPool[0]).div(poolTotalSupply[0]), 18) + ) + hoprOnXdaiScore[i] // xHOPR + wxHOPR balance + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ilv/README.md b/Implementations/API/backend/utils/snapshot/strategies/ilv/README.md new file mode 100644 index 00000000..7c78484f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ilv/README.md @@ -0,0 +1,15 @@ +# Quadratic Debt Percentage Strategy + +Calculates the quadratic weighting of voters, based on their token holding. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["ilv"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ilv/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ilv/index.ts new file mode 100644 index 00000000..401bad98 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ilv/index.ts @@ -0,0 +1,25 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'andytcf'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address) => [address[0], Math.sqrt(address[1])]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/immutable-x/examples.json b/Implementations/API/backend/utils/snapshot/strategies/immutable-x/examples.json new file mode 100644 index 00000000..dd797aae --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/immutable-x/examples.json @@ -0,0 +1,48 @@ +[ + { + "name": "Immutable X (L1 + L2) vote weighting strategy", + "strategy": { + "name": "immutable-x", + "params": { + "symbol": "imx", + "address": "0x26b81657e09d3a6a18ff1c6d776fd09f4bb9ee80", + "decimals": 18, + "pageSize": 100 + } + }, + "network": "3", + "addresses": [ + "0x6f78046c7441c9ae8063f78af53ef29b47871a3e", + "0xed6f74e896e6853959a02f27634d4ff0b58286ff", + "0x9f797c209fa19627cb4cb52232b5a615dd04f3f8", + "0x60bdc16bd3b001a4c7fb3ef598f4b075ad586d5d", + "0xc8714f989ce817e5d21349888077aa5db4a9bcf6", + "0x49632a466ee268ebb756fc4d963af98143f259d0", + "0x6b32a7add2673a3bc84bd7c5f3bccfb3e96fd611", + "0xca65c6c80562163d49bd12690c2c7ae7d005c6fb", + "0x2d7e665acb06d5907b0b169bbea61ddf5e206b95", + "0x2915f444dca40cf23e0050e12e30e35d864fb451", + "0x973eb49bbdd2754609ba5f3da0b7836948636e62", + "0xdb0d4032c8e03f946477eca6336fac87d1e8ef2f", + "0xed6f74e896e6853959a02f27634d4ff0b58286ff", + "0xdb0d4032c8e03f946477eca6336fac87d1e8ef2f", + "0xca65c6c80562163d49bd12690c2c7ae7d005c6fb", + "0xc8714f989ce817e5d21349888077aa5db4a9bcf6", + "0x9f797c209fa19627cb4cb52232b5a615dd04f3f8", + "0x973eb49bbdd2754609ba5f3da0b7836948636e62", + "0x6f78046c7441c9ae8063f78af53ef29b47871a3e", + "0x6b32a7add2673a3bc84bd7c5f3bccfb3e96fd611", + "0x60bdc16bd3b001a4c7fb3ef598f4b075ad586d5d", + "0x49632a466ee268ebb756fc4d963af98143f259d0", + "0x2d7e665acb06d5907b0b169bbea61ddf5e206b95", + "0x2915f444dca40cf23e0050e12e30e35d864fb451", + "0xed6f74e896e6853959a02f27634d4ff0b58286ff", + "0xdb0d4032c8e03f946477eca6336fac87d1e8ef2f", + "0xca65c6c80562163d49bd12690c2c7ae7d005c6fb", + "0xc8714f989ce817e5d21349888077aa5db4a9bcf6", + "0x9f797c209fa19627cb4cb52232b5a615dd04f3f8", + "0x973eb49bbdd2754609ba5f3da0b7836948636e62" + ], + "snapshot": 11850372 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/immutable-x/index.ts b/Implementations/API/backend/utils/snapshot/strategies/immutable-x/index.ts new file mode 100644 index 00000000..1cfdd4e6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/immutable-x/index.ts @@ -0,0 +1,158 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; + +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import fetch from 'cross-fetch'; +import { formatUnits } from '@ethersproject/units'; +import { strategy as getL1Balances } from '../erc20-balance-of'; + +export const author = 'immutable'; +export const version = '1.0.0'; + +export const name = 'immutable-x'; + +const snapshotPath = '/v1/snapshots/balances'; + +const networkMapping = { + 1: 'https://api.x.immutable.com', + 3: 'https://api.ropsten.x.immutable.com' +}; + +const defaultPageSize = 1000; + +interface Response { + records: Score[]; + cursor: string; +} + +interface Score { + ether_key: string; + balance: string; +} + +interface Options { + address: string; + decimals: number; + pageSize?: number; +} + +export async function strategy( + _space: unknown, + network: string, + provider: StaticJsonRpcProvider, + addresses: string[], + options: Options, + block: number | string = 'latest' +): Promise> { + return combineBalanceScores([ + await getL2Balances(network, options, addresses, block), + await getL1Balances(null, network, provider, addresses, options, block) + ]); +} + +async function getL2Balances( + network: string, + options: Options, + addresses: string[], + block: number | string +): Promise> { + const records: Record = {}; + + // Sanitize pageSize + options.pageSize = options.pageSize || defaultPageSize; + + // Loop variables + let cursor = '', + receivedLen = 0; + + // Until all records are returned + // This loop handles both: + // 1. server-side paginated requests for all addresses available; and + // 2. client-side paginated requests (addresses in json body of requests). + // There are separate completion conditions for 1. and 2. + while (true) { + // Build URL + const apiUrl = buildURL(network, options, block, cursor); + + // Send request + const response = await fetch(apiUrl, { + method: 'POST', + body: JSON.stringify({ + ether_keys: addresses.slice(receivedLen, receivedLen + options.pageSize) + }), + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }); + + // Decode response + const resJson = await response.json(); + + // Empty response indicates end of results + // for requests without specified address in json body + const respLen = (resJson as Response).records.length; + if (respLen === 0) { + break; + } + // Store result + Object.assign(records, mapL2Response(resJson, options)); + // Iterate + receivedLen += respLen; + // This indicates we have received all results for + // the addresses we asked for + if (receivedLen >= addresses.length) { + break; + } + // For paginated requests, continue w/ cursor + cursor = resJson.cursor; + } + return records; +} + +function buildURL( + network: string, + options: Options, + block: number | string, + cursor?: string +): string { + let apiUrl = networkMapping[network] + snapshotPath; + apiUrl += '/' + options.address.toLowerCase(); + apiUrl += `?page_size=${options.pageSize}`; + apiUrl += typeof block === 'number' ? `&block=${block}` : ''; + apiUrl += cursor || cursor != '' ? `&cursor=${cursor}` : ''; + return apiUrl; +} + +function mapL2Response( + data: Response, + options: Options +): Record { + return Object.fromEntries( + data.records.map((value: Score) => [ + value.ether_key, + formatBalance(value.balance, options.decimals) + ]) + ); +} + +function formatBalance( + balance: BigNumber | string, + decimals: BigNumberish +): number { + return parseFloat(formatUnits(balance, decimals)); +} + +function combineBalanceScores( + records: Record[] +): Record { + return records.reduce((aggScore, currScore) => { + for (const [address, balance] of Object.entries(currScore)) { + if (!aggScore[address]) { + aggScore[address] = balance; + } else { + aggScore[address] += balance; // sum(L1, L2) + } + } + return aggScore; + }, {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/README.md b/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/README.md new file mode 100644 index 00000000..904cd2c6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/README.md @@ -0,0 +1,5 @@ +## Impossible Finance Governance Strategy + +This strategy makes calls to check balances of IF and IF-X LP tokens associated with input addresses. For IF-X LP tokens, the strategy is able to calculate how much IF underlying should be associated with that LP token. We are also able to assign weights to tokens from LP tokens to allow stakeholders that are aligned with our ecosystem to have more voting power. + +Note that STAX tokens and STAX-X LP tokens have no voting power with this strategy. diff --git a/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/examples.json new file mode 100644 index 00000000..ccad486a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/examples.json @@ -0,0 +1,49 @@ +[ + { + "name": "Example IF governance snapshot", + "strategy": { + "name": "impossible-finance", + "params": { + "address": "0xb0e1fc65c1a741b4662b813eb787d369b8614af1", + "symbol": "IF", + "decimals": 18, + "pairs": [ + { + "address": "0xf1f52f33aBab0629EC8657C330ad556824B844E1", + "name": "IF/WBNB pool", + "decimals": 18, + "weightNumerator": 6, + "weightDenominator": 5 + }, + { + "address": "0x68a37e14D080436B57119e379a04d32F4CA7f92d", + "name": "IF/BUSD pool", + "decimals": 18, + "weightNumerator": 6, + "weightDenominator": 5 + }, + { + "address": "0xB5ceFC854801Bc6976B76dc34e2A986556F3cd11", + "name": "IF/USDT pool", + "decimals": 18, + "weightNumerator": 6, + "weightDenominator": 5 + }, + { + "address": "0x1bfE758d6061c6A221437b2C673F27876B1E3416", + "name": "IF/DAI pool", + "decimals": 18, + "weightNumerator": 6, + "weightDenominator": 5 + } + ] + } + }, + "network": "56", + "addresses": [ + "0x85511EB52e17c014A8665ED98B9ea79d4F2E30e2", + "0x3C9584426432eb851e5689230d5cFc50659103D5" + ], + "snapshot": 8925299 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/index.ts new file mode 100644 index 00000000..47272153 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/impossible-finance/index.ts @@ -0,0 +1,111 @@ +import { formatUnits } from '@ethersproject/units'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { Multicaller } from '../../utils'; + +export const author = 'impossible-finance'; +export const version = '0.0.1'; + +const abi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + // Calculates number of LP tokens each guy owns + addresses.forEach((address: any) => { + options.pairs.forEach((pair: any, idx: number) => { + multi.call( + `lpBalance.${idx}.${address}`, + options.pairs[idx].address, + 'balanceOf', + [address] + ); + }); + }); + + // Calculates total IF locked in each pair + // Also calculates total supply of LP tokens + options.pairs.forEach((pair: any, idx: number) => { + multi.call( + `pairIFBalance.${idx}`, + options.address, // IF token address + 'balanceOf', + [pair.address] + ); + multi.call(`pairTotalSupply.${idx}`, pair.address, 'totalSupply', []); + }); + + const result = await multi.execute(); + + return Object.fromEntries( + Object.entries(score).map((address) => [ + address[0], + address[1] + + options.pairs.reduce( + (prev: number, cur: any, idx: number) => + prev + + parseFloat( + formatUnits( + result.lpBalance[idx][address[0]] + .mul(result.pairIFBalance[idx]) + .mul(options.pairs[idx].weightNumerator) + .div(result.pairTotalSupply[idx]) + .div(options.pairs[idx].weightDenominator), + options.pairs[idx].decimals + ) + ), + 0 + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/index.ts b/Implementations/API/backend/utils/snapshot/strategies/index.ts new file mode 100644 index 00000000..25e4271c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/index.ts @@ -0,0 +1,938 @@ +import { readFileSync } from 'fs'; +import path from 'path'; + +import * as ecoVotingPower from './eco-voting-power'; +import * as dpsNFTStrategy from './dps-nft-strategy'; +import * as dpsNFTStrategyNova from './dps-nft-strategy-nova'; +import * as nounsPower from './nouns-rfp-power'; +import * as erc20Votes from './erc20-votes'; +import * as erc20VotesWithOverride from './erc20-votes-with-override'; +import * as antiWhale from './anti-whale'; +import * as balancer from './balancer'; +import * as balancerErc20InternalBalanceOf from './balancer-erc20-internal-balance-of'; +import * as sunder from './sunder'; +import * as balancerSmartPool from './balancer-smart-pool'; +import * as contractCall from './contract-call'; +import * as dextfVaults from './dextf-staked-in-vaults'; +import * as dfynFarms from './dfyn-staked-in-farms'; +import * as dfynVaults from './dfyn-staked-in-vaults'; +import * as vDfynVault from './balance-in-vdfyn-vault'; +import * as ensDomainsOwned from './ens-domains-owned'; +import * as ensReverseRecord from './ens-reverse-record'; +import * as ens10kClub from './ens-10k-club'; +import * as ensAllClubDigits from './ens-all-club-digits'; +import * as governorDelegator from './governor-delegator'; +import * as erc20BalanceOf from './erc20-balance-of'; +import * as erc20BalanceOfAt from './erc20-balance-of-at'; +import * as erc20BalanceOfCoeff from './erc20-balance-of-coeff'; +import * as erc20BalanceOfFixedTotal from './erc20-balance-of-fixed-total'; +import * as erc20BalanceOfCv from './erc20-balance-of-cv'; +import * as erc20WithBalance from './erc20-with-balance'; +import * as erc20BalanceOfDelegation from './erc20-balance-of-delegation'; +import * as erc20BalanceOfWithDelegation from './erc20-balance-of-with-delegation'; +import * as erc20BalanceOfQuadraticDelegation from './erc20-balance-of-quadratic-delegation'; +import * as erc20BalanceOfWeighted from './erc20-balance-of-weighted'; +import * as ethalendBalanceOf from './ethalend-balance-of'; +import * as prepoVesting from './prepo-vesting'; +import * as mintoBalanceAll from './minto-balance-of-all'; +import * as erc20BalanceOfIndexed from './erc20-balance-of-indexed'; +import * as revest from './revest'; +import * as erc20Price from './erc20-price'; +import * as balanceOfWithMin from './balance-of-with-min'; +import * as balanceOfWithThresholds from './balance-of-with-thresholds'; +import * as balanceOfWithLinearVestingPower from './balance-of-with-linear-vesting-power'; +import * as linearVestingPower from './linear-vesting-power'; +import * as thresholds from './thresholds'; +import * as ethBalance from './eth-balance'; +import * as ethWithBalance from './eth-with-balance'; +import * as ethWalletAge from './eth-wallet-age'; +import * as multichain from './multichain'; +import * as gooddollarMultichain from './gooddollar-multichain'; +import * as makerDsChief from './maker-ds-chief'; +import * as uni from './uni'; +import * as yearnVault from './yearn-vault'; +import * as fraxFinance from './frax-finance'; +import * as moloch from './moloch'; +import * as uniswap from './uniswap'; +import * as faralandStaking from './faraland-staking'; +import * as flashstake from './flashstake'; +import * as pancake from './pancake'; +import * as synthetix from './synthetix'; +import * as aelinCouncil from './aelin-council'; +import * as synthetixQuadratic from './synthetix-quadratic'; +import * as synthetixQuadraticOne from './synthetix-quadratic_1'; +import * as synthetixQuadraticTwo from './synthetix-quadratic_2'; +import * as synthetixOne from './synthetix_1'; +import * as synthetixNonQuadratic from './synthetix-non-quadratic'; +import * as synthetixNonQuadraticOne from './synthetix-non-quadratic_1'; +import * as synthetixNonQuadraticTwo from './synthetix-non-quadratic_2'; +import * as ctoken from './ctoken'; +import * as cream from './cream'; +import * as esd from './esd'; +import * as esdDelegation from './esd-delegation'; +import * as stakedUniswap from './staked-uniswap'; +import * as piedao from './piedao'; +import * as ethReceived from './eth-received'; +import * as erc20Received from './erc20-received'; +import * as ethPhilanthropy from './eth-philanthropy'; +import * as xDaiEasyStaking from './xdai-easy-staking'; +import * as xDaiPOSDAOStaking from './xdai-posdao-staking'; +import * as xDaiStakeHolders from './xdai-stake-holders'; +import * as xDaiStakeDelegation from './xdai-stake-delegation'; +import * as defidollar from './defidollar'; +import * as aavegotchi from './aavegotchi'; +import * as aavegotchiAgip from './aavegotchi-agip'; +import * as aavegotchiAgip17 from './aavegotchi-agip-17'; +import * as mithcash from './mithcash'; +import * as dittomoney from './dittomoney'; +import * as balancerUnipool from './balancer-unipool'; +import * as sushiswap from './sushiswap'; +import * as masterchef from './masterchef'; +import * as stablexswap from './stablexswap'; +import * as stakedKeep from './staked-keep'; +import * as stakedDaomaker from './staked-daomaker'; +import * as typhoon from './typhoon'; +import * as delegation from './delegation'; +import * as delegationWithOverrides from './delegation-with-overrides'; +import * as withDelegation from './with-delegation'; +import * as ticket from './ticket'; +import * as work from './work'; +import * as ticketValidity from './ticket-validity'; +import * as validation from './validation'; +import * as opium from './opium'; +import * as ocean from './ocean-marketplace'; +import * as ocean_v4 from './ocean-marketplace-v4'; +import * as theGraphBalance from './the-graph-balance'; +import * as theGraphDelegation from './the-graph-delegation'; +import * as theGraphIndexing from './the-graph-indexing'; +import * as whitelist from './whitelist'; +import * as whitelistWeighted from './whitelist-weighted'; +import * as tokenlon from './tokenlon'; +import * as rebased from './rebased'; +import * as pobHash from './pob-hash'; +import * as totalAxionShares from './total-axion-shares'; +import * as erc1155BalanceOf from './erc1155-balance-of'; +import * as erc1155BalanceOfCv from './erc1155-balance-of-cv'; +import * as erc1155WithMultiplier from './erc1155-with-multiplier'; +import * as compLikeVotes from './comp-like-votes'; +import * as governorAlpha from './governor-alpha'; +import * as pagination from './pagination'; +import * as rulerStakedToken from './ruler-staked-token'; +import * as rulerStakedLP from './ruler-staked-lp'; +import * as xcover from './xcover'; +import * as niuStaked from './niu-staked'; +import * as mushrooms from './mushrooms'; +import * as curioCardsErc20Weighted from './curio-cards-erc20-weighted'; +import * as saffronFinance from './saffron-finance'; +import * as saffronFinanceV2 from './saffron-finance-v2'; +import * as renNodes from './ren-nodes'; +import * as reverseVotingEscrow from './reverse-voting-escrow'; +import * as multisigOwners from './multisig-owners'; +import * as trancheStaking from './tranche-staking'; +import * as pepemon from './pepemon'; +import * as erc1155AllBalancesOf from './erc1155-all-balances-of'; +import * as trancheStakingLP from './tranche-staking-lp'; +import * as masterchefPoolBalance from './masterchef-pool-balance'; +import * as masterchefPoolBalancePrice from './masterchef-pool-balance-price'; +import * as avnBalanceOfStaked from './avn-balance-of-staked'; +import * as badgeth from './badgeth'; +import * as api from './api'; +import * as apiPost from './api-post'; +import * as apiV2 from './api-v2'; +import * as xseen from './xseen'; +import * as molochAll from './moloch-all'; +import * as molochLoot from './moloch-loot'; +import * as erc721Enumerable from './erc721-enumerable'; +import * as erc721WithMultiplier from './erc721-with-multiplier'; +import * as protofiErc721TierWeighted from './protofi-erc721-tier-weighted'; +import * as erc721WithTokenId from './erc721-with-tokenid'; +import * as erc721WithTokenIdRangeWeights from './erc721-with-tokenid-range-weights'; +import * as erc721WithTokenIdRangeWeightsSimple from './erc721-with-tokenid-range-weights-simple'; +import * as erc721WithTokenIdWeighted from './erc721-with-tokenid-weighted'; +import * as erc721WithMetadata from './erc721-with-metadata'; +import * as erc721WithMetadataByOwnerOf from './erc721-with-metadata-by-ownerof'; +import * as hoprUniLpFarm from './hopr-uni-lp-farm'; +import * as erc721 from './erc721'; +import * as erc721MultiRegistry from './erc721-multi-registry'; +import * as apescape from './apescape'; +import * as liftkitchen from './liftkitchen'; +import * as coordinape from './coordinape'; +import * as decentralandEstateSize from './decentraland-estate-size'; +import * as decentralandWearableRariry from './decentraland-wearable-rarity'; +import * as decentralandRentalLessors from './decentraland-rental-lessors'; +import * as iotexBalance from './iotex-balance'; +import * as iotexStakedBalance from './iotex-staked-balance'; +import * as xrc20BalanceOf from './xrc20-balance-of'; +import * as brightid from './brightid'; +import * as inverseXINV from './inverse-xinv'; +import * as modefi from './modefi'; +import * as modefiStaking from './modefi-staking'; +import * as spookyswap from './spookyswap'; +import * as squadzPower from './squadz-power'; +import * as glide from './glide'; +import * as goldfinchVotingPower from './goldfinch-voting-power'; +import * as goldfinchMembership from './goldfinch-membership'; +import * as rnbwBalance from './rnbw-balance'; +import * as celerSgnDelegation from './celer-sgn-delegation'; +import * as balancerDelegation from './balancer-delegation'; +import * as infinityProtocolPools from './infinityprotocol-liquidity-pools'; +import * as aaveGovernancePower from './aave-governance-power'; +import * as cake from './cake'; +import * as aks from './aks'; +import * as tomyumswap from './tomyumswap'; +import * as planetFinance from './planet-finance'; +import * as planetFinancev2 from './planet-finance-v2'; +import * as impossibleFinance from './impossible-finance'; +import * as immutableX from './immutable-x'; +import * as ogn from './ogn'; +import * as oolongswap from './oolongswap'; +import * as zrxVotingPower from './zrx-voting-power'; +import * as tombFinance from './tomb-finance'; +import * as trancheStakingSLICE from './tranche-staking-slice'; +import * as unipoolSameToken from './unipool-same-token'; +import * as unipoolUniv2Lp from './unipool-univ2-lp'; +import * as unipoolXSushi from './unipool-xsushi'; +import * as poap from './poap'; +import * as poapWithWeight from './poap-with-weight'; +import * as poapWithWeightV2 from './poap-with-weight-v2'; +import * as uniswapV3 from './uniswap-v3'; +import * as uniswapV3Staking from './uniswap-v3-staking'; +import * as l2Deversifi from './l2-deversifi'; +import * as vestedDeversifi from './vested-deversifi'; +import * as biswap from './biswap'; +import * as cronaswap from './cronaswap'; +import * as honeyswap from './honeyswap'; +import * as eglVote from './egl-vote'; +import * as mcnFarm from './mcn-farm'; +import * as snowswap from './snowswap'; +import * as meebitsdao from './meebitsdao'; +import * as membership from './membership'; +import * as holdsTokens from './holds-tokens'; +import * as crucibleERC20BalanceOf from './crucible-erc20-balance-of'; +import * as erc20TokenAndLpWeighted from './erc20-token-and-lp-weighted'; +import * as crucibleERC20TokenAndLpWeighted from './crucible-erc20-token-and-lp-weighted'; +import * as hasrock from './has-rock'; +import * as flexaCapacityStaking from './flexa-capacity-staking'; +import * as sunriseGamingUniv2Lp from './sunrisegaming-univ2-lp'; +import * as sunriseGamingStaking from './sunrisegaming-staking'; +import * as sUmamiHolders from './sumami-holders'; +import * as singleStakingAutoCompoundBalanceOf from './single-staking-autocompound-balanceof'; +import * as singleStakingPoolsBalanceOf from './single-staking-pools-balanceof'; +import * as occStakeOf from './occ-stake-of'; +import * as hoprStaking from './hopr-staking'; +import * as hoprStakingS2 from './hopr-staking-s2'; +import * as hoprStakingBySeason from './hopr-staking-by-season'; +import * as hoprBridgedBalance from './hopr-bridged-balance'; +import * as hoprStakeAndBalanceQV from './hopr-stake-and-balance-qv'; +import * as lootCharacterGuilds from './loot-character-guilds'; +import * as swapr from './swapr'; +import * as cyberkongz from './cyberkongz'; +import * as cyberkongzV2 from './cyberkongz-v2'; +import * as cyberkongzV3 from './cyberkongz-v3'; +import * as compLikeVotesInclusive from './comp-like-votes-inclusive'; +import * as mstable from './mstable'; +import * as hashesVoting from './hashes-voting'; +import * as hashflowGovernancePower from './hashflow-governance-power'; +import * as hashflowVeHft from './hashflow-vehft'; +import * as podLeader from './pod-leader'; +import * as aavegotchiWagmiGuild from './aavegotchi-wagmi-guild'; +import * as polisBalance from './polis-balance'; +import * as techQuadraticRankedChoice from './tech-quadratic-ranked-choice'; +import * as mutantCatsStakersAndHolders from './mutant-cats-stakers-and-holders'; +import * as vaultTokenLpBalance from './vault-token-lp-balance'; +import * as singleStakingVaultBalanceOf from './single-staking-vault-balanceof'; +import * as razorVoting from './razor-network-voting'; +import * as svsStaking from './svs-staking'; +import * as mcbBalanceFromGraph from './mcb-balance-from-graph'; +import * as colonyReputation from './colony-reputation'; +import * as radicleCommunityTokens from './radicle-community-tokens'; +import * as digitalaxMonaQuickswap from './digitalax-mona-quickswap'; +import * as digitalaxDecoToMona from './digitalax-deco-to-mona'; +import * as digitalaxGenesisContribution from './digitalax-genesis-contribution'; +import * as digitalaxLPStakers from './digitalax-lp-stakers'; +import * as digitalaxMonaStakersMatic from './digitalax-mona-stakers-matic'; +import * as digitalaxLPStakersMatic from './digitalax-lp-stakers-matic'; +import * as galaxyNftWithScore from './galaxy-nft-with-score'; +import * as galxeLoyaltyPoints from './galxe-loyalty-points'; +import * as gatenetTotalStaked from './gatenet-total-staked'; +import * as vesper from './vesper'; +import * as thales from './thales'; +import * as bscMvb from './bsc-mvb'; +import * as coinswap from './coinswap'; +import * as dgenesis from './dgenesis'; +import * as votePowerAndShare from './vote-power-and-share'; +import * as blockzerolabsCryptonauts from './blockzerolabs-cryptonauts'; +import * as math from './math'; +import * as pushVotingPower from './push-voting-power'; +import * as stakedPSPBalance from './staked-psp-balance'; +import * as erc20BalanceOfContractMultiplier from './erc20-balance-of-contract-multiplier'; +import * as agave from './agave'; +import * as juicebox from './juicebox'; +import * as snetFarmers from './snet-farmers'; +import * as snetStakers from './snet-stakers'; +import * as snetLiquidityProviders from './snet-liquidity-providers'; +import * as minMaxMcnFarm from './minmax-mcn-farm'; +import * as unstackedToadzAndStackedToadzStakers from './unstackedtoadz-and-stackedtoadz-stakers'; +import * as jadeSmrt from './jade-smrt'; +import * as oceanDAOBrightID from './ocean-dao-brightid'; +import * as saddleFinance from './saddle-finance'; +import * as saddleFinanceV2 from './saddle-finance-v2'; +import * as lydiaGovVault from './lydia-gov-vault'; +import * as xkawaFarm from './xkawa-farm'; +import * as darkforestScore from './darkforest-score'; +import * as orangeReputationBasedVoting from './orange-reputation-based-voting'; +import * as orangeReputationNftBasedVoting from './orange-reputation-nft-based-voting'; +import * as squidDao from './squid-dao'; +import * as pathBalanceStakedAndLocked from './path-balance-staked-and-locked'; +import * as bottoDao from './botto-dao'; +import * as genart from './genart'; +import * as erc721MultiRegistryWeighted from './erc721-multi-registry-weighted'; +import * as genomesdao from './genomesdao'; +import * as zorro from './zorro'; +import * as voltVotingPower from './volt-voting-power'; +import * as balancerPoolid from './balancer-poolid'; +import * as stakedBalancer from './staked-balancer'; +import * as stakedUniswapModifiable from './staked-uniswap-modifiable'; +import * as givethXdaiBalance from './giveth-xdai-balance'; +import * as givethGnosisBalanceV2 from './giveth-gnosis-balance-v2'; +import * as givethBalancerBalance from './giveth-balancer-balance'; +import * as erc1155BalanceOfIds from './erc1155-balance-of-ids'; +import * as erc1155BalanceOfIdsWeighted from './erc1155-balance-of-ids-weighted'; +import * as erc1155weighted from './erc1155-weighted-by-id'; +import * as stakersAndHolders from './stakers-and-holders'; +import * as banksyDao from './banksy-dao'; +import * as spacey2025 from './spacey2025'; +import * as sandmanDao from './sandman-dao'; +import * as ethercatsFounderSeries from './ethercats-founder-series'; +import * as veBalanceOfAt from './ve-balance-of-at'; +import * as veRibbon from './ve-ribbon'; +import * as veRibbonVotingPower from './ve-ribbon-voting-power'; +import * as chubbykaijudao from './chubbykaijudao'; +import * as landDaoTiers from './landdao-token-tiers'; +import * as defiplaza from './defiplaza'; +import * as stakingClaimedUnclaimed from './staking-claimed-unclaimed'; +import * as gysrStakingBalance from './gysr-staking-balance'; +import * as gysrPendingRewards from './gysr-pending-rewards'; +import * as gysrLPStakingBalance from './gysr-lp-staking-balance'; +import * as wanakafarmStaking from './wanakafarm-staking'; +import * as starsharks from './starsharks'; +import * as printerFinancial from './printer-financial'; +import * as ethercatsFoundersSeries from './ethercats-founders-series'; +import * as potion from './potion'; +import * as MinotaurMoney from './minotaur-money'; +import * as safetyModuleBptPower from './safety-module-bpt-power'; +import * as convFinance from './conv-finance'; +import * as sdBoost from './sd-boost'; +import * as capitalDaoStaking from './capitaldao-staking'; +import * as erc20RebaseWrapper from './erc20-rebase-wrapper'; +import * as wanakafarmLandIngame from './wanakafarm-land-ingame'; +import * as meebitsDaoDelegation from './meebitsdao-delegation'; +import * as starcatchersTopWindow from './starcatchers-top-window'; +import * as gno from './gno'; +import * as umaVoting from './uma-voting'; +import * as masterchefPoolBalanceNoRewarddebt from './masterchef-pool-balance-no-rewarddebt'; +import * as proofOfHumanity from './proof-of-humanity'; +import * as samuraiLegendsGeneralsBalance from './samurailegends-generals-balance'; +import * as dogsUnchained from './dogs-unchained'; +import * as stakeDAOGovernanceUpdate from './stakedao-governance-update'; +import * as umamiVoting from './umami-voting'; +import * as liquidityTokenProvide from './liquidity-token-provide'; +import * as gamiumVoting from './gamium-voting'; +import * as citydaoSquareRoot from './citydao-square-root'; +import * as recusalList from './recusal-list'; +import * as rowdyRoos from './rowdy-roos'; +import * as ethermon721 from './ethermon-erc721'; +import * as etherorcsComboBalanceOf from './etherorcs-combo-balanceof'; +import * as hedgey from './hedgey'; +import * as hedgeyMulti from './hedgey-multi'; +import * as hedgeyDelegate from './hedgey-delegate'; +import * as sybilProtection from './sybil-protection'; +import * as veBalanceOfAtNFT from './ve-balance-of-at-nft'; +import * as genzeesFromSubgraph from './genzees-from-subgraph'; +import * as ginFinance from './gin-finance'; +import * as positionGovernancePower from './position-governance-power'; +import * as creditLp from './credit-lp'; +import * as helix from './helix'; +import * as arrakisFinance from './arrakis-finance'; +import * as auraFinance from './aura-vlaura-vebal'; +import * as auraFinanceWithOverrides from './aura-vlaura-vebal-with-overrides'; +import * as auraBalanceOfVlauraVebal from './aura-balance-of-vlaura-vebal'; +import * as auraBalanceOfSingleAsset from './aura-vault-balance-of-single-asset'; +import * as rocketpoolNodeOperator from './rocketpool-node-operator'; +import * as earthfundChildDaoStakingBalance from './earthfund-child-dao-staking-balance'; +import * as unipilotVaultPilotBalance from './unipilot-vault-pilot-balance'; +import * as sdBoostTWAVP from './sd-boost-twavp'; +import * as apeswap from './apeswap'; +import * as fortaShares from './forta-shares'; +import * as solvVoucherClaimable from './solv-voucher-claimable'; +import * as h2o from './h2o'; +import * as dopamine from './dopamine'; +import * as lrcL2SubgraphBalanceOf from './lrc-l2-subgraph-balance-of'; +import * as lrcL2NftBalanceOf from './lrc-l2-nft-balance-of'; +import * as lrcLPSubgraphBalanceOf from './lrc-lp-subgraph-balance-of'; +import * as lrcNFTDAOSearch from './lrc-nft-dao-search'; +import * as lrcNFTmult from './lrc-nft-search-mult'; +import * as erc3525VestingVoucher from './erc3525-vesting-voucher'; +import * as rariFuse from './rari-fuse'; +import * as selfswap from './selfswap'; +import * as xrookBalanceOfUnderlyingWeighted from './xrook-balance-of-underlying-weighted'; +import * as bancorPoolTokenUnderlyingBalance from './bancor-pool-token-underlying-balance'; +import * as orbsNetworkDelegation from './orbs-network-delegation'; +import * as balanceOfSubgraph from './balance-of-subgraph'; +import * as wagdieSubgraph from './wagdie-subgraph'; +import * as erc3525FlexibleVoucher from './erc3525-flexible-voucher'; +import * as erc721PairWeights from './erc721-pair-weights'; +import * as harmonyStaking from './harmony-staking'; +import * as echelonCachedErc1155Decay from './echelon-cached-erc1155-decay'; +import * as orcaPod from './orca-pod'; +import * as metropolisPod from './metropolis-pod'; +import * as proxyProtocolErc20BalanceOf from './proxyprotocol-erc20-balance-of'; +import * as proxyProtocolErc721BalanceOf from './proxyprotocol-erc721-balance-of'; +import * as proxyProtocolErc1155BalanceOf from './proxyprotocol-erc1155-balance-of'; +import * as arrowVesting from './arrow-vesting'; +import * as tutellusProtocol from './tutellus-protocol'; +import * as fightClub from './fight-club'; +import * as tproStaking from './tpro-staking'; +import * as safeVested from './safe-vested'; +import * as riskharborUnderwriter from './riskharbor-underwriter'; +import * as otterspaceBadges from './otterspace-badges'; +import * as syntheticNounsClaimerOwner from './synthetic-nouns-with-claimer'; +import * as depositInSablierStream from './deposit-in-sablier-stream'; +import * as echelonWalletPrimeAndCachedKey from './echelon-wallet-prime-and-cached-key'; +import * as nation3VotesWIthDelegations from './nation3-votes-with-delegations'; +import * as aavegotchiAgip37WapGhst from './aavegotchi-agip-37-wap-ghst'; +import * as aavegotchiAgip37GltrStakedLp from './aavegotchi-agip-37-gltr-staked-lp'; +import * as posichainStaking from './posichain-staking'; +import * as posichainTotalBalance from './posichain-total-balance'; +import * as erc20TokensPerUni from './erc20-tokens-per-uni'; +import * as bancorStandardRewardsUnderlyingBalance from './bancor-standard-rewards-underlying-balance'; +import * as sdVoteBoost from './sd-vote-boost'; +import * as sdVoteBoostTWAVP from './sd-vote-boost-twavp'; +import * as clqdrBalanceWithLp from './clqdr-balance-with-lp'; +import * as ninechroniclesStakedAndDcc from './ninechronicles-staked-and-dcc'; +import * as spreadsheet from './spreadsheet'; +import * as offchainDelegation from './offchain-delegation'; +import * as dslaParametricStakingServiceCredits from './dsla-parametric-staking-service-credits'; +import * as rep3Badges from './rep3-badges'; +import * as marsecosystem from './marsecosystem'; +import * as ari10StakingLocked from './ari10-staking-locked'; +import * as multichainSerie from './multichain-serie'; +import * as ctsiStaking from './ctsi-staking'; +import * as ctsiStakingPool from './ctsi-staking-pool'; +import * as skaleDelegationWeighted from './skale-delegation-weighted'; +import * as reliquary from './reliquary'; +import * as acrossStakedAcx from './across-staked-acx'; +import * as vstaPoolStaking from './vsta-pool-staking'; +import * as lodestarVesting from './lodestar-vesting'; +import * as lodestarStakedLp from './lodestar-staked-lp'; +import * as jpegdLockedJpegOf from './jpegd-locked-jpeg-of'; +import * as litDaoGovernance from './lit-dao-governance'; +import * as babywealthyclub from './babywealthyclub'; +import * as battleflyVGFLYAndStakedGFLY from './battlefly-vgfly-and-staked-gfly'; +import * as nexonArmyNFT from './nexon-army-nft'; +import * as moonbeamFreeBalance from './moonbeam-free-balance'; +import * as stakedotlinkVesting from './stakedotlink-vesting'; +import * as pspInSePSP2Balance from './psp-in-sepsp2-balance'; +import * as pdnBalancesAndVests from './pdn-balances-and-vests'; +import * as izumiVeiZi from './izumi-veizi'; +import * as lqtyProxyStakers from './lqty-proxy-stakers'; +import * as echelonWalletPrimeAndCachedKeyGated from './echelon-wallet-prime-and-cached-key-gated'; +import * as rdntCapitalVoting from './rdnt-capital-voting'; +import * as stakedDefiBalance from './staked-defi-balance'; +import * as degenzooErc721AnimalsWeighted from './degenzoo-erc721-animals-weighted'; +import * as capVotingPower from './cap-voting-power'; +import * as zunamiPoolGaugeAggregatedBalanceOf from './zunami-pool-gauge-aggregated-balance-of'; +import * as erc721CollateralHeld from './erc721-collateral-held'; +import * as starlayVeBalanceOfLockerId from './starlay-ve-balance-of-locker-id'; +import * as winrStaking from './winr-staking'; +import * as spaceid from './spaceid'; + +const strategies = { + 'cap-voting-power': capVotingPower, + 'izumi-veizi': izumiVeiZi, + 'eco-voting-power': ecoVotingPower, + 'forta-shares': fortaShares, + 'across-staked-acx': acrossStakedAcx, + 'ethermon-erc721': ethermon721, + 'etherorcs-combo-balanceof': etherorcsComboBalanceOf, + 'recusal-list': recusalList, + 'landdao-token-tiers': landDaoTiers, + 'giveth-balancer-balance': givethBalancerBalance, + 'giveth-xdai-balance': givethXdaiBalance, + 'giveth-gnosis-balance-v2': givethGnosisBalanceV2, + 'nouns-rfp-power': nounsPower, + coordinape, + 'anti-whale': antiWhale, + balancer, + sunder, + 'balancer-smart-pool': balancerSmartPool, + 'lit-dao-governance': litDaoGovernance, + 'balancer-erc20-internal-balance-of': balancerErc20InternalBalanceOf, + 'balance-in-vdfyn-vault': vDfynVault, + 'erc20-received': erc20Received, + 'contract-call': contractCall, + defiplaza: defiplaza, + 'dextf-staked-in-vaults': dextfVaults, + 'dfyn-staked-in-farms': dfynFarms, + 'dfyn-staked-in-vaults': dfynVaults, + 'dps-nft-strategy': dpsNFTStrategy, + 'dps-nft-strategy-nova': dpsNFTStrategyNova, + 'eth-received': ethReceived, + 'eth-philanthropy': ethPhilanthropy, + 'ens-domains-owned': ensDomainsOwned, + 'ens-reverse-record': ensReverseRecord, + 'ens-10k-club': ens10kClub, + 'ens-all-club-digits': ensAllClubDigits, + 'governor-delegator': governorDelegator, + 'erc20-balance-of': erc20BalanceOf, + 'erc20-balance-of-at': erc20BalanceOfAt, + 'erc20-votes': erc20Votes, + 'erc20-votes-with-override': erc20VotesWithOverride, + 'erc721-multi-registry-weighted': erc721MultiRegistryWeighted, + 'erc20-balance-of-fixed-total': erc20BalanceOfFixedTotal, + 'erc20-balance-of-cv': erc20BalanceOfCv, + 'erc20-balance-of-coeff': erc20BalanceOfCoeff, + 'erc20-with-balance': erc20WithBalance, + 'erc20-balance-of-delegation': erc20BalanceOfDelegation, + 'erc20-balance-of-with-delegation': erc20BalanceOfWithDelegation, + 'erc20-balance-of-quadratic-delegation': erc20BalanceOfQuadraticDelegation, + 'erc20-balance-of-weighted': erc20BalanceOfWeighted, + 'minto-balance-of-all': mintoBalanceAll, + 'erc20-balance-of-indexed': erc20BalanceOfIndexed, + 'erc20-price': erc20Price, + 'ethalend-balance-of': ethalendBalanceOf, + 'balance-of-with-min': balanceOfWithMin, + 'balance-of-with-thresholds': balanceOfWithThresholds, + thresholds, + 'eth-balance': ethBalance, + 'eth-with-balance': ethWithBalance, + 'eth-wallet-age': ethWalletAge, + 'maker-ds-chief': makerDsChief, + erc721, + 'erc721-enumerable': erc721Enumerable, + 'erc721-with-multiplier': erc721WithMultiplier, + 'protofi-erc721-tier-weighted': protofiErc721TierWeighted, + 'erc721-with-tokenid': erc721WithTokenId, + 'erc721-with-tokenid-range-weights': erc721WithTokenIdRangeWeights, + 'erc721-with-tokenid-range-weights-simple': + erc721WithTokenIdRangeWeightsSimple, + 'erc721-with-tokenid-weighted': erc721WithTokenIdWeighted, + 'erc721-with-metadata': erc721WithMetadata, + 'erc721-with-metadata-by-ownerof': erc721WithMetadataByOwnerOf, + 'erc721-multi-registry': erc721MultiRegistry, + 'erc1155-balance-of': erc1155BalanceOf, + 'erc1155-balance-of-cv': erc1155BalanceOfCv, + 'prepo-vesting': prepoVesting, + multichain, + 'gooddollar-multichain': gooddollarMultichain, + uni, + 'frax-finance': fraxFinance, + 'yearn-vault': yearnVault, + moloch, + masterchef, + sushiswap, + uniswap, + 'faraland-staking': faralandStaking, + flashstake, + pancake, + synthetix, + 'aelin-council': aelinCouncil, + 'synthetix-quadratic': synthetixQuadratic, + 'synthetix-quadratic_1': synthetixQuadraticOne, + 'synthetix-quadratic_2': synthetixQuadraticTwo, + synthetix_1: synthetixOne, + 'synthetix-non-quadratic': synthetixNonQuadratic, + 'synthetix-non-quadratic_1': synthetixNonQuadraticOne, + 'synthetix-non-quadratic_2': synthetixNonQuadraticTwo, + ctoken, + cream, + 'staked-uniswap': stakedUniswap, + esd, + 'esd-delegation': esdDelegation, + piedao, + 'xdai-easy-staking': xDaiEasyStaking, + 'xdai-posdao-staking': xDaiPOSDAOStaking, + 'xdai-stake-holders': xDaiStakeHolders, + 'xdai-stake-delegation': xDaiStakeDelegation, + defidollar, + aavegotchi, + 'aavegotchi-agip': aavegotchiAgip, + 'aavegotchi-agip-17': aavegotchiAgip17, + mithcash, + stablexswap, + dittomoney, + 'staked-keep': stakedKeep, + 'staked-daomaker': stakedDaomaker, + 'balancer-unipool': balancerUnipool, + typhoon, + delegation, + 'delegation-with-overrides': delegationWithOverrides, + 'with-delegation': withDelegation, + ticket, + work, + 'ticket-validity': ticketValidity, + validation, + opium, + 'ocean-marketplace': ocean, + 'ocean-marketplace-v4': ocean_v4, + 'the-graph-balance': theGraphBalance, + 'the-graph-delegation': theGraphDelegation, + 'the-graph-indexing': theGraphIndexing, + whitelist, + 'whitelist-weighted': whitelistWeighted, + tokenlon, + rebased, + 'pob-hash': pobHash, + 'total-axion-shares': totalAxionShares, + 'comp-like-votes': compLikeVotes, + 'governor-alpha': governorAlpha, + pagination, + 'ruler-staked-token': rulerStakedToken, + 'ruler-staked-lp': rulerStakedLP, + xcover, + 'niu-staked': niuStaked, + mushrooms: mushrooms, + 'curio-cards-erc20-weighted': curioCardsErc20Weighted, + 'ren-nodes': renNodes, + 'reverse-voting-escrow': reverseVotingEscrow, + 'multisig-owners': multisigOwners, + 'tranche-staking': trancheStaking, + pepemon, + 'erc1155-all-balances-of': erc1155AllBalancesOf, + 'erc1155-with-multiplier': erc1155WithMultiplier, + 'saffron-finance': saffronFinance, + 'saffron-finance-v2': saffronFinanceV2, + 'tranche-staking-lp': trancheStakingLP, + 'masterchef-pool-balance': masterchefPoolBalance, + 'masterchef-pool-balance-price': masterchefPoolBalancePrice, + 'avn-balance-of-staked': avnBalanceOfStaked, + api, + 'api-post': apiPost, + 'api-v2': apiV2, + xseen, + 'moloch-all': molochAll, + 'moloch-loot': molochLoot, + 'hopr-uni-lp-farm': hoprUniLpFarm, + apescape, + liftkitchen, + 'decentraland-estate-size': decentralandEstateSize, + 'decentraland-wearable-rarity': decentralandWearableRariry, + 'decentraland-rental-lessors': decentralandRentalLessors, + brightid, + 'inverse-xinv': inverseXINV, + modefi, + 'modefi-staking': modefiStaking, + 'iotex-balance': iotexBalance, + 'iotex-staked-balance': iotexStakedBalance, + 'xrc20-balance-of': xrc20BalanceOf, + spookyswap, + 'squadz-power': squadzPower, + glide, + 'goldfinch-voting-power': goldfinchVotingPower, + 'goldfinch-membership': goldfinchMembership, + 'rnbw-balance': rnbwBalance, + 'celer-sgn-delegation': celerSgnDelegation, + 'balancer-delegation': balancerDelegation, + 'infinityprotocol-liquidity-pools': infinityProtocolPools, + 'aave-governance-power': aaveGovernancePower, + cake, + aks, + tomyumswap, + 'planet-finance': planetFinance, + 'planet-finance-v2': planetFinancev2, + ogn, + oolongswap, + 'impossible-finance': impossibleFinance, + 'immutable-x': immutableX, + badgeth, + 'zrx-voting-power': zrxVotingPower, + 'tomb-finance': tombFinance, + 'tranche-staking-slice': trancheStakingSLICE, + 'unipool-same-token': unipoolSameToken, + 'unipool-univ2-lp': unipoolUniv2Lp, + 'unipool-xsushi': unipoolXSushi, + poap: poap, + 'poap-with-weight': poapWithWeight, + 'poap-with-weight-v2': poapWithWeightV2, + 'uniswap-v3': uniswapV3, + 'uniswap-v3-staking': uniswapV3Staking, + 'l2-deversifi': l2Deversifi, + 'vested-deversifi': vestedDeversifi, + biswap, + cronaswap, + honeyswap, + 'egl-vote': eglVote, + 'mcn-farm': mcnFarm, + snowswap, + meebitsdao, + 'crucible-erc20-balance-of': crucibleERC20BalanceOf, + 'erc20-token-and-lp-weighted': erc20TokenAndLpWeighted, + 'crucible-erc20-token-and-lp-weighted': crucibleERC20TokenAndLpWeighted, + 'has-rock': hasrock, + 'flexa-capacity-staking': flexaCapacityStaking, + 'sunrisegaming-univ2-lp': sunriseGamingUniv2Lp, + 'sunrisegaming-staking': sunriseGamingStaking, + 'single-staking-autocompound-balanceof': singleStakingAutoCompoundBalanceOf, + 'single-staking-pools-balanceof': singleStakingPoolsBalanceOf, + 'hopr-staking': hoprStaking, + 'hopr-staking-s2': hoprStakingS2, + 'hopr-staking-by-season': hoprStakingBySeason, + 'hopr-stake-and-balance-qv': hoprStakeAndBalanceQV, + 'hopr-bridged-balance': hoprBridgedBalance, + 'occ-stake-of': occStakeOf, + swapr, + 'holds-tokens': holdsTokens, + 'loot-character-guilds': lootCharacterGuilds, + cyberkongz: cyberkongz, + 'cyberkongz-v2': cyberkongzV2, + 'cyberkongz-v3': cyberkongzV3, + 'comp-like-votes-inclusive': compLikeVotesInclusive, + mstable, + 'hashes-voting': hashesVoting, + 'hashflow-governance-power': hashflowGovernancePower, + 'hashflow-vehft': hashflowVeHft, + 'pod-leader': podLeader, + 'aavegotchi-wagmi-guild': aavegotchiWagmiGuild, + 'polis-balance': polisBalance, + 'vault-token-lp-balance': vaultTokenLpBalance, + 'single-staking-vault-balanceof': singleStakingVaultBalanceOf, + 'mutant-cats-stakers-and-holders': mutantCatsStakersAndHolders, + 'razor-network-voting': razorVoting, + 'svs-staking': svsStaking, + 'mcb-balance-from-graph': mcbBalanceFromGraph, + 'radicle-community-tokens': radicleCommunityTokens, + 'digitalax-mona-quickswap': digitalaxMonaQuickswap, + 'digitalax-deco-to-mona': digitalaxDecoToMona, + 'digitalax-genesis-contribution': digitalaxGenesisContribution, + 'digitalax-lp-stakers': digitalaxLPStakers, + 'digitalax-mona-stakers-matic': digitalaxMonaStakersMatic, + 'digitalax-lp-stakers-matic': digitalaxLPStakersMatic, + 'colony-reputation': colonyReputation, + 'galaxy-nft-with-score': galaxyNftWithScore, + 'galxe-loyalty-points': galxeLoyaltyPoints, + 'gatenet-total-staked': gatenetTotalStaked, + vesper, + thales, + 'tech-quadratic-ranked-choice': techQuadraticRankedChoice, + 'bsc-mvb': bscMvb, + coinswap, + dgenesis, + 'vote-power-and-share': votePowerAndShare, + 'blockzerolabs-cryptonauts': blockzerolabsCryptonauts, + math, + 'push-voting-power': pushVotingPower, + 'staked-psp-balance': stakedPSPBalance, + 'erc20-balance-of-contract-multiplier': erc20BalanceOfContractMultiplier, + agave, + juicebox, + 'snet-farmers': snetFarmers, + 'snet-stakers': snetStakers, + 'snet-liquidity-providers': snetLiquidityProviders, + 'minmax-mcn-farm': minMaxMcnFarm, + 'unstackedtoadz-and-stackedtoadz-stakers': + unstackedToadzAndStackedToadzStakers, + 'jade-smrt': jadeSmrt, + 'ocean-dao-brightid': oceanDAOBrightID, + 'saddle-finance': saddleFinance, + 'saddle-finance-v2': saddleFinanceV2, + membership: membership, + 'lydia-gov-vault': lydiaGovVault, + 'xkawa-farm': xkawaFarm, + 'darkforest-score': darkforestScore, + 'orange-reputation-based-voting': orangeReputationBasedVoting, + 'orange-reputation-nft-based-voting': orangeReputationNftBasedVoting, + 'squid-dao': squidDao, + 'botto-dao': bottoDao, + genart, + genomesdao, + 'path-balance-staked-and-locked': pathBalanceStakedAndLocked, + 'sumami-holders': sUmamiHolders, + zorro, + 'volt-voting-power': voltVotingPower, + 'balancer-poolid': balancerPoolid, + 'staked-balancer': stakedBalancer, + 'staked-uniswap-modifiable': stakedUniswapModifiable, + 'erc1155-balance-of-ids': erc1155BalanceOfIds, + 'erc1155-balance-of-ids-weighted': erc1155BalanceOfIdsWeighted, + 'erc1155-weighted-by-id': erc1155weighted, + 'stakers-and-holders': stakersAndHolders, + 'banksy-dao': banksyDao, + spacey2025: spacey2025, + 'sandman-dao': sandmanDao, + 'ethercats-founder-series': ethercatsFounderSeries, + 've-balance-of-at': veBalanceOfAt, + 've-ribbon': veRibbon, + 've-ribbon-voting-power': veRibbonVotingPower, + chubbykaijudao: chubbykaijudao, + revest: revest, + 'staking-claimed-unclaimed': stakingClaimedUnclaimed, + 'gysr-staking-balance': gysrStakingBalance, + 'gysr-pending-rewards': gysrPendingRewards, + 'gysr-lp-staking-balance': gysrLPStakingBalance, + 'wanakafarm-staking': wanakafarmStaking, + starsharks, + 'printer-financial': printerFinancial, + 'ethercats-founders-series': ethercatsFoundersSeries, + potion, + 'safety-module-bpt-power': safetyModuleBptPower, + 'minotaur-money': MinotaurMoney, + 'conv-finance': convFinance, + 'sd-boost': sdBoost, + 'capitaldao-staking': capitalDaoStaking, + 'erc20-rebase-wrapper': erc20RebaseWrapper, + 'wanakafarm-land-ingame': wanakafarmLandIngame, + 'meebitsdao-delegation': meebitsDaoDelegation, + 'starcatchers-top-window': starcatchersTopWindow, + gno: gno, + 'gno-vote-weight': gno, + 'uma-voting': umaVoting, + 'masterchef-pool-balance-no-rewarddebt': masterchefPoolBalanceNoRewarddebt, + 'proof-of-humanity': proofOfHumanity, + 'sybil-protection': sybilProtection, + 'samurailegends-generals-balance': samuraiLegendsGeneralsBalance, + 'dogs-unchained': dogsUnchained, + 'stakedao-governance-update': stakeDAOGovernanceUpdate, + 'umami-voting': umamiVoting, + 'liquidity-token-provide': liquidityTokenProvide, + 'gamium-voting': gamiumVoting, + 'citydao-square-root': citydaoSquareRoot, + 'rowdy-roos': rowdyRoos, + hedgey, + 'hedgey-multi': hedgeyMulti, + 'hedgey-delegate': hedgeyDelegate, + 've-balance-of-at-nft': veBalanceOfAtNFT, + 'genzees-from-subgraph': genzeesFromSubgraph, + 'gin-finance': ginFinance, + 'position-governance-power': positionGovernancePower, + 'credit-lp': creditLp, + helix, + 'arrakis-finance': arrakisFinance, + 'aura-vlaura-vebal': auraFinance, + 'aura-vlaura-vebal-with-overrides': auraFinanceWithOverrides, + 'aura-balance-of-vlaura-vebal': auraBalanceOfVlauraVebal, + 'aura-vault-balance-of-single-asset': auraBalanceOfSingleAsset, + 'rocketpool-node-operator': rocketpoolNodeOperator, + 'earthfund-child-dao-staking-balance': earthfundChildDaoStakingBalance, + 'sd-boost-twavp': sdBoostTWAVP, + 'unipilot-vault-pilot-balance': unipilotVaultPilotBalance, + 'solv-voucher-claimable': solvVoucherClaimable, + 'balance-of-with-linear-vesting-power': balanceOfWithLinearVestingPower, + 'linear-vesting-power': linearVestingPower, + apeswap, + h2o, + dopamine, + 'lrc-l2-subgraph-balance-of': lrcL2SubgraphBalanceOf, + 'lrc-l2-nft-balance-of': lrcL2NftBalanceOf, + 'lrc-lp-subgraph-balance-of': lrcLPSubgraphBalanceOf, + 'lrc-nft-dao-search': lrcNFTDAOSearch, + 'lrc-nft-search-mult': lrcNFTmult, + 'rari-fuse': rariFuse, + 'bancor-pool-token-underlying-balance': bancorPoolTokenUnderlyingBalance, + selfswap, + 'erc3525-vesting-voucher': erc3525VestingVoucher, + 'xrook-balance-of-underlying-weighted': xrookBalanceOfUnderlyingWeighted, + 'orbs-network-delegation': orbsNetworkDelegation, + 'balance-of-subgraph': balanceOfSubgraph, + 'wagdie-subgraph': wagdieSubgraph, + 'erc721-pair-weights': erc721PairWeights, + 'harmony-staking': harmonyStaking, + 'echelon-cached-erc1155-decay': echelonCachedErc1155Decay, + 'erc3525-flexible-voucher': erc3525FlexibleVoucher, + 'orca-pod': orcaPod, + 'metropolis-pod': metropolisPod, + 'proxyprotocol-erc20-balance-of': proxyProtocolErc20BalanceOf, + 'proxyprotocol-erc721-balance-of': proxyProtocolErc721BalanceOf, + 'proxyprotocol-erc1155-balance-of': proxyProtocolErc1155BalanceOf, + 'posichain-staking': posichainStaking, + 'posichain-total-balance': posichainTotalBalance, + 'arrow-vesting': arrowVesting, + 'tutellus-protocol': tutellusProtocol, + 'fight-club': fightClub, + 'tpro-staking': tproStaking, + 'safe-vested': safeVested, + 'riskharbor-underwriter': riskharborUnderwriter, + 'otterspace-badges': otterspaceBadges, + 'synthetic-nouns-with-claimer': syntheticNounsClaimerOwner, + 'deposit-in-sablier-stream': depositInSablierStream, + 'echelon-wallet-prime-and-cached-key': echelonWalletPrimeAndCachedKey, + 'nation3-votes-with-delegations': nation3VotesWIthDelegations, + 'aavegotchi-agip-37-wap-ghst': aavegotchiAgip37WapGhst, + 'aavegotchi-agip-37-gltr-staked-lp': aavegotchiAgip37GltrStakedLp, + 'erc20-tokens-per-uni': erc20TokensPerUni, + 'bancor-standard-rewards-underlying-balance': + bancorStandardRewardsUnderlyingBalance, + 'sd-vote-boost': sdVoteBoost, + 'sd-vote-boost-twavp': sdVoteBoostTWAVP, + 'clqdr-balance-with-lp': clqdrBalanceWithLp, + spreadsheet, + 'offchain-delegation': offchainDelegation, + 'ninechronicles-staked-and-dcc': ninechroniclesStakedAndDcc, + 'dsla-parametric-staking-service-credits': + dslaParametricStakingServiceCredits, + 'rep3-badges': rep3Badges, + marsecosystem, + 'ari10-staking-locked': ari10StakingLocked, + 'multichain-serie': multichainSerie, + 'ctsi-staking': ctsiStaking, + 'ctsi-staking-pool': ctsiStakingPool, + 'skale-delegation-weighted': skaleDelegationWeighted, + reliquary, + 'vsta-pool-staking': vstaPoolStaking, + 'jpegd-locked-jpeg-of': jpegdLockedJpegOf, + 'lodestar-vesting': lodestarVesting, + 'lodestar-staked-lp': lodestarStakedLp, + babywealthyclub, + 'battlefly-vgfly-and-staked-gfly': battleflyVGFLYAndStakedGFLY, + 'nexon-army-nft': nexonArmyNFT, + 'moonbeam-free-balance': moonbeamFreeBalance, + 'stakedotlink-vesting': stakedotlinkVesting, + 'psp-in-sepsp2-balance': pspInSePSP2Balance, + 'pdn-balances-and-vests': pdnBalancesAndVests, + 'lqty-proxy-stakers': lqtyProxyStakers, + 'echelon-wallet-prime-and-cached-key-gated': + echelonWalletPrimeAndCachedKeyGated, + 'rdnt-capital-voting': rdntCapitalVoting, + 'staked-defi-balance': stakedDefiBalance, + 'degenzoo-erc721-animals-weighted': degenzooErc721AnimalsWeighted, + 'zunami-pool-gauge-aggregated-balance-of': zunamiPoolGaugeAggregatedBalanceOf, + 'erc721-collateral-held': erc721CollateralHeld, + 'starlay-ve-balance-of-locker-id': starlayVeBalanceOfLockerId, + 'winr-staking': winrStaking, + spaceid +}; + +Object.keys(strategies).forEach(function (strategyName) { + let examples = null; + let schema = null; + let about = ''; + + try { + examples = JSON.parse( + readFileSync(path.join(__dirname, strategyName, 'examples.json'), 'utf8') + ); + } catch (error) { + examples = null; + } + + try { + schema = JSON.parse( + readFileSync(path.join(__dirname, strategyName, 'schema.json'), 'utf8') + ); + } catch (error) { + schema = null; + } + + try { + about = readFileSync( + path.join(__dirname, strategyName, 'README.md'), + 'utf8' + ); + } catch (error) { + about = ''; + } + strategies[strategyName].examples = examples; + strategies[strategyName].schema = schema; + strategies[strategyName].about = about; +}); + +export default strategies; diff --git a/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/README.md b/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/README.md new file mode 100644 index 00000000..cce3450d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/README.md @@ -0,0 +1,26 @@ +# Liquidity Providers + +This strategy will return the scores of all users who have provided token liquidity on any Uniswap style exchange. Users can change the subGraphURL field to direct their request to a different subgraph. + + +## Example + +The space config will look like this: + +```JSON +{ + "strategies": [ + ["infinityprotocol-liquidity-pools", { + // token parameters + "params": { + "address": "0xc168e40227e4ebd8c1cae80f7a55a4f0e6d66c97", + "symbol": "DFYN" + // subgraphURL for the request + "subGraphURL": "https://api.thegraph.com/subgraphs/name/ss-sonic/dfyn-v5", + // scoreMultiplier can be used to increase users' scores by a certain magnitude + "scoreMultiplier": 1, + }, + }], + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/examples.json b/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/examples.json new file mode 100644 index 00000000..f238dffc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "infinityprotocol-liquidity-pools", + "params": { + "address": "0xd8a1734945b9ba38eb19a291b475e31f49e59877", + "symbol": "SHARD", + "scoreMultiplier": 1 + } + }, + "network": "56", + "addresses": [ + "0x2130fd01026867a6c8dde24ad5e64f2e2dfce196", + "0x1d197fd4e7975efd21add0ce3128c488bbd70b83", + "0x81c2a7b359855520d0ca251975218fa82ee83fba", + "0xf17067c512d0ca64900e724172761a0f0527466f" + ], + "snapshot": 8438110 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/index.ts b/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/index.ts new file mode 100644 index 00000000..14982f5b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/infinityprotocol-liquidity-pools/index.ts @@ -0,0 +1,87 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const INFINITYPROTOCOL_SUBGRAPH_URL = { + '56': 'https://api.thegraph.com/subgraphs/name/infinitywallet/infinity-protocol' +}; + +export const author = 'vfatouros'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + liquidityPositions: { + __args: { + where: { + liquidityTokenBalance_gt: 0 + } + }, + liquidityTokenBalance: true, + pair: { + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.liquidityPositions.__args.block = { number: snapshot }; + } + const tokenAddress = options.address.toLowerCase(); + const result = await subgraphRequest( + options.subGraphURL + ? options.subGraphURL + : INFINITYPROTOCOL_SUBGRAPH_URL[network], + params + ); + const score = {}; + if (result && result.users) { + result.users.forEach((u) => { + u.liquidityPositions + .filter( + (lp) => + lp.pair.token0.id == tokenAddress || + lp.pair.token1.id == tokenAddress + ) + .forEach((lp) => { + const token0perShard = lp.pair.reserve0 / lp.pair.totalSupply; + const token1perShard = lp.pair.reserve1 / lp.pair.totalSupply; + let userScore = + lp.pair.token0.id == tokenAddress + ? token0perShard * lp.liquidityTokenBalance + : token1perShard * lp.liquidityTokenBalance; + if (options.scoreMultiplier) { + userScore = userScore * options.scoreMultiplier; + } + const userAddress = getAddress(u.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScore; + }); + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/README.md b/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/README.md new file mode 100644 index 00000000..366731fc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/README.md @@ -0,0 +1,11 @@ +# inverse-xinv + +This is a strategy for counting staked INV balance inside of anchor. Referred to as xINV. +Here is an example of parameters: + +```json +{ + "symbol": "xINV", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/examples.json b/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/examples.json new file mode 100644 index 00000000..b0cc9775 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "inverse-xinv", + "params": { + "symbol": "xINV", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x08d816526bdc9d077dd685bd9fa49f58a5ab8e48", + "0x3ee505ba316879d246a8fd2b3d7ee63b51b44fab" + ], + "snapshot": 12511580 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/index.ts b/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/index.ts new file mode 100644 index 00000000..1a8b495e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/inverse-xinv/index.ts @@ -0,0 +1,51 @@ +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = '0xKiwi'; +export const version = '0.1.0'; + +const xINV = '0x65b35d6Eb7006e0e607BC54EB2dFD459923476fE'; +const ONE_E18 = parseUnits('1', 18); + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function exchangeRateStored() external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const exchangeRateResp = await multicall( + network, + provider, + abi, + [[xINV, 'exchangeRateStored', []]], + { blockTag } + ); + const exchangeRate = exchangeRateResp[0][0]; + + const balanceResp = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [xINV, 'balanceOf', [address]]), + { blockTag } + ); + return Object.fromEntries( + balanceResp.map((value, i) => [ + addresses[i], + parseFloat( + formatUnits( + value[0].mul(exchangeRate).div(ONE_E18).toString(), + options.decimals + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/iotex-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/iotex-balance/examples.json new file mode 100644 index 00000000..7fa6bbe8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/iotex-balance/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "iotex balance query", + "strategy": { + "name": "iotex-balance", + "params": { + "address": "0x1904BFcb93EdC9BF961Eead2e5c0de81dCc1D37D", + "symbol": "IOTX", + "decimals": 18 + } + }, + "network": "4689", + "addresses": [ + "0x515c2c35c3ec82c30affc5ec06da9a30ef008741", + "0xa00744882684c3e4747faefd68d283ea44099d03" + ], + "snapshot": 13000000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/iotex-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/iotex-balance/index.ts new file mode 100644 index 00000000..0ecf21b2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/iotex-balance/index.ts @@ -0,0 +1,56 @@ +import { strategy as EthBalanceStrategy } from '../eth-balance'; +import fetch from 'cross-fetch'; +interface ApiReturn { + balance: string[]; +} + +export const author = 'iotexproject'; +export const version = '0.0.2'; + +const testNetUrl = 'https://analyser-api.testnet.iotex.io'; +const mainNetUrl = 'https://analyser-api.iotex.io'; + +function getUrl(network) { + return network == 4689 ? mainNetUrl : testNetUrl; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + if (blockTag == 'latest') + return EthBalanceStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const apiUrl = getUrl(network); + const response = await fetch( + `${apiUrl}/api.AccountService.IotexBalanceByHeight`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + address: addresses, + height: snapshot + }) + } + ); + + const ret: ApiReturn = await response.json(); + return Object.fromEntries( + ret.balance.map((v, i) => [addresses[i], parseFloat(v)]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/iotex-staked-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/iotex-staked-balance/examples.json new file mode 100644 index 00000000..baf2669c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/iotex-staked-balance/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "iotex query", + "strategy": { + "name": "iotex-staked-balance", + "params": { + "address": "0x1904BFcb93EdC9BF961Eead2e5c0de81dCc1D37D", + "symbol": "IOTX", + "decimals": 18 + } + }, + "network": "4689", + "addresses": [ + "0x49e4dbff86a2e5da27c540c9a9e8d2c3726e278f", + "0x4757ce43dc5429b8f1a132dc29ef970e55ae722b" + ], + "snapshot": 5161495 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/iotex-staked-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/iotex-staked-balance/index.ts new file mode 100644 index 00000000..b0c364af --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/iotex-staked-balance/index.ts @@ -0,0 +1,42 @@ +import fetch from 'cross-fetch'; +interface ApiReturn { + voteWeight: string[]; +} + +export const author = 'iotex'; +export const version = '0.0.2'; + +const testNetUrl = 'https://analyser-api.testnet.iotex.io'; +const mainNetUrl = 'https://analyser-api.iotex.io'; + +function getUrl(network) { + return network == 4689 ? mainNetUrl : testNetUrl; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const height = typeof snapshot === 'number' ? snapshot : 10000000000; + const apiUrl = getUrl(network); + const response = await fetch(`${apiUrl}/api.StakingService.VoteByHeight`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + address: addresses, + height + }) + }); + + const ret: ApiReturn = await response.json(); + return Object.fromEntries( + ret.voteWeight.map((v, i) => [addresses[i], parseFloat(v)]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/README.md b/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/README.md new file mode 100644 index 00000000..cfc14200 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/README.md @@ -0,0 +1,13 @@ +# erc20-balance-of + +This is a strategy which used for counting veizi for izumi voting, it returns the balances of the voters for the veiZi including the amount of staked veNFT. + +Here is an example of parameters: + +```json +{ + "address": "0xB56A454d8DaC2AD4cB82337887717a2a427Fcd00", + "symbol": "veiZi", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/examples.json b/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/examples.json new file mode 100644 index 00000000..1ff27bf1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/examples.json @@ -0,0 +1,31 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "izumi-veizi", + "params": { + "address": "0xB56A454d8DaC2AD4cB82337887717a2a427Fcd00", + "symbol": "veiZi", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x6534069842F1aFCbb5b6e27eA2bAc2243f85FFF8", + "0x423F9B19Db22337D249c1d85eE045700846953B3", + "0x9558ab3bd4429B1D5658D9ea6C5E8F2b8B4B7431", + "0x9997d7120862CEda99aB4a1c43698e0941F87b20", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x381F298A34D286CBe36b1C17097aE36350B66DC8", + "0x760484042a7856E62B627318796Ebb609C8131a1", + "0x72956796334bA8EB7c10a474e97F6F8773E40cFf", + "0x5b54a9439c0b2B88E35DB66f93882479fEd4FDfb", + "0x5096905F6CFC0F7BBEE09D72Bd97DAE4a95Bc3d6", + "0xbCe0AF8E8e47C6f3E727cD95c4118f14a34A0DFE", + "0xDaB558F089dc6CBD96eae358bB12Cb4aD6043647", + "0x8E79c736De9c10c4b9909077560DcD2F78b0B969", + "0x7C28A20A66d687107dfA810566f237fb3810CBf4" + ], + "snapshot": 16619300 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/index.ts b/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/index.ts new file mode 100644 index 00000000..9146cd2f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/index.ts @@ -0,0 +1,83 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'izumiFinance'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function nftVeiZiAt(uint256 nftId, uint256 timestamp) external view returns (uint256)', + 'function stakedNft(address) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const block = await provider.getBlock(snapshot); + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const timeStamp = block.timestamp; + const ans = {} as Record; + + const nftBalance = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + nftBalance.call(address, options.address, 'balanceOf', [address]) + ); + const balance: Record = await nftBalance.execute(); + + const nftIdCall = new Multicaller(network, provider, abi, { blockTag }); + Object.entries(balance).map(async ([address, balance]) => { + const num = Number(balance.toString()); + for (let i = 0; i < num; i++) { + const path = address + '-' + i; + nftIdCall.call(path, options.address, 'tokenOfOwnerByIndex', [ + address, + i + ]); + } + }); + + const stakingCheck = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => { + const stakedPath = address + '-' + 'staked'; + stakingCheck.call(stakedPath, options.address, 'stakedNft', [address]); + }); + + const ids: Record = await nftIdCall.execute(); + const stakedIds: Record = await stakingCheck.execute(); + Object.assign(ids, stakedIds); + + const pointCall = new Multicaller(network, provider, abi, { blockTag }); + Object.entries(ids).map(([path, id]) => { + pointCall.call(path, options.address, 'nftVeiZiAt', [id, timeStamp]); + }); + const points: Record = await pointCall.execute(); + + Object.entries(points).map(([path, point]) => { + const owner = path.split('-')[0]; + const tmp = Object.keys(ans).find((t) => { + return t === owner; + }); + const decimalPoint = Number( + Math.floor(parseFloat(formatUnits(point, options.decimals)) * 100) / 100 + ); + if (tmp !== undefined) { + ans[owner] = Number(ans[owner]) + decimalPoint; + } else { + Object.assign(ans, { [owner]: decimalPoint }); + } + }); + + return Object.fromEntries( + Object.entries(ans).map(([address, point]) => [ + address, + Math.floor(point / 10) * 10 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/schema.json b/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/schema.json new file mode 100644 index 00000000..f2cf0991 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/izumi-veizi/schema.json @@ -0,0 +1,33 @@ +{ + "$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. veiZi"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0xB56A454d8DaC2AD4cB82337887717a2a427Fcd00"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/README.md b/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/README.md new file mode 100644 index 00000000..0126da0f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/README.md @@ -0,0 +1,7 @@ +# jade-smrt + +This is a strategy for counting: +- JADE and sJADE token held on BSC +- SMRT and SMRTR token held on Avalanche c-chain (including the ones in +the SMRTR/AVAX pool), which are then weighted by the price_SMRT/price_JADE +and price_SMRTR/price_JADE \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/examples.json b/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/examples.json new file mode 100644 index 00000000..87af5aa7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/examples.json @@ -0,0 +1,73 @@ +[ + { + "name": "Jade-Smrt multichain voting", + "strategy": { + "name": "jade-smrt", + "params": { + "symbol": "jade-smrt", + + "JADE": { + "network": "56", + "address": "0x7ad7242A99F21aa543F9650A56D141C57e4F6081", + "decimals": 9 + }, + + "SJADE": { + "network": "56", + "address": "0x94CEA04C51E7d3EC0a4A97Ac0C3B3c9254c2aD41", + "decimals": 9 + }, + + "JADELP": { + "network": "56", + "address": "0x46503d91D7a41FCbDC250E84ceE9D457d082D7b4", + "decimals": 18 + }, + + "SMRTR": { + "network": "43114", + "address": "0x6D923f688C7FF287dc3A5943CAeefc994F97b290", + "decimals": 18 + }, + + "SMRT": { + "network": "43114", + "address": "0xCC2f1d827b18321254223dF4e84dE399D9Ff116c", + "decimals": 18 + }, + + "SMRTLP": { + "network": "43114", + "address": "0xf070843Ba9ed0ab85B0d15f9E8D67A5A8E073254", + "decimals": 18 + }, + + "SMRTRLP": { + "network": "43114", + "address": "0x7b7617c7b2236d7871741783cae8bcc222c2e05d", + "decimals": 18 + }, + + "avaxGraph": "https://api.thegraph.com/subgraphs/name/elkfinance/avax-blocks", + + "startBlocks": { + "56": 12858840, + "43114": 7639389 + } + } + }, + + "network": "56", + "addresses": [ + "0x422Cb45F4DCB3f798B53835F8671288D424d881F", + "0x0E916175eFa7633b5703cb9c50A99602e6c65530", + "0xbDe951E26aae4F39e20628724f58293A4E6457D4", + "0xA932857fA2Dbf18A8EeB50e359E99EDD40E27F7f", + "0xe0d7069685AF2F940D60685d93673Ee1141473C4", + "0xB0354be8EDD26d154dCf10BE3c47C88Ee6150DDB", + "0x20c204adad9C991ED35ce2778e705439227EA406", + "0x7AaCE7fb8CD4CEC3588F4a0b0A0DBdd470b20FbD" + ], + "snapshot": 13350104 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/index.ts b/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/index.ts new file mode 100644 index 00000000..8596b4c5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/jade-smrt/index.ts @@ -0,0 +1,170 @@ +import { subgraphRequest } from '../../utils'; +import { Multicaller, getProvider } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'drgorillamd'; +export const version = '1.0.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; + +const BUSD = '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56'; +const WAVAX = '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7'; +const USDC = '0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664'; +const WAVAXUSDC = '0xA389f9430876455C36478DeEa9769B7Ca4E3DDB1'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const block = await provider.getBlock(blockTag); + const timestamp = block.timestamp; + const avaxBlockTag = await getAvaxBlockTag(timestamp, options); + + // BSC balances: + const multiBsc = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address: string) => { + multiBsc.call(address + '-jade', options.JADE.address, 'balanceOf', [ + address + ]); + multiBsc.call(address + '-sjade', options.SJADE.address, 'balanceOf', [ + address + ]); + }); + + // BUSD per JADE spot - pick CAREFULLY the block height, it is NOT a twap + // as a twap would require 2 additionnal multicalls (and therefore be above the Snapshot 5 calls limit) + multiBsc.call('jadeLPBalance', options.JADE.address, 'balanceOf', [ + options.JADELP.address + ]); // jade balance in jade-busd pool + multiBsc.call('busdLPBalance', BUSD, 'balanceOf', [options.JADELP.address]); // BUSD balance in jade-busd pool + + // Avax balances: + const multiAvax = new Multicaller('43114', getProvider('43114'), abi, { + blockTag: avaxBlockTag + }); + addresses.forEach((address: string) => { + multiAvax.call(address + '-smrt', options.SMRT.address, 'balanceOf', [ + address + ]); + multiAvax.call(address + '-smrtR', options.SMRTR.address, 'balanceOf', [ + address + ]); + multiAvax.call(address + '-smrtRLp', options.SMRTRLP.address, 'balanceOf', [ + address + ]); + }); + + // WAVAX per SMRT spot + multiAvax.call('smrtLPBalance', options.SMRT.address, 'balanceOf', [ + options.SMRTLP.address + ]); // SMRT in SMRT/WAVAX pool balance + multiAvax.call('wavaxSmrtLPBalance', WAVAX, 'balanceOf', [ + options.SMRTLP.address + ]); // wavax in SMRT/WAVAX pool balance + + // WAVAX per SMRTR spot + multiAvax.call('smrtRLPBalance', options.SMRTR.address, 'balanceOf', [ + options.SMRTRLP.address + ]); + multiAvax.call('wavaxSmrtRLPBalance', WAVAX, 'balanceOf', [ + options.SMRTRLP.address + ]); // SMRT SMRT/WAVAX pool balance + + // USD per WAVAX spot + multiAvax.call('UsdLPBalance', USDC, 'balanceOf', [WAVAXUSDC]); // SMRT SMRT/WAVAX pool balance + multiAvax.call('wavaxUsdLPBalance', WAVAX, 'balanceOf', [WAVAXUSDC]); // SMRT SMRT/WAVAX pool balance + + // Avax SMRTR/WAVAX pool: LP token total supply + multiAvax.call('smrtRLPSupply', options.SMRTRLP.address, 'totalSupply', []); + + let resBsc: Record | number = { 0: 0 }, + resAvax: + | [number, number, number, Record, Record] + | number = [0, 0, 0, { 0: 0 }, { 0: 0 }]; + + [resBsc, resAvax] = await Promise.all([ + multiBsc.execute(), + multiAvax.execute() + ]); + + // All prices in USDish (BUSD or USDC.e) + const jadePrice: number = + parseFloat(formatUnits(resBsc['busdLPBalance'], 18)) / + parseFloat(formatUnits(resBsc['jadeLPBalance'], 9)); + const wavaxPrice: number = + parseFloat(formatUnits(resAvax['UsdLPBalance'], 6)) / + parseFloat(formatUnits(resAvax['wavaxUsdLPBalance'], 18)); + const smrtPrice: number = + wavaxPrice / + (parseFloat(formatUnits(resAvax['smrtLPBalance'], 18)) / + parseFloat(formatUnits(resAvax['wavaxSmrtLPBalance'], 18))); + const smrtRPrice: number = + wavaxPrice / + (parseFloat(formatUnits(resAvax['smrtRLPBalance'], 18)) / + parseFloat(formatUnits(resAvax['wavaxSmrtRLPBalance'], 18))); + const smrtRLPBalance: number = parseFloat( + formatUnits(resAvax['smrtRLPBalance'], 18) + ); + const smrtRLPSupply: number = parseFloat( + formatUnits(resAvax['smrtRLPSupply'], 18) + ); + + return Object.fromEntries( + addresses.map((adr: string) => { + let bal = parseFloat( + formatUnits(resBsc[adr + '-jade'], options.JADE.decimals) + ); + bal += parseFloat( + formatUnits(resBsc[adr + '-sjade'], options.SJADE.decimals) + ); + + // SMRT balance * SMRT price/JADE price: + const parsedSmrt = parseFloat( + formatUnits(resAvax[adr + '-smrt'], options.SMRT.decimals) + ); + bal += (parsedSmrt * smrtPrice) / jadePrice; + + // SMRTR balance * SMRTR price/JADE price: + const parsedSrmtr = parseFloat( + formatUnits(resAvax[adr + '-smrtR'], options.SMRTR.decimals) + ); + bal += (parsedSrmtr * smrtRPrice) / jadePrice; + + // LP token held * smrtr pool balance / LP token total supply: + const LPHeld = + (parseFloat(formatUnits(resAvax[adr + '-smrtRLp'], 18)) * + smrtRLPBalance) / + smrtRLPSupply; + bal += (LPHeld * smrtRPrice) / jadePrice; + + return [adr, bal]; + }) + ); +} + +async function getAvaxBlockTag(timestamp: number, options): Promise { + const query = { + blocks: { + __args: { + first: 1, + orderBy: 'number', + orderDirection: 'desc', + where: { + timestamp_lte: timestamp + } + }, + number: true, + timestamp: true + } + }; + const data = await subgraphRequest(options.avaxGraph, query); + return Number(data.blocks[0].number); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/README.md new file mode 100644 index 00000000..92436e6f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/README.md @@ -0,0 +1,3 @@ +# jpegd-locked-jpeg-of + +This strategy returns voters' aggregated locked JPEG token balance for Trait and LTV Boosts on the JPEG'd platform diff --git a/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/examples.json new file mode 100644 index 00000000..c1517e14 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "jpegd-locked-jpeg-of", + "params": {} + }, + "network": "1", + "addresses": [ + "0x9c5083dd4838e120dbeac44c052179692aa5dac5", + "0xB0dAfc466871c29662E5cbf4227322C96A8Ccbe9", + "0x0017dfe08bcc0dc9a323ca5d4831e371534e9320" + ], + "snapshot": 16119881 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/index.ts new file mode 100644 index 00000000..8a9bdad0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/jpegd-locked-jpeg-of/index.ts @@ -0,0 +1,70 @@ +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import { multicall } from '../../utils'; +import { subgraphRequest } from '../../utils'; + +export const author = '0xleez'; +export const version = '0.1.1'; + +const SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/jpegd/jpegd-core-mainnet' +}; + +const abi = [ + 'function traitBoostPositions(uint256 _nftIndex) view returns (address owner, uint256 unlockAt, uint256 lockedValue)', + 'function ltvBoostPositions(uint256 _nftIndex) view returns (address owner, uint256 unlockAt, uint256 lockedValue)' +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const params = { + jpeglocks: { + __args: { + where: { + owner_in: addresses.map((address) => address.toLowerCase()) + }, + block: blockTag != 'latest' ? { number: blockTag } : null + }, + owner: { id: true }, + collection: { id: true, nftValueProviderAddress: true }, + type: true, + nftIndex: true, + amount: true, + unlockTime: true + } + }; + + const result = await subgraphRequest(SUBGRAPH_URL[network], params); + const jpegLocks = result.jpeglocks ?? []; + + const responses = await multicall( + network, + provider, + abi, + jpegLocks.map((jpegLock: any) => [ + getAddress(jpegLock.collection.nftValueProviderAddress), + jpegLock.type === 'LTV' ? 'ltvBoostPositions' : 'traitBoostPositions', + [jpegLock.nftIndex] + ]), + { blockTag } + ); + + return responses.reduce((acc, response, index) => { + const jpegLock = jpegLocks[index]; + const address = getAddress(jpegLock.owner.id); + + if (!acc[address]) acc[address] = 0; + + const lockedJpeg = Number(formatUnits(response.lockedValue.toString(), 18)); + acc[address] += lockedJpeg; + return acc; + }, {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/juicebox/examples.json b/Implementations/API/backend/utils/snapshot/strategies/juicebox/examples.json new file mode 100644 index 00000000..2c7062d6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/juicebox/examples.json @@ -0,0 +1,59 @@ +[ + { + "name": "Juicebox Projects", + "strategy": { + "name": "juicebox", + "params": { + "projectId": "1", + "symbol": "JBX", + "protocolVersion": "1" + } + }, + "network": "1", + "addresses": [ + "0x63A2368F4B509438ca90186cb1C15156713D5834", + "0x6877be9E00d0bc5886c28419901E8cC98C1c2739", + "0x823b92d6a4b2AED4b15675c7917c9f922ea8ADAD", + "0x1e54110DC48dD83F48187b846DF7E32bB76d0D0b" + ], + "snapshot": 15971323 + }, + { + "name": "Juicebox Projects", + "strategy": { + "name": "juicebox", + "params": { + "projectId": "1", + "symbol": "JBX", + "protocolVersion": "2" + } + }, + "network": "1", + "addresses": [ + "0x63A2368F4B509438ca90186cb1C15156713D5834", + "0x6877be9E00d0bc5886c28419901E8cC98C1c2739", + "0x823b92d6a4b2AED4b15675c7917c9f922ea8ADAD", + "0x1e54110DC48dD83F48187b846DF7E32bB76d0D0b" + ], + "snapshot": 15971323 + }, + { + "name": "Juicebox Projects", + "strategy": { + "name": "juicebox", + "params": { + "projectId": "311", + "symbol": "GO", + "protocolVersion": "3" + } + }, + "network": "1", + "addresses": [ + "0x63A2368F4B509438ca90186cb1C15156713D5834", + "0x6877be9E00d0bc5886c28419901E8cC98C1c2739", + "0x823b92d6a4b2AED4b15675c7917c9f922ea8ADAD", + "0x1e54110DC48dD83F48187b846DF7E32bB76d0D0b" + ], + "snapshot": 15971421 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/juicebox/index.ts b/Implementations/API/backend/utils/snapshot/strategies/juicebox/index.ts new file mode 100644 index 00000000..0036ae46 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/juicebox/index.ts @@ -0,0 +1,44 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'drgorillamd'; +export const version = '0.2.0'; + +const JBTokenStore = { + 1: '0xee2eBCcB7CDb34a8A822b589F9E8427C24351bfc', + 2: '0xCBB8e16d998161AdB20465830107ca298995f371', + 3: '0x6FA996581D7edaABE62C15eaE19fEeD4F1DdDfE7' +}; +const abi = ['function balanceOf(address, uint256) view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.protocolVersion + ? JBTokenStore[options.protocolVersion] + : JBTokenStore['3'], + 'balanceOf', + [address, options.projectId] + ]), + { blockTag } + ); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/l2-deversifi/examples.json b/Implementations/API/backend/utils/snapshot/strategies/l2-deversifi/examples.json new file mode 100644 index 00000000..eb7c6c3c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/l2-deversifi/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "l2-deversifi", + "params": { + "symbol": "L2", + "api": "https://api.deversifi.dev/v1/trading/r/tokenBalancesHistory", + "token": "xDVF" + } + }, + "network": "3", + "addresses": [ + "0xd4fcde55c9bbe8eff062f92a59047393b44abaee", + "0x9ab450355b4ab504cbc0e4de484781dac08e6a26", + "0xed48ba4eb763b8514210b53cd272688c66175533" + ], + "snapshot": 10802061 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/l2-deversifi/index.ts b/Implementations/API/backend/utils/snapshot/strategies/l2-deversifi/index.ts new file mode 100644 index 00000000..4a1e5711 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/l2-deversifi/index.ts @@ -0,0 +1,51 @@ +import fetch from 'cross-fetch'; +import examplesFile from './examples.json'; + +export const author = 'deversifi'; +export const version = '0.1.0'; +export const examples = examplesFile; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const { api, token, limit = 300 } = options; + + const pages = Math.ceil(addresses.length / limit); + const promises: any = []; + + let api_url = api; + api_url += `?blockNumber=${snapshot}`; + api_url += `&token=${token}`; + + Array.from(Array(pages)).forEach((x, i) => { + const pageAddresses = addresses.slice(limit * i, limit * (i + 1)); + promises.push( + fetch(`${api_url}&addresses=${pageAddresses.join('&addresses=')}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }) + ); + }); + + const results = await Promise.all(promises); + const resultsJson = await Promise.all(results.map((r: any) => r.json())); + + const data: any = resultsJson.reduce((res: any, item: any) => { + if (item.score) { + return [...res, ...item.score]; + } + return res; + }, []); + + return Object.fromEntries( + data.map((value) => [value.address, parseFloat(value.score)]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/README.md b/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/README.md new file mode 100644 index 00000000..f9e27ac9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/README.md @@ -0,0 +1,13 @@ +# erc721 with tokenid scattered weights + +This strategy allows you to weight erc721's with different values from ipfs. + +Here is an example of parameters: + +```json +{ + "address": "0xEa25e2b3E35c67876957EE00a28Cd912ff113F54", + "symbol": "LAND", + "tokenWeightIPFS": "QmXrDNrjX8xZu9bdNA1iiW7Ee12RADQFwwhCeLQnwqG2TJ" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/examples.json b/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/examples.json new file mode 100644 index 00000000..f2f13772 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "landdao-token-tiers", + "params": { + "address": "0xEa25e2b3E35c67876957EE00a28Cd912ff113F54", + "symbol": "LAND", + "tokenWeightIPFS": "QmXrDNrjX8xZu9bdNA1iiW7Ee12RADQFwwhCeLQnwqG2TJ" + } + }, + "network": "1", + "addresses": [ + "0x863379Ab401d454834E1FE2eCe48F51a29eE9d7A", + "0x7374DDf849566c28372AD830DA45a1c9D088453B", + "0x0F98A3F6BB46e30013E8967b3615296c0adb9037", + "0x8e7795f783635ea08097d2aaf19da672bf79dde0", + "0xabab67aed584bdcfc6c9d59740b4181820cd04ac", + "0xbd8a92e249090249c5fb2ff71e47b69fb650d3af", + "0x67f72412a592d066a2e688e62664116deabeab29" + ], + "snapshot": 14100000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/index.ts b/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/index.ts new file mode 100644 index 00000000..1cb860f3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/landdao-token-tiers/index.ts @@ -0,0 +1,83 @@ +import fetch from 'cross-fetch'; +import { Multicaller } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'ethantddavis'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner) external view returns (uint256 balance)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // get token balance + const callWalletToBalanceOf = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToBalanceOf.call(walletAddress, options.address, 'balanceOf', [ + walletAddress + ]); + } + const walletToBalanceOf: Record = + await callWalletToBalanceOf.execute(); + + // get tokenIds + const callWalletToAddresses = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletAddress, count] of Object.entries(walletToBalanceOf)) { + for (let index = 0; index < count.toNumber(); index++) { + callWalletToAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToAddresses: Record = + await callWalletToAddresses.execute(); + + // fetch ipfs tier weights + const response = await fetch( + 'https://ipfs.io/ipfs/' + options.tokenWeightIPFS, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + } + ); + const weights = await response.json(); + + // sum the weights for each token ID + const walletToLpBalance = {} as Record; + for (const [walletID, tokenId] of Object.entries(walletIDToAddresses)) { + const address = walletID.split('-')[0]; + + const tokenIdValue = weights[tokenId.toString()]; + + walletToLpBalance[address] = walletToLpBalance[address] + ? walletToLpBalance[address].add(BigNumber.from(tokenIdValue)) + : BigNumber.from(tokenIdValue); + } + + return Object.fromEntries( + Object.entries(walletToLpBalance).map(([address, balance]) => [ + address, + balance.toNumber() + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/liftkitchen/examples.json b/Implementations/API/backend/utils/snapshot/strategies/liftkitchen/examples.json new file mode 100644 index 00000000..80385771 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/liftkitchen/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "liftkitchen", + "params": { + "ctrl": "0xA31fDbaA772745D11843EFEDA9922dcbf5460672", + "boardroom": "0x3223689b39Db8a897a9A9F0907C8a75d42268787", + "symbol": "CTRL", + "decimals": 18 + } + }, + "network": "1", + "addresses": ["0x3223689b39Db8a897a9A9F0907C8a75d42268787"], + "snapshot": 12377525 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/liftkitchen/index.ts b/Implementations/API/backend/utils/snapshot/strategies/liftkitchen/index.ts new file mode 100644 index 00000000..72a0653d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/liftkitchen/index.ts @@ -0,0 +1,135 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'Gruffin'; +export const version = '0.1.1'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'getbalanceOfControl', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'earned', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + }, + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const queries: any[] = []; + + const addressCount = addresses.length; + + addresses.forEach((address) => { + queries.push([options.ctrl, 'balanceOf', [address]]); + }); + + addresses.forEach((address) => { + queries.push([options.boardroom, 'getbalanceOfControl', [address]]); + }); + + addresses.forEach((address) => { + queries.push([options.boardroom, 'earned', [address]]); + }); + + const response = await multicall(network, provider, abi, queries, { + blockTag + }); + + const ctrlOwned = response.slice(0, addressCount); + const ctrlStaked = response.slice(addressCount, addressCount * 2); + const ctrlEarned = response.slice(addressCount * 2, addressCount * 3); + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + const score = ctrlOwned[i][0] + .add(ctrlStaked[i][0]) + .add(ctrlEarned[i][0]) + .add(ctrlEarned[i][1]); + + return [ + addresses[i], + parseFloat(formatUnits(score.toString(), options.decimals)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/linear-vesting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/linear-vesting-power/examples.json new file mode 100644 index 00000000..24396811 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/linear-vesting-power/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "linear-vesting-power", + "params": { + "DSSVestAddress": "0x6017dd61f4d0C8123f160F99058Adc5671dF6447", + "decimals": 18, + "vestingNetwork": 1, + "cliffDuration": 15768000 + } + }, + "network": "1", + "addresses": [ + "0xd9286ab8Ac06a428Bbb45Bfd785f9A22FD0ef0Bd", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 15076584 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/linear-vesting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/linear-vesting-power/index.ts new file mode 100644 index 00000000..6c03e96e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/linear-vesting-power/index.ts @@ -0,0 +1,139 @@ +import { call, Multicaller } from '../../utils'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'morpho-labs'; +export const version = '0.1.0'; + +const DSSVestAbi = [ + 'function usr(uint256 _id) external view returns (address)', + 'function tot(uint256 _id) external view returns (uint256)', + 'function accrued(uint256 _id) external view returns (uint256)', + 'function bgn(uint256 _id) external view returns (uint256)', + 'function fin(uint256 _id) external view returns (uint256)', + 'function ids() external view returns (uint256)' +]; + +const vestedAmountPower = ( + totalVestedNotClaimed: BigNumberish, + startDate: BigNumberish, + period: BigNumberish, + now: BigNumberish +) => { + now = BigNumber.from(now); + const amount = BigNumber.from(totalVestedNotClaimed); + if (now.lte(startDate)) return BigNumber.from(0); + if (now.gt(BigNumber.from(startDate).add(period))) + return totalVestedNotClaimed; + return now.sub(startDate).mul(amount).div(period); +}; + +const idsArray = (maxId: number) => + Array.from({ length: maxId }, (_, i) => i + 1); + +/** + * @notice This strategy returns the total amount of vested tokens for a given user + * The particularity of this strategy is the ability to skip the cliff period for the voting power. + * So if Alice has a vesting line starting on August 2022, with a cliff period of 6 months, + * a vesting period of 3 years and a total amount of 1000 tokens, she is going to accumulate voting power from August 2022, + * and in January 2023, she will have 1/6 of the total voting power, i.e. 1000/6 = 166.66 voting power. + * As a consequence, any voter has its full voting power from the vesting contract 6 month before the end of the vesting period. + * + * @notice In order to handle claimed tokens, we are linearizing the amount accumulated and not claimed by the user from the beginning + * of the vesting power distribution (start - cliff) to the current date, at the rate of the vesting duration. + * So if Alice is claiming 166 tokens on August 2023 (6 months after the beginning of the distribution), whe are going to have + * 1000 - 166 = 834 tokens left to linearize from (start - cliff) to August 2023. This is an approximation of the voting power from + * the vesting amount. + */ +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const block = await provider.getBlock(blockTag); + const now = block.timestamp; + // fetch the number of vesting accounts + const maxId: BigNumber = await call( + provider, + DSSVestAbi, + [options.DSSVestAddress, 'ids', []], + { + blockTag + } + ); + + // create an array of each vesting ID: [1:maxId] + const ids = idsArray(maxId.toNumber()); + + // And then, we fetch the vesting data for each vesting ID + // 1. vester address + const multiVest = new Multicaller( + options.vestingNetwork, + provider, + DSSVestAbi, + { + blockTag + } + ); + + ids.forEach((id) => multiVest.call(id, options.DSSVestAddress, 'usr', [id])); + const vestedAddresses: Record = await multiVest.execute(); + + // 1. total vested + // 2. total claimed + // 3. beginning time (after cliff period: bgn = startVesting + cliff) + // 4. end time (with cliff period: end = startVesting + cliff + duration = bgn + duration) + + const multiVestCaller = new Multicaller( + options.vestingNetwork, + provider, + DSSVestAbi, + { blockTag } + ); + ids.forEach((id) => { + multiVestCaller.call('tot' + id, options.DSSVestAddress, 'tot', [id]); + multiVestCaller.call('accrued' + id, options.DSSVestAddress, 'accrued', [ + id + ]); + multiVestCaller.call('bgn' + id, options.DSSVestAddress, 'bgn', [id]); + multiVestCaller.call('fin' + id, options.DSSVestAddress, 'fin', [id]); + }); + + const multiVestResult: Record = + await multiVestCaller.execute(); + + return Object.fromEntries( + addresses.map((address) => { + const initialVotingPower = [address, 0]; + // fetch vested users data + const ids = Object.entries(vestedAddresses) + .filter(([, _address]) => _address === address) + .map(([id]) => id); + + if (!ids.length) return initialVotingPower; + const votingPower = ids.reduce((vp, id) => { + const totalVested = multiVestResult['tot' + id]; + const totalAccrued = multiVestResult['accrued' + id]; + + const bgn = multiVestResult['bgn' + id]; + const fin = multiVestResult['fin' + id]; + if (!(id && totalAccrued && totalVested && bgn && fin)) return vp; + return vp.add( + vestedAmountPower( + BigNumber.from(totalVested).sub(totalAccrued), + // Here, we are slicing the start to start minus cliff duration, in order to skip the cliff for the voting power calculation + BigNumber.from(bgn).sub(options.cliffDuration), + BigNumber.from(fin).sub(bgn).add(options.cliffDuration), + now + ) + ); + }, BigNumber.from(0)); + return [address, parseFloat(formatUnits(votingPower, options.decimals))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/README.md b/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/README.md new file mode 100644 index 00000000..558a4b0d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/README.md @@ -0,0 +1,22 @@ +# Liquidity Providers + + +Fork from infinityprotocol-liquidity-pools. Fix there is some protocol can't query `users` issue. + +This strategy will return the scores of all users who have provided token liquidity on any Uniswap style exchange. Users can change the subGraphURL field to direct their request to a different subgraph. + + +## Example + +The space config will look like this: + +```JSON +{ + "address": "0xffffffff2ba8f66d4e51811c5190992176930278", + "symbol": "COMBO" + // subgraphURL for the request + "subGraphURL": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2", + // scoreMultiplier can be used to increase users' scores by a certain magnitude + "scoreMultiplier": 1, +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/examples.json b/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/examples.json new file mode 100644 index 00000000..3f7590ed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "liquidity-token-provide", + "params": { + "address": "0xffffffff2ba8f66d4e51811c5190992176930278", + "symbol": "COMBO" + } + }, + "network": "1", + "addresses": [ + "0xe4ef29545db14e252AeC1c660A004e2408Dc62d2", + "0xa3c1c91403f0026b9dd086882adbc8cdbc3b3cfb" + ], + "snapshot": 14716396 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/index.ts b/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/index.ts new file mode 100644 index 00000000..800b30d0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/liquidity-token-provide/index.ts @@ -0,0 +1,77 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/dinngodev/furucombo-tokenomics-mainnet', + '137': + 'https://api.thegraph.com/subgraphs/name/dinngodev/furucombo-tokenomics-polygon' +}; + +export const author = 'weizard'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + liquidityPositions: { + __args: { + where: { + user_in: addresses.map((address) => address.toLowerCase()), + liquidityTokenBalance_gt: 0 + } + }, + liquidityTokenBalance: true, + user: { + id: true + }, + pair: { + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.liquidityPositions.__args.block = { number: snapshot }; + } + const tokenAddress = options.address.toLowerCase(); + const result = await subgraphRequest( + options.subGraphURL ? options.subGraphURL : SUBGRAPH_URL[network], + params + ); + const score = {}; + if (result && result.liquidityPositions) { + result.liquidityPositions.forEach((lp) => { + if ( + lp.pair.token0.id !== tokenAddress && + lp.pair.token1.id !== tokenAddress + ) + return; + let userScore = + lp.pair.token0.id == tokenAddress + ? (lp.pair.reserve0 / lp.pair.totalSupply) * lp.liquidityTokenBalance + : (lp.pair.reserve1 / lp.pair.totalSupply) * lp.liquidityTokenBalance; + if (options.scoreMultiplier) { + userScore = userScore * options.scoreMultiplier; + } + const userAddress = getAddress(lp.user.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScore; + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/README.md b/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/README.md new file mode 100644 index 00000000..316fc440 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/README.md @@ -0,0 +1,5 @@ +# lit-dao-governance + +This strategy is specific to lit-dao and should not be used in any context outside of the official LIT snapshot space. + +There are no parameters for this strategy diff --git a/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/examples.json new file mode 100644 index 00000000..76cdb8ad --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "lit-dao-governance", + "params": { + "symbol": "LIT" + } + }, + "network": "1", + "addresses": [ + "0xc2e5e9Cc4F26Dc179C3f386e702283576B5157Fb", + "0x515fdd6f50cb6330399f5948f6422d8e27af012d", + "0xe129ed3b7Cb2AccE3A760bf66046ab53DF9D689d" + ], + "snapshot": 16388642 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/index.ts new file mode 100644 index 00000000..55ba5f6f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/index.ts @@ -0,0 +1,102 @@ +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import { multicall } from '../../utils'; +import { strategy as masterchefStrategy } from '../masterchef-pool-balance'; + +export const author = 'wizardlabsxyz'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const degenContract = '0x5f5541C618E76ab98361cdb10C67D1dE28740cC3'; + + // Use the existing masterchef strategy to get the staked lit balance + const stakedLitResults = await masterchefStrategy( + space, + network, + provider, + addresses, + { + pid: '0', + symbol: 'Staked LIT', + weight: 1, + tokenIndex: null, + chefAddress: '0x6c4f932a367ebbfef5528022459b47274618aaaf', + uniPairAddress: null, + weightDecimals: 0 + }, + snapshot + ); + + // Book + // LP2 + // LP2F + // Squiggle + // Degenaissance + const contracts: string[] = [ + '0x915bDf48e61fB3Cb39c8339Fb10108D9B596171C', + '0xFC0946B334B3bA133D239207a4d01Da1B75CF51B', + '0x76723D9524a743d8908458082FBdFAAf7F60B3eD', + '0x6F75bEAa3D3d8A15e08a9F499464C696fC4D4cde', + + // DEGEN HAS TO BE LAST FOR THE MULTIPLIER + degenContract + ]; + + const calls: any[] = []; + contracts.forEach((contract) => { + addresses.forEach((address: any) => { + calls.push([contract, 'balanceOf', [address]]); + }); + }); + + const response = await multicall(network, provider, abi, calls, { blockTag }); + + const merged = {}; + const degenOwnership = {}; + response.map((value: any, i: number) => { + const address = calls[i][2][0]; + merged[address] = (merged[address] || 0) as number; + + const parsedValue = parseFloat(formatUnits(value.toString(), 0)); + if (calls[i][0] == degenContract && parsedValue > 0) { + // Track degen ownership to apply multiplier to staked lit balance later + degenOwnership[address] = true; + + // Apply degen multiplier to NFT based voting power + merged[address] *= 2; + } else { + merged[address] += parsedValue; + } + }); + + Object.keys(stakedLitResults).map((address: any) => { + const stakedLitVotingPower = Math.floor( + stakedLitResults[address] / 3000000 + ); + if (degenOwnership[address]) { + merged[address] += stakedLitVotingPower * 2; + } else { + merged[address] += stakedLitVotingPower; + } + }); + + return Object.fromEntries( + Object.entries(merged).map((address: any) => [ + getAddress(address[0]), + address[1] + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/schema.json b/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/schema.json new file mode 100644 index 00000000..2f5ed9c6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lit-dao-governance/schema.json @@ -0,0 +1,20 @@ +{ + "$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. DOODLE"], + "maxLength": 16 + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/README.md b/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/README.md new file mode 100644 index 00000000..724a9c3f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/README.md @@ -0,0 +1,18 @@ +# lodestar-staked-lp + +Used for fetching the staked LP token balance in a single staking pool and calculating +associated voting power. Voting power per staked LP token is calculated in terms of a "token weight", or the amount of the voting token that comprises 1 LP token (total amount of voting token in LP pool * 2 / total supply of LP tokens). The address' balance of LP tokens is multiplied by token weight to yield total voting power from staked LP tokens. + +Here is an example of parameters: + +```json +{ + "stakingPoolAddresses": [ + "0x4Ce0C8C8944205C0A134ef37A772ceEE327B4c11" + ], + "tokenAddress": "0xF19547f9ED24aA66b03c3a552D181Ae334FBb8DB", + "lpTokenAddress": "0xFB36f24872b9C57aa8264E1F9a235405C4D3fC36", + "symbol": "LODE", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/examples.json new file mode 100644 index 00000000..1f8e3c88 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "lodestar-staked-lp", + "params": { + "stakingPoolAddresses": ["0x4Ce0C8C8944205C0A134ef37A772ceEE327B4c11"], + "tokenAddress": "0xF19547f9ED24aA66b03c3a552D181Ae334FBb8DB", + "lpTokenAddress": "0xFB36f24872b9C57aa8264E1F9a235405C4D3fC36", + "symbol": "LODE", + "decimals": 18 + } + }, + "addresses": [ + "0xcada41bdb95227ee3f3c2f4a37a843d384fbb48d", + "0xdee375c0976e1e5b5a2cfdf1846eb055d6d107ed", + "0xa79456cc03fc4eca28ecd675988a6c78e421e12e" + ], + "network": 42161, + "snapshot": 44754575 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/index.ts new file mode 100644 index 00000000..9825d5b4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lodestar-staked-lp/index.ts @@ -0,0 +1,90 @@ +/* eslint-disable prettier/prettier */ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, multicall } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = '0xAppo'; +export const version = '0.1.0'; + + +const abi = [ + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)', + 'function balanceOf(address account) external view returns (uint256)', + 'function decimals() external view returns (uint8)', + 'function token0() external view returns (address)', + 'function token1() external view returns (address)', + 'function totalSupply() external view returns (uint256)', + 'function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const fetchContractData = await multicall( + network, + provider, + abi, + [ + [options.lpTokenAddress, 'token0', []], + [options.lpTokenAddress, 'token1', []], + [options.lpTokenAddress, 'getReserves', []], + [options.lpTokenAddress, 'totalSupply', []], + [options.lpTokenAddress, 'decimals', []], + [options.tokenAddress, 'decimals', []] + ], + { blockTag } + ); + + const token0Address = fetchContractData[0][0]; + const token1Address = fetchContractData[1][0]; + const lpTokenReserves = fetchContractData[2]; + const lpTokenTotalSupply = fetchContractData[3][0]; + const lpTokenDecimals = fetchContractData[4][0]; + const tokenDecimals = fetchContractData[5][0]; + + let tokenWeight; + + if (token0Address === options.tokenAddress) { + tokenWeight = + (parseFloat(formatUnits(lpTokenReserves._reserve0, tokenDecimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals))) * + 2; + } else if (token1Address === options.tokenAddress) { + tokenWeight = + (parseFloat(formatUnits(lpTokenReserves._reserve1, tokenDecimals)) / + parseFloat(formatUnits(lpTokenTotalSupply, lpTokenDecimals))) * + 2; + } else { + tokenWeight = 0; + } + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + if (options.stakingPoolAddresses.length >= 10) { + throw new Error('Too many stake pool addresses provided.') + } + + options.stakingPoolAddresses.forEach(stakingPoolAddress => { + addresses.forEach((address) => + multi.call(address, stakingPoolAddress, 'userInfo', [address]) + ); + }) + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, userInfo]) => [ + getAddress(address), + parseFloat(formatUnits(userInfo.amount, options.decimals)) * tokenWeight + ]) + ); +} + + diff --git a/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/README.md b/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/README.md new file mode 100644 index 00000000..0bfde7b7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/README.md @@ -0,0 +1,19 @@ +# lodestar-vesting + +Calculates voting power of locked & vesting ERC20 tokens. The indices of the beneficiary addresses and their vesting contract addresses MUST match in order to properly match beneficiaries with their associated vesting contracts. + +Here is an example of parameters: + +```json +{ + "address": "0xF19547f9ED24aA66b03c3a552D181Ae334FBb8DB", + "symbol": "LODE", + "decimals": 18, + "beneficiaryAddresses": [ + "0x41C2F1Af5a4a4C65b580c1397141684F96B68aAb" + ], + "contractAddresses": [ + "0x658fD8f0e4380c9823C6a18974096A2b2aC8842e" + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/examples.json new file mode 100644 index 00000000..3fe70385 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/examples.json @@ -0,0 +1,30 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "lodestar-vesting", + "params": { + "address": "0xF19547f9ED24aA66b03c3a552D181Ae334FBb8DB", + "symbol": "LODE", + "decimals": 18, + "beneficiaryAddresses": [ + "0x719Df6573bfA3bD240932cAD63839cfB85de3aB5", + "0xeAc36dF6A6212CC1D5c59b6F59547eeC083A1bDa", + "0x85EB5a2077E88559B8A63206Fbc9A9311f8e63a5" + ], + "contractAddresses": [ + "0x71C85F343715C406C58c1e8099F13890f2925c85", + "0x2DD5B039a7c54132B8733573a28Cd9d1a5Fa5328", + "0x05bc2c8310D18dB816264E95383b1C50FC32d297" + ] + } + }, + "network": "42161", + "addresses": [ + "0xeAc36dF6A6212CC1D5c59b6F59547eeC083A1bDa", + "0x719Df6573bfA3bD240932cAD63839cfB85de3aB5", + "0x85EB5a2077E88559B8A63206Fbc9A9311f8e63a5" + ], + "snapshot": 44776525 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/index.ts new file mode 100644 index 00000000..5f8542db --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lodestar-vesting/index.ts @@ -0,0 +1,46 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = '0xAppo'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + if ( + options.beneficiaryAddresses.length >= 25 || + options.contractAddresses.length >= 25 + ) { + throw new Error('Too many beneficiary/contract addresses provided.'); + } + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => { + if (options.beneficiaryAddresses.includes(address)) { + const index = options.beneficiaryAddresses.indexOf(address); + const contractAddress = options.contractAddresses[index]; + multi.call(address, options.address, 'balanceOf', [contractAddress]); + } + }); + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + getAddress(address), + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/loot-character-guilds/examples.json b/Implementations/API/backend/utils/snapshot/strategies/loot-character-guilds/examples.json new file mode 100644 index 00000000..731bc657 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/loot-character-guilds/examples.json @@ -0,0 +1,145 @@ +[ + { + "name": "Loot Character Guild ", + "strategy": { + "name": "loot-character-guilds", + "params": { + "symbol": "LOOT", + "lootAddress": "0xFF9C1b15B16263C61d017ee9F65C50e4AE0113D7", + "lootCharacterGuildsAddress": "0xBbb55b52d2186C4dd99cDba7AF483459E517BE3d", + "guildId": 5 + } + }, + "network": "1", + "addresses": [ + "0x08D816526BdC9d077DD685Bd9FA49F58A5Ab8e48", + "0xF4b0498e71485717f6f9b6117672C19A0dbA5100", + "0x8e246c8c37c82D3F89726773e58b43BAa8614214", + "0x9Ed0318B4d343390B47C92FE46E35f28D2edcffc", + "0x808700A1eDFEDa468d34d08cA8a0397bad2298d9", + "0x87E386cEdb82CF8F080b3505ADaE1eF699e65057", + "0x628061dE62c327A8be104f92746c56645995eAe7", + "0x6c0D14C67CCa5E944f2eB168EC4196e2c4658068", + "0xc0Bbc17910BafCAB69F6bEF8574C7625134df13f", + "0xc887fAdC6baAa8cD234d9104A65b37265522b024", + "0x7896062Cbee5B512ea2b5A6182d1c89438aaeE5f", + "0x2d639D4A4D471FB54dA6d88dEf2d263acF3108A8", + "0x06Ac1F9f86520225b73EFCe4982c9d9505753251", + "0x38Cb027d65AaF2d3f401Ff9bD0599AFe86F1b627", + "0x7CcaCab19ee2B7EC643Ef2d436c235A5c1E76Fa9", + "0xF090DC8364cdA35c8E911336026BCe394539c9D3", + "0xA153dcFE203509CF292634Ac3BcaF5Ca1D4CC517", + "0x92572aae1c9C4BA92011a6A56579c79dBb272Cd8", + "0x9c88e9bd9daE5Cc0a7A2034DB653D668a3b0b3F9", + "0xf27822E9AB6DA97481E6E1533a2314eDC44F41E4", + "0x84AB05F09B5ad3a1de6941FBf29BdF77CC7E2100", + "0x8A7f7C5b556B1298a74c0e89df46Eba117A2F6c1", + "0x80778937998a7b9D80Ae116918242065eD1a1d22", + "0xB62E9f2e6babC43F2e0E38FcF951Dc546706B191", + "0x21316179A55c963171f55A154c11Ce7314d9DAa6", + "0x47e5C9e909133DbB1d705286842d2DE1f89536A3", + "0x27824f56bb0e722bDe0a6B1b28dFb2FE9D4285ca", + "0xff563d18B082aE0EE76bce29bCbc7FD86Dc0Eb9A", + "0xE0BD70803045a9D877D39bb3Cd44a5d59a378663", + "0x06B3564ef920FD55f19f95EB7114EAb3eE83A192", + "0x7e9045bE45bAD250d06416a877f00320971c97E0", + "0xba818e7F6b208F5A4f7Cf282A9248D221144E4e5", + "0x96774B92C7D0CC97a83AF06Ce68195Fa68590270", + "0x360cbbdd4e42683Ba40Ee6A9BDAb3E4Dd6f27087", + "0x4b2AcB5A7FCfeE322Ce3Cd8344Ea466954Fde7dB", + "0x7250982aee4667254a47916Dc4523537EE59fe8B", + "0xbbd75dB64A9674D35DAD240811bA0880AD0E106F", + "0xd6CB70a88bB0D8fB1be377bD3E48e603528AdB54", + "0xC6c41119Af1e0840357245c66baAf0e21B694D4d", + "0x84Ead439d77BA323C40b615283AED434180df2D0", + "0x521CcFf42a8277b6b5D29c0AcbEC2Cdd3Af9C332", + "0x6F1B12a415E035eEbDc8559130eA8bdb96ADd48c", + "0x8be902e0873a2a8bf8a4218e75c7e2b4419d0d04", + "0x23324ed44904260fE555B18E5Ba95C6030B9227d", + "0x1668c9725e27Bf5943bBD43886E1Fb5AFe75c46C", + "0xfB843f8c4992EfDb6b42349C35f025ca55742D33", + "0x5793b2C0B3bf7C9cf9e73C24EE9F7230bE057d08", + "0x4cFde2011629A443A77F6a9137186d6B36C39938", + "0x7e023390133398fEA29AB8Fa950C3D3e36939E08", + "0x1e341Aa44c293d95d13d778492D417D1BE4E63D5", + "0x512fce9B07Ce64590849115EE6B32fd40eC0f5F3", + "0x957291Eae3d53b5A7c0A6BF2C0F08de5d87b14f6", + "0xA351a4FffCeed60b6d4351e1B20C55E3A6fB5503", + "0x175554B30E4dc15F0875A1ee00F137eac9461a46", + "0x8404EFA98a4e129855c200FC4e90c257078d9eD3", + "0xB670768c72510493AC1Fd2B9AfF66DE1F669E05E", + "0xaFbc3F98EEDB5f9A25a4AB2232d1346612efE77C", + "0x089FCE8bE711CABc806d69B1d0d5aebd52A06455", + "0xDBfb45ac11CE31757E09e96e0820831C6cff37af", + "0x08F78C6228879B2839fcF84eD3c22E9a30A5A2D3", + "0x832668b0fc7D007f5C40375457ce147D6235cA9D", + "0x380E5b655d459A0cFa6fa2DFBbE586bf40DcFd7F", + "0xF296178d553C8Ec21A2fBD2c5dDa8CA9ac905A00", + "0xA494876207Ae80D8669Dd347Fe11Fadae31c48E7", + "0x1ec9503BbdaD721EF3aD485b4B14Ce0671Dc9925", + "0xBF8AfA76BcC2f7Fee2F3b358571F426a698E5edD", + "0x87D9C2D9990A9f3A18F1377Ff20d9945F9eB3792", + "0x8e3e4f8884C093cc2df3BC4D49b3Ab3d558b4e13", + "0x32Ce58aa25161e9D53010cE33295A05037F6e69c", + "0x003F35595dce3187B4Fff2B5A2c4303f7158208a", + "0x74B78e98093F5B522A7eBDAc3B994641cA7c2b20", + "0x5069304255FF9b5f0d57b59F79A8865b4e622279", + "0x392027fDc620d397cA27F0c1C3dCB592F27A4dc3", + "0x5090c4Fead5Be112b643BC75d61bF42339675448", + "0xf5f0B993d69e3E6480f7F5c2531154eCf60278cF", + "0xD9Ec0058d49254E8E7e8f021A056865C3a0A2C0b", + "0xeB369F468d91E575fD30712a0ABfcF99FdF75e30", + "0x14A03CA8740A0044E63d3Bb0432540d9509473d1", + "0xc6025ED82cf2f3d87595195Ed6a1ae1a5a94Ecee", + "0xd6464a34847188540EB0860d660866F732C38744", + "0xa1122D9fbe7f60fE8e328FE5Bb1534D72fBD3B59", + "0x2Afc91a1c9a94dCDabc5dDe6A7ab810668C49583", + "0xF73FE15cFB88ea3C7f301F16adE3c02564ACa407", + "0x4654F56a64301b9b582F843F97332D96Ead11FF8", + "0x3aDFA4AB755E96b84cC7a00F8f375D7A4A203E8a", + "0x98733a6517cDb5da94a2115d5AddD1C386f7Ef8f", + "0xD5520354A956A736F67c3F03c7755Ff0486D9F0e", + "0x78AB747D6675AB1dfeE31D0BfB6862124565AefC", + "0xB0623C91c65621df716aB8aFE5f66656B21A9108", + "0x8b7a5B22175614EE194E9e02e9fE0A1B5414C75E", + "0x649f69CCd077Da03dFb11f4b1daAb4B625f5E9A3", + "0x9623A7dfb605f693C9725278cF78bd2F53F0D407", + "0xa4FBcd39Dc0E8c6B1c3d89751fF86855d8F5b98d", + "0x9433601511580b7bd3F875C57E5a82Bf9A6a5997", + "0x21631d18d9681D4fFDd460FC45fA52159fCd95c8", + "0x1eE199C1832D4cFd075ad2708FEB75fC8c80b8C6", + "0x52932f5B2767D917c3134140168f2176C94E8B2C", + "0x1cC1983396E55e925b777A0D70dFe8b56EdEE942", + "0xa8dd3c464c57881ee0411b7a5Ee9aC5430E83122", + "0xdBdc061Ff54314f0A394f6F850fF2415c8D1DF1b", + "0xE1D49F083207eEed71414c0AA28236e97b1793c0", + "0xC823127743196981af46bC542028C3578e674C4e", + "0x9E9EF0B615d4aF21C01121273498Ad5DEB5A3785", + "0xC8B810Fe39952AA65CA5A9387a3546B9b6bF5780", + "0xb61193014Fc983b3475d6bF365B7647c2E52b713", + "0x34978fAf3A9f469da7248d1365Ddf69Ac099588C", + "0x2C90f88BE812349fdD5655c9bAd8288c03E7fB23", + "0x1dBA5711CC83D2F6502eFBfeBAc48B14714cCE2e", + "0x18C2f8a7e2696d28661D59785B18C6794cd8D8e1", + "0xdCf809BBa1d9ee75c6991f1E2974163cF2555418", + "0x080d222fc30aC35f52d169faa9062Ec29D13d187", + "0x138d65D1ca31A707E06ef148707b7Cee5f85E11E", + "0xF654Cbd0d7B1765091b035BBB0205Dd4b6b49C7F", + "0x03B79C0C1487a68AeabD9AA4ce779DaD77855F52", + "0x0fb95b8C2d19F1Cbd2dfe9ECB3BdF992cB9E8b92", + "0x091e2f9422A22a09E478455Fd59bE8CB6ee56463", + "0xA5Cc7F3c81681429b8e4Fc074847083446CfDb99", + "0xda6BB8C5507aff8C9eD9d787c1e8A82A0a79d629", + "0xfB33ad9bBA1612Fd9deFb207cdA9a56263b55743", + "0x596E7fe8a3FD3AB4dEdC227c848569a0265828Ff", + "0x1015F376319B48C16078339534Fb9D83F75Bf15C", + "0xAf751f153503D726FdAce94558FF4974CC688bDc", + "0x8a32Af4AfFD816978C2Fdfa7C08c27681d05d4Fe", + "0x40415c1c0E7a8ff3E184b51A94a2Fc5B92Df9434", + "0xd30874ca82901D41FE66deD3012909D81924a0B8", + "0x890F1815a0935B10126bcfe6Dd48CE37eD3064ed", + "0xbbe3aba57ac2a5dc36eBb9769fCbD53FD5153F93" + ], + "snapshot": 13315111 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/loot-character-guilds/index.ts b/Implementations/API/backend/utils/snapshot/strategies/loot-character-guilds/index.ts new file mode 100644 index 00000000..f9988a2d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/loot-character-guilds/index.ts @@ -0,0 +1,82 @@ +import { Multicaller } from '../../utils'; + +export const author = 'jordanmessina'; +export const version = '0.1.0'; + +const lootAbi = [ + 'function balanceOf(address owner) external view returns (uint256 balance)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId)' +]; + +const lootCharacterGuildsAbi = [ + 'function guildLoots(uint256 tokenId) external view returns (uint256 guild)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const lootBalanceOfMulti = new Multicaller(network, provider, lootAbi, { + blockTag + }); + addresses.forEach((address) => { + lootBalanceOfMulti.call(address, options.lootAddress, 'balanceOf', [ + address + ]); + }); + const lootBalanceOfResult = await lootBalanceOfMulti.execute(); + + const lootTokenOwnerMulti = new Multicaller(network, provider, lootAbi, { + blockTag + }); + for (const [address, balance] of Object.entries( + lootBalanceOfResult + )) { + for (let i = 0; i < balance; i++) { + lootTokenOwnerMulti.call( + `${address}-${i}`, + options.lootAddress, + 'tokenOfOwnerByIndex', + [address, String(i)] + ); + } + } + const lootBagOwners = await lootTokenOwnerMulti.execute(); + + const guildLootsMulti = new Multicaller( + network, + provider, + lootCharacterGuildsAbi, + { blockTag } + ); + for (const [addressAndBagIndex, bagId] of Object.entries(lootBagOwners)) { + guildLootsMulti.call( + addressAndBagIndex, + options.lootCharacterGuildsAddress, + 'guildLoots', + [String(bagId)] + ); + } + const lootOwnerToGuild = await guildLootsMulti.execute(); + + const votes = {}; + for (const [addressAndBagIndex, guild] of Object.entries(lootOwnerToGuild)) { + if (String(guild) === String(options.guildId)) { + const address = addressAndBagIndex.split('-')[0]; + votes[address] = address in votes ? votes[address] + 1 : 1; + } + } + + return Object.fromEntries( + addresses.map((address: any) => [ + address, + address in votes ? votes[address] : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/README.md b/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/README.md new file mode 100644 index 00000000..97f94956 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/README.md @@ -0,0 +1,4 @@ +# lqty-proxy-stakers + +This is a strategy that returns how much LQTY a user is staking via his DSProxy + diff --git a/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/examples.json new file mode 100644 index 00000000..971877d8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Lqty proxy stakers", + "strategy": { + "name": "lqty-proxy-stakers", + "params": { + "proxyRegistryAddr": "0x4678f0a6958e4D2Bc4F1BAF7Bc52E8F3564f3fE4", + "lqtyStakingAddr": "0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d" + } + }, + "network": "1", + "addresses": [ + "0x352c7DCcA6dD89A75efCBebc77BCF8Efb2128248", + "0x554Fe9292Cd2E2b9469E19e814842C060312FF00", + "0x39e99d18634E63Df7867287E3059F39b6C6f428d" + ], + "snapshot": 16718874 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/index.ts new file mode 100644 index 00000000..217f692d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lqty-proxy-stakers/index.ts @@ -0,0 +1,50 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'majkic99'; +export const version = '0.1.0'; + +const abiStaking = ['function stakes(address) public view returns (uint256)']; +const abiProxyRegistry = [ + 'function proxies(address) public view returns (address)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const proxyMulti = new Multicaller(network, provider, abiProxyRegistry, { + blockTag + }); + addresses.forEach((address) => + proxyMulti.call(address, options.proxyRegistryAddr, 'proxies', [address]) + ); + const proxyAddresses: Record = + await proxyMulti.execute(); + + const stakersMulti = new Multicaller(network, provider, abiStaking, { + blockTag + }); + + addresses.forEach((address) => { + stakersMulti.call(address, options.lqtyStakingAddr, 'stakes', [ + proxyAddresses[address] + ]); + }); + + const result: Record = await stakersMulti.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/README.md new file mode 100644 index 00000000..121c5c97 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/README.md @@ -0,0 +1,21 @@ +# lrc-l2-nft-balance-of + +Strategy to read account balances for NFTs (72 or 1155) from LoopringV2 subgraph. Assumes we only want tokens minted by a specific account id. + +Here is an example of parameters: + +```json +{ + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36", + "minter_account_id": "74447", + "tokens": ["token (Collection) id's to include"], + "blacklisted_account_ids": ["38482"], + "blacklisted_nft_ids": ["... nft id's to exclude ..."] +} +``` + +Use explorer.loopring.io to look up addresses and find account id's. + +Account id `38482` maps to `0x000000000000000000000000000000000000dead` and is used for burning tokens. + +to note: either the `minter_account_id` or the `tokens` parameter must be provided for this query to work. You do not need to specify both, just one of them. diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/examples.json new file mode 100644 index 00000000..0f2c7cec --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/examples.json @@ -0,0 +1,29 @@ +[ + { + "name": "LoopringV2NFT", + "strategy": { + "name": "lrc-l2-nft-balance-of", + "params": { + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36", + "minter_account_id": "74447", + "tokens": ["0xc76eca2937b006606ebe717621409e4c2df906f1"], + "blacklisted_account_ids": ["38482"], + "blacklisted_nft_ids": [ + "0x94743548ba8d82a4ee8ea3dfad589ea501ad2738-0-0xc76eca2937b006606ebe717621409e4c2df906f1-0x15f13a1431906d9ca5b24df9de0b443690bf822d012e5da30b18d36ffad545aa-5" + ] + } + }, + "network": "1", + "addresses": [ + "0xad29249bd6d203b3c824d2514b8221c83eb608d1", + "0x3c41f4e24e52f624fd4bf00dfe90d63c64cf964c", + "0xe4c9487dbef67405d4469ae27837e5f42197af21", + "0xacf9c4d62f00ed3e47e3d8aabfc80300be02cb4d", + "0x6efd66a92a22561c3b2e08b5253a2bbd64c3edcc", + "0x197f6515eb864133ce1d689f50a912620f3953ac", + "0xecd5d7ca36538174c977b42c08e924b4341ffbf0", + "0x0a437c0caeda1e081ba413229da0bf132fe74584" + ], + "snapshot": 15175152 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/index.ts new file mode 100644 index 00000000..e2820c1a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-nft-balance-of/index.ts @@ -0,0 +1,102 @@ +import { subgraphRequest } from '../../utils'; +export const author = 'karamorf'; +export const version = '0.1.1'; + +const LIMIT = 1000; + +function makeQuery( + snapshot, + minter, + tokens, + skip, + blacklisted_account_ids, + blacklisted_nft_ids +) { + const query: any = { + accountNFTSlots: { + __args: { + where: { + nft_: { + id_not_in: blacklisted_nft_ids + }, + account_not_in: blacklisted_account_ids + }, + first: LIMIT, + skip: skip + }, + account: { address: true }, + balance: true + } + }; + + if (minter && minter !== '') { + query.accountNFTSlots.__args.where.nft_.minter = minter; + } + + if (tokens && tokens.length > 0) { + query.accountNFTSlots.__args.where.nft_.token_in = tokens; + } + + if (snapshot !== 'latest') { + query.accountNFTSlots.__args = { + ...query.accountNFTSlots.__args, + block: { + number: snapshot + } + }; + } + + return query; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + let blacklisted_account_ids = options.blacklisted_account_ids; + let blacklisted_nft_ids = options.blacklisted_nft_ids; + const balances = {}; + let skip = 0; + let response_size = 0; + + if (!blacklisted_account_ids || blacklisted_account_ids.length === 0) { + blacklisted_account_ids = ['']; + } + + if (!blacklisted_nft_ids || blacklisted_nft_ids.length === 0) { + blacklisted_nft_ids = ['']; + } + + do { + const response = await subgraphRequest( + options.graph, + makeQuery( + snapshot, + options.minter_account_id, + options.tokens, + skip, + blacklisted_account_ids, + blacklisted_nft_ids + ) + ); + + response.accountNFTSlots.forEach((slot) => { + if (!balances.hasOwnProperty(slot.account.address)) { + balances[slot.account.address] = 0; + } + balances[slot.account.address] += parseInt(slot.balance); + }); + response_size = response.accountNFTSlots.length; + skip += response_size; + } while (response_size == LIMIT); + + const scores = Object.fromEntries( + addresses.map((address) => [address, balances[address.toLowerCase()]]) + ); + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/README.md new file mode 100644 index 00000000..e77643f3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/README.md @@ -0,0 +1,14 @@ +# lrc-l2-subgraph-balance-of + +Strategy to read account LRC balance from LoopringV2 subgraph. + +Here is an example of parameters: + +```json +{ + "address": "0x17ea92d6ffbaa1c7f6b117c1e9d0c88abdc8b84c", + "symbol": "LRC", + "tokenId": 1, + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/examples.json new file mode 100644 index 00000000..e5e05807 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "LoopringV2", + "strategy": { + "name": "lrc-l2-subgraph-balance-of", + "params": { + "symbol": "LRC", + "tokenId": 1, + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36" + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 14437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/index.ts new file mode 100644 index 00000000..c144566e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-l2-subgraph-balance-of/index.ts @@ -0,0 +1,93 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest } from '../../utils'; + +export const author = 'shad-k'; +export const version = '0.1.1'; + +const LIMIT = 500; + +function makeQuery(snapshot, addresses, tokenId) { + const query: any = { + accounts: { + __args: { + where: { + address_in: addresses + }, + first: LIMIT + }, + balances: { + __args: { + where: { + token_in: [`${tokenId}`] + } + }, + balance: true, + token: { + decimals: true + } + }, + address: true + } + }; + + if (snapshot !== 'latest') { + query.accounts.__args = { + ...query.accounts.__args, + block: { + number: snapshot + } + }; + } + + return query; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const _addresses = addresses.map((address) => address.toLowerCase()); + const addressSubsets = Array.apply( + null, + Array(Math.ceil(_addresses.length / LIMIT)) + ).map((_e, i) => _addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + + const response = await Promise.all( + addressSubsets.map((subset) => + subgraphRequest( + options.graph, + makeQuery(snapshot, subset, options.tokenId) + ) + ) + ); + + const accounts = response.map((data) => data.accounts).flat(); + const addressToBalanceMap = Object.fromEntries( + accounts.map((account) => { + if (account.balances.length > 0) { + return [ + account.address, + BigNumber.from(account.balances[0].balance) + .div( + BigNumber.from(10).pow(account.balances[0]?.token?.decimals ?? 18) + ) + .toNumber() + ]; + } + + return [account.address, 0]; + }) + ); + + const scores = Object.fromEntries( + addresses.map((address) => [ + address, + addressToBalanceMap[address.toLowerCase()] + ]) + ); + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/README.md new file mode 100644 index 00000000..de722654 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/README.md @@ -0,0 +1,19 @@ +# lrc-l2-subgraph-balance-of + +Strategy to read account LRC LP balance from LoopringV2 subgraph. + +Here is an example of parameters: + +```json +{ + "symbol": "LRC", + "tokenIdToPoolMap": { + "235": "0x194db39e4c99f6c8dd81b4647465f7599f3c215a", + "102": "0xe6cc0d45c4e4f81be340f4d176e6ce0d63ad5743", + "83": "0x18920d6e6fb7ebe057a4dd9260d6d95845c95036", + "168": "0xfa6680779dc9168600bcdcaff28b41c8fa568d98", + "200": "0xc8f242b2ac6069ebdc876ba0ef42efbf03c5ba4b" + }, + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/examples.json new file mode 100644 index 00000000..280a0578 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/examples.json @@ -0,0 +1,42 @@ +[ + { + "name": "LoopringV2 LP Balance", + "strategy": { + "name": "lrc-lp-subgraph-balance-of", + "params": { + "symbol": "LRC", + "tokenIdToPoolMap": { + "235": "0x194db39e4c99f6c8dd81b4647465f7599f3c215a", + "102": "0xe6cc0d45c4e4f81be340f4d176e6ce0d63ad5743", + "83": "0x18920d6e6fb7ebe057a4dd9260d6d95845c95036", + "168": "0xfa6680779dc9168600bcdcaff28b41c8fa568d98", + "200": "0xc8f242b2ac6069ebdc876ba0ef42efbf03c5ba4b" + }, + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36" + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268", + "0x2b1284fa49af4f7b1a6563e1ec7f88767f2a3900" + ], + "snapshot": 15482770 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/index.ts new file mode 100644 index 00000000..89ccfb99 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-lp-subgraph-balance-of/index.ts @@ -0,0 +1,214 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest } from '../../utils'; + +export const author = 'shad-k'; +export const version = '0.1.1'; + +const LIMIT = 500; + +function makeUserQuery(snapshot, addresses, tokenIds) { + const query: any = { + accounts: { + __args: { + where: { + address_in: addresses + }, + first: LIMIT + }, + balances: { + __args: { + where: { + token_in: tokenIds + } + }, + balance: true, + token: { + id: true, + decimals: true + } + }, + address: true + } + }; + + if (snapshot !== 'latest') { + query.accounts.__args = { + ...query.accounts.__args, + block: { + number: snapshot + } + }; + } + + return query; +} + +function makePoolQuery(snapshot, addresses, tokenIds) { + const query: any = { + pools: { + __args: { + where: { + address_in: addresses + } + }, + address: true, + balances: { + __args: { + where: { + token_in: ['1', ...tokenIds] + } + }, + balance: true, + token: { + id: true, + decimals: true + } + } + } + }; + + if (snapshot !== 'latest') { + query.pools.__args = { + ...query.pools.__args, + block: { + number: snapshot + } + }; + } + + return query; +} + +function calculateUserScore( + addressToBalancesMap, + calculatedPoolMultipliers, + tokenIdToPoolMap +): number { + let score = 0; + + addressToBalancesMap.forEach(({ balance, token }) => { + const tokenBalance = BigNumber.from(balance); + + const lpTokenScore = tokenBalance + .mul( + calculatedPoolMultipliers[tokenIdToPoolMap[token?.id]].numOfLRCInPool + ) + .div( + calculatedPoolMultipliers[tokenIdToPoolMap[token?.id]].totalLPTokens + ); + + score += lpTokenScore.toNumber(); + }); + + return score; +} + +function calculatePoolMultipliers( + poolToBalancesMap +): Record> { + return Object.fromEntries( + Object.keys(poolToBalancesMap).map((poolAddress) => { + let numOfLRCInPool = BigNumber.from(0); + let totalLPTokens = BigNumber.from(0); + + poolToBalancesMap[poolAddress].forEach(({ balance, token }) => { + if (parseInt(token.id) === 1) { + const lrcTokenBalance = BigNumber.from(balance).div( + BigNumber.from(10).pow(token?.decimals ?? 18) + ); + numOfLRCInPool = lrcTokenBalance; + } else { + const lpTokenBalance = BigNumber.from(balance); + totalLPTokens = BigNumber.from(2).pow(96).sub(lpTokenBalance); + } + }); + + return [poolAddress, { numOfLRCInPool, totalLPTokens }]; + }) + ); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const _addresses = addresses.map((address) => address.toLowerCase()); + const addressSubsets = Array.apply( + null, + Array(Math.ceil(_addresses.length / LIMIT)) + ).map((_e, i) => _addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + + const tokenIds: Array = []; + const poolAddresses: Array = []; + + // get tokenIds and poolAddresses from tokenIdToPoolMap + for (const [tokenId, poolAddress] of Object.entries( + options.tokenIdToPoolMap + )) { + tokenIds.push(tokenId); + poolAddresses.push(poolAddress as string); + } + + // fetch user LP token balances + const response = await Promise.all( + addressSubsets.map((subset) => + subgraphRequest(options.graph, makeUserQuery(snapshot, subset, tokenIds)) + ) + ); + + const accounts = response.map((data) => data.accounts).flat(); + const addressToLPBalancesMap = Object.fromEntries( + accounts.map((account) => { + if (account.balances.length > 0) { + return [account.address, account.balances]; + } + + return [account.address, []]; + }) + ); + + // fetch pool LP and LRC token balances + const poolAccountBalances = await subgraphRequest( + options.graph, + makePoolQuery(snapshot, poolAddresses, tokenIds) + ); + + const poolToBalancesMap = Object.fromEntries( + poolAccountBalances.pools.map((pool) => { + if (pool.balances.length > 0) { + return [pool.address, pool.balances]; + } + + return [pool.address, 0]; + }) + ); + + // calculate numOfLRCInPool and totalLPTokens for each pool + const calculatedPoolMultipliers = calculatePoolMultipliers(poolToBalancesMap); + + // calculate user score + const addressToScoreMap = Object.fromEntries( + Object.keys(addressToLPBalancesMap).map((userAddress) => { + const userScore = calculateUserScore( + addressToLPBalancesMap[userAddress], + calculatedPoolMultipliers, + options.tokenIdToPoolMap + ); + + return [userAddress.toLowerCase(), userScore]; + }) + ); + + const scores = Object.fromEntries( + addresses.map((address) => [ + address, + addressToScoreMap[address.toLowerCase()] + ]) + ); + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/README.md b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/README.md new file mode 100644 index 00000000..f7112f18 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/README.md @@ -0,0 +1,28 @@ +# lrc-nft-dao-search + +This is an improvement of karamorf's lrc-l2-nft-balance of Snapshot voting strategy by raecaug(system32). +The extended functionality allows space owners to(alongside all of lrc-l2-nft-balance-of's functionality) specify individual nft ids within a token contract with the nft_ids option. +This option is not needed, and if excluded the query will search for all nfts minted under that token contract address. +No other behavior has been changed. + + +Strategy to read account balances for NFTs (72 or 1155) from LoopringV2 subgraph. Assumes we only want tokens minted by a specific account id. + +Here is an example of parameters: + +```json +{ + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36", + "minter_account_id": "74447", + "tokens": ["token (Collection contract address) to include"], + "nft_ids": ["nftIDs, unique to every nft, even those under the same token contract"], + "blacklisted_account_ids": ["38482"], + "blacklisted_nft_ids": ["... nft id's to exclude ..."] +} +``` + +Use explorer.loopring.io to look up addresses and find account id's. + +Account id `38482` maps to `0x000000000000000000000000000000000000dead` and is used for burning tokens. + +to note: either the `minter_account_id` or the `tokens` parameter must be provided for this query to work. You do not need to specify both, just one of them. diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/examples.json new file mode 100644 index 00000000..a944db03 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "LoopringDAOSearch", + "strategy": { + "name": "lrc-nft-dao-search", + "params": { + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36", + "minter_account_id": "157510", + "tokens": ["0xb6d91e38e4ac53c9f8952c6c6b1c7aee66c8b6f0"], + "nft_ids": [ + "0x1e31297dd163ca44a5fad74de4ffbebf1ba11d46e1b448b0e105449d827fb264" + ], + "blacklisted_account_ids": [""], + "blacklisted_nft_ids": [""] + } + }, + "network": "1", + "addresses": ["0xeE253D3fCC30787a1E58570E355010d0b9C33B60"], + "snapshot": 15677787 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/index.ts new file mode 100644 index 00000000..a18cba42 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-dao-search/index.ts @@ -0,0 +1,126 @@ +// Original code by Karamorf, upgraded by Raecaug(system32) +// Allows querys of Loopring L2 accounts & balances by specifying a nft minter, token contract address(and optionally specifying individual ids to white/blacklist) + +import { subgraphRequest } from '../../utils'; +export const author = 'raecaug'; +export const version = '0.1.2'; + +const LIMIT = 1000; + +function makeQuery( + snapshot, // This is an Ethereum block # or defaults to 'latest' + minter, // This is referred to as account # or account id on the Loopring L2 block explorer + tokens, // NFT collection contract addresses, also referred to as 'token address' + skip, // Used to skip response lines in requests + blacklisted_account_ids, // Ditto properties of 'minter' + blacklisted_nft_ids, // This is the nft id, which is unique for every nft ever minted, allows distinction between nfts in a collection at the chain level + nft_ids // Ditto properties of blacklisted version +) { + const query: any = { + // Query constructor, builds request with params from snapshot space settings + accountNFTSlots: { + __args: { + where: { + nft_: { + id_not_in: blacklisted_nft_ids, // Excluding blacklisted nft ids + nftID_in: nft_ids // Including uniquely specified nft ids + }, + account_not_in: blacklisted_account_ids // Excluding blacklisted account ids + }, + first: LIMIT, + skip: skip + }, + account: { address: true }, + balance: true + } + }; + + if (minter && minter !== '') { + //Check to ensure minter id is specified and not blank + query.accountNFTSlots.__args.where.nft_.minter = minter; + } + + if (tokens && tokens.length > 0) { + //Check to ensure at least 1 token to search for is specified + query.accountNFTSlots.__args.where.nft_.token_in = tokens; + } + + if (snapshot !== 'latest') { + // If the snapshot date is manually specified, overwrite the 'latest' block, strict inequality check operand used + query.accountNFTSlots.__args = { + ...query.accountNFTSlots.__args, + block: { + number: snapshot + } + }; + } + + return query; +} + +export async function strategy( // *****Logical execution begins here; args passed in by Snapshot settings***** + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + let blacklisted_account_ids = options.blacklisted_account_ids; + let blacklisted_nft_ids = options.blacklisted_nft_ids; + + let nft_ids = options.nft_ids; // Unique NFT ids, distinguishable from 1155 token contracts + + const balances = {}; // Initialization + let skip = 0; + let response_size = 0; + + if (!blacklisted_account_ids || blacklisted_account_ids.length === 0) { + // If no blacklisted accts specified, set to empty + blacklisted_account_ids = ['']; + } + + if (!blacklisted_nft_ids || blacklisted_nft_ids.length === 0) { + // If no unique nft_ids specified, set to empty + blacklisted_nft_ids = ['']; + } + + if (!nft_ids || nft_ids.length === 0) { + // If no unique nft_ids specified, set to empty + nft_ids = ['']; + } + + do { + // Transmit query and await results + const response = await subgraphRequest( + // Constructs response variable from subgraph query function + options.graph, // Parameter 1, options specified + makeQuery( + // Query constructor(defined above) called, results are the second parameter + snapshot, + options.minter_account_id, + options.tokens, + skip, + blacklisted_account_ids, + blacklisted_nft_ids, + nft_ids + ) + ); + + response.accountNFTSlots.forEach((slot) => { + // Checking against each accountNFTSlot element + if (!balances.hasOwnProperty(slot.account.address)) { + balances[slot.account.address] = 0; // If nothing returned, set this accounts balance to 0 + } + balances[slot.account.address] += parseInt(slot.balance); // Otherwise, a bigint is returned, parse it and store in balances array + }); + response_size = response.accountNFTSlots.length; // Value is set to 0 on loop entry, updated here, will break loop for anything other than 1000 + skip += response_size; + } while (response_size == LIMIT); + + const scores = Object.fromEntries( + addresses.map((address) => [address, balances[address.toLowerCase()]]) // Map returned addresses and balances as scores array + ); + + return scores; // Returns addresses and balances to Snapshot +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/README.md b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/README.md new file mode 100644 index 00000000..f98fdfff --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/README.md @@ -0,0 +1,29 @@ +# lrc-nft-search-mult + +This is a further improvement of karamorf's lrc-l2-nft-balance of Snapshot voting strategy by raecaug(system32). + +This strategy is an extension of lrc-nft-dao-search, allowing space owners to apply a multiplier to counted votes. +This can then be combined with additional voting strategies to allow for complex DAO setups with vote weighting of specific NFTs. +Providing a multiplier is necessary; if default behavior is desired, simply specify '1'. + +Strategy to read account balances for NFTs (72 or 1155) from LoopringV2 subgraph. Assumes we only want tokens minted by a specific account id. + +Here is an example of parameters: + +```json +{ + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36", + "minter_account_id": "74447", + "tokens": ["token (Collection contract address) to include"], + "nft_ids": ["nftIDs, unique to every nft, even those under the same token contract"], + "blacklisted_account_ids": ["38482"], + "blacklisted_nft_ids": ["... nft id's to exclude ..."] + "multiplier": "3" // Setting this to '1' will result in normal vote counting behavior. +} +``` + +Use explorer.loopring.io to look up addresses and find account id's. + +Account id `38482` maps to `0x000000000000000000000000000000000000dead` and is used for burning tokens. + +to note: either the `minter_account_id` or the `tokens` parameter must be provided for this query to work. You do not need to specify both, just one of them. diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/examples.json new file mode 100644 index 00000000..e0eaba89 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "lrcNFTmult", + "strategy": { + "name": "lrc-nft-search-mult", + "params": { + "graph": "https://api.thegraph.com/subgraphs/name/juanmardefago/loopring36", + "minter_account_id": "157510", + "tokens": ["0xb6d91e38e4ac53c9f8952c6c6b1c7aee66c8b6f0"], + "nft_ids": [ + "0x1e31297dd163ca44a5fad74de4ffbebf1ba11d46e1b448b0e105449d827fb264" + ], + "blacklisted_account_ids": [""], + "blacklisted_nft_ids": [""], + "multiplier": "1" + } + }, + "network": "1", + "addresses": [ + "0xeE253D3fCC30787a1E58570E355010d0b9C33B60", + "0xddCCE06088517c56FA938bD99cD0820094010F8e", + "0x9Fd19B8ca6E49eD92142339F54026497cE913492" + ], + "snapshot": 15677787 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/index.ts new file mode 100644 index 00000000..89ba6230 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lrc-nft-search-mult/index.ts @@ -0,0 +1,128 @@ +// Original code by Karamorf, upgraded by Raecaug(system32) +// Allows querys of Loopring L2 accounts & balances by specifying a nft minter, token contract address(and optionally specifying individual ids to white/blacklist) + +import { subgraphRequest } from '../../utils'; +export const author = 'raecaug'; +export const version = '0.1.0'; + +const LIMIT = 1000; + +function makeQuery( + snapshot, // This is an Ethereum block # or defaults to 'latest' + minter, // This is referred to as account # or account id on the Loopring L2 block explorer + tokens, // NFT collection contract addresses, also referred to as 'token address' + skip, // Used to skip response lines in requests + blacklisted_account_ids, // Ditto properties of 'minter' + blacklisted_nft_ids, // This is the nft id, which is unique for every nft ever minted, allows distinction between nfts in a collection at the chain level + nft_ids // Ditto properties of blacklisted version +) { + const query: any = { + // Query constructor, builds request with params from snapshot space settings + accountNFTSlots: { + __args: { + where: { + nft_: { + id_not_in: blacklisted_nft_ids, // Excluding blacklisted nft ids + nftID_in: nft_ids // Including uniquely specified nft ids + }, + account_not_in: blacklisted_account_ids // Excluding blacklisted account ids + }, + first: LIMIT, + skip: skip + }, + account: { address: true }, + balance: true + } + }; + + if (minter && minter !== '') { + //Check to ensure minter id is specified and not blank + query.accountNFTSlots.__args.where.nft_.minter = minter; + } + + if (tokens && tokens.length > 0) { + //Check to ensure at least 1 token to search for is specified + query.accountNFTSlots.__args.where.nft_.token_in = tokens; + } + + if (snapshot !== 'latest') { + // If the snapshot date is manually specified, overwrite the 'latest' block, strict inequality check operand used + query.accountNFTSlots.__args = { + ...query.accountNFTSlots.__args, + block: { + number: snapshot + } + }; + } + + return query; +} + +export async function strategy( // *****Logical execution begins here; args passed in by Snapshot settings***** + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + let blacklisted_account_ids = options.blacklisted_account_ids; + let blacklisted_nft_ids = options.blacklisted_nft_ids; + + const multiplier = options.multiplier; // Multiplier to be applied against returned NFT amounts + + let nft_ids = options.nft_ids; // Unique NFT ids, distinguishable from 1155 token contracts + + const balances = {}; // Initialization + let skip = 0; + let response_size = 0; + + if (!blacklisted_account_ids || blacklisted_account_ids.length === 0) { + // If no blacklisted accts specified, set to empty + blacklisted_account_ids = ['']; + } + + if (!blacklisted_nft_ids || blacklisted_nft_ids.length === 0) { + // If no unique nft_ids specified, set to empty + blacklisted_nft_ids = ['']; + } + + if (!nft_ids || nft_ids.length === 0) { + // If no unique nft_ids specified, set to empty + nft_ids = ['']; + } + + do { + // Transmit query and await results + const response = await subgraphRequest( + // Constructs response variable from subgraph query function + options.graph, // Parameter 1, options specified + makeQuery( + // Query constructor(defined above) called, results are the second parameter + snapshot, + options.minter_account_id, + options.tokens, + skip, + blacklisted_account_ids, + blacklisted_nft_ids, + nft_ids + ) + ); + + response.accountNFTSlots.forEach((slot) => { + // Checking against each accountNFTSlot element + if (!balances.hasOwnProperty(slot.account.address)) { + balances[slot.account.address] = 0; // If nothing returned, set this accounts balance to 0 + } + balances[slot.account.address] += multiplier * parseInt(slot.balance); // Otherwise, a bigint is returned; parse it, apply multiplier and store in balances array + }); + response_size = response.accountNFTSlots.length; // Value is set to 0 on loop entry, updated here, will break loop for anything other than 1000 + skip += response_size; + } while (response_size == LIMIT); + + const scores = Object.fromEntries( + addresses.map((address) => [address, balances[address.toLowerCase()]]) // Map returned addresses and balances as scores array + ); + + return scores; // Returns addresses and balances to Snapshot +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/README.md b/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/README.md new file mode 100644 index 00000000..4296821a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/README.md @@ -0,0 +1,3 @@ +# Lydia Finance Governance Vault Balance Strategy + +Calculates voting power based on user balance in the Lydia Governance pool where users lock their LYD tokens and get right to participate in governance. diff --git a/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/examples.json b/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/examples.json new file mode 100644 index 00000000..b98d2983 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "lydia-gov-vault", + "strategy": { + "name": "lydia-gov-vault", + "params": { + "address": "0x59002fcbC7D19B7c91D530F739d71507B83CaC53", + "symbol": "GovLYD" + } + }, + "network": "43114", + "addresses": [ + "0xB1cbb693D6cF15a88d251b7361C8925BeA58db5f", + "0x806EB1C67f54D286239E0aC953Aa5514B7958Cc8", + "0x2e8Ac6830CcAA5a767ddf96c10E4A9772D19C2A8", + "0x91BF066D64Ef577b64A8445343834c7f5Cbbd0CD" + ], + "snapshot": 8088843 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/index.ts b/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/index.ts new file mode 100644 index 00000000..8ae1cc90 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/lydia-gov-vault/index.ts @@ -0,0 +1,42 @@ +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'manes-codes'; +export const version = '1.0.0'; + +const abi = [ + 'function sharesOf(address account) view returns (uint256)', + 'function getPricePerFullShare() view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const queries: any[] = []; + + addresses.forEach((voter) => { + queries.push([options.address, 'sharesOf', [voter]]); + }); + queries.push([options.address, 'getPricePerFullShare']); + + const response = ( + await multicall(network, provider, abi, queries, { blockTag }) + ).map((r) => r[0]); + const sharePrice = response[response.length - 1]; + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + const balanceBN = response[i].mul(sharePrice).div(parseUnits('1', 18)); + return [addresses[i], parseFloat(formatUnits(balanceBN, 18))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/maker-ds-chief/examples.json b/Implementations/API/backend/utils/snapshot/strategies/maker-ds-chief/examples.json new file mode 100644 index 00000000..229f60a9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/maker-ds-chief/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "maker-ds-chief", + "params": { + "symbol": "DAI", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 13494433 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/maker-ds-chief/index.ts b/Implementations/API/backend/utils/snapshot/strategies/maker-ds-chief/index.ts new file mode 100644 index 00000000..df2f1231 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/maker-ds-chief/index.ts @@ -0,0 +1,59 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +const MAKER_DS_CHIEF_ADDRESS = { + '1': '0x9ef05f7f6deb616fd37ac3c959a2ddd25a54e4f5' +}; + +const abi = [ + { + constant: true, + inputs: [ + { + name: '', + type: 'address' + } + ], + name: 'deposits', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + MAKER_DS_CHIEF_ADDRESS[network], + 'deposits', + [address] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/README.md b/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/README.md new file mode 100644 index 00000000..043a10f1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/README.md @@ -0,0 +1,69 @@ +# marsecosystem + +This is the most common strategy, it returns the balances of the voters for a specific ERC20 token. + +Here is an example of parameters: + +```json +{ + "token": "0x7859B01BbF675d67Da8cD128a50D155cd881B576", + "miningMasters": [ + { "address": "0xc7B8285a9E099e8c21CA5516D23348D8dBADdE4a", "pid": 0 }, + { "address": "0x48C42579D98Aa768cde893F8214371ed607CABE3", "pid": 0 } + ], + "upMiningMasters": [ + { "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", "pid": 0 }, + { "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", "pid": 1 }, + { "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", "pid": 2 }, + { "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", "pid": 3 } + ], + "lps": [ + { + "lpToken": "0x40B605d8BeED09568E702Deadce90fb23cfd74d8", + "multi": 1500, + "miningMasters": [ + { + "address": "0xc7B8285a9E099e8c21CA5516D23348D8dBADdE4a", + "pid": 1 + }, + { + "address": "0x48C42579D98Aa768cde893F8214371ed607CABE3", + "pid": 1 + } + ], + "upMiningMasters": [ + { + "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", + "pid": 4 + }, + { + "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", + "pid": 5 + } + ] + }, + { + "lpToken": "0xe47cCE810174Ac2aCEaB936e6FF93690888bcF24", + "multi": 2000, + "miningMasters": [ + { + "address": "0xc7B8285a9E099e8c21CA5516D23348D8dBADdE4a", + "pid": 7 + }, + { + "address": "0x22D8d50454203bd5a41B49ef515891f1aD9f3e53", + "pid": 3 + } + ], + "upMiningMasters": [ + { + "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", + "pid": 6 + } + ] + } + ], + "symbol": "XMS", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/examples.json b/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/examples.json new file mode 100644 index 00000000..bda9fca1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/examples.json @@ -0,0 +1,92 @@ +[ + { + "name": "Example", + "strategy": { + "name": "marsecosystem", + "params": { + "token": "0x7859B01BbF675d67Da8cD128a50D155cd881B576", + "miningMasters": [ + { "address": "0xc7B8285a9E099e8c21CA5516D23348D8dBADdE4a", "pid": 0 }, + { "address": "0x48C42579D98Aa768cde893F8214371ed607CABE3", "pid": 0 } + ], + "upMiningMasters": [ + { "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", "pid": 0 }, + { "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", "pid": 1 }, + { "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", "pid": 2 }, + { "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", "pid": 3 } + ], + "lps": [ + { + "lpToken": "0x40B605d8BeED09568E702Deadce90fb23cfd74d8", + "multi": 1500, + "miningMasters": [ + { + "address": "0xc7B8285a9E099e8c21CA5516D23348D8dBADdE4a", + "pid": 1 + }, + { + "address": "0x48C42579D98Aa768cde893F8214371ed607CABE3", + "pid": 1 + } + ], + "upMiningMasters": [ + { + "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", + "pid": 4 + }, + { + "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", + "pid": 5 + } + ] + }, + { + "lpToken": "0xe47cCE810174Ac2aCEaB936e6FF93690888bcF24", + "multi": 2000, + "miningMasters": [ + { + "address": "0xc7B8285a9E099e8c21CA5516D23348D8dBADdE4a", + "pid": 7 + }, + { + "address": "0x22D8d50454203bd5a41B49ef515891f1aD9f3e53", + "pid": 3 + } + ], + "upMiningMasters": [ + { + "address": "0x38823CB52d152e0fFe637D63F97f6F771a071Ea0", + "pid": 6 + } + ] + } + ], + "symbol": "XMS", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0x3139f8eb391f8bfb478b9c155e009c59a689d7e0", + "0xc03f7c895ae99bd63b2b9569d8465c1cb8f48807", + "0x74e3a06361b0c67d736020c3b09196aa84019bff", + "0xae2113de1cd10c5d626e902760638a4f5ae53d7a", + "0xa3abadcddf9c7194b7a9059ea76b5cfb6aa31c6f", + "0x1b983e23b541785e44fe1dae6e0d67d7d1ac4912", + "0xbc8da18ef4cfc2282487c0fba2daa8fa7623045f", + "0xbda6ba3ad624582495c55451a1840e563fc16389", + "0xdab35f3e690b005b6cec2ed692def81b9702186a", + "0x9a85032ce857d536784a6493273ba32e8562c6e2", + "0xbfb7746a642e4ac4549da3f203da3d110f0c4a4d", + "0xe1b7b6216cc1b73a35e651674d43a4aad495ac6c", + "0x1265a989b92bdcb9a5cb86d86f3701fca5514cb7", + "0xea013d2a1eaaf64808d6e1d2227564ec69d977fd", + "0xeaac9ebbdd62c7a7f3d881763dbb23770deb353b", + "0x1d9155f814a832af287bf6965bd66fbf12c69d4f", + "0x711cae85542082daf93a14403d148bb91299c6fb", + "0x75f9a46a78757d0ea1557abf6cb04499960ef951", + "0xbf329b00b533485987c957182bfb02bcee9c3994" + ], + "snapshot": 22704707 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/index.ts b/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/index.ts new file mode 100644 index 00000000..4238dea2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/marsecosystem/index.ts @@ -0,0 +1,148 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'etedwardelric'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOf(address _owner) view returns (uint256 balance)', + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)', + 'function stakedWantTokens(uint256 _pid, address _user) view returns (uint256)', + 'function totalSupply() view returns (uint256)' +]; + +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 }); + + if (options.miningMasters) + options.miningMasters = options.miningMasters.slice(0, 20); + if (options.upMiningMasters) + options.upMiningMasters = options.upMiningMasters.slice(0, 20); + if (options.lps) { + options.lps = options.lps.slice(0, 5); + options.lps.forEach((lp) => { + if (lp.miningMasters) lp.miningMasters = lp.miningMasters.slice(0, 20); + if (lp.upMiningMasters) + lp.upMiningMasters = lp.upMiningMasters.slice(0, 20); + }); + } + + addresses.forEach((address) => { + multi.call(`${address}-${options.token}`, options.token, 'balanceOf', [ + address + ]); + if (options.miningMasters) + options.miningMasters.forEach((miningMaster) => + multi.call( + `${address}-${miningMaster.address}-${miningMaster.pid}`, + miningMaster.address, + 'userInfo', + [miningMaster.pid, address] + ) + ); + if (options.upMiningMasters) + options.upMiningMasters.forEach((upMiningMaster) => + multi.call( + `${address}-${upMiningMaster.address}-${upMiningMaster.pid}`, + upMiningMaster.address, + 'stakedWantTokens', + [upMiningMaster.pid, address] + ) + ); + }); + + addresses.forEach((address) => + options.lps.forEach((lp) => { + multi.call(`${lp.lpToken}-${options.token}`, options.token, 'balanceOf', [ + lp.lpToken + ]); + multi.call(`${lp.lpToken}`, lp.lpToken, 'totalSupply'); + multi.call(`${address}-${lp.lpToken}`, lp.lpToken, 'balanceOf', [ + address + ]); + if (lp.miningMasters) + lp.miningMasters.forEach((miningMaster) => + multi.call( + `${address}-${lp.lpToken}-${miningMaster.address}-${miningMaster.pid}`, + miningMaster.address, + 'userInfo', + [miningMaster.pid, address] + ) + ); + if (lp.upMiningMasters) + lp.upMiningMasters.forEach((upMiningMaster) => + multi.call( + `${address}-${lp.lpToken}-${upMiningMaster.address}-${upMiningMaster.pid}`, + upMiningMaster.address, + 'stakedWantTokens', + [upMiningMaster.pid, address] + ) + ); + }) + ); + + const result = await multi.execute(); + + return Object.fromEntries( + addresses.map((address) => { + let amount = BigNumber.from(0); + amount = amount.add(result[`${address}-${options.token}`]); + if (options.miningMasters) + for (const miningMaster of options.miningMasters) { + amount = amount.add( + result[`${address}-${miningMaster.address}-${miningMaster.pid}`][0] + ); + } + if (options.upMiningMasters) + for (const upMiningMaster of options.upMiningMasters) { + amount = amount.add( + result[`${address}-${upMiningMaster.address}-${upMiningMaster.pid}`] + ); + } + for (const lp of options.lps) { + amount = amount.add( + result[`${address}-${lp.lpToken}`] + .mul(result[`${lp.lpToken}-${options.token}`]) + .mul(lp.multi ?? 1000) + .div(result[`${lp.lpToken}`]) + .div(1000) + ); + if (lp.miningMasters) + for (const miningMaster of lp.miningMasters) { + amount = amount.add( + result[ + `${address}-${lp.lpToken}-${miningMaster.address}-${miningMaster.pid}` + ][0] + .mul(result[`${lp.lpToken}-${options.token}`]) + .mul(lp.multi ?? 1000) + .div(result[`${lp.lpToken}`]) + .div(1000) + ); + } + if (lp.upMiningMasters) + for (const upMiningMaster of lp.upMiningMasters) { + amount = amount.add( + result[ + `${address}-${lp.lpToken}-${upMiningMaster.address}-${upMiningMaster.pid}` + ] + .mul(result[`${lp.lpToken}-${options.token}`]) + .mul(lp.multi ?? 1000) + .div(result[`${lp.lpToken}`]) + .div(1000) + ); + } + } + return [address, parseFloat(formatUnits(amount))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-no-rewarddebt/examples.json b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-no-rewarddebt/examples.json new file mode 100644 index 00000000..da1af35c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-no-rewarddebt/examples.json @@ -0,0 +1,41 @@ +[ + { + "name": "RXS Example", + "strategy": { + "name": "masterchef-pool-balance-no-rewarddebt", + "params": { + "symbol": "RXS", + "chefAddress": "0xcfA857d6EC2F59b050D7296FbcA8a91D061451f3", + "uniPairAddress": null, + "tokenIndex": null, + "pid": "75", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "56", + "addresses": [ + "0x191727d22f2693100acef8e48F8FeaEaa06d30b1", + "0xFAdE3DE0E802B9Bf52bD457389d933F9483fF2fD" + ], + "snapshot": 16801195 + }, + { + "name": "RXS-BUSD LP Example", + "strategy": { + "name": "masterchef-pool-balance-no-rewarddebt", + "params": { + "symbol": "RXS-BUSD LP", + "chefAddress": "0xcfA857d6EC2F59b050D7296FbcA8a91D061451f3", + "uniPairAddress": "0xE576ebF57b3776f8892e2dF1787Cb163f41a1242", + "tokenIndex": 0, + "pid": "74", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "56", + "addresses": ["0xe563983d6f46266Ad939c16bD59E5535Ab6E774D"], + "snapshot": 16801195 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-no-rewarddebt/index.ts b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-no-rewarddebt/index.ts new file mode 100644 index 00000000..0d7650ab --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-no-rewarddebt/index.ts @@ -0,0 +1,96 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'defimatt'; +export const version = '0.0.1'; + +/* + * Masterchef pool balance strategy. Differs from strategy masterchef-pool-balance by working with masterchefs + * that only return an amount from userInfo, not a rewardDebt. + * Accepted options: + * - chefAddress: masterchef contract address + * - pid: mastechef pool id (starting with zero) + * - uniPairAddress: address of a uniswap pair (or a sushi pair or any other with the same interface) + * - if the uniPairAddress option is provided, converts staked LP token balance to base token balance + * (based on the pair total supply and base token reserve) + * - if uniPairAddress is null or undefined, returns staked token balance as is + * - tokenIndex: index of a token in LP pair, optional, by default 0 + * - weight: integer multiplier of the result (for combining strategies with different weights, totally optional) + */ + +const abi = [ + 'function userInfo(uint256, address) view returns (uint256 amount)', + 'function totalSupply() view returns (uint256)', + 'function getReserves() view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)' +]; + +// calls is a 1-dimensional array so we just push 3 calls for every address +const getCalls = (addresses: any[], options: any) => { + const result: any[] = []; + for (const address of addresses) { + result.push([options.chefAddress, 'userInfo', [options.pid, address]]); + if (options.uniPairAddress != null) { + result.push([options.uniPairAddress, 'totalSupply', []]); + result.push([options.uniPairAddress, 'getReserves', []]); + } + } + return result; +}; + +function arrayChunk(arr: T[], chunkSize: number): T[][] { + const result: T[][] = []; + for (let i = 0, j = arr.length; i < j; i += chunkSize) { + result.push(arr.slice(i, i + chunkSize)); + } + return result; +} + +// values is an array of (chunked) call results for every input address +// for setups with uniPairAddress each chunk has 3 items, for setups without, only 1 item +function processValues(values: any[], options: any): number { + const poolStaked = values[0][0]; + const weight: BigNumber = BigNumber.from(options.weight || 1); + const weightDecimals: BigNumber = BigNumber.from(10).pow( + BigNumber.from(options.weightDecimals || 0) + ); + let result: BigNumber; + if (!options.uniPairAddress) { + result = poolStaked.mul(weight).div(weightDecimals); + } else { + const uniTotalSupply = values[1][0]; + const uniReserve = values[2][options.tokenIndex || 0]; + const precision = BigNumber.from(10).pow(18); + const tokensPerLp = uniReserve.mul(precision).div(uniTotalSupply); + result = poolStaked + .mul(tokensPerLp) + .mul(weight) + .div(weightDecimals) + .div(precision); + } + return parseFloat(formatUnits(result.toString(), options.decimals || 18)); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + getCalls(addresses, options), + { blockTag } + ); + return Object.fromEntries( + // chunk to response so that we can process values for each address + arrayChunk(response, options.uniPairAddress == null ? 1 : 3).map( + (value, i) => [addresses[i], processValues(value, options)] + ) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/README.md b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/README.md new file mode 100644 index 00000000..2781f46a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/README.md @@ -0,0 +1,369 @@ +# Generic masterchef pool balance or price strategy + +## Description + +This strategy gets the balance or price of any combination of tokens for a pool using a masterchef contract. + +It allows to get the balance or price of any of the pool pair tokens separately or the combination of both. + +Optionally, an anti-whale measure can be applied to reduce the impact of big wallets in the the resulting value. + +The price is sourced from CoinGecko. + +## Accepted options + +- **chefAddress:** masterchef contract address + +- **pid:** mastechef pool id (starting with zero) + +- **uniPairAddress:** Address of a uniswap pair (or a sushi pair or any other with the same interface) + - If the uniPairAddress option is provided, converts staked LP token balance to base token balance (based on the pair total supply and base token reserve) + + - If uniPairAddress is null or undefined, returns staked token balance of the pool + +- **tokenAddress**: Address of a token for single token Pools. + - If the uniPairAddress is provided the tokenAddress is ignored. + +- **weight:** Integer multiplier of the result (for combining strategies with different weights, totally optional) + +- **weightDecimals:** Integer value of number of decimal places to apply to the final result + +- **token0.address:** Address of the uniPair token 0. If defined, the strategy will return the result for the token0. + - can be used in conjunction with token1Address to get the sum of tokens or the UniPair token price when used with usePrice and token1Address. + + - Can be used with usePrice to get the price value of the staked amount of token0 + +- **token0.weight:** Integer multiplier of the result for token0 + +- **token0.weightDecimals:** Integer value of number of decimal places to apply to the result of token0 + +- **token1.address:** Address of the uniPair token 1. If defined, the strategy will return the result for the token1. + - can be used in conjunction with token0Address to get the sum of tokens or the UniPair token price + + - when used with usePrice and token0Address. + + - can be used with usePrice to get the price value of the staked amount of token1. + +- **token1.weight:** Integer multiplier of the result for token1. + +- **token1.weightDecimal:** Integer value of number of decimal places to apply to the result of token1 + +- **usePrice:** Boolean flag return the result in usd instead of token count + +- **currency:** currency for the price. (defaulted to `usd`). + +- **log:** Boolean flag to enable or disable logging to the console (used for debugging purposes during development) + +- **antiWhale.enable:** Boolean flag to apply an anti-whale measure reducing the effect on the voting power as the token amount increases. + - if enabled will apply the the following to the result: + + ```none + If result > antiWhale.threshold + result = antiWhale.inflectionPoint * ( result / antiWhale.inflectionPoint ) ^ antiWhale.exponent + + If result <= antiWhale.threshold { + thresholdMultiplier = ( antiWhale.inflectionPoint * ( antiWhale.threshold / antiWhale.inflectionPoint )^antiWhale.exponent ) / antiWhale.threshold + + result = result * thresholdMultiplier + } + ``` + + - **thresholdMultiplier:** The multiplier at which all results below threshold are multiplied. This is ratio of antiWhale/result at the threshold point. + +- **antiWhale.threshold:** Point at which antiWhale effect no longer applies. Results less than this will be treated with a static multiplier. This is to reduce infinite incentive for multiple wallet exploits. + - default: 1625. + + - lower cap: > 0 - set to default if <= 0. + +- **antiWhale.inflectionPoint:** Point at which output matches result. Results less than this increase output. Results greater than this decrease output. + - default: 6500. + + - lower cap: > 0 - set to default if <= 0. + + - must be >= antiWhale.threshold. Otherwise will be same as antiWhale.threshold. + +- **antiWhale.exponent:** The exponent is responsible for the antiWhale effect. Must be less than one, or else it will have a pro-whale effect. Must be greater than zero, or else it will cause total voting power to trend to zero. + - default: 0.5. + + - upper cap: 1. + + - lower cap: > 0 - set to default if <= 0. + +## Examples + +```json +[ + { + "name": "Example query - Count of tokens in single token Pool", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "tokenAddress": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "pid": "3", + "weight": 1, + "weightDecimals": 0, + "decimals": 0 + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 17368223 + }, + { + "name": "Example query - Price of tokens in single token Pool", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "tokenAddress": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "pid": "3", + "weight": 1, + "weightDecimals": 0, + "decimals": 0, + "usePrice": true + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 17368223 + }, + { + "name": "Example query - Sum of uniPair token0 and token1 count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + }, + { + "name": "Example query - Sum of uniPair token0 and token1 count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token price", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0, + "usePrice": true + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token0 count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token0 price", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0, + "usePrice": true + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token1 count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token1 price", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0, + "usePrice": true + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token price with anti-whale measure", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0xd4689694e9928564647ad483c075f271419b2a5f", + "token0": { + "address": "0x16eccfdbb4ee1a85a33f3a9b21175cd7ae753db4", + "weight": 1, + "weightDecimals": 0 + }, + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "12", + "weight": 1, + "weightDecimals": 0, + "usePrice": true, + "currency": "eur", + "antiWhale": { + "enable": true, + "inflectionPoint": 1000, + "threshold": 250, + "exponent": 0.5 + } + } + }, + "network": "137", + "addresses": [ + "0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF" + ], + "snapshot": 16828978 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/examples.json b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/examples.json new file mode 100644 index 00000000..847fbb68 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/examples.json @@ -0,0 +1,253 @@ +[ + { + "name": "Example query - Count of tokens in single token Pool", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "symbol": "CHEF", + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "tokenAddress": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "pid": "3", + "weight": 1, + "weightDecimals": 0, + "decimals": 0 + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 17368223 + }, + { + "name": "Example query - Price of tokens in single token Pool", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "tokenAddress": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "pid": "3", + "weight": 1, + "weightDecimals": 0, + "decimals": 0, + "usePrice": true + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 17368223 + }, + { + "name": "Example query - Sum of uniPair token0 and token1 count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + }, + { + "name": "Example query - Sum of uniPair token0 and token1 count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token price", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0, + "usePrice": true + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token0 count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token0 price", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token0": { + "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0, + "usePrice": true + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token1 count", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token1 price", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0x668269d6E5D2c2dE31D132Ac218044211643622B", + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0, + "usePrice": true + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + }, + { + "name": "Example query - uniPair token price with anti-whale measure", + "strategy": { + "name": "masterchef-pool-balance-price", + "params": { + "chefAddress": "0x8bE82Ab9B6179bE6EB88431E3E4E0fd93b9E607C", + "uniPairAddress": "0xd4689694e9928564647ad483c075f271419b2a5f", + "token0": { + "address": "0x16eccfdbb4ee1a85a33f3a9b21175cd7ae753db4", + "weight": 1, + "weightDecimals": 0 + }, + "token1": { + "address": "0x72572ccf5208b59f4bcc14e6653d8c31cd1fc5a0", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "12", + "weight": 1, + "weightDecimals": 0, + "usePrice": true, + "currency": "eur", + "antiWhale": { + "enable": true, + "inflectionPoint": 1000, + "threshold": 250, + "exponent": 0.5 + } + } + }, + "network": "137", + "addresses": ["0x4f9c817035Ac15A3c4C17FD3b60fabE9a4A8EEEF"], + "snapshot": 16828978 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/index.ts b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/index.ts new file mode 100644 index 00000000..17667d21 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance-price/index.ts @@ -0,0 +1,518 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; +import fetch from 'cross-fetch'; + +export const author = 'joaomajesus'; +export const version = '0.2.0'; + +/* + * Generic masterchef pool balance or price strategy. Accepted options: + * - chefAddress: Masterchef contract address + * - pid: Mastechef pool id (starting with zero) + * + * - uniPairAddress: Address of a uniswap pair (or a sushi pair or any other with the same interface) + * - If the uniPairAddress option is provided, converts staked LP token balance to base token balance + * (based on the pair total supply and base token reserve) + * - If uniPairAddress is null or undefined, returns staked token balance of the pool + * + * - tokenAddress: Address of a token for single token Pools. + * - if the uniPairAddress is provided the tokenAddress is ignored. + * + * - weight: Integer multiplier of the result (for combining strategies with different weights, totally optional) + * - weightDecimals: Integer value of number of decimal places to apply to the final result + * + * - token0.address: Address of the uniPair token 0. If defined, the strategy will return the result for the token0. + * can be used in conjunction with token1Address to get the sum of tokens or the UniPair token price + * when used with usePrice and token1Address. + * Can be used with usePrice to get the price value of the staked amount of token0 + * - token0.weight: Integer multiplier of the result for token0 + * - token0.weightDecimals: Integer value of number of decimal places to apply to the result of token0 + * + * - token1.address: Address of the uniPair token 1. If defined, the strategy will return the result for the token1. + * can be used in conjunction with token0Address to get the sum of tokens or the UniPair token price + * when used with usePrice and token0Address. + * can be used with usePrice to get the price value of the staked amount of token1 + * - token1,weight: Integer multiplier of the result for token1 + * - token1.weightDecimal: Integer value of number of decimal places to apply to the result of token1 + * + * - usePrice: Boolean flag return the result in usd instead of token count + * + * - currency: currency for the price. (defaulted to 'usd'). + * + * - log: Boolean flag to enable or disable logging to the console (used for debugging purposes during development) + * + * - antiWhale.enable: Boolean flag to apply an anti-whale measure reducing the effect on the voting power as the token amount increases. + * - if enabled will apply the the following to the result: + * + * If result > antiWhale.threshold + * result = antiWhale.inflectionPoint * ( result / antiWhale.inflectionPoint ) ^ antiWhale.exponent + * + * If result <= antiWhale.threshold + * thresholdMultiplier = ( antiWhale.inflectionPoint * ( antiWhale.threshold / antiWhale.inflectionPoint )^antiWhale.exponent ) / antiWhale.threshold + * result = result * thresholdMultiplier + * + * - thresholdMultiplier: The multiplier at which all results below threshold are multiplied. This is ratio of antiWhale/result at the threshold point. + * - antiWhale.threshold: Point at which antiWhale effect no longer applies. Results less than this will be treated with a static multiplier. + * This is to reduce infinite incentive for multiple wallet exploits. + * - default: 1625. + * - lower cap: > 0 - set to default if <= 0. + * - antiWhale.inflectionPoint: Point at which output matches result. Results less than this increase output. Results greater than this decrease output. + * - default: 6500. + * - lower cap: > 0 - set to default if <= 0. + * - must be >= antiWhale.threshold. Otherwise will be same as antiWhale.threshold. + * - antiWhale.exponent: The exponent is responsible for the antiWhale effect. Must be less than one, or else it will have a pro-whale effect. + * Must be greater than zero, or else it will cause total voting power to trend to zero. + * - default: 0.5. + * - upper cap: 1. + * - lower cap: > 0 - set to default if <= 0. + * + * Check the examples.json file for how to use the options. + */ + +const abi = [ + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)', + 'function totalSupply() view returns (uint256)', + 'function getReserves() view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)', + 'function token0() view returns (address)', + 'function token1() view returns (address)', + 'function decimals() view returns (uint8)' +]; + +const networksWithPlatforms = { + 1: 'ethereum', + 56: 'binance-smart-chain', + 66: 'okex-chain', + 88: 'tomochain', + 100: 'xdai', + 128: 'huobi-token', + 137: 'polygon-pos', + 250: 'fantom', + 42220: 'celo', + 43114: 'avalanche', + 1666600000: 'harmony-shard-0' +}; + +const priceCache = new Map(); +const blockCache = new Map(); +let log: string[] = []; +let _options; + +const getUserInfoCalls = (addresses: any[]) => { + const result: any[] = []; + + for (const address of addresses) { + result.push([_options.chefAddress, 'userInfo', [_options.pid, address]]); + } + + return result; +}; + +const getTokenCalls = () => { + const result: any[] = []; + + if (_options.uniPairAddress != null) { + result.push([_options.uniPairAddress, 'totalSupply', []]); + result.push([_options.uniPairAddress, 'getReserves', []]); + result.push([_options.uniPairAddress, 'token0', []]); + result.push([_options.uniPairAddress, 'token1', []]); + result.push([_options.uniPairAddress, 'decimals', []]); + + if (_options.token0?.address != null) { + result.push([_options.token0.address, 'decimals', []]); + } + + if (_options.token1?.address != null) { + result.push([_options.token1.address, 'decimals', []]); + } + } else if (_options.tokenAddress != null) { + result.push([_options.tokenAddress, 'decimals', []]); + } + + return result; +}; + +function arrayChunk(arr: T[], chunkSize: number): T[][] { + const result: T[][] = []; + + for (let i = 0, j = arr.length; i < j; i += chunkSize) { + result.push(arr.slice(i, i + chunkSize)); + } + + return result; +} + +async function processValues( + values: any[], + tokenValues: any[], + network: any, + provider: any, + blockTag: string | number +) { + log.push(`values = ${JSON.stringify(values, undefined, 2)}`); + log.push(`tokenValues = ${JSON.stringify(tokenValues, undefined, 2)}`); + printLog(); + + const poolStaked = values[0][0] as BigNumber; + const weight = BigNumber.from(_options.weight || 1); + const weightDecimals = BigNumber.from(10).pow( + BigNumber.from(_options.weightDecimals || 0) + ); + + let result = 0; + + if (_options.uniPairAddress == null) { + log.push(`poolStaked = ${poolStaked}`); + + if (_options.tokenAddress != null) { + const tokenDecimals = BigNumber.from(10).pow( + BigNumber.from(tokenValues[0][0]) + ); + + log.push(`tokenDecimals = ${tokenDecimals}`); + log.push(`decimals = ${_options.decimals}`); + printLog(); + + result = toFloat(poolStaked.div(tokenDecimals), _options.decimals); + + if (_options.usePrice == true) { + const price = await getTokenPrice( + _options.tokenAddress, + network, + provider, + blockTag + ); + + result *= price; + } + } else { + printLog(); + result = toFloat(poolStaked, _options.decimals); + } + } else { + const uniTotalSupply = tokenValues[0][0]; + const uniReserve0 = tokenValues[1][0]; + const uniReserve1 = tokenValues[1][1]; + const uniPairDecimalsIndex: any = + _options.uniPairAddress != null ? 4 : null; + const uniPairDecimalsCount = tokenValues[uniPairDecimalsIndex][0]; + const uniPairDecimals = + uniPairDecimalsIndex != null + ? BigNumber.from(10).pow(BigNumber.from(uniPairDecimalsCount || 0)) + : BigNumber.from(1); + + const token0Address = tokenValues[2][0]; + const useToken0 = + _options.token0?.address != null && + _options.token0.address.toString().toLowerCase() == + token0Address?.toString().toLowerCase(); + + log.push(`useToken0 = ${useToken0}`); + + if (useToken0) { + const token0DecimalsIndex = 5; + + log.push(`token0DecimalsIndex = ${token0DecimalsIndex}`); + log.push(`tokenValues = ${JSON.stringify(tokenValues, undefined, 2)}`); + printLog(); + + result += await GetTokenValue( + network, + provider, + blockTag, + uniTotalSupply, + uniReserve0, + uniPairDecimals, + poolStaked, + tokenValues, + token0Address, + token0DecimalsIndex, + _options.token0?.weight, + _options.token0?.weightDecimals + ); + } + + const token1Address = tokenValues[3][0]; + const useToken1 = + _options.token1?.address != null && + _options.token1.address.toString().toLowerCase() == + token1Address?.toString().toLowerCase(); + + log.push(`useToken1 = ${useToken1}`); + + if (useToken1) { + const token1DecimalsIndex = _options.token0?.address != null ? 6 : 5; + + log.push(`token1DecimalsIndex = ${token1DecimalsIndex}`); + log.push(`tokenValues = ${JSON.stringify(tokenValues, undefined, 2)}`); + printLog(); + + result += await GetTokenValue( + network, + provider, + blockTag, + uniTotalSupply, + uniReserve1, + uniPairDecimals, + poolStaked, + tokenValues, + token1Address, + token1DecimalsIndex, + _options.token1?.weight, + _options.token1?.WeightDecimals + ); + } + + if (!useToken0 && !useToken1) { + log.push(`poolStaked = ${poolStaked}`); + log.push(`uniPairDecimals = ${uniPairDecimals}`); + printLog(); + + const tokenCount = poolStaked.toNumber() / 10 ** uniPairDecimalsCount; + + log.push(`tokenCount = ${tokenCount}`); + + result = tokenCount / 10 ** (_options.decimals || 0); + } + } + + log.push(`result = ${result}`); + printLog(); + + result *= weight.toNumber() / weightDecimals.toNumber(); + + log.push(`weight = ${weight}`); + log.push(`weightDecimals = ${weightDecimals}`); + log.push(`result = ${result}`); + printLog(); + + return applyAntiWhaleMeasures(result); +} + +function applyAntiWhaleMeasures(result) { + log.push(`antiWhale = ${_options.antiWhale?.enable}`); + + if (_options.antiWhale?.enable != true) { + printLog(); + return result; + } + + const threshold = + _options.antiWhale.threshold == null || _options.antiWhale.threshold <= 0 + ? 1625 + : _options.antiWhale.threshold; + + let inflectionPoint = + _options.antiWhale.inflectionPoint == null || + _options.antiWhale.inflectionPoint <= 0 + ? 6500 + : _options.antiWhale.inflectionPoint; + + inflectionPoint = inflectionPoint < threshold ? threshold : inflectionPoint; + + const exponent = + _options.antiWhale.exponent == null || _options.antiWhale.exponent <= 0 + ? 0.5 + : _options.antiWhale.exponent > 1 + ? 1 + : _options.antiWhale.exponent; + + log.push(`inflectionPoint = ${inflectionPoint}`); + log.push(`exponent = ${exponent}`); + log.push(`threshold = ${threshold}`); + printLog(); + + if (result > threshold) { + result = inflectionPoint * (result / inflectionPoint) ** exponent; + } else { + const thresholdMultiplier = + (inflectionPoint * (threshold / inflectionPoint) ** exponent) / threshold; + + log.push(`thresholdMultiplier = ${thresholdMultiplier}`); + + result = result * thresholdMultiplier; + } + + log.push(`result = ${result}`); + printLog(); + + return result; +} + +function toFloat(value: BigNumber, decimals: any): number { + const decimalsResult = decimals === 0 ? 0 : decimals || 18; + + log.push(`toFloat value = ${value}`); + log.push(`toFloat decimals = ${decimals}`); + log.push(`toFloat decimalsResult = ${decimalsResult}`); + printLog(); + + return parseFloat(formatUnits(value.toString(), decimalsResult)); +} + +async function GetTokenValue( + network: any, + provider: any, + blockTag: string | number, + uniTotalSupply: any, + uniReserve: any, + uniPairDecimals: BigNumber, + poolStaked: BigNumber, + tokenValues: any[], + tokenAddress: any, + tokenDecimalsIndex: any, + tokenWeight: any, + tokenWeightDecimals: any +) { + const weightDecimals = BigNumber.from(10).pow( + BigNumber.from(tokenWeightDecimals || 0) + ); + const weight = BigNumber.from(tokenWeight || 1); + const tokensPerLp = uniReserve.mul(uniPairDecimals).div(uniTotalSupply); + + const tokenDecimals = + tokenDecimalsIndex != null + ? BigNumber.from(10).pow( + BigNumber.from(tokenValues[tokenDecimalsIndex][0] || 0) + ) + : BigNumber.from(1); + const price = await getTokenPrice(tokenAddress, network, provider, blockTag); + + log.push(`tokenAddress = ${tokenAddress}`); + log.push(`tokenDecimals = ${tokenDecimals}`); + log.push(`poolStaked = ${poolStaked}`); + log.push(`uniReserve = ${uniReserve}`); + log.push(`uniPairDecimals = ${uniPairDecimals}`); + log.push(`uniTotalSupply = ${uniTotalSupply}`); + log.push(`tokensPerLp = ${tokensPerLp}`); + log.push(`tokenWeight = ${weight}`); + log.push(`tokenWeightDecimals = ${weightDecimals}`); + log.push(`price = ${price}`); + + printLog(); + + const tokenCount = poolStaked + .mul(tokensPerLp) + .div(tokenDecimals) + .mul(weight) + .div(weightDecimals); + + log.push(`tokenCount = ${tokenCount}`); + + return toFloat(tokenCount, _options.decimals) * price; +} + +function printLog() { + if (_options.log || false) { + console.debug(log); + log = []; + } +} + +async function getTokenPrice( + tokenAddress: any, + network: any, + provider: any, + blockTag: string | number +) { + let price = 1; + const cacheKey = tokenAddress + blockTag; + + if (_options.usePrice === true && !priceCache.has(cacheKey)) { + log.push( + `calling getPrice for token address: ${tokenAddress} and blockTag: ${blockTag}` + ); + + priceCache.set( + cacheKey, + (await getPrice(network, provider, tokenAddress, blockTag)) || 1 + ); + } + + price = priceCache.get(cacheKey) || 1; + + return price; +} + +async function getPrice(network, provider, address, blockTag) { + if (!blockCache.has(blockTag)) { + blockCache.set(blockTag, await provider.getBlock(blockTag)); + } + + const block = blockCache.get(blockTag); + const platform = networksWithPlatforms[network]; + const currency = _options.currency || 'usd'; + const from = block.timestamp - 100000; + const to = block.timestamp; + const coingeckoApiURL = `https://api.coingecko.com/api/v3/coins/${platform}/contract/${new String( + address + ).toLowerCase()}/market_chart/range?vs_currency=${currency}&from=${from}&to=${to}`; + + log.push(`platform = ${platform}`); + log.push(`from = ${from}`); + log.push(`to = ${from}`); + log.push(`coingeckoApiURL = ${coingeckoApiURL}`); + + const coingeckoData = await fetch(coingeckoApiURL) + .then(async (r) => { + log.push(`coingeco response = ${JSON.stringify(r, undefined, 2)}`); + + const json = await r.json(); + + log.push(`coingecko json = ${JSON.stringify(json, undefined, 2)}`); + + return json; + }) + .catch((e) => { + console.error(e); + throw new Error( + 'Strategy masterchef-pool-balance-of-token: coingecko api failed' + ); + }); + + return (coingeckoData.prices?.pop()?.pop() || 0) as number; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + _options = options; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const userInfoCalls = getUserInfoCalls(addresses); + const tokenCalls = getTokenCalls(); + const entries = new Map(); + + const userInfoResponse = await multicall( + network, + provider, + abi, + userInfoCalls, + { blockTag } + ); + + const userInfoChunks = arrayChunk(userInfoResponse, 1); + + const tokenResponse = await multicall(network, provider, abi, tokenCalls, { + blockTag + }); + + for (let i = 0; i < userInfoChunks.length; i++) { + const value = userInfoChunks[i]; + const score = await processValues( + value, + tokenResponse, + network, + provider, + blockTag + ); + + entries.set(addresses[i], score); + } + + return Object.fromEntries(entries); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance/examples.json new file mode 100644 index 00000000..2b639f8a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance/examples.json @@ -0,0 +1,46 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "masterchef-pool-balance", + "params": { + "symbol": "CHEF", + "chefAddress": "0xD38abbAeC03a9FF287eFc9a5F0d0580E07335D1D", + "uniPairAddress": null, + "tokenIndex": null, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "1", + "addresses": [ + "0xfCA5a27d4cfF104FC276897CA3f32cFeDc6f50BA", + "0x577bfa0898187c10bbbbb3d001c94aafb5cfc0e4", + "0x068e48e984b83387afe772614916780a753929d9" + ], + "snapshot": 12288851 + }, + { + "name": "Pool 1 balance", + "strategy": { + "name": "masterchef-pool-balance", + "params": { + "symbol": "CHEF LP", + "chefAddress": "0xD38abbAeC03a9FF287eFc9a5F0d0580E07335D1D", + "uniPairAddress": "0xe0b1433e0174b47e8879ee387f1069a0dbf94137", + "tokenIndex": 0, + "pid": "1", + "weight": 5, + "weightDecimals": 1 + } + }, + "network": "1", + "addresses": [ + "0xfCA5a27d4cfF104FC276897CA3f32cFeDc6f50BA", + "0x577bfa0898187c10bbbbb3d001c94aafb5cfc0e4", + "0x068e48e984b83387afe772614916780a753929d9" + ], + "snapshot": 12288851 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance/index.ts new file mode 100644 index 00000000..08f553e3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef-pool-balance/index.ts @@ -0,0 +1,162 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'my-swarm'; +export const version = '0.1.0'; + +/* + * Generic masterchef pool balance strategy. Accepted options: + * - chefAddress: masterchef contract address + * - pid: mastechef pool id (starting with zero) + * - uniPairAddress: address of a uniswap pair (or a sushi pair or any other with the same interface) + * - if the uniPairAddress option is provided, converts staked LP token balance to base token balance + * (based on the pair total supply and base token reserve) + * - if uniPairAddress is null or undefined, returns staked token balance as is + * - tokenIndex: index of a token in LP pair, optional, by default 0 + * - weight: integer multiplier of the result (for combining strategies with different weights, totally optional) + */ + +const abi = [ + // to get a user/pool balance from masterchef + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + }, + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + // to get supply/reserve from uni pair + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'getReserves', + outputs: [ + { + internalType: 'uint112', + name: '_reserve0', + type: 'uint112' + }, + { + internalType: 'uint112', + name: '_reserve1', + type: 'uint112' + }, + { + internalType: 'uint32', + name: '_blockTimestampLast', + type: 'uint32' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +// calls is a 1-dimensional array so we just push 3 calls for every address +const getCalls = (addresses: any[], options: any) => { + const result: any[] = []; + for (const address of addresses) { + result.push([options.chefAddress, 'userInfo', [options.pid, address]]); + if (options.uniPairAddress != null) { + result.push([options.uniPairAddress, 'totalSupply', []]); + result.push([options.uniPairAddress, 'getReserves', []]); + } + } + return result; +}; + +function arrayChunk(arr: T[], chunkSize: number): T[][] { + const result: T[][] = []; + for (let i = 0, j = arr.length; i < j; i += chunkSize) { + result.push(arr.slice(i, i + chunkSize)); + } + return result; +} + +// values is an array of (chunked) call results for every input address +// for setups with uniPairAddress each chunk has 3 items, for setups without, only 1 item +function processValues(values: any[], options: any): number { + const poolStaked = values[0][0]; + const weight: BigNumber = BigNumber.from(options.weight || 1); + const weightDecimals: BigNumber = BigNumber.from(10).pow( + BigNumber.from(options.weightDecimals || 0) + ); + let result: BigNumber; + if (!options.uniPairAddress) { + result = poolStaked.mul(weight).div(weightDecimals); + } else { + const uniTotalSupply = values[1][0]; + const uniReserve = values[2][options.tokenIndex || 0]; + const precision = BigNumber.from(10).pow(18); + const tokensPerLp = uniReserve.mul(precision).div(uniTotalSupply); + result = poolStaked + .mul(tokensPerLp) + .mul(weight) + .div(weightDecimals) + .div(precision); + } + return parseFloat(formatUnits(result.toString(), options.decimals || 18)); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + getCalls(addresses, options), + { blockTag } + ); + return Object.fromEntries( + // chunk to response so that we can process values for each address + arrayChunk(response, options.uniPairAddress == null ? 1 : 3).map( + (value, i) => [addresses[i], processValues(value, options)] + ) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef/examples.json b/Implementations/API/backend/utils/snapshot/strategies/masterchef/examples.json new file mode 100644 index 00000000..11f4d79e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "masterchef", + "params": { + "_comment_": "NOTE: this strategy only supports 1 pool in masterchef. If you need better support, contact @0xKiwi", + "address": "0x4688a8b1f292fdab17e9a90c8bc379dc1dbd8713", + "symbol": "COVER (Masterchef)" + } + }, + "network": "1", + "addresses": ["0x2073513fdd01a70d4a1eb8b0361b992eb6f1c13b"], + "snapshot": 11838909 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/masterchef/index.ts b/Implementations/API/backend/utils/snapshot/strategies/masterchef/index.ts new file mode 100644 index 00000000..b018295d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/masterchef/index.ts @@ -0,0 +1,147 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest } from '../../utils'; + +const MASTERCHEF_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/sushiswap/master-chef' +}; + +const SUSHISWAP_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/sushiswap/exchange' +}; + +export const author = '0xKiwi'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const tokenAddress = options.address.toLowerCase(); + const sushiPools0Params = { + pairs: { + __args: { + where: { + token0: tokenAddress + }, + first: 100 + }, + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + }; + const sushiPools1Params = { + pairs: { + __args: { + where: { + token1: tokenAddress + }, + first: 100 + }, + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + sushiPools0Params.pairs.__args.block = { number: snapshot }; + // @ts-ignore + sushiPools1Params.pairs.__args.block = { number: snapshot }; + } + const sushiPools0Result = await subgraphRequest( + SUSHISWAP_SUBGRAPH_URL[network], + sushiPools0Params + ); + const sushiPools1Result = await subgraphRequest( + SUSHISWAP_SUBGRAPH_URL[network], + sushiPools1Params + ); + if (!sushiPools0Result || !sushiPools1Result) { + return; + } + const allSushiPools = sushiPools0Result.pairs.concat(sushiPools1Result.pairs); + + const pools = allSushiPools.map(({ id }) => id.toLowerCase()); + + const masterchefParams = { + pools: { + __args: { + where: { + pair_in: pools + }, + first: 100 + }, + id: true, + pair: true, + users: { + __args: { + where: { + amount_gt: 0, + address_in: addresses.map((address) => address.toLowerCase()) + } + }, + address: true, + amount: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + masterchefParams.pools.__args.block = { number: snapshot }; + } + const masterchefResult = await subgraphRequest( + MASTERCHEF_SUBGRAPH_URL[network], + masterchefParams + ); + + const one_gwei = BigNumber.from(10).pow(9); + let stakedBalances = []; + if (masterchefResult && masterchefResult.pools.length == 1) { + stakedBalances = masterchefResult.pools[0].users.map((u) => { + return { + address: u.address, + amount: u.amount + }; + }); + } + const score = {}; + if (allSushiPools && allSushiPools.length > 0) { + // We assume there is only one pool in masterchef here, for simplicity. + const pair = allSushiPools.filter( + ({ id }) => id == masterchefResult.pools[0].pair + )[0]; + // console.log(pair); + const token0perUni = pair.reserve0 / pair.totalSupply; + const token1perUni = pair.reserve1 / pair.totalSupply; + stakedBalances.forEach((u: any) => { + const userScore = + (u.amount / one_gwei.toNumber()) * + (pair.token0.id == tokenAddress ? token0perUni : token1perUni); + const userScoreInEther = userScore / one_gwei.toNumber(); + const userAddress = getAddress(u.address); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScoreInEther; + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/math/README.md b/Implementations/API/backend/utils/snapshot/strategies/math/README.md new file mode 100644 index 00000000..8a4340a9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/math/README.md @@ -0,0 +1,89 @@ +# math + +Apply common mathematical operations on outputs from other strategies. + +## Operations + +Currently supported operations are: + +| Operation | Operand Count | Description | +| ------------- | ------------- | ------------------------------------------ | +| `square-root` | 1 | takes the square root of the operand | +| `cube-root` | 1 | takes the cube root of the operand | +| `multiply` | 2 | x * a | +| `min` | 2 | takes the smaller number of the 2 operands | +| `max` | 2 | takes the larger number of the 2 operands | +| `a-if-lt-b` | 3 | (x, a, b) = x < b ? a : x | +| `a-if-lte-b` | 3 | (x, a, b) = x <= b ? a : x | +| `a-if-gt-b` | 3 | (x, a, b) = x > b ? a : x | +| `a-if-gte-b` | 3 | (x, a, b) = x >= b ? a : x | +| `minus` | 2 | x - a | + +## Examples + +The following example takes the square root of a user's DAI token balance as voting score. + +```json +{ + "symbol": "MATH", + "operands": [ + { + "type": "strategy", + "strategy": { + "name": "erc20-balance-of", + "network": "1", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + } + } + ], + "operation": "square-root" +} +``` + +Here's another example that sets any score from the result above that's less than 100 to zero. + +```json +{ + "symbol": "MATH", + "operands": [ + { + "type": "strategy", + "strategy": { + "name": "math", + "network": "1", + "symbol": "MATH", + "params": { + "operands": [ + { + "type": "strategy", + "strategy": { + "name": "erc20-balance-of", + "network": "1", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + } + } + ], + "operation": "square-root" + } + } + }, + { + "type": "constant", + "value": 0 + }, + { + "type": "constant", + "value": 100 + } + ], + "operation": "a-if-lt-b" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/math/examples.json b/Implementations/API/backend/utils/snapshot/strategies/math/examples.json new file mode 100644 index 00000000..95de058f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/math/examples.json @@ -0,0 +1,230 @@ +[ + { + "name": "Example simple contract call plus / minus", + "strategy": { + "name": "math", + "params": { + "symbol": "MATH", + "operands": [ + { + "type": "strategy", + "strategy": { + "network": "1", + "name": "erc20-balance-of", + "params": { + "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "symbol": "USDT", + "decimals": 6 + } + } + }, + { + "type": "constant", + "value": 6 + } + ], + "operation": "minus" + } + }, + "network": "1", + "addresses": [ + "0x5C52cC7c96bDE8594e5B77D5b76d042CB5FaE5f2", + "0x4D19C0a5357bC48be0017095d3C871D9aFC3F21d", + "0xf59869753f41Db720127Ceb8DbB8afAF89030De4" + ], + "snapshot": 16847746 + }, + { + "name": "Example nested query", + "strategy": { + "name": "math", + "params": { + "symbol": "MATH", + "operands": [ + { + "type": "strategy", + "strategy": { + "network": "1", + "name": "erc20-balance-of", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + } + }, + { + "type": "constant", + "value": 6 + } + ], + "operation": "multiply" + } + }, + "network": "1", + "addresses": [ + "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0xCba1A275e2D858EcffaF7a87F606f74B719a8A93", + "0x35137867d87Bf78f8c4340C00872930CBb5f92e0", + "0x2802247b16c302B811988Bf4Ac0e8DBFB85CDe36", + "0x5ef01a9aB62f700BB0BCC0F11f9CF7aa8fc543fd" + ], + "snapshot": 14400123 + }, + { + "name": "Example nested query", + "strategy": { + "name": "math", + "params": { + "symbol": "MATH", + "operands": [ + { + "type": "strategy", + "strategy": { + "name": "math", + "network": "1", + "symbol": "MATH", + "params": { + "operands": [ + { + "type": "strategy", + "strategy": { + "name": "erc20-balance-of", + "network": "1", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + } + } + ], + "operation": "square-root" + } + } + }, + { + "type": "constant", + "value": 0 + }, + { + "type": "constant", + "value": 100 + } + ], + "operation": "a-if-lt-b" + } + }, + "network": "1", + "addresses": [ + "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + }, + { + "name": "Example simple query", + "strategy": { + "name": "math", + "params": { + "symbol": "MATH", + "operands": [ + { + "type": "strategy", + "strategy": { + "name": "erc20-balance-of", + "network": "1", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + } + } + ], + "operation": "square-root" + } + }, + "network": "1", + "addresses": [ + "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + }, + { + "name": "Example legacy query", + "strategy": { + "name": "math", + "params": { + "symbol": "MATH", + "strategy": { + "name": "erc20-balance-of", + "network": "1", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + }, + "operation": "square-root" + } + }, + "network": "1", + "addresses": [ + "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/math/index.ts b/Implementations/API/backend/utils/snapshot/strategies/math/index.ts new file mode 100644 index 00000000..5a302628 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/math/index.ts @@ -0,0 +1,190 @@ +import strategies from '..'; + +import { + ConstantOperand, + migrateLegacyOptions, + Operand, + OperandType, + Operation, + OptionalOptions, + Options, + StrategyOperand, + validateOptions +} from './options'; + +export const author = 'xJonathanLEI'; +export const version = '0.2.2'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const rawOptions: OptionalOptions = migrateLegacyOptions(options); + const strategyOptions: Options = validateOptions(rawOptions); + + // Recursively resolve operands + const operandPromises: Promise>[] = + strategyOptions.operands.map((item) => + resolveOperand(item, addresses, space, network, provider, snapshot) + ); + const resolvedOperands: Record[] = await Promise.all( + operandPromises + ); + + const finalResult: Record = resolveOperation( + strategyOptions.operation, + resolvedOperands + ); + + return finalResult; +} + +function resolveOperation( + operation: Operation, + resolvedOperands: Record[] +): Record { + switch (operation) { + case Operation.SquareRoot: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [address, Math.sqrt(score)] + ) + ); + } + case Operation.CubeRoot: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [address, Math.cbrt(score)] + ) + ); + } + case Operation.Min: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [ + address, + Math.min(score, resolvedOperands[1][address]) + ] + ) + ); + } + case Operation.Max: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [ + address, + Math.max(score, resolvedOperands[1][address]) + ] + ) + ); + } + case Operation.AIfLtB: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [ + address, + score < resolvedOperands[2][address] + ? resolvedOperands[1][address] + : score + ] + ) + ); + } + case Operation.AIfLteB: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [ + address, + score <= resolvedOperands[2][address] + ? resolvedOperands[1][address] + : score + ] + ) + ); + } + case Operation.AIfGtB: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [ + address, + score > resolvedOperands[2][address] + ? resolvedOperands[1][address] + : score + ] + ) + ); + } + case Operation.AIfGteB: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [ + address, + score >= resolvedOperands[2][address] + ? resolvedOperands[1][address] + : score + ] + ) + ); + } + case Operation.Multiply: { + return Object.fromEntries( + Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [ + address, + score * resolvedOperands[1][address] + ] + ) + ); + } + case Operation.MINUS: { + const arr = Object.entries(resolvedOperands[0]).map( + ([address, score]: [string, number]) => [ + address, + score > resolvedOperands[1][address] + ? score - resolvedOperands[1][address] + : 0 + ] + ); + return Object.fromEntries(arr); + } + } +} + +async function resolveOperand( + operand: Operand, + addresses: string[], + space: any, + network: any, + provider: any, + snapshot: any +): Promise> { + switch (operand.type) { + case OperandType.Strategy: { + const strategyOperand: StrategyOperand = operand as StrategyOperand; + + const upstreamResult: Record = await strategies[ + strategyOperand.strategy.name + ].strategy( + space, + strategyOperand.strategy.network ?? network, + provider, + addresses, + strategyOperand.strategy.params, + snapshot + ); + + return upstreamResult; + } + case OperandType.Constant: { + const constantOperand: ConstantOperand = operand as ConstantOperand; + + return Object.fromEntries( + addresses.map((address) => [address, constantOperand.value]) + ); + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/math/options.ts b/Implementations/API/backend/utils/snapshot/strategies/math/options.ts new file mode 100644 index 00000000..82ba85ed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/math/options.ts @@ -0,0 +1,152 @@ +export type Operand = StrategyOperand | ConstantOperand; + +export interface Options { + operands: Operand[]; + operation: Operation; +} + +export interface StrategyOperand { + type: OperandType.Strategy; + strategy: any; +} + +export interface ConstantOperand { + type: OperandType.Constant; + value: number; +} + +export enum OperandType { + Strategy = 'strategy', + Constant = 'constant' +} + +export enum Operation { + SquareRoot = 'square-root', + CubeRoot = 'cube-root', + Min = 'min', + Max = 'max', + AIfLtB = 'a-if-lt-b', + AIfLteB = 'a-if-lte-b', + AIfGtB = 'a-if-gt-b', + AIfGteB = 'a-if-gte-b', + Multiply = 'multiply', + MINUS = 'minus' +} + +interface LegacyFields { + strategy: any; // Legacy option used in v0.1.0 +} + +export type OptionalOperand = OptionalStrategyOperand | OptionalConstantOperand; + +export interface OptionalOptions { + operands: OptionalOperand[] | undefined; + operation: Operation | undefined; +} + +export interface OptionalStrategyOperand { + type: OperandType.Strategy | undefined; + strategy: any | undefined; +} + +export interface OptionalConstantOperand { + type: OperandType.Constant | undefined; + value: number | undefined; +} + +const operandCountByOperation: Record = { + [Operation.SquareRoot]: 1, + [Operation.CubeRoot]: 1, + [Operation.Multiply]: 2, + [Operation.Min]: 2, + [Operation.Max]: 2, + [Operation.AIfLtB]: 3, + [Operation.AIfLteB]: 3, + [Operation.AIfGtB]: 3, + [Operation.AIfGteB]: 3, + [Operation.MINUS]: 2 +}; + +export function validateOptions(rawOptions: OptionalOptions): Options { + if (!rawOptions.operands) { + throw new Error('Field `operands` missing'); + } + if (!rawOptions.operation) { + throw new Error('Field `operation` missing'); + } + if ( + rawOptions.operation !== Operation.SquareRoot && + rawOptions.operation !== Operation.CubeRoot && + rawOptions.operation !== Operation.Min && + rawOptions.operation !== Operation.Max && + rawOptions.operation !== Operation.AIfLtB && + rawOptions.operation !== Operation.AIfLteB && + rawOptions.operation !== Operation.AIfGtB && + rawOptions.operation !== Operation.AIfGteB && + rawOptions.operation !== Operation.Multiply && + rawOptions.operation !== Operation.MINUS + ) { + throw new Error('Invalid `operation`'); + } + if ( + rawOptions.operands.length !== operandCountByOperation[rawOptions.operation] + ) { + throw new Error('Operand count mismatch'); + } + + const options: Options = { + operands: [], + operation: rawOptions.operation + }; + + for (const operand of rawOptions.operands) { + switch (operand.type) { + case OperandType.Strategy: { + options.operands.push({ + type: OperandType.Strategy, + strategy: operand.strategy + }); + break; + } + case OperandType.Constant: { + if (operand.value === undefined) { + throw new Error('Invalid constant value'); + } + + options.operands.push({ + type: OperandType.Constant, + value: operand.value + }); + break; + } + default: { + throw new Error(`Invalid operand type: ${operand.type}`); + } + } + } + + return options; +} + +export function migrateLegacyOptions( + options: OptionalOptions & LegacyFields +): OptionalOptions { + if (options.strategy && options.operands) { + throw new Error('Only one of `strategy` and `operands` can be used'); + } + + // `strategy` was used in v0.1.0 + if (options.strategy) { + return { + operands: [ + { + type: OperandType.Strategy, + strategy: options.strategy + } + ], + operation: options.operation + }; + } else { + return options; + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/README.md b/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/README.md new file mode 100644 index 00000000..8941c70e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/README.md @@ -0,0 +1,24 @@ +This is simple strategy to use balance from graph instead of archive node. + +The configuration would be like this: + +```json +{ + "symbol": "MCB", + "graph": "the api address of your graph", + "decimals": 18 +} +``` + +The schema of the graph project is: + +``` +Account { + id: ID! + balance: BigDecimal! +} +``` + +As a example, here is a the graph project for MCB token: [mcb-balance](https://thegraph.com/hosted-service/subgraph/renpu-mcarlo/mcb-balance). + +And the `decimals` is an optional field. Ignore it if you have already handle it in graph, or set it to the exact decimals of the raw token balance. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/examples.json b/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/examples.json new file mode 100644 index 00000000..bd2b6806 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "mcb-balance-from-graph", + "params": { + "symbol": "MCB", + "graph": "https://api.thegraph.com/subgraphs/name/renpu-mcarlo/mcb-balance" + } + }, + "network": "1", + "addresses": [ + "0xfdf6c6096e70799e53d6172d32c6c3b6f3d1f38f", + "0xb796a0868a8fe1f93df3ea856daa6e169f7c37db", + "0x0cefe050ff97872c4390bdf28cd87822185ab266", + "0xe3124B1F5B7c793B6B4C97aFcAD7C6df6c1A9bc9" + ], + "snapshot": 2393677 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/index.ts b/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/index.ts new file mode 100644 index 00000000..c0d2ed5a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mcb-balance-from-graph/index.ts @@ -0,0 +1,65 @@ +import { subgraphRequest } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'yangzhao28'; +export const version = '1.0.0'; + +const LIMIT = 500; + +function makeQuery(snapshot, addressSet) { + const query = { + accounts: { + __args: { + where: { + id_in: addressSet + }, + first: LIMIT + }, + id: true, + balance: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + query.accounts.__args.block = { number: snapshot }; + } + return query; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const _addresses = addresses.map((x) => x.toLowerCase()); + const addressSubsets = Array.apply( + null, + Array(Math.ceil(_addresses.length / LIMIT)) + ).map((_e, i) => _addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + + const returnedFromSubgraph = await Promise.all( + addressSubsets.map((subset) => + subgraphRequest(options.graph, makeQuery(snapshot, subset)) + ) + ); + + const result = returnedFromSubgraph.map((x) => x.accounts).flat(); + const scores = {}; + const scaler = BigNumber.from(10).pow(options.decimals || 18); + addresses.forEach((address) => { + const account = result.filter((x) => x.id == address.toLowerCase())[0]; + let score = 0; + if (account) { + if (options.decimals) { + score = BigNumber.from(account.balance).div(scaler).toNumber(); + } else { + score = parseFloat(account.balance); + } + } + scores[address] = score; + }); + return scores || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/README.md b/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/README.md new file mode 100644 index 00000000..f6583fd6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/README.md @@ -0,0 +1,15 @@ +# mcn-farm + +This strategy returns the total user-owned MCN rewards (staked tokens and pending rewards) held by the farm contract. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "tokenAddress": "0xD91E9a0fEf7C0fa4EBdAF4d0aCF55888949A2a9b", + "lpAddress": "0x2Ef2cb6af83de4171A69EE2f7C677079fFD9BcD0", + "stakingAddress": "0x15dEd15fE32EBac0b6cFb08cdAB112cca8380423" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/examples.json b/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/examples.json new file mode 100644 index 00000000..fdcf5a60 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "mcn-farm", + "params": { + "tokenAddress": "0xD91E9a0fEf7C0fa4EBdAF4d0aCF55888949A2a9b", + "symbol": "MCN", + "decimals": "18", + "lpAddress": "0x2Ef2cb6af83de4171A69EE2f7C677079fFD9BcD0", + "stakingAddress": "0x15dEd15fE32EBac0b6cFb08cdAB112cca8380423" + } + }, + "network": "1", + "addresses": [ + "0xCba1A275e2D858EcffaF7a87F606f74B719a8A93", + "0x0E530732a9ecc807Cafc1665ADa8C27C197a7cb1", + "0xF4a616612D95FfcC667F2cCa19b41842Cea78DE6", + "0x1096cB8F6e17dB5a072fa5b19dC4b8F212CB36E3", + "0xD0996fd8113838Be107539dAbFa5EbD89583d45d", + "0x5e7C21DefE711bCd5CEa1B267d2e87F7913D510F", + "0xf1F56a5a6E9aCf0cF316Da9f2DcFc7312F4CAba9", + "0x87616fA850c87a78f307878f32D808dad8f4d401" + ], + "snapshot": 13050000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/index.ts b/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/index.ts new file mode 100644 index 00000000..a7404ca1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mcn-farm/index.ts @@ -0,0 +1,98 @@ +import { formatUnits } from '@ethersproject/units'; +import { call, multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'drop-out-dev'; +export const version = '0.1.0'; + +const FARM_ADDRESS = '0x15dEd15fE32EBac0b6cFb08cdAB112cca8380423'; +const MCN_ADDRESS = '0xD91E9a0fEf7C0fa4EBdAF4d0aCF55888949A2a9b'; +const MCN_LP_ADDRESS = '0x2Ef2cb6af83de4171A69EE2f7C677079fFD9BcD0'; + +const abi = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address account) view returns (uint256)', + 'function getPoolList() view returns (address[])', + 'function getPool(address _lpToken) view returns (tuple(tuple(address bonusTokenAddr, uint48 startTime, uint48 endTime, uint256 weeklyRewards, uint256 accRewardsPerToken, uint256 remBonus)[] bonuses, uint256 lastUpdatedAt, uint256 amount))', + 'function getUser(address _lpToken, address _account) view returns (tuple(uint256 amount, uint256[] rewardsWriteoffs) user, uint256[] rewards)' +]; + +export async function strategy( + space, + network, + provider, + addresses: string[], + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const lpAddress = options.lpAddress || MCN_LP_ADDRESS; + const tokenAddress = options.tokenAddress || MCN_ADDRESS; + const farmAddress = options.stakingAddress || FARM_ADDRESS; + const pools = await call(provider, abi, [farmAddress, 'getPoolList', []]); + const flatten = (arr) => [].concat.apply([], arr); + const product = (...sets) => { + return sets.reduce( + (acc, set) => flatten(acc.map((x) => set.map((y) => [...x, y]))), + [[]] + ); + }; + const params = product(pools, addresses); + const res = await multicall( + network, + provider, + abi, + [ + [lpAddress, 'totalSupply', []], + [tokenAddress, 'balanceOf', [lpAddress]] + ] + .concat(pools.map((p) => [farmAddress, 'getPool', [p]])) + .concat(params.map((p) => [farmAddress, 'getUser', p])), + { blockTag } + ); + const [totalSupply] = res[0]; + const [tokenBalanceInLP] = res[1]; + const tokensPerLP = tokenBalanceInLP.div(totalSupply); + const poolInfo = res.slice(2, 2 + pools.length); + // rewardToken_i maps pool index => pool bonus token index matching tokenAddress (if applicable) + const rewardToken_i = {}; + for (let i = 0; i < pools.length; i++) { + const bonuses = poolInfo[i][0].bonuses; + if (bonuses === undefined) continue; + for (let j = 0; j < bonuses.length; j++) { + if (bonuses[j].bonusTokenAddr == tokenAddress) { + rewardToken_i[i] = j; + continue; + } + } + } + const response = res.slice(2 + pools.length); + const values = {}; + Object.values(addresses).forEach( + (address: string) => (values[address] = BigNumber.from(0)) + ); + response.forEach(([userInfo, rewards], i) => { + const address_i = i % addresses.length; + const address = addresses[address_i]; + const pool_i = Math.floor(i / addresses.length); + const bonus_i = rewardToken_i[pool_i]; + if (bonus_i && rewards.length > bonus_i) { + values[address].add(rewards[bonus_i]); + } + if (pool_i === 0) { + // this is the MCN staking pool + values[address] = values[address].add(userInfo.amount); // add staked amount + } else if (pool_i === 1) { + // this is the MCN-USDC staking pool + values[address] = values[address].add(userInfo.amount.mul(tokensPerLP)); + } + }); + + for (const address in values) { + if (values.hasOwnProperty(address)) { + const value = parseFloat(formatUnits(values[address], 18)); + values[address] = value; + } + } + return values; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/README.md new file mode 100644 index 00000000..20421578 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/README.md @@ -0,0 +1,32 @@ +# meebitsdao-delegation + +This delegation strategy returns the balances of the voters for specific ERC20 [mVOX](https://polygonscan.com/address/0x7C1a4c36D9BDa5C568f0E4877CD8E27D74Ae66c6) and ERC721 [MFND](https://polygonscan.com/address/0xc34cbca32e355636c7f52dd8beab0af2396ebd79). The calcualation is as follows: users with MFND tokens can hold voting power and can be assigned as delegates, while users with mVOX tokens can delegate votes (to those who hold MFND tokens) but cannot hold actual vote themselves. This is different from the standard Snapshot voting strategy in that votes from delegators do not count themselves, if the votes are not delegated. + +Here is an example of parameters: + +```json +{ + "symbol": "mVOX, MFND, Meebits", + "tokenAddresses": [ + { + "address": "0x7C1a4c36D9BDa5C568f0E4877CD8E27D74Ae66c6", + "symbol": "mVOX", + "decimals": 0, + "network": "137" + }, + { + "address": "0xc34cbca32e355636c7f52dd8beab0af2396ebd79", + "symbol": "MFND", + "apiUrl": "https://api.meebitsdao.com/user/token_status/", + "startingTokenId": 1, + "endingTokenId": 200, + "network": "137" + }, + { + "address": "0x7Bd29408f11D2bFC23c34f18275bBf23bB716Bc7", + "symbol": "Meebits", + "network": "1" + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/examples.json new file mode 100644 index 00000000..5214de83 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/examples.json @@ -0,0 +1,58 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "meebitsdao-delegation", + "params": { + "symbol": "mVOX, MFND, Meebits", + "tokenAddresses": [ + { + "address": "0x7C1a4c36D9BDa5C568f0E4877CD8E27D74Ae66c6", + "symbol": "mVOX", + "decimals": 0, + "network": "137" + }, + { + "address": "0xc34cbca32e355636c7f52dd8beab0af2396ebd79", + "symbol": "MFND", + "apiUrl": "https://api.meebitsdao.com/user/token_status/", + "startingTokenId": 1, + "endingTokenId": 200, + "network": "137" + }, + { + "address": "0x7Bd29408f11D2bFC23c34f18275bBf23bB716Bc7", + "symbol": "Meebits", + "network": "1" + } + ] + } + }, + "network": "137", + "addresses": [ + "0x35001A8Bdb3a224D05F086094c12fd4c9009986D", + "0x22aD49d3E1F81A2A626E2DF64Fbdabe1536CaB07", + "0x6f35B0Cfc58Eb1e21eeF8a439BbB0cE4C929d32a", + "0xEDe64a571CFe98B936271B935a955620f387E05A", + "0x4613739e5BCea4730F2a4983CE2100432415b01B", + "0xd0Fd6D1C05b15F5B141253F13855c86cC3E655B8", + "0x2D1547D6C43Eb8185379D11Cf1A6dDAf2780B62A", + "0xc842Ce7214A14Fa98186a010bcb43c7e99e4caF3", + "0x8F903cFC0Af3C2EC0d872c57538AF5e071544a57", + "0x19FfA65cF48B8D0cD9081958e69b7A170824955C", + "0xf4fF19C30c98533fd6D3cEcF09b3d6802e470dD0", + "0xc03a694c5f7BDf2D32583F4E2421E3Fb3426D3aD", + "0xC1dFeDD7B8Fc3010a42466E51c773068fb95F485", + "0x614b89F072eA263a9387460963142e73548FBAF1", + "0x2F28c719768f71ad3657463f230292A0b63b65d0", + "0xC5e38233Cc0D7CFf9340e6139367aBA498EC9b18", + "0xF134c8f76214c500dcDC9188A21bD405CdAAf4c8", + "0x2009a752a50D3CDe486d7b5921944377B729E747", + "0x017a04631caF0Bacec325f68caAd2838a80Dd91D", + "0x0A1ee64e760cE54D38958E7b9c2a429CFaE050c4", + "0x0Cbf9fA338048BB8A8A0eA33f9C59f9d0407D692", + "0xffB6D3c42B07c17ED0B6bE1136D2EBC599b2562e" + ], + "snapshot": 25925040 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/index.ts new file mode 100644 index 00000000..eca3299e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao-delegation/index.ts @@ -0,0 +1,135 @@ +import { getAddress } from '@ethersproject/address'; +import { strategy as meebitsdaoStrategy } from '../meebitsdao'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { strategy as erc721Strategy } from '../erc721'; +import { subgraphRequest, getProvider, getSnapshots } from '../../utils'; + +export const author = 'maikir'; +export const version = '0.2.0'; + +const MEEBITSDAO_DELEGATION_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/maikir/meebitsdao-delegation'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blocks = await getSnapshots( + network, + snapshot, + provider, + options.tokenAddresses.map((s) => s.network || network) + ); + + const PAGE_SIZE = 1000; + let result: any = []; + let page = 0; + const params: any = { + delegations: { + __args: { + first: PAGE_SIZE, + skip: 0 + }, + delegator: true, + delegate: true + } + }; + if (snapshot !== 'latest') { + params.delegations.__args.block = { number: snapshot }; + } + + //This function may only run to 6000 queries total (first: 1000 * 6 pages). After that, the query may return 0 results even though there may be more. + while (true) { + params.delegations.__args.skip = page * PAGE_SIZE; + const pageResult = await subgraphRequest( + MEEBITSDAO_DELEGATION_SUBGRAPH_URL, + params + ); + const pageDelegations = pageResult.delegations || []; + result = result.concat(pageDelegations); + page++; + if (pageDelegations.length < PAGE_SIZE) break; + } + + const lowerCaseAddresses: string[] = []; + + addresses.forEach((address) => { + lowerCaseAddresses.push(address.toLowerCase()); + }); + + const mvoxAddresses: string[] = []; + result.forEach((delegation) => { + mvoxAddresses.push(delegation.delegator); + }); + + const mvoxScores = await erc20BalanceOfStrategy( + space, + network, + provider, + mvoxAddresses, + options.tokenAddresses[0], + snapshot + ); + + const mfndScores = await meebitsdaoStrategy( + space, + network, + provider, + lowerCaseAddresses, + options.tokenAddresses[1], + snapshot + ); + + const meebitsScores = await erc721Strategy( + space, + options.tokenAddresses[2].network, + getProvider(options.tokenAddresses[2].network), + lowerCaseAddresses, + options.tokenAddresses[2], + blocks[options.tokenAddresses[2].network] + ); + + const delegations = {}; + + result.forEach((delegation) => { + let meebitsScore = 0; + let mvoxScore = 0; + if ( + delegation.delegator in mvoxScores && + delegation.delegate in meebitsScores + ) { + meebitsScore = Math.max( + 1, + Math.min(20, meebitsScores[delegation.delegate]) + ); + mvoxScore = mvoxScores[delegation.delegator]; + } + + if (delegation.delegate in delegations) { + delegations[delegation.delegate] += mvoxScore * meebitsScore; + } else { + delegations[delegation.delegate] = mvoxScore * meebitsScore; + } + }); + + const entries = Object.entries(mfndScores).map((address: any) => { + const founderAddress = address[0].toLowerCase(); + return [ + getAddress(founderAddress), + Math.min( + founderAddress in delegations + ? Math.max(address[1], delegations[founderAddress]) + : address[1], + 1000 + ) + ]; + }); + + const score = Object.fromEntries(entries); + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/README.md b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/README.md new file mode 100644 index 00000000..1fadb4e2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/README.md @@ -0,0 +1,15 @@ +# meebitsdao + +This strategy return the if the voter has an activated Founder's Token. + +Here is an example of parameters: + +```json +{ + "address": "0xc34cbca32e355636c7f52dd8beab0af2396ebd79", + "symbol": "MFND", + "apiUrl": "https://api.meebitsdao.com/user/token_status/", + "startingTokenId": 1, + "endingTokenId": 200 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/examples.json b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/examples.json new file mode 100644 index 00000000..773f0e39 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "meebitsdao", + "params": { + "address": "0xc34cbca32e355636c7f52dd8beab0af2396ebd79", + "symbol": "MFND", + "apiUrl": "https://api.meebitsdao.com/user/token_status/", + "startingTokenId": 1, + "endingTokenId": 200 + } + }, + "network": "137", + "addresses": [ + "0x2009a752a50D3CDe486d7b5921944377B729E747", + "0xf9e20049d1634b3ad75dedc6072b04ff68fe01db", + "0x4D949AA6994242031460fa8E3edD783cf77B5e2E" + ], + "snapshot": 18213580 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/index.ts b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/index.ts new file mode 100644 index 00000000..af6940bd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/meebitsdao/index.ts @@ -0,0 +1,59 @@ +import { multicall } from '../../utils'; +import fetch from 'cross-fetch'; + +export const author = 'peters-josh'; +export const version = '0.1.0'; + +const abi = [ + 'function ownerOf(uint256 tokenId) public view returns (address owner)', + 'function tokenURI(uint256 tokenId) public view returns (string uri)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const uriCalls: any[] = []; + for (let i = options.startingTokenId; i <= options.endingTokenId; i++) { + uriCalls.push([options.address, 'tokenURI', [i]]); + } + + const ownerCalls: any[] = []; + for (let i = options.startingTokenId; i <= options.endingTokenId; i++) { + ownerCalls.push([options.address, 'ownerOf', [i]]); + } + + const ownerResponse = await multicall(network, provider, abi, ownerCalls, { + blockTag + }); + + const resp = await fetch(options.apiUrl); + const tokenStatus = await resp.json(); + + function checkActivated(address: any) { + const index = ownerResponse.findIndex( + (res: any) => res.owner.toLowerCase() === address.toLowerCase() + ); + if (index == -1) { + return 0; + } + if (tokenStatus[index].Active == true) { + return 1; + } + return 0; + } + + const votes = addresses.map((address: any) => checkActivated(address)); + + const scores = {}; + votes.map((value, i) => { + scores[addresses[i]] = value; + }); + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/membership/README.md b/Implementations/API/backend/utils/snapshot/strategies/membership/README.md new file mode 100644 index 00000000..334c3b91 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/membership/README.md @@ -0,0 +1,29 @@ +# membership + +This strategy allows you to combine any arbitrary "membership" strategy with any "voting power" strategy. The idea is that you can only vote if you pass the membership strategy. This is super useful to enable things like quadratic voting or other non plutocratic systems that require sybil resistance. For example, Compound could say that voting power is your COMP balance, but only if you also possess a [UID](https://etherscan.io/address/0xba0439088dc1e75F58e0A7C107627942C15cbb41) or a PUNK (`erc721` strategy). Or you must pass BrightID verification (`brightid` strategy). Or make your own verification strategy based on on-chain behavior! The sky is the limit! + +Then you can use quadratic voting, or one-member-one-vote, or any other system you can think of. With a sense of identity, the design space for governance opens up significantly. + +Note: The membership portion is binary. If the membership strategy returns any number > 0 for an address, then you're a member. Otherwise voting power for that address is zero. + +Here is an example of parameters: + +```json + { + "membershipStrategy": { + "name": "erc721", + "params": { + "address": "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb" + } + }, + "votingPowerStrategy": { + "name": "erc20-balance-of", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18, + } + }, + "symbol": "DAI" + } +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/membership/examples.json b/Implementations/API/backend/utils/snapshot/strategies/membership/examples.json new file mode 100644 index 00000000..447324b5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/membership/examples.json @@ -0,0 +1,33 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "membership", + "params": { + "symbol": "DAI", + "membershipStrategy": { + "name": "erc721", + "params": { + "address": "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb", + "symbol": "PUNK" + } + }, + "votingPowerStrategy": { + "name": "erc20-balance-of", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + } + } + } + }, + "network": "1", + "addresses": [ + "0x3295df41a2f288da03818ae32565e1599f1b2eee", + "0x207230E06e14d1fa94e703f4784F819667472247", + "0x00555966521df9edbca9b8a497ad8031cf33f72a" + ], + "snapshot": 13791476 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/membership/index.ts b/Implementations/API/backend/utils/snapshot/strategies/membership/index.ts new file mode 100644 index 00000000..aeb2aa5b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/membership/index.ts @@ -0,0 +1,100 @@ +import _strategies from '..'; + +export const author = 'blakewest'; +export const version = '1.0.0'; + +/* +Membership Based Voting Strategy + +Options: { + membershipStrategy: { + name: {strategyName}, + options: { + ... options for the strategy + } + }, + votingPowerStrategy: { + name: {strategyName}, + options: { + ... options for the strategy + } + } +} + +*/ + +export async function strategy( + space, + network, + provider, + addresses, + options: { + name: string; + membershipStrategy: { + name: string; + params: Record; + }; + votingPowerStrategy: { + name: string; + params: Record; + }; + }, + snapshot +) { + const validAddresses = await membershipCheck( + space, + network, + provider, + addresses, + options.membershipStrategy, + snapshot + ); + const votingPowerStrategy = + _strategies[options.votingPowerStrategy.name].strategy; + const scores = await votingPowerStrategy( + space, + network, + provider, + validAddresses, + options.votingPowerStrategy.params, + snapshot + ); + + // Set invalid addresses to 0 + addresses + .filter((addr) => !validAddresses.includes(addr)) + .forEach((addr) => (scores[addr] = 0)); + + return scores; +} + +async function membershipCheck( + space: any, + network: string, + provider: any, + addresses: string[], + strategy: { + name: string; + params: Record; + }, + snapshot: number | string | undefined +): Promise { + const strategyFn = _strategies[strategy.name].strategy; + const result: { [address: string]: number } = await strategyFn( + space, + network, + provider, + addresses, + strategy.params, + snapshot + ); + return Object.entries(result) + .map(([address, val]) => { + if (val > 0) { + return address; + } else { + return ''; + } + }) + .filter((item) => item); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/README.md b/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/README.md new file mode 100644 index 00000000..7bacb434 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/README.md @@ -0,0 +1,20 @@ +# metropolis-pod + +This strategy gives one voting power to each member of a specific Metropolis Pod NFT, specified by the ERC1155 Token ID - which can be found in the [Metropolis web app](https://pod.xyz) or in your wallet. + +## Parameters + +| Param Name | Description | +| ----------- | ----------- | +| id | Token ID of the pod | +| weight (optional) | Multiplier of the voting power - Default is `1` | + +Here is an example of the parameters: + +```json +{ + "symbol": "METRO", + "id": "1", + "weight": 100 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/examples.json b/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/examples.json new file mode 100644 index 00000000..0e6c7893 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "metropolis-pod", + "params": { + "symbol": "METRO", + "id": "1", + "weight": 100 + } + }, + "network": "1", + "addresses": [ + "0x094a473985464098b59660b37162a284b5132753", + "0x4B4C43F66ec007D1dBE28f03dAC975AAB5fbb888", + "0x403f69b1092cf1cB82487CD137F96E8200f03BD5" + ], + "snapshot": 15188155 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/index.ts b/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/index.ts new file mode 100644 index 00000000..c6371ea0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/index.ts @@ -0,0 +1,28 @@ +import { strategy as erc1155BalanceOfIdsWeightedStrategy } from '../erc1155-balance-of-ids-weighted'; + +export const author = 'itsdanwu'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc1155BalanceOfIdsWeightedStrategy( + space, + network, + provider, + addresses, + { + address: '0x0762aa185b6ed2dca77945ebe92de705e0c37ae3', + ids: [options.id], + weight: parseFloat(options.weight || 1) + }, + snapshot + ); + + return Object.fromEntries(Object.entries(score).map((a) => [a[0], a[1]])); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/schema.json b/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/schema.json new file mode 100644 index 00000000..91cc5fcb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/metropolis-pod/schema.json @@ -0,0 +1,30 @@ +{ + "$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. METRO"], + "maxLength": 16 + }, + "id": { + "type": "string", + "title": "Token ID", + "examples": ["1"] + }, + "weight": { + "type": "number", + "title": "Weight", + "examples": ["e.g. 100"] + } + }, + "required": ["id"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/README.md b/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/README.md new file mode 100644 index 00000000..f48af959 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/README.md @@ -0,0 +1,15 @@ +# minmax-mcn-farm + +This strategy returns the total amount of user-owned MAX tokens rewards, including the token in wallet, staked token and pending rewards held by the farm contract. + +## Examples + +Rhe space config will look like this: + +```JSON +{ + "tokenAddress": "0xe45d95a66cfF6aB5E9b796CF5A36F0669AF3Ec98", + "lpAddress": "0x88137f2a610693e975b17d7cf940bf014cf0f325", + "stakingAddress": "0xf3a640eeb661cdf78f1817314123e8bbd12e191f" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/examples.json b/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/examples.json new file mode 100644 index 00000000..6c46be2f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "minmax-mcn-farm", + "params": { + "tokenAddress": "0xe45d95a66cfF6aB5E9b796CF5A36F0669AF3Ec98", + "symbol": "MAX", + "decimals": "18", + "lpAddress": "0x88137f2a610693e975b17d7cf940bf014cf0f325", + "stakingAddress": "0xf3a640eeb661cdf78f1817314123e8bbd12e191f" + } + }, + "network": "4689", + "addresses": [ + "0x207dCfFf0bF605e892d7b036617FfF1A9B7a3038", + "0x1c4922142585881d24a0374a26368224974ba730", + "0x9125b2457479964540a0557e3b010681317b635e", + "0x226f272a2635523b9d50a8d2b481ced204b4d9c2", + "0x2a4aa0c965e7903b230036169803d00c886c0d11" + ], + "snapshot": 14629883 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/index.ts b/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/index.ts new file mode 100644 index 00000000..420da060 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/minmax-mcn-farm/index.ts @@ -0,0 +1,90 @@ +import { multicall } from '../../utils'; + +export const author = 'LeifuChen'; +export const version = '0.1.0'; + +const FARM_ADDRESS = '0xf3a640eeb661cdf78f1817314123e8bbd12e191f'; +const MAX_ADDRESS = '0xe45d95a66cff6ab5e9b796cf5a36f0669af3ec98'; +const MAX_LP_ADDRESS = '0x88137f2a610693e975b17d7cf940bf014cf0f325'; + +const abi = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address account) view returns (uint256)', + 'function getUser(address _lpToken, address _account) view returns (tuple(uint256 amount, uint256[] rewardsWriteoffs) user, uint256[] rewards)' +]; + +export async function strategy( + space, + network, + provider, + addresses: string[], + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const lpAddress = options.lpAddress || MAX_LP_ADDRESS; + const tokenAddress = options.tokenAddress || MAX_ADDRESS; + const farmAddress = options.stakingAddress || FARM_ADDRESS; + const pools = [ + '0x88137F2a610693E975b17D7Cf940BF014CF0f325', // BUSD_MAX => MAX token in LP + '0xC35257624b01932e521bc5D9dc07e4F9ed21ED28', // minmaxB3 + '0x14D66e676b978255C719B2771c657ACc418Bb9Fa', // minmaxE4 + '0xdFf5DC9d8dAC189324452D54e2df19d2Bdba78CE', // minmaxM3 + '0x425C2c686f12d61ECD4dFD1170214E3BEFEbBe33' // minmaxUSDT + ]; + + const flatten = (arr) => [].concat.apply([], arr); + const product = (...sets) => { + return sets.reduce( + (acc, set) => flatten(acc.map((x) => set.map((y) => [...x, y]))), + [[]] + ); + }; + const params = product(pools, addresses); + const res = await multicall( + network, + provider, + abi, + [ + [lpAddress, 'totalSupply', []], + [tokenAddress, 'balanceOf', [lpAddress]] + ] + .concat(addresses.map((p) => [lpAddress, 'balanceOf', [p]])) + .concat(addresses.map((p) => [tokenAddress, 'balanceOf', [p]])) + .concat(params.map((p) => [farmAddress, 'getUser', p])), + { blockTag } + ); + + const values = {}; + Object.values(addresses).forEach((address: string) => (values[address] = 0)); + + // MAX token in user's wallet + const walletInfo = res.slice(2 + addresses.length, 2 + addresses.length * 2); + for (let i = 0; i < addresses.length; i++) { + values[addresses[i]] += walletInfo[i] / 10 ** 18; + } + + // MAX token in pendingRewards + const poolInfo = res.slice(2 + addresses.length * 2); + poolInfo.forEach(({ 1: reward }, i) => { + values[addresses[i % addresses.length]] += reward / 10 ** 18; + }); + + // MAX token in MAX_BUSD LP (MCN Farm) + const [totalSupply] = res[0]; + const [tokenBalanceInLP] = res[1]; + const tokensPerLP = tokenBalanceInLP / totalSupply; + const lpInfo = res.slice(2 + addresses.length * 2, 2 + addresses.length * 3); + lpInfo.forEach(({ 0: userInfo }, i) => { + values[addresses[i % addresses.length]] += + (userInfo[0] / 10 ** 18) * tokensPerLP; + }); + + // MAX token in MAX_BUSD LP (User Wallet) + const lpInWallet = res.slice(2, 2 + addresses.length); + for (let i = 0; i < addresses.length; i++) { + values[addresses[i]] += (lpInWallet[i] / 10 ** 18) * tokensPerLP; + } + + return values; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/minotaur-money/examples.json b/Implementations/API/backend/utils/snapshot/strategies/minotaur-money/examples.json new file mode 100644 index 00000000..409e47d2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/minotaur-money/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "MinotaurMoney", + "strategy": { + "name": "minotaur-money", + "params": { + "symbol": "MINO" + } + }, + "network": "25", + "addresses": [ + "0x4E5D385E44DCD0b7adf5fBe03A6BB867A8A90E7B", + "0x86Fdd9980aCD3e2C8e7959Db344Ff6D5FD5743F5" + ], + "snapshot": 4521240 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/minotaur-money/index.ts b/Implementations/API/backend/utils/snapshot/strategies/minotaur-money/index.ts new file mode 100644 index 00000000..7bf88da3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/minotaur-money/index.ts @@ -0,0 +1,138 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller, call } from '../../utils'; + +export const author = 'pepperstepper'; +export const version = '0.0.4'; + +const minoContractAddress = '0x3A1138075bd97a33F23A87824b811146FA44288E'; +const sMinoContractAddress = '0xB46fe6791A30d51970EA3B840C9fa5F1F107b86F'; +const wsMinoContractAddress = '0x1066c6753FFaf8540F691643A6D683e23599c4ab'; + +//const mmfPoolAddressOld = '0x57E8f8F7447D8d02fe4D291378D37E67D393257A'; +//const mmfPoolAddressOld = '0x849f97c5452cc4bad1069b8efe2b3561b06694c3'; +//const mmfPoolAddressNew = '0xf6a96e753dec01acb659acbe75deba46d53ebc5e'; +const mmfPoolAddressNewNew = '0x687a0275aE620FB7868b09f16d3FeF862824317d'; + +const erc20ContractAbi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; +const sMinoContractAbi = ['function index() external view returns (uint256)']; +const mmfPoolAbi = [ + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +type MultiCallResult = Record; +type MultiCallObjectResult = Record; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const callIndex = () => { + return call(provider, sMinoContractAbi, [ + sMinoContractAddress, + 'index', + [] + ]); + }; + + const makeMulticaller = ( + abi, + contractAddress, + functionSignature, + multicaller, + callBatchIdx = '0' + ) => { + let multiCaller = new Multicaller(network, provider, abi, { blockTag }); + if (callBatchIdx !== '0') { + multiCaller = multicaller; + } + addresses.forEach((address) => + multiCaller.call( + address + callBatchIdx, + contractAddress, + functionSignature, + [address] + ) + ); + return multiCaller; + }; + + const sMinoMulti = makeMulticaller( + erc20ContractAbi, + sMinoContractAddress, + 'balanceOf', + null, + '0' + ); + + const wsMinoMulti = makeMulticaller( + erc20ContractAbi, + wsMinoContractAddress, + 'balanceOf', + sMinoMulti, + '1' + ); + + const minoMulti = makeMulticaller( + erc20ContractAbi, + minoContractAddress, + 'balanceOf', + wsMinoMulti, + '2' + ); + const wsMinoInMMFMulti = makeMulticaller( + mmfPoolAbi, + mmfPoolAddressNewNew, + 'userInfo', + null, + '0' + ); + + const [index]: [BigNumber] = await Promise.all([callIndex()]); + + const [minoBalances, mmfUserInfo]: [MultiCallResult, MultiCallObjectResult] = + await Promise.all([minoMulti.execute(), wsMinoInMMFMulti.execute()]); + + const scores: Record = {}; + + for (const address of addresses) { + const wsMinoScore = BigNumber.from( + mmfUserInfo[address + '0'] //from mmf pool + ? mmfUserInfo[address + '0']['amount'] + : 0 + ) + .add(minoBalances[address + '1'] || 0) // wsMinoBalances + .mul(index); // timeses by 10^9 effectively + + const minoScore = wsMinoScore + .add( + BigNumber.from(minoBalances[address + '0'] || 0).mul( + BigNumber.from(10).pow(18) + ) + ) // mino balances + .add( + BigNumber.from(minoBalances[address + '2'] || 0).mul( + BigNumber.from(10).pow(18) + ) + ); // sMino balances + + scores[address] = minoScore; + } + + const scoresNumber = Object.fromEntries( + Object.entries(scores).map(([address, balance]) => [ + address, + balance.div(BigNumber.from(10).pow(18)).toNumber() / 1000000000 + ]) + ); + + return scoresNumber; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/README.md b/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/README.md new file mode 100644 index 00000000..be2fd8de --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/README.md @@ -0,0 +1,15 @@ +# minto-balance-of-all + +This is the minto strategy, it returns sum the balances of the voters from minto erc20, minto staking, minto staking. + +Here is an example of parameters: + +```json +{ + "address": "0x410a56541bD912F9B60943fcB344f1E3D6F09567", + "symbol": "BTCMT", + "decimals": 18, + "stakingAddress": "0x78ae303182FCA96A4629A78Ee13235e6525EbcFb", + "autoStakingAddress": "0xE751ffdC2a684EEbcaB9Dc95fEe05c083F963Bf1" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/examples.json b/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/examples.json new file mode 100644 index 00000000..2d63f741 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "minto-balance-of-all", + "params": { + "address": "0x410a56541bD912F9B60943fcB344f1E3D6F09567", + "symbol": "BTCMT", + "decimals": 18, + "stakingAddress": "0x78ae303182FCA96A4629A78Ee13235e6525EbcFb", + "autoStakingAddress": "0xE751ffdC2a684EEbcaB9Dc95fEe05c083F963Bf1" + } + }, + "network": "128", + "addresses": [ + "0x05903a958710ec752dd4a25dd3157ccee9d6c92e", + "0x03bb74c32acb025effab4d31e579d3751363c216", + "0x0369a66d5d25734a9aef4689546a809888fe5bfe", + "0x0743a7eb79cf4b478ba0bba22bdf2f11bc8ad5de", + "0x00ce049d0624a087473a4bd5078ac90ad25175e8", + "0x01f3a3962668df7799429ae827df59bc32a5d761", + "0x020ce48bb83cd6cb1483149ed7a6d2bca4526c84", + "0x0235ac7285d4ad1a5261904539e4862c13a830af", + "0x0369a66d5d25734a9aef4689546a809888fe5bfe", + "0x03bb74c32acb025effab4d31e579d3751363c216", + "0x05903a958710ec752dd4a25dd3157ccee9d6c92e", + "0x0636b47164ea3099c6fc39c52a3f4b7b7ecaa03b", + "0x0686b62cd088715dcb690e6514aaeca229bbac07", + "0x34db5451844371226c7737137f9cdf30638733f2", + "0xe2096f4a5d1ea4501e27ffafc1e910e35a2c200a", + "0xf4ad32a268d49f72e7bd1b89e2a63d89ac5e8b62", + "0x0dC14192333A736a3caDfBD3f9f883106584614A" + ], + "snapshot": 12883023 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/index.ts b/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/index.ts new file mode 100644 index 00000000..009f367b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/minto-balance-of-all/index.ts @@ -0,0 +1,63 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'btcmt-minto'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOfSum(address account) external view returns (uint256)' +]; + +const stakingAbi = [ + 'function userStakes(address account) external view returns (uint256, uint256, uint256, uint256, uint256, uint256, uint256)' +]; + +const autoStakingAbi = [ + 'function userStake(address account) external view returns (uint256, uint256, uint256)' +]; + +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 }); + + const multiStaking = new Multicaller(network, provider, stakingAbi, { + blockTag + }); + const multiAutoStaking = new Multicaller(network, provider, autoStakingAbi, { + blockTag + }); + + addresses.forEach((address) => { + multi.call(address, options.address, 'balanceOfSum', [address]); + multiStaking.call(address, options.stakingAddress, 'userStakes', [address]); + multiAutoStaking.call(address, options.autoStakingAddress, 'userStake', [ + address + ]); + }); + + const [result, resultStaking, resultAutoStaking] = await Promise.all([ + multi.execute(), + multiStaking.execute(), + multiAutoStaking.execute() + ]); + + return Object.fromEntries( + addresses.map((address) => { + const sum = + parseFloat(formatUnits(result[address], options.decimals)) + + parseFloat(formatUnits(resultStaking[address][1], options.decimals)) + + parseFloat( + formatUnits(resultAutoStaking[address][0], options.decimals) + ); + return [address, sum]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/mithcash/examples.json b/Implementations/API/backend/utils/snapshot/strategies/mithcash/examples.json new file mode 100644 index 00000000..a537853c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mithcash/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "MITH Cash", + "strategy": { + "name": "mithcash", + "params": { + "sushiswap": "0x066F3A3B7C8Fa077c71B9184d862ed0A4D5cF3e0", + "boardroom": "0xb35f89160d1Dc47B6EAC1986D7821505c327AE09", + "sharePool": "0x14E33e1D6Cc4D83D7476492C0A52b3d4F869d892", + "token": "0x4b4D2e899658FB59b1D518b68fe836B100ee8958", + "symbol": "MIS", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x0c11d0bde11fc110caf84d9361a1466adedf59b6", + "0xbEA07b01E8Fe3936A3D206158521A87addB65cfE", + "0xF5ecA360c5dE26A46D54b72E49800c87801c719b" + ], + "snapshot": 11605878 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/mithcash/index.ts b/Implementations/API/backend/utils/snapshot/strategies/mithcash/index.ts new file mode 100644 index 00000000..8f2d179d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mithcash/index.ts @@ -0,0 +1,116 @@ +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'jeremyHD'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const response = await multicall( + network, + provider, + abi, + [ + [options.token, 'balanceOf', [options.sushiswap]], + [options.sushiswap, 'totalSupply'], + ...addresses.map((address: any) => [ + options.sushiswap, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.sharePool, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.token, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.boardroom, + 'balanceOf', + [address] + ]) + ], + { blockTag } + ); + + const misPerLP = parseUnits(response[0][0].toString(), 18).div( + response[1][0] + ); + const lpBalances = response.slice(2, addresses.length + 2); + const stakedLpBalances = response.slice( + addresses.length + 2, + addresses.length * 2 + 2 + ); + const tokenBalances = response.slice( + addresses.length * 2 + 2, + addresses.length * 3 + 2 + ); + const boardroomBalances = response.slice( + addresses.length * 3 + 2, + addresses.length * 4 + 2 + ); + + return Object.fromEntries( + Array(addresses.length) + .fill('') + .map((_, i) => { + const lpBalance = lpBalances[i][0].add(stakedLpBalances[i][0]); + const misLpBalance = lpBalance.mul(misPerLP).div(parseUnits('1', 18)); + + return [ + addresses[i], + parseFloat( + formatUnits( + misLpBalance + .add(tokenBalances[i][0]) + .add(boardroomBalances[i][0]), + options.decimals + ) + ) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/modefi-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/modefi-staking/examples.json new file mode 100644 index 00000000..bfda47cd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/modefi-staking/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "modefiStaking", + "strategy": { + "network": "250", + "name": "modefi-staking", + "params": { + "address": "0xe64b9fd040d1f9d4715c645e0d567ef69958d3d9", + "stakingContract": "0xaeA240AF3C46D65dfF0EbFcb5fb923365E0aB623", + "symbol": "MOD", + "decimals": 18 + } + }, + "network": "250", + "addresses": [ + "0x66566af6359cacfab858c0c50c5d22895ab877d6", + "0x237e61fa6baaf3248d4a61d98d6ab00a43448969" + ], + "snapshot": 10928059 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/modefi-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/modefi-staking/index.ts new file mode 100644 index 00000000..1e8ead5a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/modefi-staking/index.ts @@ -0,0 +1,83 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'Modefi'; +export const version = '0.0.1'; + +const stakingPoolAbi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: '_stakers', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'distributed', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'staked', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakeResponse = await multicall( + network, + provider, + stakingPoolAbi, + addresses.map((address: any) => [ + options.stakingContract, + '_stakers', + [address] + ]), + { blockTag } + ); + + return Object.fromEntries( + stakeResponse.map((value, i) => { + const stakedBalance = stakeResponse[i].amount; + + return [ + addresses[i], + parseFloat(formatUnits(stakedBalance.toString(), options.decimals)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/modefi/examples.json b/Implementations/API/backend/utils/snapshot/strategies/modefi/examples.json new file mode 100644 index 00000000..9f6d4eb1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/modefi/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "modefi", + "strategy": { + "name": "modefi", + "params": { + "address": "0xea1ea0972fa092dd463f2968f9bb51cc4c981d71", + "symbol": "MOD", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x6f0b57730f7d51476e93cc46093ec9139010caa2", + "0xe5aec22bb09186e205cf3366236f1fc8e9606fa4" + ], + "snapshot": 12560234 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/modefi/index.ts b/Implementations/API/backend/utils/snapshot/strategies/modefi/index.ts new file mode 100644 index 00000000..eae748da --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/modefi/index.ts @@ -0,0 +1,93 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'OxAL7'; +export const version = '0.0.1'; + +const MOD_POOL_ADDRESS = '0x3093896c81c8d8b9bf658fbf1aede09207850ca2'; + +const abi = [ + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +const stakingPoolAbi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const balanceResponse = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [options.address, 'balanceOf', [address]]), + { blockTag } + ); + + const stakeResponse = await multicall( + network, + provider, + stakingPoolAbi, + addresses.map((address: any) => [MOD_POOL_ADDRESS, 'userInfo', [address]]), + { blockTag } + ); + return Object.fromEntries( + balanceResponse.map((value, i) => { + const balance1 = value[0]; + const balance2 = stakeResponse[i].amount; + const sum = balance1.add(balance2); + + return [ + addresses[i], + parseFloat(formatUnits(sum.toString(), options.decimals)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/moloch-all/examples.json b/Implementations/API/backend/utils/snapshot/strategies/moloch-all/examples.json new file mode 100644 index 00000000..6d6eb729 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moloch-all/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Moloch All", + "strategy": { + "name": "moloch-all", + "params": { + "symbol": "MCV", + "address": "0x4570b4faf71e23942b8b9f934b47ccedf7540162", + "decimals": 0 + } + }, + "network": "1", + "addresses": [ + "0xc9283bbd79b016230838e57ce19e6aca12dd2c0d", + "0x6159aaaf32d1ac8b4b96b1a9d7197aed612a59cb", + "0x442eC679337Ab3Ad148A8c3D61db54AB75816D1f", + "0x85ac9e682995ebebde8ff107fbbbfe7c40992e4a", + "0x3d1df1a816577a62db61281f673c4f43ae063490", + "0xd6e371526cdaee04cd8af225d42e37bc14688d9e", + "0x839395e20bbb182fa440d08f850e6c7a8f6f0780" + ], + "snapshot": 14883665 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/moloch-all/index.ts b/Implementations/API/backend/utils/snapshot/strategies/moloch-all/index.ts new file mode 100644 index 00000000..cb7eedea --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moloch-all/index.ts @@ -0,0 +1,59 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'scottrepreneur'; +export const version = '0.1.0'; + +const abi = [ + 'function memberAddressByDelegateKey(address) view returns (address)', + 'function members(address) view returns (address delegateKey, uint256 shares, uint256 loot, bool exists, uint256 highestIndexYesVote, uint256 jailed)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const memberAddresses = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'memberAddressByDelegateKey', + [address] + ]), + { blockTag } + ); + + const response = await multicall( + network, + provider, + abi, + memberAddresses + .filter( + (addr) => + addr.toString() !== '0x0000000000000000000000000000000000000000' + ) + .map((addr: any) => [options.address, 'members', [addr.toString()]]), + { blockTag } + ); + + const addressesWithMemberAddress = addresses.filter( + (addr, i) => + memberAddresses[i].toString() !== + '0x0000000000000000000000000000000000000000' + ); + + return Object.fromEntries( + response.map((value, i) => [ + addressesWithMemberAddress[i], + parseFloat(formatUnits(value.shares.toString(), options.decimals)) + + parseFloat(formatUnits(value.loot.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/moloch-loot/examples.json b/Implementations/API/backend/utils/snapshot/strategies/moloch-loot/examples.json new file mode 100644 index 00000000..0be8fed4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moloch-loot/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Moloch Loot", + "strategy": { + "name": "moloch-loot", + "params": { + "symbol": "LOOT", + "address": "0xb152b115c94275b54a3f0b08c1aa1d21f32a659a", + "decimals": 0 + } + }, + "network": "100", + "addresses": [ + "0xc9283bbd79b016230838e57ce19e6aca12dd2c0d", + "0x85ac9e682995ebebde8ff107fbbbfe7c40992e4a", + "0x3d1df1a816577a62db61281f673c4f43ae063490", + "0xd6e371526cdaee04cd8af225d42e37bc14688d9e", + "0x839395e20bbb182fa440d08f850e6c7a8f6f0780" + ], + "snapshot": 15848545 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/moloch-loot/index.ts b/Implementations/API/backend/utils/snapshot/strategies/moloch-loot/index.ts new file mode 100644 index 00000000..755c28bc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moloch-loot/index.ts @@ -0,0 +1,126 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'scottrepreneur'; +export const version = '0.1.0'; + +const abi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'memberAddressByDelegateKey', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'members', + outputs: [ + { + internalType: 'address', + name: 'delegateKey', + type: 'address' + }, + { + internalType: 'uint256', + name: 'shares', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'loot', + type: 'uint256' + }, + { + internalType: 'bool', + name: 'exists', + type: 'bool' + }, + { + internalType: 'uint256', + name: 'highestIndexYesVote', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'jailed', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalShares', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const memberAddresses = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'memberAddressByDelegateKey', + [address] + ]), + { blockTag } + ); + + const response = await multicall( + network, + provider, + abi, + memberAddresses + .filter( + (addr) => + addr.toString() !== '0x0000000000000000000000000000000000000000' + ) + .map((addr: any) => [options.address, 'members', [addr.toString()]]), + { blockTag } + ); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.loot.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/moloch/examples.json b/Implementations/API/backend/utils/snapshot/strategies/moloch/examples.json new file mode 100644 index 00000000..c61ef0cf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moloch/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "moloch", + "params": { + "symbol": "CELO", + "address": "0x471ece3750da237f93b8e339c536989b8978a438", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 13494433 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/moloch/index.ts b/Implementations/API/backend/utils/snapshot/strategies/moloch/index.ts new file mode 100644 index 00000000..b5efd96c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moloch/index.ts @@ -0,0 +1,126 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'adridadou'; +export const version = '0.1.0'; + +const abi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'memberAddressByDelegateKey', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'members', + outputs: [ + { + internalType: 'address', + name: 'delegateKey', + type: 'address' + }, + { + internalType: 'uint256', + name: 'shares', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'loot', + type: 'uint256' + }, + { + internalType: 'bool', + name: 'exists', + type: 'bool' + }, + { + internalType: 'uint256', + name: 'highestIndexYesVote', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'jailed', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalShares', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const memberAddresses = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'memberAddressByDelegateKey', + [address] + ]), + { blockTag } + ); + + const response = await multicall( + network, + provider, + abi, + memberAddresses + .filter( + (addr) => + addr.toString() !== '0x0000000000000000000000000000000000000000' + ) + .map((addr: any) => [options.address, 'members', [addr.toString()]]), + { blockTag } + ); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.shares.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/README.md new file mode 100644 index 00000000..bca71d05 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/README.md @@ -0,0 +1,24 @@ +# Moonbeam Free Balance strategy + +This strategy return the free balances on Moonbeam network. The free balance includes the "locked" tokens, which can be used for voting, but not the "reserved" tokens. + +## Examples + +```JSON +[ + { + "name": "moonbeam-free-balance", + "strategy": { + "name": "moonbeam-free-balance", + "params": {} + }, + "network": "1284", + "addresses": [ + "0xf02ddb48eda520c915c0dabadc70ba12d1b49ad2", + "0x01bb6ce8b88f09a7d0bfb40eff7f2ad5e0df2e98", + "0xe751b9ea560a200161d1b70249495e3d22ec5b00" + ], + "snapshot": 14129872 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/examples.json new file mode 100644 index 00000000..22afee92 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "moonbeam-free-balance", + "strategy": { + "name": "moonbeam-free-balance", + "params": { + "decimals": 18 + } + }, + "network": "1284", + "addresses": [ + "0xf02ddb48eda520c915c0dabadc70ba12d1b49ad2", + "0x01bb6ce8b88f09a7d0bfb40eff7f2ad5e0df2e98", + "0xe751b9ea560a200161d1b70249495e3d22ec5b00" + ], + "snapshot": 3044139 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/index.ts new file mode 100644 index 00000000..9225e01b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/moonbeam-free-balance/index.ts @@ -0,0 +1,119 @@ +import { getAddress } from '@ethersproject/address'; +import { fetchJson } from '@ethersproject/web'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { blake2bHex } from 'blakejs'; +import { formatFixed } from '@ethersproject/bignumber'; + +export const author = 'crystalin'; +export const version = '0.1.0'; + +export function readLittleEndianBigInt(hex: string) { + return BigInt(`0x${hex.match(/../g)?.reverse().join('')}`); +} + +export async function strategy( + space: string, + network: string, + provider: JsonRpcProvider, + addresses: string[], + options: { decimals } +) { + // Pre-encoded key prefix for "system.account" storage + const accountPrefix = `0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9`; + const { decimals } = options; + + const batchSize = 100; + const batches = addresses.reduce((resultArray, item, index) => { + const chunkIndex = Math.floor(index / batchSize); + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = []; // start a new chunk + } + resultArray[chunkIndex].push(item); + return resultArray; + }, [] as string[][]); + + // Stores all the retrieved balances + const balances: { [k: string]: number } = {}; + + for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) { + const addressBatch = batches[batchIndex]; + + // Convert address to storage key. + const keys = await Promise.all( + addressBatch.map( + async (address) => + `${accountPrefix}${await blake2bHex( + Buffer.from(address.substring(2), 'hex'), + undefined, + 16 + )}${address.substring(2)}` + ) + ); + + // Build batch request for all the storage keys of the batch. + const reqs = keys.map((key, index) => ({ + method: 'state_getStorage', + params: [key], + id: batchIndex * batchSize + index + 1, + jsonrpc: '2.0' + })); + + // Query batch of storage items + const payloads: { id: number; result: string }[] = await fetchJson( + provider.connection, + JSON.stringify(reqs), + (payload: { + error?: { code?: number; data?: any; message?: string }; + result?: any; + }) => { + if (payload.error) { + const error: any = new Error(payload.error.message); + error.code = payload.error.code; + error.data = payload.error.data; + throw error; + } + return payload; + } + ); + + for (const payload of payloads) { + if (payload.result === null) { + break; + } + // Computes "system.account" key for given address + // Retrieves storage data for the "system.account" key + // account data structure (little endian): + // { + // nonce: 8 bits + // consumers: 8 bits + // providers: 8 bits + // sufficients: 8 bits + // data: { + // free: 32 bits + // reserved: 32 bits + // miscFrozen: 32 bits + // feeFrozen: 32 bits + // } + // } + // Converts the bigint into number. + // Result won't be precise, but should be lower than real value. + + const free = + payload.result.length >= 2 + 8 + 8 + 8 + 8 + 32 + ? parseFloat( + formatFixed( + readLittleEndianBigInt( + payload.result.substring( + 2 + 8 + 8 + 8 + 8, + 2 + 8 + 8 + 8 + 8 + 32 + ) + ), + decimals + ) + ) + : 0; + balances[getAddress(addressBatch[(payload.id - 1) % batchSize])] = free; + } + } + return balances; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/mstable/README.md b/Implementations/API/backend/utils/snapshot/strategies/mstable/README.md new file mode 100644 index 00000000..d80eca54 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mstable/README.md @@ -0,0 +1,19 @@ +# mstable + +Calls getVotes() on both the stkMTA and stkBPT contracts, these balances are then summed to generate a users vMTA balance. + +## Examples + +```JSON +{ + "strategies": [ + { + "name": "mstable", + "params": { + "stkBPT": "0xefbe22085d9f29863cfb77eed16d3cc0d927b011", + "stkMTA": "0x8f2326316eC696F6d023E37A9931c2b2C177a3D7", + } + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/mstable/examples.json b/Implementations/API/backend/utils/snapshot/strategies/mstable/examples.json new file mode 100644 index 00000000..3f82dfff --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mstable/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "mstable", + "strategy": { + "name": "mstable", + "params": { + "symbol": "MSTABLE", + "stkBPT": "0xefbe22085d9f29863cfb77eed16d3cc0d927b011", + "stkMTA": "0x8f2326316eC696F6d023E37A9931c2b2C177a3D7" + } + }, + "network": "1", + "addresses": [ + "0x2ee8670d2b936985d5fb1ee968810c155d3bb9ca", + "0x19f12c947d25ff8a3b748829d8001ca09a28d46d" + ], + "snapshot": 13236086 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/mstable/index.ts b/Implementations/API/backend/utils/snapshot/strategies/mstable/index.ts new file mode 100644 index 00000000..052b60b9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mstable/index.ts @@ -0,0 +1,61 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'chrisjgf'; +export const version = '0.0.1'; + +const abi = ['function getVotes(address account) view returns (uint256)']; + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stkMTAQuery = addresses.map((address: any) => [ + options.stkMTA, + 'getVotes', + [address] + ]); + + const stkBPTQuery = addresses.map((address: any) => [ + options.stkBPT, + 'getVotes', + [address] + ]); + + const response = await multicall( + network, + provider, + abi, + [...stkMTAQuery, ...stkBPTQuery], + { blockTag } + ); + + const chunks = chunk(response, addresses.length); + const stkMTABalances = chunks[0]; + const stkBPTBalances = chunks[1]; + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => [ + addresses[i], + parseFloat( + formatUnits( + stkMTABalances[i][0].add(stkBPTBalances[i][0]).toString(), + 18 + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/multichain-serie/examples.json b/Implementations/API/backend/utils/snapshot/strategies/multichain-serie/examples.json new file mode 100644 index 00000000..70aa3639 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/multichain-serie/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Multichain serie", + "strategy": { + "name": "multichain-serie", + "params": { + "symbol": "MULTI", + "strategies": [ + { + "name": "erc20-balance-of", + "network": "137", + "params": { + "address": "0xB9638272aD6998708de56BBC0A290a1dE534a578", + "decimals": 18 + } + }, + { + "name": "erc20-balance-of", + "network": "56", + "params": { + "address": "0x0e37d70b51ffa2b98b4d34a5712c5291115464e3", + "decimals": 18 + } + } + ] + } + }, + "network": "1", + "addresses": [ + "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5", + "0x9feab70f3c4a944b97b7565bac4991df5b7a69ff", + "0xaca39b187352d9805deced6e73a3d72abf86e7a0" + ], + "snapshot": 13035566 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/multichain-serie/index.ts b/Implementations/API/backend/utils/snapshot/strategies/multichain-serie/index.ts new file mode 100644 index 00000000..98b3d998 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/multichain-serie/index.ts @@ -0,0 +1,53 @@ +import { getProvider, getSnapshots } from '../../utils'; +import strategies from '..'; + +export const author = 'kesar'; +export const version = '1.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const results: any = []; + const blocks = await getSnapshots( + network, + snapshot, + provider, + options.strategies.map((s) => s.network || network) + ); + + for (const strategy of options.strategies) { + // If snapshot is taken before a network is activated then ignore its strategies + if ( + options.startBlocks && + blocks[strategy.network] < options.startBlocks[strategy.network] + ) { + continue; + } + + results.push( + await strategies[strategy.name].strategy( + space, + strategy.network, + getProvider(strategy.network), + addresses, + strategy.params, + blocks[strategy.network] + ) + ); + } + + return results.reduce((finalResults: any, strategyResult: any) => { + for (const [address, value] of Object.entries(strategyResult)) { + if (!finalResults[address]) { + finalResults[address] = 0; + } + finalResults[address] += value; + } + return finalResults; + }, {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/multichain/README.md b/Implementations/API/backend/utils/snapshot/strategies/multichain/README.md new file mode 100644 index 00000000..2cfec8c9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/multichain/README.md @@ -0,0 +1,52 @@ +# multichain + +If you want to calculate the balance from various chains like Ethereum, Binance smart chain, polygon etc. and use them for voting using various strategies, you can do it by using a strategy called “multichain strategy”. This allows cross chain voting in which multiple chains can be used together to calculate the voting power. + +In multichain strategy, the params should define sub strategies which would use different networks mentioned in the field to combine the voting power. + +In order to provide multichain functionality, this strategy provides a way to calculate which block number should be used on additional chains: If a snapshot was created on block 125 on mainnet, it will find the timestamp for that block and go find which block number corresponds to that same timestamp on every other wanted chain. This way it can accurately represent an address' voting power at a given point in time. + +Here is an example of parameters: + +In the below example, the tokens on the three networks namely ethereum, polygon and bsc denotes combined voting power + +```json +{ + "symbol": "MULTI", + "strategies": [ + { + "name": "erc20-balance-of", + "network": "1", + "params": { + "address": "0x579cea1889991f68acc35ff5c3dd0621ff29b0c9", + "decimals": 18 + } + }, + { + "name": "erc20-balance-of", + "network": "137", + "params": { + "address": "0xB9638272aD6998708de56BBC0A290a1dE534a578", + "decimals": 18 + } + }, + { + "name": "erc20-balance-of", + "network": "56", + "params": { + "address": "0x0e37d70b51ffa2b98b4d34a5712c5291115464e3", + "decimals": 18 + } + }, + { + "name": "erc20-balance-of", + "network": 137, + "params": { + "address": "0xfC0fA725E8fB4D87c38EcE56e8852258219C64Ee", + "decimals": 18 + } + } + ] +} + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/multichain/examples.json b/Implementations/API/backend/utils/snapshot/strategies/multichain/examples.json new file mode 100644 index 00000000..04e7a2a7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/multichain/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Multichain Voting", + "strategy": { + "name": "multichain", + "params": { + "symbol": "MULTI", + "strategies": [ + { + "name": "erc20-balance-of", + "network": "137", + "params": { + "address": "0xB9638272aD6998708de56BBC0A290a1dE534a578", + "decimals": 18 + } + }, + { + "name": "erc20-balance-of", + "network": "56", + "params": { + "address": "0x0e37d70b51ffa2b98b4d34a5712c5291115464e3", + "decimals": 18 + } + } + ] + } + }, + "network": "1", + "addresses": [ + "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5", + "0x9feab70f3c4a944b97b7565bac4991df5b7a69ff", + "0xaca39b187352d9805deced6e73a3d72abf86e7a0" + ], + "snapshot": 13035566 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/multichain/index.ts b/Implementations/API/backend/utils/snapshot/strategies/multichain/index.ts new file mode 100644 index 00000000..19c2dbed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/multichain/index.ts @@ -0,0 +1,54 @@ +import { getProvider, getSnapshots } from '../../utils'; +import strategies from '..'; + +export const author = 'kesar'; +export const version = '1.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const promises: any = []; + const blocks = await getSnapshots( + network, + snapshot, + provider, + options.strategies.map((s) => s.network || network) + ); + + for (const strategy of options.strategies) { + // If snapshot is taken before a network is activated then ignore its strategies + if ( + options.startBlocks && + blocks[strategy.network] < options.startBlocks[strategy.network] + ) { + continue; + } + + promises.push( + strategies[strategy.name].strategy( + space, + strategy.network, + getProvider(strategy.network), + addresses, + strategy.params, + blocks[strategy.network] + ) + ); + } + + const results = await Promise.all(promises); + return results.reduce((finalResults: any, strategyResult: any) => { + for (const [address, value] of Object.entries(strategyResult)) { + if (!finalResults[address]) { + finalResults[address] = 0; + } + finalResults[address] += value; + } + return finalResults; + }, {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/README.md b/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/README.md new file mode 100644 index 00000000..b10bd50e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/README.md @@ -0,0 +1,6 @@ +# Multisig Strategy + +To use this strategy, you would have a multisig contract which has a set of owners. Pass the +address of that multisig into the params for the strategy. Then, each owner of the multisig will +have one vote for the strategy. This is great when you have a council of multisig members, where +each members vote is worth one. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/examples.json b/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/examples.json new file mode 100644 index 00000000..a0e9bc30 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Multisig owners", + "strategy": { + "name": "multisig-owners", + "params": { + "symbol": "MSO", + "address": "0x48301Fe520f72994d32eAd72E2B6A8447873CF50" + } + }, + "network": "1", + "addresses": [ + "0x47c6c166F462a2886cb45EFeE7FEAf29941ECEb2", + "0x65DCD62932fEf5af25AdA91F0F24658e94e259c5", + "0x38FA68D1C06BD272893908a74E6BcC67E28d4Da8", + "0xE0FADeFDb233C32C67a1e428951aEfE8dF6ce639", + "0xE1FDD398329C6b74C14cf19100316f0826a492d3" + ], + "snapshot": 12115328 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/index.ts b/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/index.ts new file mode 100644 index 00000000..e100e22b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/multisig-owners/index.ts @@ -0,0 +1,49 @@ +import { multicall } from '../../utils'; + +export const author = 'davekaj'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'owner', + type: 'address' + } + ], + name: 'isOwner', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [options.address, 'isOwner', [address]]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [addresses[i], value[0] ? 1 : 0]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/mushrooms/examples.json b/Implementations/API/backend/utils/snapshot/strategies/mushrooms/examples.json new file mode 100644 index 00000000..7695d357 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mushrooms/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "mushrooms", + "params": { + "symbol": "MM-USDC UNI LP", + "_comment_": "This strategy supports LP pool in masterchef style staking. by @MushroomsFinan1", + "masterchef": "0xf8873a6080e8dbF41ADa900498DE0951074af577", + "pool": 2, + "type": "lp", + "govtoken": "0xa283aA7CfBB27EF0cfBcb2493dD9F4330E0fd304" + } + }, + "network": "1", + "addresses": ["0x98E55d2288385bc1B0EBBE0e56eAc6AEB099C496"], + "snapshot": 12087660 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/mushrooms/index.ts b/Implementations/API/backend/utils/snapshot/strategies/mushrooms/index.ts new file mode 100644 index 00000000..c64d5379 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mushrooms/index.ts @@ -0,0 +1,186 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = '@MushroomsFinan1'; +export const version = '0.1.0'; + +const erc20Abi = [ + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +const masterChefAbi = [ + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + }, + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + name: 'poolInfo', + outputs: [ + { + internalType: 'contract IERC20', + name: 'lpToken', + type: 'address' + }, + { + internalType: 'uint256', + name: 'allocPoint', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'lastRewardBlock', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'accMMPerShare', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const masterChefMulti = new Multicaller(network, provider, masterChefAbi, { + blockTag + }); + addresses.forEach((address) => { + masterChefMulti.call( + `${address}.userInfo.amount`, + options.masterchef, + 'userInfo', + [options.pool, address] + ); + }); + + if (options.type === 'lp') { + masterChefMulti.call('poolInfo.lpToken', options.masterchef, 'poolInfo', [ + options.pool + ]); + const masterChefResult = await masterChefMulti.execute(); + + const erc20Multi = new Multicaller(network, provider, erc20Abi, { + blockTag + }); + erc20Multi.call( + 'lpTotalSupply', + masterChefResult.poolInfo.lpToken[0], + 'totalSupply' + ); + erc20Multi.call('poolMMBalance', options.govtoken, 'balanceOf', [ + masterChefResult.poolInfo.lpToken[0] + ]); + const erc20Result = await erc20Multi.execute(); + + return Object.fromEntries( + addresses.map((address) => { + return [ + address, + parseFloat( + formatUnits( + masterChefResult[address].userInfo.amount[0] + .mul(erc20Result.poolMMBalance) + .div(erc20Result.lpTotalSupply) + .toString(), + 18 + ) + ) + ]; + }) + ); + } else { + const masterChefResult = await masterChefMulti.execute(); + return Object.fromEntries( + addresses.map((address) => { + return [ + address, + parseFloat( + formatUnits( + masterChefResult[address].userInfo.amount.toString(), + 18 + ) + ) + ]; + }) + ); + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/README.md b/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/README.md new file mode 100644 index 00000000..b25b719b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/README.md @@ -0,0 +1,32 @@ +# Mutant Cats stakers and holders strategy + +This strategy return the balances of the voters for Mutant Cats project from both staking pool and ERC721 NFT. + +## Accepted options + +- **staking:** Mutant Cats staking pool address. + +- **token:** Mutant Cats ERC721 NFT address. + +## Examples + +```JSON +[ + { + "name": "Mutant Cats Stakers and Holders", + "strategy": { + "name": "mutant-cats-stakers-and-holders", + "params": { + "staking": "0xd09656a2EE7E5Ee3404fAce234e683D3337dA014", + "token": "0xaAdBA140Ae5e4c8a9eF0Cc86EA3124b446e3E46A" + } + }, + "network": "1", + "addresses": [ + "0xcb5C730A85795b20C1fdB543B64B2ED164333803", + "0x4252a493899D1E2D1573Ff4084446C095C75055E" + ], + "snapshot": 13439719 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/examples.json b/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/examples.json new file mode 100644 index 00000000..77943527 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Mutant Cats Stakers and Holders", + "strategy": { + "name": "mutant-cats-stakers-and-holders", + "params": { + "staking": "0xd09656a2EE7E5Ee3404fAce234e683D3337dA014", + "token": "0xaAdBA140Ae5e4c8a9eF0Cc86EA3124b446e3E46A", + "symbol": "MUTCATS", + "decimals": 0 + } + }, + "network": "1", + "addresses": [ + "0xcb5C730A85795b20C1fdB543B64B2ED164333803", + "0x4252a493899D1E2D1573Ff4084446C095C75055E" + ], + "snapshot": 13439719 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/index.ts b/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/index.ts new file mode 100644 index 00000000..6b0fffc2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/mutant-cats-stakers-and-holders/index.ts @@ -0,0 +1,49 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = '69hunter'; +export const version = '0.1.0'; + +const stakingAbi = [ + 'function depositsOf(address account) external view returns (uint256[] memory)' +]; + +const tokenAbi = [ + 'function balanceOf(address owner) public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPool = new Multicaller(network, provider, stakingAbi, { + blockTag + }); + const tokenPool = new Multicaller(network, provider, tokenAbi, { + blockTag + }); + + addresses.forEach((address) => { + stakingPool.call(address, options.staking, 'depositsOf', [address]); + tokenPool.call(address, options.token, 'balanceOf', [address]); + }); + + const [stakingResponse, tokenResponse]: [ + Record, + Record + ] = await Promise.all([stakingPool.execute(), tokenPool.execute()]); + + return Object.fromEntries( + addresses.map((address) => { + const stakingCount = stakingResponse[address].length; + const tokenCount = BigNumber.from(tokenResponse[address]).toNumber(); + return [address, stakingCount + tokenCount]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/README.md b/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/README.md new file mode 100644 index 00000000..467ae2dd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/README.md @@ -0,0 +1,13 @@ +# Nation3 voting power strategy + +Calculates voting power based on a user's staked tokens and ownership of nation3 passport (_erc721 NFT_). It also takes into account whether the NFT owner delegated his voting power to another account (using the `setSigner` function) + +## Requires 2 input parameters: + +**erc20** + +The address of veNation tokens contract + +**erc721** + +The address of nation3 passport tokens contract \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/examples.json b/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/examples.json new file mode 100644 index 00000000..602c1130 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Voting Power with ERC20 Balances (only for ERC721 Holders) ", + "strategy": { + "name": "nation3-votes-with-delegations", + "params": { + "erc721": "0x3337dac9f251d4e403d6030e18e3cfb6a2cb1333", + "erc20": "0xf7def1d2fbda6b74bee7452fdf7894da9201065d" + } + }, + "network": "1", + "addresses": [ + "0xEdd000B7Db3cb8931d4E0cb1D0DBe6B947Ceb09A", + "0x47d80912400ef8f8224531EBEB1ce8f2ACf4b75a", + "0x636d65212C815b93B8E5b069f7082169cec851b7", + "0x460AF11e497dc273fC163414943C6fd95d17B1fd", + "0x1512BB3b85696Fa4D85Cb4bbf78e9C4FE95DB90F", + "0x79438224Bc21b0E6B45ECF9F8caADfBdB874DedD" + ], + "snapshot": 16399660 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/index.ts b/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/index.ts new file mode 100644 index 00000000..4843fa03 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/index.ts @@ -0,0 +1,187 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'nation3'; +export const version = '0.2.0'; +const DECIMALS = 18; + +const balanceAbi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +const ownerAbi = ['function ownerOf(uint256 id) public view returns (address)']; + +const signerAbi = [ + 'function signerOf(uint256 id) external view returns (address)' +]; + +const lastTokenIdAbi = ['function getNextId() external view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses: string[], + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const formattedAddressesThatVoted = addresses.map((addr) => getAddress(addr)); + + const erc721OwnerCaller = new Multicaller(network, provider, ownerAbi, { + blockTag + }); + const erc721SignerCaller = new Multicaller(network, provider, signerAbi, { + blockTag + }); + const erc721LastTokenIdCaller = new Multicaller( + network, + provider, + lastTokenIdAbi, + { blockTag } + ); + + const erc20BalanceCaller = new Multicaller(network, provider, balanceAbi, { + blockTag + }); + + erc721LastTokenIdCaller.call('lastTokenId', options.erc721, 'getNextId'); + + const lastIndex = await erc721LastTokenIdCaller.execute(); + const lastTokenId = BigNumber.from(lastIndex.lastTokenId).toNumber(); + + for (let i = 0; i < lastTokenId; i++) { + erc721SignerCaller.call(i, options.erc721, 'signerOf', [i]); + erc721OwnerCaller.call(i, options.erc721, 'ownerOf', [i]); + } + + const [erc721Signers, erc721Owners]: [ + Record, + Record + ] = await Promise.all([ + erc721SignerCaller.execute(), + erc721OwnerCaller.execute() + ]); + + const erc721OwnersArr = Object.entries(erc721Owners); + const erc721SignersArr = Object.entries(erc721Signers); + + //There is slightly confusing logic here, but ultimately the + //resultant Map below Maps the Voting Address to the list of + //addresses for which they are voting on behalf of. In the + //majority cases this will be a one to one mapping. + const votingAddressToOwnerAddressMap = buildPowerMap( + formattedAddressesThatVoted, + erc721OwnersArr, + erc721SignersArr + ); + + const erc20Balances: Record = + await getVEBalancesForAddressMap( + votingAddressToOwnerAddressMap, + erc20BalanceCaller, + options.erc20 + ); + + const agg = formattedAddressesThatVoted.map((addr) => { + const holderAddresses = votingAddressToOwnerAddressMap.get(addr); + const total = + holderAddresses?.reduce((sum, addr) => { + return sum.add(erc20Balances[addr] || 0); + }, BigNumber.from(0)) || 0; + return [addr, parseFloat(formatUnits(total, DECIMALS))]; + }); + const result = Object.fromEntries(agg); + return result; +} + +function buildPowerMap( + formattedAddressesThatVoted: string[], + erc721OwnersArr: [string, string][], + erc721SignersArr: [string, string][] +) { + const delegatedTokens = erc721SignersArr.filter( + ([id, address]) => address !== erc721OwnersArr[id] + ); + + const votingAddressToOwnerAddressMap = mapOwnersVotingPower( + formattedAddressesThatVoted, + erc721OwnersArr, + delegatedTokens + ); + + addDelegatedVotingPowerToMap( + formattedAddressesThatVoted, + erc721OwnersArr, + erc721SignersArr, + delegatedTokens, + votingAddressToOwnerAddressMap + ); + + return votingAddressToOwnerAddressMap; +} + +function mapOwnersVotingPower( + formattedAddressesThatVoted: string[], + erc721OwnersArr: [string, string][], + delegatedTokens: [string, string][] +) { + return erc721OwnersArr.reduce((acc, [id, addr]) => { + if (!formattedAddressesThatVoted.includes(addr)) { + return acc; + } + //If the address that voted is the owner, but the owner has + //delegated their vote on this passport then they do not + //get any voting power from this passport. + if (delegatedTokens.some((delegated) => delegated[0] === id)) { + return acc; + } + acc.set(addr, [addr]); + return acc; + }, new Map()); +} + +function addDelegatedVotingPowerToMap( + formattedAddressesThatVoted: string[], + erc721OwnersArr: [string, string][], + erc721SignersArr: [string, string][], + delegatedTokens: [string, string][], + mapThatGetsUpdated: Map +) { + erc721SignersArr.reduce((acc, [id, addr]) => { + if (!formattedAddressesThatVoted.includes(addr)) { + return acc; + } + + //This works because the signerOf is defaulted to the ownerOf value + if (!delegatedTokens.some((delegated) => delegated[0] === id)) { + return acc; + } + + if (!mapThatGetsUpdated.has(addr)) { + acc.set(addr, []); + } + acc.get(addr)?.push(erc721OwnersArr[id][1]); + + return acc; + }, mapThatGetsUpdated); +} + +async function getVEBalancesForAddressMap( + votingAddressToOwnerAddressMap: Map, + erc20BalanceCaller, + veNationAddress: string +) { + votingAddressToOwnerAddressMap.forEach((addresses) => { + addresses.forEach((address) => { + erc20BalanceCaller.call(address, veNationAddress, 'balanceOf', [address]); + }); + }); + + const erc20Balances: Record = + await erc20BalanceCaller.execute(); + return erc20Balances; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/schema.json b/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/schema.json new file mode 100644 index 00000000..8318dcad --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/nation3-votes-with-delegations/schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "erc20": { + "type": "string", + "title": "veNation", + "examples": ["e.g. 0xf7def1d2fbda6b74bee7452fdf7894da9201065d"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "erc721": { + "type": "string", + "title": "nation3 passport", + "examples": ["e.g. 0x3337dac9f251d4e403d6030e18e3cfb6a2cb1333"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["erc20", "erc721"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/nexon-army-nft/examples.json b/Implementations/API/backend/utils/snapshot/strategies/nexon-army-nft/examples.json new file mode 100644 index 00000000..40647977 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/nexon-army-nft/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Nexon Army NFT Holders", + "strategy": { + "name": "nexon-army-nft" + }, + "network": "137", + "addresses": [ + "0xd43BA4193920dA3A288AAf3400dcb5be62fB1dee", + "0x8966D8aCbb4b0d5830fca4b0Dd3b134e41049B19", + "0x6A6B81b62389ef53e201B855b30b1B92D73B1C4F", + "0xDb8D2DBB24A346b5b84981B1D5e741F67596009C" + ], + "snapshot": 38466628 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/nexon-army-nft/index.ts b/Implementations/API/backend/utils/snapshot/strategies/nexon-army-nft/index.ts new file mode 100644 index 00000000..ae91b074 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/nexon-army-nft/index.ts @@ -0,0 +1,70 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'nexonfidev'; +export const version = '0.1.0'; + +const LIMIT = 1000; + +export const SUBGRAPH_URL = { + '137': 'https://api.thegraph.com/subgraphs/name/nexon-finance/nexon-army-nft' +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockNumber = + typeof snapshot === 'number' + ? snapshot + : (await provider.getBlock('latest')).number; + + const _addresses = addresses.map((x) => x.toLowerCase()); + const addressSubsets = Array.apply( + null, + Array(Math.ceil(_addresses.length / LIMIT)) + ).map((_e, i) => _addresses.slice(i * LIMIT, (i + 1) * LIMIT)); + + const returnedFromSubgraph = await Promise.all( + addressSubsets.map((subset) => + subgraphRequest(SUBGRAPH_URL[network], makeQuery(blockNumber, subset)) + ) + ); + + const subgraphResult = returnedFromSubgraph + .map((x) => x.votingWeights) + .flat(); + if (!subgraphResult) return; + + const score = {}; + subgraphResult.forEach((owner) => { + if (!score[getAddress(owner.address)]) { + score[getAddress(owner.address)] = parseFloat(owner.weight); + } + }); + + return score; +} + +function makeQuery(snapshot, addressSet) { + const query = { + votingWeights: { + __args: { + where: { + address_in: addressSet, + block_lte: snapshot + }, + orderBy: 'block', + orderDirection: 'desc', + first: LIMIT + }, + address: true, + weight: true + } + }; + return query; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/README.md b/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/README.md new file mode 100644 index 00000000..48b9c65e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/README.md @@ -0,0 +1,73 @@ +# Nine Chronicles DAO voting strategy (by staked assets and D:CC) + +This strategy returns calculated score based on staked assets ([Nine Chronicles Gold (NCG)][NCG], [Wrapped Nine Chronicles Gold (wNCG)][wNCG] and [D:CC]) of given accounts. + +[NCG]: https://docs.nine-chronicles.com/introduction/intro/nine-chronicles-gold-ncg +[wNCG]: https://etherscan.io/token/0xf203ca1769ca8e9e8fe1da9d147db68b6c919817 +[D:CC]: https://dcc.nine-chronicles.com/ + + +## Calculation + +The `score` representing the account's voting power will be calculated with: + +``` +score = (w1 * staked_ncg) + (w2 * staked_wncg) + (w3 * dcc_balance) +``` + +- `staked_ncg`: The staked NCG amounts of given account. (on Nine Chronicles mainnet) +- `staked_wncg`: The staked wNCG amounts of given account. (on Ethereum mainnet) +- `w1`, `w2`, `w3`: Weights for each amounts. it can be configured by parameters. + +Also, the `staked_wncg` of the account will be calculated with: + +``` +staked_wncg = (wncg_in_vault / total_lp_supply) * staked_lp_tokens +``` + +- `wncg_in_vault`: The total wNCG amounts that [Balancer] Vault held. +- `total_lp_supply`: The total supply of LP token that Balancer pool issued. +- `staked_lp_tokens`: The staked LP token amounts in the staking contract of given account. + - This value only refers staking contract from [Planetarium] and staked value on its backed pool (from Balancer) will be ignored. + +[Balancer]: https://balancer.fi/ +[Planetarium]: https://planetariumhq.com/ + + +## Parameters + +- `symbol` - (**Optional**, `string`): The symbol of voting score. +- `ethLPTokenStakingAddress` - (**Required**, `string`): The address of staking contract on Ethereum. +- `ethLPTokenAddres` - (**Required**, `string`): The address of LP token contract on Ethereum. +- `ethWNCGAddress` - (**Required**, `string`): The address of wNCG token contract on Ethereum. +- `ethBalancerVaultAddress` - (**Required**, `string`): The address of Balancer Vault contract on Ethereum. +- `ethDccAddress` - (**Required**, `string`): The address of D:CC token contract on Ethereum. +- `ncGraphQLEndpoint` - (**Required**, `string`): The GraphQL endpoint of Nine Chronicles to query. +- `ncBlockHash` - (**Required**, `string`): The target Block Hash of Nine Chronicles. +- `wNCGDecimal` - (**Optional**, `number`): The decimal precision for wNCG. default is 18. +- `weights` - (**Optional**, `number`): Weight values for the fomular . these values must be integers without decimal parts. + - `stakedNCG`: Weight for staked NCG. (`w1`) + - `stakedWNCG`: Weight for staked wNCG. (`w2`) + - `dcc`: Weight for D:CC. (`w3`) + + +## Examples + +```json +{ + "symbol": "Staked 9c assets + DCC", + "ethLPTokenStakingAddress": "0xc53b567a70db04e928fb96d6a417971aa88fda38", + "ethLPTokenAddress": "0xe8cc7e765647625b95f59c15848379d10b9ab4af", + "ethWNCGAddress": "0xf203ca1769ca8e9e8fe1da9d147db68b6c919817", + "ethBalancerVaultAddress": "0xba12222222228d8ba445958a75a0704d566bf2c8", + "ethDccAddress": "0xcea65a86195c911912ce48b6636ddd365c208130", + "ncGraphQLEndpoint": "http://rpc-for-snapshot.nine-chronicles.com/graphql", + "ncBlockHash": "711ce4bcdc0fb5264577876f217a794b2448ccce24e3c7ea0fb9794e420863e4", + "wNCGDecimals": 18, + "weights": { + "stakedWNCG": 1, + "stakedNCG": 1, + "dcc": 999 + } +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/examples.json new file mode 100644 index 00000000..e2fc45bf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ninechronicles-staked-and-dcc", + "params": { + "symbol": "Staked 9c assets + DCC", + "ethLPTokenStakingAddress": "0xc53b567A70dB04E928FB96D6A417971aa88fdA38", + "ethLPTokenAddress": "0xe8cc7e765647625b95f59c15848379d10b9ab4af", + "ethWNCGAddress": "0xf203ca1769ca8e9e8fe1da9d147db68b6c919817", + "ethBalancerVaultAddress": "0xba12222222228d8ba445958a75a0704d566bf2c8", + "ethDccAddress": "0xcea65a86195c911912ce48b6636ddd365c208130", + "ncGraphQLEndpoint": "http://rpc-for-snapshot.nine-chronicles.com/graphql", + "ncBlockHash": "711ce4bcdc0fb5264577876f217a794b2448ccce24e3c7ea0fb9794e420863e4", + "wNCGDecimals": 18, + "weights": { + "stakedWNCG": 1, + "stakedNCG": 1, + "dcc": 999 + } + } + }, + "network": "1", + "addresses": [ + "0x10e19ba32927b28eb5424f7b6a3e2eaa5a607f47", + "0x97aa9e72FAf4B83A719D31B840Bd4F60282E9833", + "0x7916d7c934a7a5ba67f4c88ddb0c9fec76ebd8b5", + "0x93b220bc7c36ea8e4c64192301b680273a184ec3", + "0x30f1d1ffad34b24bb8310ad9dd237b854b4daea7", + "0x961c18a23306fe44c4323adcb3bc343b0d193670", + "0x03eA88F51D9D290e5E72e6F7C70abD2bB9CE9b78", + "0x8D45A60b37D09bEEdA9c1C86b7e4d714Ae625254" + ], + "snapshot": 15797760 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/index.ts new file mode 100644 index 00000000..4d42e497 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ninechronicles-staked-and-dcc/index.ts @@ -0,0 +1,159 @@ +import { getAddress as formatEthAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { strategy as erc721BalanceOfStrategy } from '../erc721'; +import { Multicaller, subgraphRequest } from '../../utils'; + +export const author = 'longfin'; +export const version = '1.1.0'; +export const dependOnOtherAddress = false; + +const lpStakingABI = [ + 'function stakedTokenBalance(address account) view returns (uint256)' +]; + +const erc20ABI = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address account) view returns (uint256)' +]; + +interface Options { + ethLPTokenStakingAddress: string; + ethLPTokenAddress: string; + ethWNCGAddress: string; + ethBalancerVaultAddress: string; + ethDccAddress: string; + ncBlockHash: string; + ncGraphQLEndpoint: string; + wncgDecimals: number; + weights: { + stakedWNCG: number; + dcc: number; + stakedNCG: number; + }; +} + +export async function strategy( + space, + network, + provider, + addresses: string[], + options, + snapshot +): Promise> { + const { + ethLPTokenStakingAddress, + ethLPTokenAddress, + ethWNCGAddress, + ethBalancerVaultAddress, + ethDccAddress, + ncBlockHash, + ncGraphQLEndpoint, + wncgDecimals = 18, + weights: { + stakedWNCG: stakedWNCGWeight = 1, + dcc: dccWeight = 999, + stakedNCG: stakedNCGWeight = 1 + } + }: Options = options; + + addresses = addresses.map(formatEthAddress); + + const dccScores = erc721BalanceOfStrategy( + space, + network, + provider, + addresses, + { address: ethDccAddress }, + snapshot + ).then((scores) => { + Object.keys(scores).forEach((addr) => { + scores[addr] *= dccWeight; + }); + return scores; + }); + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const { wNCGInVault, totalLPSupply }: Record = + await new Multicaller(network, provider, erc20ABI, { blockTag }) + .call('wNCGInVault', ethWNCGAddress, 'balanceOf', [ + ethBalancerVaultAddress + ]) + .call('totalLPSupply', ethLPTokenAddress, 'totalSupply', []) + .execute(); + + const lpStakingChecker = new Multicaller(network, provider, lpStakingABI, { + blockTag + }); + addresses.forEach((address) => + lpStakingChecker.call( + address, + ethLPTokenStakingAddress, + 'stakedTokenBalance', + [address] + ) + ); + const stakedWNCGScores = lpStakingChecker + .execute() + .then((rawScores: Record) => { + const scores = {}; + Object.keys(rawScores).forEach((addr) => { + const amount = rawScores[addr] + .mul(wNCGInVault) + .mul(stakedWNCGWeight) + .div(totalLPSupply); + scores[addr] = parseFloat(formatUnits(amount, wncgDecimals)); + }); + + return scores; + }); + const stakedNCGQuery = { + stateQuery: { + __args: { + hash: ncBlockHash + }, + + stakeStates: { + __args: { + addresses: addresses + }, + + deposit: true + } + } + }; + + const stakedNCGScores = subgraphRequest( + ncGraphQLEndpoint, + stakedNCGQuery + ).then((resp) => { + const scores: Record = {}; + addresses.forEach((addr, i) => { + const stakeState = resp.stateQuery.stakeStates[i]; + scores[addr] = parseFloat( + formatUnits( + parseUnits(stakeState?.deposit ?? '0.00', 2).mul(stakedNCGWeight), + 2 + ) + ); + }); + + return scores; + }); + + const allScores = await Promise.all([ + dccScores, + stakedWNCGScores, + stakedNCGScores + ]); + + return allScores.reduce((total, scores) => { + for (const [address, value] of Object.entries(scores)) { + if (!total[address]) { + total[address] = 0; + } + total[address] += value; + } + return total; + }, {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/niu-staked/examples.json b/Implementations/API/backend/utils/snapshot/strategies/niu-staked/examples.json new file mode 100644 index 00000000..94e130dd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/niu-staked/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "niu-staked", + "params": { + "address": "0xd53105e8efee1461d8c103d7ba979561d2fb4abe", + "symbol": "NIU (staked)", + "pool": 6, + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C" + ], + "snapshot": 8027193 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/niu-staked/index.ts b/Implementations/API/backend/utils/snapshot/strategies/niu-staked/index.ts new file mode 100644 index 00000000..09ff046b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/niu-staked/index.ts @@ -0,0 +1,51 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +const abi = [ + { + inputs: [ + { internalType: 'uint256', name: '', type: 'uint256' }, + { internalType: 'address', name: '', type: 'address' } + ], + name: 'userInfo', + outputs: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'uint256', name: 'rewardDebt', type: 'uint256' } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'userInfo', + [options.pool, address] + ]), + { blockTag } + ); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.amount.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/nouns-rfp-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/nouns-rfp-power/examples.json new file mode 100644 index 00000000..867b578a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/nouns-rfp-power/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example of Nouns RFP Power Strategy", + "strategy": { + "name": "nouns-rfp-power", + "params": { + "governanceStrategy": "0x50Acc1831E954fB5bB407d7A3CFe1e7769A41ab0", + "powerType": "vote", + "symbol": "Voting Power", + "decimals": 18 + } + }, + "network": "4", + "addresses": [ + "0x5BC928BF0DAb1e4A2ddd9e347b0F22e88026D76c", + "0xc0768A60Cf71341C942930E077b7EDf390c3E4c7" + ], + "snapshot": 9647529 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/nouns-rfp-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/nouns-rfp-power/index.ts new file mode 100644 index 00000000..f0dbbb86 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/nouns-rfp-power/index.ts @@ -0,0 +1,58 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'waterdrops'; +export const version = '0.1.0'; + +/** + * Nouns RFP Strategy to measure voting and proposition power + * --derived from aave's Governance Power Strategy. + */ + +const abi = [ + 'function getVotingPower(address user) view returns (uint256)', + 'function getPropositionPower(address user) view returns (uint256)' +]; + +const powerTypesToMethod = { + vote: 'getVotingPower', + proposition: 'getPropositionPower' +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = + typeof snapshot === 'number' + ? snapshot + : await provider.getBlockNumber(snapshot); + + // Early return 0 voting power if governanceStrategy or powerType is not correctly set + if (!options.governanceStrategy || !powerTypesToMethod[options.powerType]) { + return Object.fromEntries(addresses.map((address) => [address, '0'])); + } + + const response: BigNumber[] = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.governanceStrategy, + powerTypesToMethod[options.powerType], + [address.toLowerCase()] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/README.md new file mode 100644 index 00000000..cb913775 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/README.md @@ -0,0 +1,15 @@ +# occ-stake-of + +This is the OCC staking strategy, it returns the OCC staking balances of the voters. + +Here is an example of parameters: + +```json +{ + "stakingContractAddress": [ + "0xeBC86Fb12ab0fFaC6CBcaFCe2f049BfE7eFAda0D" + ], + "symbol": "OCC", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/examples.json new file mode 100644 index 00000000..afdc48e3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "occ-stake-of", + "params": { + "stakingContractAddress": [ + "0xeBC86Fb12ab0fFaC6CBcaFCe2f049BfE7eFAda0D" + ], + "symbol": "OCC", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa95f8f54dc55281852b5c972e4874264c7b5d3de", + "0x77752aE2Cb765BDd09568E69B740a2276187F052", + "0xd3aecf9e0856822bd320873e905ae9f78a2977e7", + "0x2C2ADD1C863551A0644876be227604C8E458dD7e", + "0x8af630f68dd02c589b9449158455735814b452a3", + "0x252e2e0e4cc5bd7391acb47634fb6bb382d08586", + "0x4bf455074e67848dc7e1c55b45a32aafca2e9c9c", + "0xb623964e5761C1b04A4a46b9aE8D1809dEFa7efB" + ], + "snapshot": 12381658 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/index.ts new file mode 100644 index 00000000..471373be --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/occ-stake-of/index.ts @@ -0,0 +1,38 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'OccamFi'; +export const version = '0.1.0'; + +const abi = [ + 'function getStake(address user) public view returns (uint stake)' +]; + +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) => + multi.call(address, options.stakingContractAddress[0], 'getStake', [ + address + ]) + ); + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, stake]) => [ + address, + parseFloat(formatUnits(stake, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/README.md b/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/README.md new file mode 100644 index 00000000..7042f9bb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/README.md @@ -0,0 +1,94 @@ +# Ocean DAO BrightID Snapshot strategy + +`version 0.1` + +This strategy sums the scores of given multiple strategies for each voter. If the voter is verified on the BrightId registry contract, the score of the voter is multiplied by a factor of `brightIdMultiplier` parameter. + +Example parameters: + +```json +{ + "brightIdMultiplier": 5, + "brightIdNetwork": "4", + "registry": "0xbD45cf7C9f8eE04409C31D0ef939D4b0926263Ae", + "symbol": "OCEAN", + "brightIdNetwork":4, + "brightIdSnapshot":100000, + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "symbol": "OCEAN", + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "decimals": 18 + } + }, + { + "name": "ocean-marketplace", + "params": { + "symbol": "OCEAN", + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "decimals": 18 + } + }, + { + "name": "sushiswap", + "params": { + "symbol": "OCEAN", + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "decimals": 18 + } + }, + { + "name": "uniswap", + "params": { + "symbol": "OCEAN", + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "decimals": 18 + } + }, + { + "name": "contract-call", + "params": { + "address": "0x9712Bb50DC6Efb8a3d7D12cEA500a50967d2d471", + "args": [ + "%{address}", + "0xCDfF066eDf8a770E9b6A7aE12F7CFD3DbA0011B5", + "0x967da4048cD07aB37855c090aAF366e4ce1b9F48" + ], + "decimals": 18, + "symbol": "OCEAN", + "methodABI": { + "inputs": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "internalType": "address", + "name": "poolToken", + "type": "address" + }, + { + "internalType": "address", + "name": "reserveToken", + "type": "address" + } + ], + "name": "totalProviderAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + } + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/examples.json new file mode 100644 index 00000000..ef3f3d4c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/examples.json @@ -0,0 +1,141 @@ +[ + { + "name": "Ocean Marketplace BrightID Stategy", + "strategy": { + "name": "ocean-dao-brightid", + "params": { + "brightIdMultiplier": 4, + "notVerifiedMultiplier": 1, + "brightIdNetwork": "1", + "registry": "0xc37F8341Ac6e4a94538302bCd4d49Cf0852D30C0", + "symbol": "OCEAN", + "delegationSpace": "officialoceandao.eth", + "strategies": { + "137": [ + { + "name": "erc20-balance-of", + "params": { + "symbol": "OCEAN", + "address": "0x282d8efCe846A88B159800bd4130ad77443Fa1A1", + "decimals": 18 + } + } + ], + "1": [ + { + "name": "erc20-balance-of", + "params": { + "symbol": "OCEAN", + "address": "0x967da4048cd07ab37855c090aaf366e4ce1b9f48", + "decimals": 18 + } + }, + { + "name": "ocean-marketplace", + "params": { + "symbol": "OCEAN", + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "decimals": 18 + } + }, + { + "name": "sushiswap", + "params": { + "symbol": "OCEAN", + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "decimals": 18 + } + }, + { + "name": "uniswap", + "params": { + "symbol": "OCEAN", + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "decimals": 18 + } + }, + { + "name": "contract-call", + "params": { + "address": "0x9712Bb50DC6Efb8a3d7D12cEA500a50967d2d471", + "args": [ + "%{address}", + "0xCDfF066eDf8a770E9b6A7aE12F7CFD3DbA0011B5", + "0x967da4048cD07aB37855c090aAF366e4ce1b9F48" + ], + "decimals": 18, + "symbol": "OCEAN", + "methodABI": { + "inputs": [ + { + "internalType": "address", + "name": "provider", + "type": "address" + }, + { + "internalType": "address", + "name": "poolToken", + "type": "address" + }, + { + "internalType": "address", + "name": "reserveToken", + "type": "address" + } + ], + "name": "totalProviderAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + } + } + ] + } + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268", + "0xf25B7b8dC2B264Be6c3410e2CAE339c041B854C2", + "0x655eFe6Eb2021b8CEfE22794d90293aeC37bb325", + "0xCe7BE31f48205C48A91A84E777a66252Bba87F0b", + "0x788550D00579F66c06ce209D14056C8F2c0A8188", + "0x18F93FA526e598769fd909F1D2C82315219d49Ed", + "0xA321025df54C18CEeFdE118c0671735390aCB318", + "0x8a4C36a99b0418BF95A155160C31A267EBCA8754", + "0x5e22b34F2708EA7e25918B8FCF816F9CfF27a331", + "0x12BD31628075C20919BA838b89F414241b8c4869", + "0xf88bD9c3F81f7148E4076bBB8b2e2B0951A8cE38", + "0xAAB9EaBa1AA2653c1Dda9846334700b9F5e14E44", + "0xB9b14C4d0cbc18FC5f54701D578e85968cf19FD5", + "0xceE1C921067E5fe65CD71fb8c0627d86Ce91d746", + "0xde1E6A7ED0ad3F61D531a8a78E83CcDdbd6E0c49", + "0x830dff6a094b417ff5a78f989054a873dfd8fa2b" + ], + "snapshot": 14048401 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/index.ts new file mode 100644 index 00000000..b62449ea --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-dao-brightid/index.ts @@ -0,0 +1,133 @@ +import { + getProvider, + getScoresDirect, + multicall, + subgraphRequest +} from '../../utils'; +import { getDelegations } from '../../utils/delegation'; + +export const author = 'trizin'; +export const version = '0.2.0'; + +const abi = [ + 'function isVerifiedUser(address _user) external view returns (bool)' +]; + +/* Code from multichain strategy */ + +async function getBlocks(snapshot, provider, options, network) { + const blocks = {}; + Object.keys(options.strategies).forEach((s) => (blocks[s] = 'latest')); + const block = await provider.getBlock(snapshot); + const query = { + blocks: { + __args: { + where: { + ts: block.timestamp, + network_in: Object.keys(blocks) + } + }, + network: true, + number: true + } + }; + const url = 'https://blockfinder.snapshot.org'; + const data = await subgraphRequest(url, query); + data.blocks.forEach((block) => (blocks[block.network] = block.number)); + data.blocks[network] = snapshot; + return blocks; +} + +//////////////////////////////////////////////////////////////////////////////// + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const chainBlocks = await getBlocks(snapshot, provider, options, network); + const delegatitonSpace = options.delegationSpace || space; + const delegations = await getDelegations( + delegatitonSpace, + network, + addresses, + snapshot + ); + + const brightIdNetwork = options.brightIdNetwork || network; + const response = await multicall( + brightIdNetwork, + getProvider(brightIdNetwork), + abi, + addresses.map((address: any) => [ + options.registry, + 'isVerifiedUser', + [address] + ]), + { blockTag: chainBlocks[brightIdNetwork] } + ); + + const totalScores = {}; + const delegatorAddresses = Object.values(delegations).reduce( + (a: string[], b: string[]) => a.concat(b) + ); + + // remove duplicates + const allAddresses = addresses + .concat(delegatorAddresses) + .filter((address, index, self) => self.indexOf(address) === index); // Remove duplicates + + for (const chain of Object.keys(options.strategies)) { + let scores = await getScoresDirect( + space, + options.strategies[chain], + chain, + getProvider(chain), + allAddresses, + chainBlocks[chain] + ); + + // [{ address: '0x...', score: 0.5 },{ address: '0x...', score: 0.5 }] + // sum scores for each address and return + scores = scores.reduce((finalScores: any, score: any) => { + for (const [address, value] of Object.entries(score)) { + if (!finalScores[address]) { + finalScores[address] = 0; + } + finalScores[address] += value; + } + return finalScores; + }, {}); + // { address: '0x...55', score: 1.0 } + + // sum delegations + addresses.forEach((address) => { + if (!scores[address]) scores[address] = 0; + if (delegations[address]) { + delegations[address].forEach((delegator: string) => { + scores[address] += scores[delegator] ?? 0; // add delegator score + scores[delegator] = 0; // set delegator score to 0 + }); + } + }); + + for (const key of Object.keys(scores)) { + totalScores[key] = totalScores[key] + ? totalScores[key] + scores[key] + : scores[key]; + } + } + + return Object.fromEntries( + addresses.map((address, index) => { + let addressScore = totalScores[address]; + addressScore *= response[index][0] + ? options.brightIdMultiplier // brightId multiplier + : options.notVerifiedMultiplier; // not verified multiplier + return [address, addressScore]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/README.md b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/README.md new file mode 100644 index 00000000..71121343 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/README.md @@ -0,0 +1,51 @@ +# Ocean marketplace v4 + +```version 0.1``` + +This strategy gives score aka votes to the liquidity providers on the [Ocean marketplace v4](https://market.oceanprotocol.com). This means that LP participants can vote for OceanDAO grants via Snapshot without removing their liquidity. + +## Solution description + +The solution pulls the needed data from all Ocean Protocol subgraphs using the following path: +```https://subgraph.{network}.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph``` + +This strategy is designed to give voting scores only to marketplace liquidity providers by calculating their individual pool shares. + +The remaining vote count comes from other strategies configured in [OceanDAO Snapshot Page](https://vote.oceanprotocol.com/#/). + +For v4, we only look at pools w/ OCEAN as the basetoken, and pools that have been proprely initialized. We then attributes votes to LP'ers like this: +``` +user_votes = user_pool_shares * (total_Ocean_in_the_pool / total_number_of_pool_shares) +``` +This is done for all pools and the votes for the users are added up. + +To extend or run this strategy please use the setup described [here](https://docs.snapshot.page/strategies). + +## v3 vs. v4 differences +- In v4 we have to check if the pool basetoken is Ocean. Only Ocean tokens will obtain voting power. +- In v4, pools.datatoken.holderCount is always 0 as datatokens are consumed as soon as they are purchased +- In v4, pools.isFinalized checks if the pool has been properly setup. In v3 the equivalent was pools.active + +## Ocean ERC20 Addresses +You need to submit the ERC20 Ocean tokenAddress via the stratgy parameters. These [can be found here](https://github.com/oceanprotocol/contracts/blob/v4main/addresses/address.json) + +Addresses by network: +``` +'1': '0x967da4048cD07aB37855c090aAF366e4ce1b9F48', //mainnet +'3': '0x5e8DCB2AfA23844bcc311B00Ad1A0C30025aADE9', //ropsten +'42': '0x8967bcf84170c91b0d24d4302c2376283b0b3a07', //rinkeby +'56': '0xDCe07662CA8EbC241316a15B611c89711414Dd1a', //bsc +'137': '0x282d8efCe846A88B159800bd4130ad77443Fa1A1', //poly +'246': '0x593122AAE80A6Fc3183b2AC0c4ab3336dEbeE528', //ewt +'1285': '0x99C409E5f62E4bd2AC142f17caFb6810B8F0BAAE', //movr +'1287': '0xF6410bf5d773C7a41ebFf972f38e7463FA242477', //glmr +'80001': '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8' //mumbai +``` + +## How to Test +(1) Remove comments around debug logs. +(2) Enable code block inside `index.ts` that verifies expected results. +``` +if (options.expectedResults) { +``` +(3) Use the regular testing functionality `yarn test -strategy=ocean-marktplace-v4` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/examples.json new file mode 100644 index 00000000..d4986c39 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/examples.json @@ -0,0 +1,94 @@ +[ + { + "name": "ERC-20 OCEAN staked in mainnet v4 marketplace via subgraph", + "strategy": { + "name": "ocean-marketplace-v4", + "params": { + "address": "0x967da4048cD07aB37855c090aAF366e4ce1b9F48", + "expectedResults": { + "scores": { + "0x7842Fa3B2d87Ff1cd52C4152382f7C4B3406E5A6": 250, + "0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0": 200, + "0x903322C7E45A60d7c8C3EA236c5beA9Af86310c7": 50 + } + } + } + }, + "network": "1", + "addresses": [ + "0x7842Fa3B2d87Ff1cd52C4152382f7C4B3406E5A6", + "0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0", + "0x903322C7E45A60d7c8C3EA236c5beA9Af86310c7" + ], + "snapshot": 15006609 + }, + { + "name": "ERC-20 OCEAN staked in polygon v4 marketplace via subgraph", + "strategy": { + "name": "ocean-marketplace-v4", + "params": { + "address": "0x282d8efCe846A88B159800bd4130ad77443Fa1A1", + "expectedResults": { + "scores": { + "0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4": 10762.326857765422, + "0xbbd33AFa85539fa65cc08A2e61a001876D2f13FE": 5752.362504190221, + "0x0363F3C31076a64b85Ceb69a28f958A7c1181CEe": 1750, + "0xC5320Dc3956484662cF3FE3B9355AEA93729783D": 778.6503526737412 + } + } + } + }, + "network": "137", + "addresses": [ + "0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4", + "0xbbd33AFa85539fa65cc08A2e61a001876D2f13FE", + "0x0363F3C31076a64b85Ceb69a28f958A7c1181CEe", + "0xC5320Dc3956484662cF3FE3B9355AEA93729783D" + ], + "snapshot": 29358635 + }, + { + "name": "ERC-20 OCEAN staked in EWT v4 marketplace via subgraph", + "strategy": { + "name": "ocean-marketplace-v4", + "params": { + "address": "0x593122AAE80A6Fc3183b2AC0c4ab3336dEbeE528", + "expectedResults": { + "scores": { + "0x159924ca0F47D6F704B97E29099b89e518A17B5E": 3489.1235163210085, + "0x4F20e69E7bA5aB2Fb2ae25A1d17C93fE5307faA9": 500.45061032028195 + } + } + } + }, + "network": "246", + "addresses": [ + "0x159924ca0F47D6F704B97E29099b89e518A17B5E", + "0x4F20e69E7bA5aB2Fb2ae25A1d17C93fE5307faA9" + ], + "snapshot": 18788160 + }, + { + "name": "ERC-20 OCEAN staked in MOVR v4 marketplace via subgraph", + "strategy": { + "name": "ocean-marketplace-v4", + "params": { + "address": "0x99C409E5f62E4bd2AC142f17caFb6810B8F0BAAE", + "expectedResults": { + "scores": { + "0xc97fa83746aDe91b0eeB16cb51326a0A980Af7c3": 200, + "0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0": 200, + "0xFcd3DfCc1E793D4D03fB4ffFf2B7Eb5A20FCfe4E": 0 + } + } + } + }, + "network": "1285", + "addresses": [ + "0xc97fa83746aDe91b0eeB16cb51326a0A980Af7c3", + "0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0", + "0xFcd3DfCc1E793D4D03fB4ffFf2B7Eb5A20FCfe4E" + ], + "snapshot": 2150228 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/index.ts new file mode 100644 index 00000000..b86da322 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/index.ts @@ -0,0 +1,171 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +// import { verifyResultsLength, verifyResults } from './oceanUtils'; + +export const author = 'oceanprotocol'; +export const version = '0.1.0'; + +const OCEAN_ERC20_DECIMALS = 18; +const OCEAN_SUBGRAPH_URL = { + '1': 'https://v4.subgraph.mainnet.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '3': 'https://v4.subgraph.ropsten.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '42': 'https://v4.subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '56': 'https://v4.subgraph.bsc.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '137': + 'https://v4.subgraph.polygon.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '246': + 'https://v4.subgraph.energyweb.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '1285': + 'https://v4.subgraph.moonriver.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '1287': + 'https://v4.subgraph.moonbase.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '80001': + 'https://v4.subgraph.mumbai.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph' +}; + +// Returns a BigDecimal as a BigNumber with 10^decimals extra zeros +export function bdToBn(bd, decimals) { + let bn; + const splitDecimal = bd.split('.'); + + if (splitDecimal.length > 1) { + bn = `${splitDecimal[0]}.${splitDecimal[1].slice( + 0, + decimals - splitDecimal[0].length - 1 + )}`; + } else { + bn = `${splitDecimal[0]}`; + } + + const bn2 = parseUnits(bn, decimals); + return bn2; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const oceanToken = options.address.toLowerCase(); + const params = { + pools: { + __args: { + where: { + baseToken_in: [oceanToken] + }, + first: 1000, + orderBy: 'baseTokenLiquidity', + orderDirection: 'desc' + }, + isFinalized: true, + totalShares: true, + baseTokenLiquidity: true, + shares: { + __args: { + where: { + user_in: addresses.map((address) => address.toLowerCase()) + }, + orderBy: 'shares', + orderDirection: 'desc' + }, + user: { + id: true + }, + shares: true + }, + datatoken: { + holderCount: true + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.pools.__args.block = { number: +snapshot }; + } + + // Retrieve the top 1000 pools + const graphResults = await subgraphRequest( + OCEAN_SUBGRAPH_URL[network], + params + ); + + // Get total votes, for ALL addresses, inside top 1000 pools, with a minimum of 0.0001 shares + const score = {}; + const userAddresses: string[] = []; + const return_score = {}; + + // console.log( + // `graph results for network: ${network} at snapshot: ${snapshot}` + // ); + // console.log('results: ', graphResults); + + if (graphResults && graphResults.pools) { + graphResults.pools.forEach((pool) => { + if (pool.isFinalized) { + pool.shares.map((share) => { + const userAddress = getAddress(share.user.id); + // const shares = share.shares; + // console.log( + // `High Level - User address: ${userAddress} user poolShares: ${shares} baseTokenLiquidity: ${pool.baseTokenLiquidity} poolTotalShares: ${pool.totalShares}` + // ); + if (!userAddresses.includes(userAddress)) + userAddresses.push(userAddress); + if (!score[userAddress]) score[userAddress] = BigNumber.from(0); + const userShare = + share.shares * (pool.baseTokenLiquidity / pool.totalShares); + if (userShare > 0.0001) { + score[userAddress] = score[userAddress].add( + bdToBn(userShare.toString(), OCEAN_ERC20_DECIMALS) + ); + } + }); + } + }); + + // We then sum total votes, per user address + userAddresses.forEach((address) => { + const parsedSum = parseFloat( + formatUnits(score[address], OCEAN_ERC20_DECIMALS) + ); + return_score[address] = parsedSum * 2; //this resolves the 50% discrepancy due to 50/50 OCEAN/DT LP + + // console.log(`Score for address: ${address} is: ${return_score[address]}`); + }); + } + + // We then filter only the addresses expected + const results = Object.fromEntries( + Object.entries(return_score).filter(([k]) => addresses.indexOf(k) >= 0) + ); + + // Test validation: Update examples.json w/ expectedResults to reflect LPs @ blockHeight + // Success criteria: Address scores and length, must match expectedResults. Order not validated. + // From GRT's graphUtils.ts => verifyResults => Scores need to match expectedResults. + // npm run test --strategy=ocean-marketplace | grep -E 'SUCCESS|ERROR' + // if (options.expectedResults) { + // const expectedResults = {}; + // Object.keys(options.expectedResults.scores).forEach(function (key) { + // expectedResults[key] = results[key]; + // }); + // + // verifyResults( + // JSON.stringify(expectedResults), + // JSON.stringify(options.expectedResults.scores), + // 'Scores' + // ); + // + // verifyResultsLength( + // Object.keys(expectedResults).length, + // Object.keys(options.expectedResults.scores).length, + // 'Scores' + // ); + // } + + return results || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/oceanUtils.ts b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/oceanUtils.ts new file mode 100644 index 00000000..be3b2209 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace-v4/oceanUtils.ts @@ -0,0 +1,31 @@ +export function verifyResultsLength( + result: number, + expectedResults: number, + type: string +): void { + if (result === expectedResults) { + console.log( + `>>> SUCCESS: ${type} result:[${result}] match expected results:[${expectedResults}] - length` + ); + } else { + console.error( + `>>> ERROR: ${type} result:[${result}] do not match expected results:[${expectedResults}] - length` + ); + } +} + +export function verifyResults( + result: string, + expectedResults: string, + type: string +): void { + if (result === expectedResults) { + console.log( + `>>> SUCCESS: ${type} result:[${result}] match expected results:[${expectedResults}]` + ); + } else { + console.error( + `>>> ERROR: ${type} result:[${result}] do not match expected results:[${expectedResults}]` + ); + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/README.md b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/README.md new file mode 100644 index 00000000..da1d0626 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/README.md @@ -0,0 +1,67 @@ +# Ocean marketplace Snapshot strategy + +```version 0.1``` + +This strategy gives score aka votes to the liquidity providers on the [Ocean marketplace](https://market.oceanprotocol.com). This means that they can vote for OceanDAO votes hosted on the Snapshot platform without the need to remove their liquidity. + +## Solution description + +The solution pulls the needed data from the Ocean Protocol mainnet subgraph endpoint: +```https://subgraph.mainnet.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph``` + +It is pulling a bit more information than currently used so the solution can also be extended. The current limitation comes from only considering liquidity providers and ignoring pure token holders. This means that the tokens added to the liquidity pool by the pure token holders are accredited to the liquidity providers. This can be fixed but is a bit more complicated as the ratio of datatokens to Ocean tokens has to be considered in a general manner. And the token holders for each pool have to be extracted from the subgraph. + +So the current solution checks for active pools with participants and then attributes votes to them like this: +``` +user_votes = user_pool_shares * (total_Ocean_in_the_pool / total_number_of_pool_shares) +``` +This is done for all pools and the votes for the users are added up. + +To extend or run this strategy please use the setup described [here](https://docs.snapshot.page/strategies). + +## GraphQL queries: + +### Pools + +``` +pools (first: 1000, orderBy: oceanReserve, orderDirection: desc) { + id, + holderCount, + oceanReserve, + active, + totalShares, + shares (first: 1000) { + id, + userAddress { + id, + tokensOwned { + id + } + }, + balance + }, + tokens { + balance, + denormWeight, + tokenId { + id + } + } +} +``` + +### Datatokens + +``` +{ +datatokens { + id, + balances { + userAddress { + id + } + balance + } +} +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/examples.json new file mode 100644 index 00000000..f2aa79c0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/examples.json @@ -0,0 +1,41 @@ +[ + { + "name": "ERC-20 OCEAN tokens staked on the marketplace", + "strategy": { + "name": "ocean-marketplace", + "params": { + "symbol": "OCEAN", + "expectedResults": { + "scores": { + "0x655eFe6Eb2021b8CEfE22794d90293aeC37bb325": 234872.06400571144, + "0xCe7BE31f48205C48A91A84E777a66252Bba87F0b": 184463.92011443214, + "0x788550D00579F66c06ce209D14056C8F2c0A8188": 7288.420566804952, + "0x18F93FA526e598769fd909F1D2C82315219d49Ed": 6163.508200256553, + "0xA321025df54C18CEeFdE118c0671735390aCB318": 4532.791480521258, + "0x8a4C36a99b0418BF95A155160C31A267EBCA8754": 3627.4502012669805, + "0x5e22b34F2708EA7e25918B8FCF816F9CfF27a331": 4460.415948645305, + "0xAAB9EaBa1AA2653c1Dda9846334700b9F5e14E44": 0, + "0x12BD31628075C20919BA838b89F414241b8c4869": 250944.65161828607, + "0xf88bD9c3F81f7148E4076bBB8b2e2B0951A8cE38": 34084.17596472631, + "0xB9b14C4d0cbc18FC5f54701D578e85968cf19FD5": 2706.7321282426365 + } + } + } + }, + "network": "1", + "addresses": [ + "0x655eFe6Eb2021b8CEfE22794d90293aeC37bb325", + "0xCe7BE31f48205C48A91A84E777a66252Bba87F0b", + "0x788550D00579F66c06ce209D14056C8F2c0A8188", + "0x18F93FA526e598769fd909F1D2C82315219d49Ed", + "0xA321025df54C18CEeFdE118c0671735390aCB318", + "0x8a4C36a99b0418BF95A155160C31A267EBCA8754", + "0x5e22b34F2708EA7e25918B8FCF816F9CfF27a331", + "0x12BD31628075C20919BA838b89F414241b8c4869", + "0xf88bD9c3F81f7148E4076bBB8b2e2B0951A8cE38", + "0xAAB9EaBa1AA2653c1Dda9846334700b9F5e14E44", + "0xB9b14C4d0cbc18FC5f54701D578e85968cf19FD5" + ], + "snapshot": 12097120 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/index.ts new file mode 100644 index 00000000..de9b9bfd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/index.ts @@ -0,0 +1,144 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { verifyResultsLength, verifyResults } from './oceanUtils'; + +export const author = 'w1kke'; +export const version = '0.1.0'; + +const OCEAN_ERC20_DECIMALS = 18; +const OCEAN_SUBGRAPH_URL = { + '1': 'https://subgraph.mainnet.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '42': 'https://subgraph.rinkeby.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '56': 'https://subgraph.bsc.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + '137': + 'https://subgraph.polygon.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph' +}; + +// Returns a BigDecimal as a BigNumber with 10^decimals extra zeros +export function bdToBn(bd, decimals) { + let bn; + const splitDecimal = bd.split('.'); + + if (splitDecimal.length > 1) { + bn = `${splitDecimal[0]}.${splitDecimal[1].slice( + 0, + decimals - splitDecimal[0].length - 1 + )}`; + } else { + bn = `${splitDecimal[0]}`; + } + + const bn2 = parseUnits(bn, decimals); + return bn2; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const params = { + pools: { + __args: { + first: 1000, + orderBy: 'oceanReserve', + orderDirection: 'desc' + }, + active: true, + totalShares: true, + holderCount: true, + oceanReserve: true, + shares: { + __args: { + where: { + userAddress_in: addresses.map((address) => address.toLowerCase()) + }, + orderBy: 'balance', + orderDirection: 'desc' + }, + userAddress: { + id: true + }, + balance: true + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.pools.__args.block = { number: +snapshot }; + } + + // Retrieve the top 1000 pools + const graphResults = await subgraphRequest( + OCEAN_SUBGRAPH_URL[network], + params + ); + + // Get total votes, for ALL addresses, inside top 1000 pools, with a minimum of 0.0001 shares + const score = {}; + const userAddresses: string[] = []; + const return_score = {}; + if (graphResults && graphResults.pools) { + graphResults.pools.forEach((pool) => { + if (pool.holderCount > 0 && pool.active) { + pool.shares.map((share) => { + const userAddress = getAddress(share.userAddress.id); + if (!userAddresses.includes(userAddress)) + userAddresses.push(userAddress); + if (!score[userAddress]) score[userAddress] = BigNumber.from(0); + const userShare = + share.balance * (pool.oceanReserve / pool.totalShares); + if (userShare > 0.0001) { + score[userAddress] = score[userAddress].add( + bdToBn(userShare.toString(), OCEAN_ERC20_DECIMALS) + ); + } + }); + } + }); + + // We then sum total votes, per user address + userAddresses.forEach((address) => { + const parsedSum = parseFloat( + formatUnits(score[address], OCEAN_ERC20_DECIMALS) + ); + return_score[address] = parsedSum; + }); + } + + // We then filter only the addresses expected + const results = Object.fromEntries( + Object.entries(return_score).filter(([k]) => addresses.indexOf(k) >= 0) + ); + + // Test validation: Update examples.json w/ expectedResults to reflect LPs @ blockHeight + // Success criteria: Address scores and length, must match expectedResults. Order not validated. + // From GRT's graphUtils.ts => verifyResults => Scores need to match expectedResults. + // npm run test --strategy=ocean-marketplace | grep -E 'SUCCESS|ERROR' + if (options.expectedResults) { + const expectedResults = {}; + Object.keys(options.expectedResults.scores).forEach(function (key) { + expectedResults[key] = results[key]; + }); + + verifyResults( + JSON.stringify(expectedResults), + JSON.stringify(options.expectedResults.scores), + 'Scores' + ); + + verifyResultsLength( + Object.keys(expectedResults).length, + Object.keys(options.expectedResults.scores).length, + 'Scores' + ); + } + + return results || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/oceanUtils.ts b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/oceanUtils.ts new file mode 100644 index 00000000..0e2c8949 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ocean-marketplace/oceanUtils.ts @@ -0,0 +1,21 @@ +export function verifyResultsLength( + result: number, + expectedResults: number, + type: string +): void { + result === expectedResults + ? console.log(`>>> SUCCESS: ${type} match expected results - length`) + : console.error( + `>>> ERROR: ${type} do not match expected results - length` + ); +} + +export function verifyResults( + result: string, + expectedResults: string, + type: string +): void { + result === expectedResults + ? console.log(`>>> SUCCESS: ${type} match expected results`) + : console.error(`>>> ERROR: ${type} do not match expected results`); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/README.md new file mode 100644 index 00000000..5b5f2e71 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/README.md @@ -0,0 +1,34 @@ +# Offchain delegation + +This strategy is used to handle offchain delegations. + +## Getting started +- Check the template here: https://docs.google.com/spreadsheets/d/1IQDXROyxavyZ03ZtNW-2uuDjcI_oME-OmY1VFPTAO-M/edit?usp=sharing +- Select: File > Make a copy +- On the newly created file select: File > Share > Publish to web +- On the modal "Publish to web" click "Publish" button +- Copy the long id within the URL and use it as "sheetId" parameter for the strategy +- And use the id after "pub?gid=" as "gid" parameter +- You are ready! + +## Parameters +- `sheetId`: The id of the spreadsheet +- `gid`: The id of the sheet +- `strategies`: The strategies to use, to calculate the score of the delegations + +Here is an example of the strategy parameters: +```json +{ + "sheetId": "2PACX-1vTAo2yFq6GyBZcB3BOnIw_Rzub7KEEzqhhgFY5CD6eCW-Rfx9d4NaMJP8IP67G9YnV3PNPHKvgJeSNY", + "gid": "1557274211", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "decimals": 18 + } + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/examples.json new file mode 100644 index 00000000..eab6e07c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/examples.json @@ -0,0 +1,32 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "offchain-delegation", + "params": { + "symbol": "DAI (delegated)", + "sheetId": "2PACX-1vTAo2yFq6GyBZcB3BOnIw_Rzub7KEEzqhhgFY5CD6eCW-Rfx9d4NaMJP8IP67G9YnV3PNPHKvgJeSNY", + "gid": "1557274211", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "decimals": 18 + } + } + ] + } + }, + "network": "1", + "addresses": [ + "0x4C7909d6F029b3a5798143C843F4f8e5341a3473", + "0x47D5592606622D807F3B37E5e5C8268355E30925", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0xdD36Aa96D9BD4b49DA6E6734fF18Cc69F90F9435", + "0x77EC061aC11df6b42Af3784BCE835A5feAF247Dd", + "0x3BAe6eb858837a9855CE7747feC3FC99a87A86e0" + ], + "snapshot": 15820600 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/index.ts new file mode 100644 index 00000000..c03fc199 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/offchain-delegation/index.ts @@ -0,0 +1,74 @@ +import fetch from 'cross-fetch'; +import { getAddress } from '@ethersproject/address'; +import { getScoresDirect } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; +export const dependOnOtherAddress = true; + +const DEFAULT_SPREADSHEET_ID = + '2PACX-1vQsn8e6KQOwqfHoA4rWDke63jTwfcshHxcZwOzVharOoAARWy6aX0TvN-uzzgtmAn3F5vDbuDKnk5Jw'; +const DEFAULT_GID = '506976679'; + +function csvToJson(csv) { + const lines = csv.split('\n'); + const keys = lines[0].split(',').map((key) => key.trim()); + return lines.slice(1).map((line) => + line.split(',').reduce((acc, cur, i) => { + const toAdd = {}; + toAdd[keys[i]] = cur.trim(); + return { ...acc, ...toAdd }; + }, {}) + ); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const block = await provider.getBlock(snapshot); + const ts = block.timestamp; + const SPREADSHEET_ID = options.sheetId ?? DEFAULT_SPREADSHEET_ID; + const GID = options.gid ?? DEFAULT_GID; + const url = `https://docs.google.com/spreadsheets/d/e/${SPREADSHEET_ID}/pub?gid=${GID}&single=true&output=csv`; + const res = await fetch(url); + const text = await res.text(); + const csv = csvToJson(text) || []; + const delegations = Object.fromEntries( + csv + .map((item) => ({ + ...item, + delegator: getAddress(item.delegator), + delegate: getAddress(item.delegate), + ts: parseInt(item.timestamp || '0') + })) + .filter((item) => item.ts <= ts && !addresses.includes(item.delegator)) + .sort((a, b) => a.ts - b.ts) + .map((item) => [item.delegator, item.delegate]) + ); + + const delegatorScores = await getScoresDirect( + space, + options.strategies, + network, + provider, + Object.keys(delegations), + snapshot + ); + + const scores = {}; + delegatorScores.forEach((score) => { + Object.entries(score).forEach(([address, vp]) => { + if (delegations[address] && addresses.includes(delegations[address])) { + if (!scores[delegations[address]]) scores[delegations[address]] = 0; + scores[delegations[address]] += vp; + } + }); + }); + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ogn/README.md b/Implementations/API/backend/utils/snapshot/strategies/ogn/README.md new file mode 100644 index 00000000..caf9816e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ogn/README.md @@ -0,0 +1,17 @@ +# ogn + +A strategy for Origin Story governance. + +The score is based on the amount of OGN held in the wallet and the OGN staking contract. +An average balance is calculated at the specified block number and 30 days prior to that. + +Here is an example of parameters: + +```json +{ + "ognAddress": "0x8207c1ffc5b6804f6024322ccf34f29c3541ae26", + "stakingAddress": "0x501804B374EF06fa9C427476147ac09F1551B9A0", + "symbol": "OGN", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ogn/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ogn/examples.json new file mode 100644 index 00000000..46975e0e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ogn/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "OGN Snapshot strategy", + "strategy": { + "name": "ogn", + "params": { + "ognAddress": "0x8207c1ffc5b6804f6024322ccf34f29c3541ae26", + "seriesAddress": "0xCcE8E784c777fb9435F89f4E45f8b7FC49f7669f", + "symbol": "OGN", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xD85A569F3C26f81070544451131c742283360400", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 16767676 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ogn/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ogn/index.ts new file mode 100644 index 00000000..aa72a2cc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ogn/index.ts @@ -0,0 +1,65 @@ +import { formatUnits } from '@ethersproject/units'; +import { getBlockNumber } from '../../utils'; +import { multicall } from '../../utils'; + +export const author = 'franckc'; +export const version = '0.2.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +// Number of blocks in 30 days, assuming 15 sec per block. +const numBlock30Days = (30 * 24 * 60 * 60) / 15; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + // If a specific block number is specified, use that. + // Otherwise fetch the current block number from the network. + const blockTag1 = + typeof snapshot === 'number' ? snapshot : await getBlockNumber(provider); + const blockTag2 = blockTag1 - numBlock30Days; + + // Compile a list of calls to make against the OGN and staking contracts. + const ognCalls = addresses.map((address: any) => [ + options.ognAddress, + 'balanceOf', + [address] + ]); + const stakingCalls = addresses.map((address: any) => [ + options.seriesAddress, + 'balanceOf', + [address] + ]); + + // Make calls to the OGN and staking contract to fetch balances. + const multicalls = [ + multicall(network, provider, abi, ognCalls, { blockTag: blockTag1 }), + multicall(network, provider, abi, ognCalls, { blockTag: blockTag2 }), + multicall(network, provider, abi, stakingCalls, { blockTag: blockTag1 }), + multicall(network, provider, abi, stakingCalls, { blockTag: blockTag2 }) + ]; + + const responses = await Promise.all(multicalls); + + // Compute an average score. + const scores = {}; + addresses.forEach((address, i) => { + const balance = responses[0][i][0] + .add(responses[1][i][0]) + .add(responses[2][i][0]) + .add(responses[3][i][0]) + .div(2); + scores[address] = parseFloat( + formatUnits(balance.toString(), options.decimals) + ); + }); + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/oolongswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/oolongswap/examples.json new file mode 100644 index 00000000..c8b9490e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/oolongswap/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "oolongswap", + "params": { + "address": "0x66a2a913e447d6b4bf33efbec43aaef87890fbbc", + "symbol": "USDC" + } + }, + "network": "288", + "addresses": ["0x4f6798d42feb2a9169f4fbe0f986d005e1b76180"], + "snapshot": 399991 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/oolongswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/oolongswap/index.ts new file mode 100644 index 00000000..843fda96 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/oolongswap/index.ts @@ -0,0 +1,83 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const OOLONGSWAP_SUBGRAPH_URL = { + '288': 'https://graph.mainnet.boba.network/subgraphs/name/oolongswap/mainnet' +}; + +export const author = 'Quantumlyy'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + liquidityPositions: { + __args: { + where: { + liquidityTokenBalance_gt: 0 + } + }, + liquidityTokenBalance: true, + pair: { + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.liquidityPositions.__args.block = { number: snapshot }; + } + const tokenAddress = options.address.toLowerCase(); + const result = await subgraphRequest( + OOLONGSWAP_SUBGRAPH_URL[network], + params + ); + const score = {}; + if (result && result.users) { + result.users.forEach((u) => { + u.liquidityPositions + .filter( + (lp) => + lp.pair.token0.id == tokenAddress || + lp.pair.token1.id == tokenAddress + ) + .forEach((lp) => { + const token0perUni = lp.pair.reserve0 / lp.pair.totalSupply; + const token1perUni = lp.pair.reserve1 / lp.pair.totalSupply; + const userScore = + lp.pair.token0.id == tokenAddress + ? token0perUni * lp.liquidityTokenBalance + : token1perUni * lp.liquidityTokenBalance; + + const userAddress = getAddress(u.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScore; + }); + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/opium/examples.json b/Implementations/API/backend/utils/snapshot/strategies/opium/examples.json new file mode 100644 index 00000000..faed58e9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/opium/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "opium", + "strategy": { + "name": "opium", + "params": { + "symbol": "OPIUM", + "OPIUM": "0x888888888889c00c67689029d7856aac1065ec11", + "wOPIUM": "0x7a8d51b82b36fa5b50fb77001d6d189e920d2f75", + "LP_1INCH_OPIUM_ETH": "0x822e00a929f5a92f3565a16f92581e54af2b90ea", + "FARMING_LP_1INCH_OPIUM_ETH": "0x18d410f651289bb978fc32f90d2d7e608f4f4560" + } + }, + "network": "1", + "addresses": [ + "0xdbc2f7f3bccccf54f1bda43c57e8ab526e379df1", + "0x65402c084f79d698e17f32617f6c4198751dc5a0", + "0xfdde7ad56320b9cc691ad319a8fec2c8d6b993e6", + "0x6ef7e40b1a35fc336cdf54141e0f11ae004337c0" + ], + "snapshot": 11766053 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/opium/index.ts b/Implementations/API/backend/utils/snapshot/strategies/opium/index.ts new file mode 100644 index 00000000..28e47b78 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/opium/index.ts @@ -0,0 +1,139 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'alirun'; +export const version = '0.0.1'; + +const DECIMALS = 18; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Fetch OPIUM Balance + const opiumQuery = addresses.map((address: any) => [ + options.OPIUM, + 'balanceOf', + [address] + ]); + + // Fetch wOPIUM Balance + const wOpiumQuery = addresses.map((address: any) => [ + options.wOPIUM, + 'balanceOf', + [address] + ]); + + // Fetch 1inch LP token OPIUM-ETH balance + const lp1inchOpiumEthQuery = addresses.map((address: any) => [ + options.LP_1INCH_OPIUM_ETH, + 'balanceOf', + [address] + ]); + + // Fetch Farming 1inch LP token OPIUM-ETH balance + const farmingLp1inchOpiumEthQuery = addresses.map((address: any) => [ + options.FARMING_LP_1INCH_OPIUM_ETH, + 'balanceOf', + [address] + ]); + + const response = await multicall( + network, + provider, + abi, + [ + // Get 1inch LP OPIUM-ETH balance of OPIUM + [options.OPIUM, 'balanceOf', [options.LP_1INCH_OPIUM_ETH]], + // Get total supply of 1inch LP OPIUM-ETH + [options.LP_1INCH_OPIUM_ETH, 'totalSupply'], + ...opiumQuery, + ...wOpiumQuery, + ...lp1inchOpiumEthQuery, + ...farmingLp1inchOpiumEthQuery + ], + { blockTag } + ); + + const opiumLp1inchOpiumEth = response[0]; + const opiumLp1inchOpiumEthTotalSupply = response[1]; + const responseClean = response.slice(2, response.length); + + const chunks = chunk(responseClean, addresses.length); + const opiumBalances = chunks[0]; + const wOpiumBalances = chunks[1]; + const lp1inchOpiumEthBalances = chunks[2]; + const farmingLp1inchOpiumEthBalances = chunks[3]; + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => [ + addresses[i], + parseFloat( + formatUnits( + // OPIUM + opiumBalances[i][0] + // wOPIUM + .add(wOpiumBalances[i][0]) + // LP 1inch OPIUM-ETH + farming + .add( + opiumLp1inchOpiumEth[0] + .mul( + lp1inchOpiumEthBalances[i][0].add( + farmingLp1inchOpiumEthBalances[i][0] + ) + ) + .div(opiumLp1inchOpiumEthTotalSupply[0]) + ) + .toString(), + DECIMALS + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/README.md b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/README.md new file mode 100644 index 00000000..b86cb115 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/README.md @@ -0,0 +1,12 @@ +# orange-reputation-based-voting + +This strategy returns a voter's reputation score evaluated by a specified Orange Model + +Here is an example of parameters: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/examples.json new file mode 100644 index 00000000..c57bc4d6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "orange-reputation-based-voting", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI" + } + }, + "network": "1", + "addresses": [ + "0xd7539FCdC0aB79a7B688b04387cb128E75cb77Dc", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/index.ts new file mode 100644 index 00000000..6265b608 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-based-voting/index.ts @@ -0,0 +1,43 @@ +import fetch from 'cross-fetch'; + +export const author = 'orange-protocol'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const query = `{ + getBasedVotingStrategy( + addrs: ${JSON.stringify(addresses)}, + space: "${space}", + snapshot: "${snapshot}", + network: "${network}", + options: { address: "${options.address}", symbol: "${options.symbol}" } + ) + { address score } + }`; + const data = { + operationName: '', + query, + variables: {} + }; + const rawResponse = await fetch( + 'https://api.orangeprotocol.io/orange2c/query', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + } + ); + const content = await rawResponse.json(); + const list = content.data.getBasedVotingStrategy; + return Object.fromEntries(list.map((item) => [item.address, item.score])); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/README.md b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/README.md new file mode 100644 index 00000000..4a54ebfa --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/README.md @@ -0,0 +1,34 @@ +# orange-reputation-nft-based-voting + +This strategy distributes voting power based on user reputation scores represented by reputation NFTs they hold. You can ask users to claim an Orange issued NFT or [issue an NFT yourself](https://docs.orangeprotocol.io/developer-guides/issue-reputation-nfts). +Users must [claim reputation NFTs](https://docs.orangeprotocol.io/user-guides/claim-orange-nfts) on the [Orange platform](https://www.orangeprotocol.io/) to vote. + +## Use this strategy + +1. Issue an NFT or select one from [Orange issued NFTs](https://app.orangeprotocol.io/) (Crypto Whale, NFT Collector or Web3 Citizen). +2. In your Space settings, select "orange-reputation-nft-based-voting" as the voting strategy. +3. Select a blockchain network according to the NFT you choose. Orange supported networks are listed on each NFT page, for example, [Crypto Whale](https://app.orangeprotocol.io/nft/1). Make sure your users can claim NFTs on the selected network. +4. Fill in `symbol` and `contract` fields as the example below to indicate the NFT you select. +5. Specify the NFT name and the network in the proposal description so your users can easily find out. + +## Example of parameters + +| Param | Description | +| :------: | :-------------------------------------------------------------------------------------------------------- | +| symbol | `calcUserAsset` for [Crypto Whale](https://app.orangeprotocol.io/nft/1);
or `calcNFTAsset` for [NFT Collector](https://app.orangeprotocol.io/nft/2);
or `calcActivity` for [Web3 Citizen](https://app.orangeprotocol.io/nft/3);
or the method name your set for your issued NFT. | +| contract | NFT contract address.
Contract address of Orange issued NFTs:
`0xdc63554f403E281f30B6103a6355F08a34d4DeB8` | + +``` +{ + "symbol": "calcUserAsset", + "contract": "0xdc63554f403E281f30B6103a6355F08a34d4DeB8" +} +``` + +## Vote on a proposal + +Users of your space should follow these steps to vote: + +1. Read the proposal and confirm the NFT and network information. +2. Claim the required NFT on the Orange platform. +3. Vote on the proposal. diff --git a/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/examples.json new file mode 100644 index 00000000..37c706a3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "orange-reputation-nft-based-voting", + "params": { + "symbol": "calcUserAsset", + "contract": "0xdc63554f403E281f30B6103a6355F08a34d4DeB8" + } + }, + "network": "56", + "addresses": [ + "0x7ff4f8fe1dbbba8dc27103359bf96e97a4d44114", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0xd1a5b91957530e1b3e9cfac1543467c60c352f69", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 15511638 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/index.ts new file mode 100644 index 00000000..b43b7d14 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orange-reputation-nft-based-voting/index.ts @@ -0,0 +1,72 @@ +import { multicall } from '../../utils'; + +export const author = 'orange-protocol'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner) view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)', + 'function tokenProperty(uint256 tokenId) view returns (tuple(tuple(string dpdid, string dpTitle, string dpmethod, string dpmethodTitle, string apdid, string apTitle, string apmethod, string apmethodTitle, uint256 validDays, string image) category, uint256 score, uint256 validTo, address originOwner))' +]; + +export async function strategy( + space, + network, + provider, + addresses, + { contract, symbol }, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const runMultiCall = (calls: any[]) => + multicall(network, provider, abi, calls, { blockTag }); + + // Get nft count of user + const countResponse = await runMultiCall( + addresses.map((address: any) => [contract, 'balanceOf', [address]]) + ); + const countList: string[] = countResponse.map((item) => item.toString()); + + // Get id of user owned nft by index + const idCallList = countList.reduce((prev: any[], curr, index) => { + if (curr === '0') { + return prev; + } + const currAddressCalls = Array.from({ + length: Number(curr) + }).map((val, i) => [ + contract, + 'tokenOfOwnerByIndex', + [addresses[index], i] + ]); + return prev.concat(currAddressCalls); + }, []); + const idResponse = await runMultiCall(idCallList); + + // Get properties of every nft + const propertyCalls = idResponse.map((item) => [ + contract, + 'tokenProperty', + [Number(item)] + ]); + const propertyResponse = await runMultiCall(propertyCalls); + const now = Date.now(); + const nftList = propertyResponse + .map(([first]) => ({ + owner: first.originOwner, + score: first.score.toNumber(), + validTo: first.validTo.toString(), + apMethod: first.category.apmethod + })) + .filter((item) => item.apMethod === symbol && item.validTo * 1000 > now); + + return Object.fromEntries( + addresses.map((value) => [ + value, + nftList + .reverse() + .find((item) => item.owner.toUpperCase() === value.toUpperCase()) + ?.score || 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/README.md new file mode 100644 index 00000000..526e5f7d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/README.md @@ -0,0 +1,17 @@ +# orbs-network-delegation + +This strategy is based on the Orbs Delegation contract and its underlying logic. +It returns the net delegated ORBS stake in the requested network(chain) PoS, after accounting for delegators who vote for themselves. +A delegator who chooses to vote will override its delegate vote. The voter stake is counted once, either for a delegate or for a delegator. + +Here is an example of parameters (Orbs Delegation contract on Ethereum Mainnet): + +```json +{ + "address": "0xB97178870F39d4389210086E4BcaccACD715c71d", + "symbol": "ORBS", + "decimals": 18 +} +``` + +This strategy can be combined with `multichain` for cross-chain Orbs PoS scores. diff --git a/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/examples.json new file mode 100644 index 00000000..9e91df8a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/examples.json @@ -0,0 +1,49 @@ +[ + { + "name": "Example query 1", + "strategy": { + "name": "orbs-network-delegation", + "params": { + "address": "0xB97178870F39d4389210086E4BcaccACD715c71d", + "symbol": "ORBS", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xc5e624d6824e626a6f14457810e794e4603cfee2", + "0x63AEf7616882F488BCa97361d1c24F05B4657ae5", + "0x3D726623456e34e8a7F5567F6249EC4D72Cc3595", + "0x8DdB908c77ccc9cfde28dDF84311CB6fDF3f3125", + "0xca565ccb6434e0adf49b2d66df12b0046b013d7f", + "0x9520F53FD81C668E8088AE194C40E3F977B73D28", + "0x588C28C19E4185A2442C4C3DD9EBD592C61ECCB9", + "0xA478C2975AB1EA89E8196811F51A7B7ADE33EB11" + ], + "snapshot": 15210123 + }, + + { + "name": "Example query 2", + "strategy": { + "name": "orbs-network-delegation", + "params": { + "address": "0x513d30e66641cB1f2670b5994DD8E2B61ED3C23c", + "symbol": "ORBS", + "decimals": 18 + } + }, + "network": "137", + "addresses": [ + "0xc5e624d6824e626a6f14457810e794e4603cfee2", + "0x63aef7616882f488bca97361d1c24f05b4657ae5", + "0x3d726623456e34e8a7f5567f6249ec4d72cc3595", + "0x8ddb908c77ccc9cfde28ddf84311cb6fdf3f3125", + "0xca565ccb6434e0adf49b2d66df12b0046b013d7f", + "0x9520f53fd81c668e8088ae194c40e3f977b73d28", + "0x588c28c19e4185a2442c4c3dd9ebd592c61eccb9", + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11" + ], + "snapshot": 31123128 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/index.ts new file mode 100644 index 00000000..b0a9915f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orbs-network-delegation/index.ts @@ -0,0 +1,55 @@ +import { BigNumberish, BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'gadcl'; +export const version = '0.1.2'; + +const abi = [ + 'function getDelegatedStake(address addr) external view returns (uint256)', + 'function getDelegationInfo(address addr) external view returns (address delegation, uint256 delegatorStake)' +]; + +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) => + multi.call(address, options.address, 'getDelegatedStake', [address]) + ); + const delegations: Record = await multi.execute(); + + Object.entries(delegations) + .filter(([, delegatedStake]) => BigNumber.from(0).eq(delegatedStake)) + .forEach(([address]) => + multi.call(address, options.address, 'getDelegationInfo', [address]) + ); + const override: Record = + await multi.execute(); + + Object.entries(override).forEach( + ([address, [delegation, delegatorStake]]) => { + const from = getAddress(address); + const to = getAddress(delegation); + delegations[from] = delegatorStake; + if (delegations[to] && !override[to]) { + delegations[to] = BigNumber.from(delegations[to]).sub(delegatorStake); + } + } + ); + + return Object.fromEntries( + Object.entries(delegations).map(([address, delegatedStake]) => [ + getAddress(address), + parseFloat(formatUnits(delegatedStake, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/orca-pod/README.md b/Implementations/API/backend/utils/snapshot/strategies/orca-pod/README.md new file mode 100644 index 00000000..02b3f5be --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orca-pod/README.md @@ -0,0 +1,20 @@ +# orca-pod + +This strategy give 1 voting power for each holder of a specific tokenId of orca's ERC1155 + +## Parameters + +| Param Name | Description | +| ----------- | ----------- | +| id | TokenId of the pod | +| weight (optional) | Multiplier of the voting power - Default is `1` | + +Here is an example of the parameters: + +```json +{ + "symbol": "ORCA", + "id": "1", + "weight": 100 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/orca-pod/examples.json b/Implementations/API/backend/utils/snapshot/strategies/orca-pod/examples.json new file mode 100644 index 00000000..106705a2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orca-pod/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "orca-pod", + "params": { + "symbol": "ORCA", + "id": "1", + "weight": 100 + } + }, + "network": "1", + "addresses": ["0x094a473985464098b59660b37162a284b5132753"], + "snapshot": 15188155 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/orca-pod/index.ts b/Implementations/API/backend/utils/snapshot/strategies/orca-pod/index.ts new file mode 100644 index 00000000..d8320488 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orca-pod/index.ts @@ -0,0 +1,28 @@ +import { strategy as erc1155BalanceOfIdsWeightedStrategy } from '../erc1155-balance-of-ids-weighted'; + +export const author = 'snapshot-labs'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc1155BalanceOfIdsWeightedStrategy( + space, + network, + provider, + addresses, + { + address: '0x0762aa185b6ed2dca77945ebe92de705e0c37ae3', + ids: [options.id], + weight: parseFloat(options.weight || 1) + }, + snapshot + ); + + return Object.fromEntries(Object.entries(score).map((a) => [a[0], a[1]])); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/orca-pod/schema.json b/Implementations/API/backend/utils/snapshot/strategies/orca-pod/schema.json new file mode 100644 index 00000000..5c3787a4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/orca-pod/schema.json @@ -0,0 +1,30 @@ +{ + "$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. ORCA"], + "maxLength": 16 + }, + "id": { + "type": "string", + "title": "Token id", + "examples": ["1"] + }, + "weight": { + "type": "number", + "title": "Weight", + "examples": ["e.g. 100"] + } + }, + "required": ["id"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/README.md b/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/README.md new file mode 100644 index 00000000..828f3ef0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/README.md @@ -0,0 +1,33 @@ +# Otterspace Badges + +- A RAFT is an NFT that represents a community (or a DAO) within Otterspace. +- A RAFT has several badge specs under it. A badge spec is the design of a badge, which is essentially described as ERC721 metadata standard. +- Each BADGE is a non-transferable token built with [ERC4973]("https://github.com/otterspace-xyz/ERC4973") spec and maximally backwards compatible with ERC721. Each badge spec may have several badges associated which indicate the badges minted for that spec that is associated to a raft + +The parameters that must be specified when using this strategy are as follows + +- A RAFT token ID +- The RAFT's contract address +- An array of weights associated to the badge specs + +If no specs are specified, all badges under the RAFT are considered equal with the weight of 1 + +Here is an example of its usage: + +```json +{ + "symbol": "BADGES", + "raftTokenId": "1", + "raftAddress": "0xa6773847d3D2c8012C9cF62818b320eE278Ff722", + "specs": [ + { + "id": "bafyreicmofif36f2s4d2iv37gy532epnw7rwjf5rygdhzzh2iw6nmbunrq", + "weight": 5 + }, + { + "id": "bafyreihj2e3mxqwk24auglrfckk3eh2hzsq7eyghjpcprmopvok5wxz3bu", + "weight": 10 + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/examples.json b/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/examples.json new file mode 100644 index 00000000..e92a36fb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/examples.json @@ -0,0 +1,98 @@ +[ + { + "name": "Example query with specific badge specs", + "strategy": { + "name": "otterspace-badges", + "params": { + "symbol": "BADGES", + "raftTokenId": "1", + "specs": [ + { + "id": "bafyreicmofif36f2s4d2iv37gy532epnw7rwjf5rygdhzzh2iw6nmbunrq", + "weight": 5 + }, + { + "id": "bafyreihj2e3mxqwk24auglrfckk3eh2hzsq7eyghjpcprmopvok5wxz3bu", + "weight": 10 + }, + { + "id": "bafyreiavo63i7sgb5dvjcj6nxfwkxd7thq37jj43l6f4mkorgdogqj2nni", + "weight": 7 + } + ] + } + }, + "network": "5", + "addresses": [ + "0xbB9ECfD5685502977B5b7C1AC0cdff8C136A4Da8", + "0x33d0BBd05cf3C7E040d33bc1F338585A087e5Dd9", + "0x0F1c502d4EF5b1940a7A77062e51353D8B366547", + "0xEAde2cEeDeA3b8d0D1d10537f507b9d262870748" + ], + "snapshot": 7509588 + }, + { + "name": "Example query for all badge specs", + "strategy": { + "name": "otterspace-badges", + "params": { + "symbol": "BADGES", + "raftTokenId": "1", + "specs": [] + } + }, + "network": "5", + "addresses": [ + "0xbB9ECfD5685502977B5b7C1AC0cdff8C136A4Da8", + "0x33d0BBd05cf3C7E040d33bc1F338585A087e5Dd9", + "0x0F1c502d4EF5b1940a7A77062e51353D8B366547", + "0xEAde2cEeDeA3b8d0D1d10537f507b9d262870748" + ], + "snapshot": 7509588 + }, + { + "name": "Example query with specific badge specs", + "strategy": { + "name": "otterspace-badges", + "params": { + "symbol": "BADGES", + "raftTokenId": "1", + "specs": [ + { + "id": "bafyreibvclrxnfm5nrchvdqfog6t2usvlqiozwgxynrxwlgftl3ln7uxqe", + "weight": 5 + }, + { + "id": "bafyreicl3unvw6tvzjfduvrhxbfi74gsob6mpf6ekn3s2nkopqz2phtx7e", + "weight": 10 + } + ] + } + }, + "network": "10", + "addresses": [ + "0x76D84163bc0BbF58d6d3F2332f8A9c5B339dF983", + "0x73677662f66088236dDFc95DA42e405Cf3F4ce13", + "0x0F1c502d4EF5b1940a7A77062e51353D8B366547" + ], + "snapshot": 21528802 + }, + { + "name": "Example query for all badge specs", + "strategy": { + "name": "otterspace-badges", + "params": { + "symbol": "BADGES", + "raftTokenId": "1", + "specs": [] + } + }, + "network": "10", + "addresses": [ + "0x76D84163bc0BbF58d6d3F2332f8A9c5B339dF983", + "0x73677662f66088236dDFc95DA42e405Cf3F4ce13", + "0x0F1c502d4EF5b1940a7A77062e51353D8B366547" + ], + "snapshot": 21528802 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/index.ts b/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/index.ts new file mode 100644 index 00000000..e124d03c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/otterspace-badges/index.ts @@ -0,0 +1,131 @@ +import { error } from 'console'; +import { subgraphRequest } from '../../utils'; + +export const author = 'otterspace-xyz'; +export const version = '1.0.1'; + +const OTTERSPACE_SUBGRAPH_API_URLS_BY_CHAIN_ID = { + '1': 'https://api.thegraph.com/subgraphs/name/otterspace-xyz/badges-mainnet', + '5': 'https://api.thegraph.com/subgraphs/name/otterspace-xyz/badges-goerli', + '10': 'https://api.thegraph.com/subgraphs/name/otterspace-xyz/badges-optimism-alpha', + '420': + 'https://api.thegraph.com/subgraphs/name/otterspace-xyz/badges-optimism-goerli', + '137': 'https://api.thegraph.com/subgraphs/name/otterspace-xyz/badges-polygon' +}; + +function fetchBadgesForRaft( + network: string, + raftTokenId: string, + specIds: string[], + blockTag: number | string +): Promise { + const url = OTTERSPACE_SUBGRAPH_API_URLS_BY_CHAIN_ID[network]; + + if (url == undefined) { + throw new error(`Unsupported network with id: ${network}`); + } + + const specFilter: any = { + raft: `rafts:${raftTokenId}` + }; + if (specIds && specIds.length > 0) { + specFilter.id_in = specIds; + } + + const query = { + badges: { + __args: { + where: { + spec_: specFilter + }, + block: blockTag != 'latest' ? { number: blockTag } : null + }, + owner: { + id: true + }, + spec: { + id: true + } + } + }; + + return subgraphRequest(url, query); +} + +function getBadgeWeight(specs: any[], badgeSpecID: string): number { + let badgeWeight = 0; + + if (specs && specs.length > 0) { + const specConfig = specs.find((spec: any) => spec.id === badgeSpecID); + + badgeWeight = + specConfig && + isBadgeActive(specConfig.status, specConfig.metadata?.expiresAt || null) + ? specConfig.weight + : 0; + } else { + badgeWeight = 1; + } + + return badgeWeight; +} + +function isBadgeActive(status: string, expiresAt: string | null): boolean { + return !['REVOKED', 'BURNED'].includes(status) && expiresAt + ? Date.now() - Number(new Date(expiresAt)) <= 0 + : true; +} + +function applyBadgeWeights(badges: [], options: any) { + const badgeWeights = {}; + + badges.forEach((badge: any) => { + const badgeAddress = badge.owner.id.toLowerCase(); + + const badgeWeight = getBadgeWeight(options.specs, badge.spec.id); + + // picks the highest weight when multiple badges are held by the same address + if ( + !badgeWeights[badgeAddress] || + badgeWeight > badgeWeights[badgeAddress] + ) { + badgeWeights[badgeAddress] = badgeWeight; + } else { + return; + } + }); + + return badgeWeights; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const getBadgesResponse = await fetchBadgesForRaft( + network, + options.raftTokenId, + options.specs.map((spec) => spec.id), + blockTag + ); + + let badgeWeights = {}; + const badges = getBadgesResponse?.badges; + if (!badges) return badgeWeights; + + badgeWeights = applyBadgeWeights(badges, options); + + return Object.fromEntries( + addresses.map((address: string) => [ + address, + badgeWeights[address.toLowerCase()] || 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/pagination/examples.json b/Implementations/API/backend/utils/snapshot/strategies/pagination/examples.json new file mode 100644 index 00000000..83650de4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pagination/examples.json @@ -0,0 +1,40 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "pagination", + "params": { + "symbol": "DAI", + "strategy": { + "name": "erc20-balance-of", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "decimals": 18 + } + } + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/pagination/index.ts b/Implementations/API/backend/utils/snapshot/strategies/pagination/index.ts new file mode 100644 index 00000000..ea5a3b47 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pagination/index.ts @@ -0,0 +1,33 @@ +import strategies from '..'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const max = options.limit || 300; + const pages = Math.ceil(addresses.length / max); + const promises: any = []; + Array.from(Array(pages)).forEach((x, i) => { + const addressesInPage = addresses.slice(max * i, max * (i + 1)); + promises.push( + strategies[options.strategy.name].strategy( + space, + network, + provider, + addressesInPage, + options.strategy.params, + snapshot + ) + ); + }); + const results = await Promise.all(promises); + // @ts-ignore + return results.reduce((obj, result: any) => ({ ...obj, ...result }), {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/pancake/README.md b/Implementations/API/backend/utils/snapshot/strategies/pancake/README.md new file mode 100644 index 00000000..5a0b734f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pancake/README.md @@ -0,0 +1,27 @@ +# Contract call strategy + +Allows the tokens staked in chef conttracts to be used to calculate voter scores. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["pancake", { + "address": "0x009cF7bC57584b7998236eff51b98A168DceA9B0", + "chefAddresses": [ + { + "address": "0xFb1088Dae0f03C5123587d2babb3F307831E6367", + "decimals": 18 + }, + { + "address": "0x4086D46A650517fa756F620507dB704D3900Da07", + "decimals": 6 + } + ] + }] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/pancake/examples.json b/Implementations/API/backend/utils/snapshot/strategies/pancake/examples.json new file mode 100644 index 00000000..397121d4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pancake/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "pancake", + "params": { + "symbol": "DAI", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030" + ], + "snapshot": 13494433 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/pancake/index.ts b/Implementations/API/backend/utils/snapshot/strategies/pancake/index.ts new file mode 100644 index 00000000..31c9f11b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pancake/index.ts @@ -0,0 +1,129 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'pancake-swap'; +export const version = '0.0.1'; + +const sousChefabi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +const masterChefAbi = [ + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + }, + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +const masterChefContractAddress = '0x73feaa1eE314F8c655E354234017bE2193C9E24E'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const masterBalances = await multicall( + network, + provider, + masterChefAbi, + addresses.map((address: any) => [ + masterChefContractAddress, + 'userInfo', + ['0', address] + ]), + { blockTag } + ); + + const sousBalances = await Promise.all( + options.chefAddresses.map((item) => + multicall( + network, + provider, + sousChefabi, + addresses.map((address: any) => [ + item.address, + 'userInfo', + [address], + { blockTag } + ]), + { blockTag } + ) + ) + ); + + return Object.fromEntries( + Object.entries(score).map((address, index) => [ + address[0], + address[1] + + parseFloat(formatUnits(masterBalances[index].amount.toString(), 18)) + + sousBalances.reduce( + (prev: number, cur: any, idx: number) => + prev + + parseFloat( + formatUnits( + cur[index].amount.toString(), + options.chefAddresses[idx].decimals + ) + ), + 0 + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/README.md b/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/README.md new file mode 100644 index 00000000..cfbc3519 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/README.md @@ -0,0 +1,46 @@ +# Path holders, stakers and locked + +This strategy return the balances of the holders of $PATH tokens, both in staked and locked contracts +## Accepted options + +- **tokenAddress:** Token Address + +- **Staking Address:** Staking Address + +- **Locked Address:** Array of locked addresses + +- **methodABI** ABI method to get balance in locked contract (unclaimed tokens only) + +- **symbol** Token of ERC-20 + +- **decimals** Decimals for ERC-20 + +## Examples + +```JSON +[ + { + "name": "Example query", + "strategy": { + "name": "path-balance-staked-and-locked", + "params": { + "tokenAddress": "0x2a2550e0a75acec6d811ae3930732f7f3ad67588", + "stakingAddress": "0x0bF6eeA205F46d796458D090F3aA333149287854", + "lockedAddresses": [ + "0xDe48716A14C4CBc09656E21CE7F40FC1a02b3a25", + "0x67f0260254FB3Cee97dA18077927888Ed72D1f17", + "0xeE729DB66431e4401D63A38a2048d8CE0DF96eC3", + "0x8923f2A0465287E2F8564F85a7f62ed34d947594", + "0xfB05dc346f36eE88680e885CaC881983FAdEcBD4", + "0x9A319F461A84EA5b508AE1e97113fD1743ED691D" + ], + "methodABI": [ + "function getRemainingAmount(address _recipient) external view returns (uint256 amount)" + ], + "symbol": "PATH", + "decimals": 18 + } + } + } +] +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/examples.json b/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/examples.json new file mode 100644 index 00000000..2cad2b80 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/examples.json @@ -0,0 +1,47 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "path-balance-staked-and-locked", + "params": { + "tokenAddress": "0x2a2550e0a75acec6d811ae3930732f7f3ad67588", + "stakingAddress": "0x0bF6eeA205F46d796458D090F3aA333149287854", + "lockedAddresses": [ + "0xDe48716A14C4CBc09656E21CE7F40FC1a02b3a25", + "0x67f0260254FB3Cee97dA18077927888Ed72D1f17", + "0xeE729DB66431e4401D63A38a2048d8CE0DF96eC3", + "0x8923f2A0465287E2F8564F85a7f62ed34d947594", + "0xfB05dc346f36eE88680e885CaC881983FAdEcBD4", + "0x9A319F461A84EA5b508AE1e97113fD1743ED691D" + ], + "methodABI": [ + "function getRemainingAmount(address _recipient) external view returns (uint256 amount)" + ], + "symbol": "PATH", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x37672dda85f3cb8da4098baac5d84e00960cb081", + "0x1f31959dee51359ea01d3269acfd480f1746ec53", + "0xd54467dc24040f72fa163cbb491b4ab17c882781", + "0x7171dc086581b5ecf44c95bb31df10e9f1846571", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 13839820 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/index.ts b/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/index.ts new file mode 100644 index 00000000..c277a15f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/path-balance-staked-and-locked/index.ts @@ -0,0 +1,72 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'PathDAO'; +export const version = '0.1.0'; + +const constAbi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const lockedAbi = options.methodABI; + + const stakingPool = new Multicaller(network, provider, constAbi, { + blockTag + }); + const tokenPool = new Multicaller(network, provider, constAbi, { blockTag }); + const lockedPool = new Multicaller(network, provider, lockedAbi, { + blockTag + }); + + addresses.forEach((address) => { + stakingPool.call(address, options.stakingAddress, 'balanceOf', [address]); + tokenPool.call(address, options.tokenAddress, 'balanceOf', [address]); + }); + + addresses.forEach((address) => { + for (let i = 0; i < options.lockedAddresses.length; i++) { + lockedPool.call( + `locked_${i}.${address}`, + options.lockedAddresses[i], + 'getRemainingAmount', + [address] + ); + } + }); + + const [stakingResponse, tokenResponse]: [ + Record, + Record + ] = await Promise.all([stakingPool.execute(), tokenPool.execute()]); + + const lockedResponse = await lockedPool.execute(); + + return Object.fromEntries( + addresses.map((address) => { + const stakingCount = parseFloat( + formatUnits(stakingResponse[address], options.decimals) + ); + const tokenCount = parseFloat( + formatUnits(tokenResponse[address], options.decimals) + ); + let lockedCount = 0; + for (let i = 0; i < options.lockedAddresses.length; i++) { + const lockedMap = lockedResponse[`locked_${i}`]; + lockedCount += parseFloat( + formatUnits(lockedMap[address], options.decimals) + ); + } + return [address, stakingCount + tokenCount + lockedCount]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/README.md b/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/README.md new file mode 100644 index 00000000..4fff5bab --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/README.md @@ -0,0 +1,20 @@ +# pdn-balances-and-vests + +## Calculating Vested Tokens with Balances + +To find out the total number of vested tokens with balances, follow these steps: + +1. Invoke the `balanceOf` function for each address to get its token balance. +2. Use the `getVestLength` function to determine the number of vests held by that address. +3. Invoke the `getVestMetaData` function to retrieve metadata for each vest, including the amount vested. +4. Add the amount vested for each vest to the total balance for the corresponding address. + +Here is an example of parameters: + +```json +{ + "address": "0xdd0d06EC5dB655f76641bdA81Fec3221C167787e", + "symbol": "PDN", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/examples.json b/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/examples.json new file mode 100644 index 00000000..7426c231 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "PDN balances and vests", + "strategy": { + "name": "pdn-balances-and-vests", + "params": { + "address": "0xdd0d06EC5dB655f76641bdA81Fec3221C167787e", + "symbol": "PDN", + "decimals": 18 + } + }, + "network": "5", + "addresses": [ + "0xB6097b6932ad88D1159c10bA7D290ba05087507D", + "0x0a767592E4C4CbD5A65BAc08bd3c7112d68496A5", + "0x7db3c4099660a6f33bBfF63B3318CBf9b4D07743" + ], + "snapshot": 8458274 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/index.ts b/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/index.ts new file mode 100644 index 00000000..629141ef --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/index.ts @@ -0,0 +1,84 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'viganzeqiri'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function getVestLength(address _address) public view returns(uint)', + 'function getVestMetaData(uint _index, address _address) public view returns(uint, uint)' +]; + +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) => + multi.call(address, options.address, 'balanceOf', [address]) + ); + const balances: Record = await multi.execute(); + + addresses.forEach((address) => + multi.call(address, options.address, 'getVestLength', [address]) + ); + const addressesWithVestLength: Record = + await multi.execute(); + + const formatedAddressVests = Object.entries(addressesWithVestLength).reduce< + Record + >((acc, [addresses, vestLength]) => { + acc[addresses] = Number(vestLength); + return acc; + }, {}); + + Object.entries(formatedAddressVests).forEach(([address, vestLength]) => { + if (vestLength > 0) { + const vestIndexes = Array.from(Array(vestLength).keys()); + + vestIndexes.forEach((vestIndex) => { + multi.call( + `${address}-${vestIndex}`, + options.address, + 'getVestMetaData', + [vestIndex, address] + ); + }); + } + }); + + const vestsMetadata: Record = await multi.execute(); + + const metadataWithAccumulatedVests = Object.entries(vestsMetadata).reduce( + (acc, [key, value]) => { + const [address] = key.split('-'); + const amountVestes = value[0] || 0; + + acc[address] = + (acc[address] || 0) + + parseFloat(formatUnits(amountVestes, options.decimals)); + + return acc; + }, + {} + ); + + return Object.fromEntries( + Object.entries(balances).map(([address, balance]) => { + const totalBalance = + (!!metadataWithAccumulatedVests[address] + ? metadataWithAccumulatedVests[address] + : 0) + parseFloat(formatUnits(balance, options.decimals)); + + return [address, totalBalance]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/schema.json b/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/schema.json new file mode 100644 index 00000000..2113da8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pdn-balances-and-vests/schema.json @@ -0,0 +1,33 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/pepemon/examples.json b/Implementations/API/backend/utils/snapshot/strategies/pepemon/examples.json new file mode 100644 index 00000000..706a4203 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pepemon/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "PPBLZ, Uniswap and tokens+LP tokens staked on app ", + "strategy": { + "name": "pepemon", + "params": { + "tokenAddress": "0x4D2eE5DAe46C86DA2FF521F7657dad98834f97b8", + "uniswapAddress": "0x9479b62FD1CB36F8FEd1EEBb1Bb373d238d08216", + "stakingAddress": "0xf1F508c7C9f0d1b15a76fbA564eEf2d956220cf7", + "symbol": "PPBLZ", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xE71FbB197BC8fD11090FA657C100d52Dbb407662", + "0x2fe452D036014EEAdb1901C90c5BF227546Da43e", + "0xf653113e4b91aecca73d65e9745cab25bf7b9251", + "0xdaa3d460bfa9a20dc3de7ce4fd967617b25a3a51" + ], + "snapshot": "latest" + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/pepemon/index.ts b/Implementations/API/backend/utils/snapshot/strategies/pepemon/index.ts new file mode 100644 index 00000000..023683de --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pepemon/index.ts @@ -0,0 +1,164 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'pepemon'; +export const version = '0.1.0'; + +const tokenAbi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '_account', + type: 'address' + } + ], + name: 'getAddressPpblzStakeAmount', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '_account', + type: 'address' + } + ], + name: 'getAddressUniV2StakeAmount', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const res = await multicall( + network, + provider, + tokenAbi, + [ + [options.uniswapAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.uniswapAddress]] + ] + .concat( + addresses.map((address: any) => [ + options.tokenAddress, + 'balanceOf', + [address] + ]) + ) + .concat( + addresses.map((address: any) => [ + options.stakingAddress, + 'getAddressPpblzStakeAmount', + [address] + ]) + ) + .concat( + addresses.map((address: any) => [ + options.uniswapAddress, + 'balanceOf', + [address] + ]) + ) + .concat( + addresses.map((address: any) => [ + options.stakingAddress, + 'getAddressUniV2StakeAmount', + [address] + ]) + ), + { blockTag } + ); + + const totalSupply = res[0][0]; + const tokenBalanceInUni = res[1][0]; + + const p1 = res.slice(2, 2 + addresses.length); + const p2 = res.slice(2 + addresses.length, 2 + addresses.length * 2); + const p3 = res.slice(2 + addresses.length * 2, 2 + addresses.length * 3); + const p4 = res.slice(2 + addresses.length * 3, 2 + addresses.length * 4); + + return Object.fromEntries( + p1.map((values, i) => [ + addresses[i], + //ppblz_, uniV2PoolTokens + parseFloat(formatUnits(p1[i][0].toString(), 18)) + + parseFloat(formatUnits(p2[i][0].toString(), 18)) + + parseFloat( + formatUnits( + p3[i][0].mul(tokenBalanceInUni).div(totalSupply).toString(), + 18 + ) + ) + + parseFloat( + formatUnits( + p4[i][0].mul(tokenBalanceInUni).div(totalSupply).toString(), + 18 + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/piedao/examples.json b/Implementations/API/backend/utils/snapshot/strategies/piedao/examples.json new file mode 100644 index 00000000..40b626a1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/piedao/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "piedao", + "strategy": { + "name": "piedao", + "params": { + "symbol": "PIE", + "BPT": "0xFAE2809935233d4BfE8a56c2355c4A2e7d1fFf1A", + "doughv1": "0x5f5e9ed11344dadc3a08688e5f17e23f6a99bf81", + "doughv2": "0xad32A8e6220741182940c5aBF610bDE99E737b2D", + "stakedDough": "0xB9a4Bca06F14A982fcD14907D31DFACaDC8ff88E", + "eDOUGH": "0x63cbd1858bd79de1a06c3c26462db360b834912d", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x66827bcd635f2bb1779d68c46aeb16541bca6ba8", + "0x635b230c3fdf6a466bb6dc3b9b51a8ceb0659b67" + ], + "snapshot": 11350000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/piedao/index.ts b/Implementations/API/backend/utils/snapshot/strategies/piedao/index.ts new file mode 100644 index 00000000..8c4bdc1b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/piedao/index.ts @@ -0,0 +1,136 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'alexintosh'; +export const version = '0.0.1'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const doughv1Query = addresses.map((address: any) => [ + options.doughv1, + 'balanceOf', + [address] + ]); + + const doughv2Query = addresses.map((address: any) => [ + options.doughv2, + 'balanceOf', + [address] + ]); + + const eDOUGHQuery = addresses.map((address: any) => [ + options.eDOUGH, + 'balanceOf', + [address] + ]); + + const stakedDoughQuery = addresses.map((address: any) => [ + options.stakedDough, + 'balanceOf', + [address] + ]); + + const lpDoughQuery = addresses.map((address: any) => [ + options.BPT, + 'balanceOf', + [address] + ]); + + const response = await multicall( + network, + provider, + abi, + [ + [options.doughv2, 'balanceOf', [options.BPT]], + [options.BPT, 'totalSupply'], + ...doughv1Query, + ...doughv2Query, + ...eDOUGHQuery, + ...stakedDoughQuery, + ...lpDoughQuery + ], + { blockTag } + ); + + const doughv2BPT = response[0]; + const doughv2BptTotalSupply = response[1]; + const responseClean = response.slice(2, response.length); + + const chunks = chunk(responseClean, addresses.length); + const doughv1Balances = chunks[0]; + const doughv2Balances = chunks[1]; + const eDOUGHBalances = chunks[2]; + const stakedDoughBalances = chunks[3]; + const lpDoughBalances = chunks[4]; + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => [ + addresses[i], + parseFloat( + formatUnits( + doughv2BPT[0] + .mul(stakedDoughBalances[i][0]) + .div(doughv2BptTotalSupply[0]) + .add( + doughv2BPT[0] + .mul(lpDoughBalances[i][0]) + .div(doughv2BptTotalSupply[0]) + ) + .add(doughv1Balances[i][0]) + .add(doughv2Balances[i][0]) + .add(eDOUGHBalances[i][0]) + .toString(), + options.decimals + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/planet-finance-v2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/planet-finance-v2/examples.json new file mode 100644 index 00000000..bfefc746 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/planet-finance-v2/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "planet-finance-v2", + "params": { + "address": "0x72B7D61E8fC8cF971960DD9cfA59B8C829D91991", + "symbol": "AQUA", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0xc358615Fb090Db0284a73Ac690B264BFDCa141f0", + "0x19d4051F7740e6AA4494EBCc655a70f524878346", + "0xa14EdA7b66Fa8B030905A57D97Da57553c56Ec0D" + ], + "snapshot": 22001625 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/planet-finance-v2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/planet-finance-v2/index.ts new file mode 100644 index 00000000..637fe092 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/planet-finance-v2/index.ts @@ -0,0 +1,150 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { Multicaller } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'defininja'; +export const version = '0.0.2'; + +const planetFinanceFarmAbi = [ + 'function userInfo(uint256, address) view returns (uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256)' +]; + +const bep20Abi: any = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address) view returns (uint256)' +]; + +const aquaInfinityAbi = [ + 'function getUserGtokenBal(address) view returns (uint256)' +]; + +const aquaLendingAbi = [ + 'function getAccountSnapshot(address) view returns (uint256,uint256,uint256,uint256)' +]; + +const gammaFarmAddress = '0x9EBce8B8d535247b2a0dfC0494Bc8aeEd7640cF9'; + +const aquaAddress = '0x72B7D61E8fC8cF971960DD9cfA59B8C829D91991'; + +const aquaBnbLpTokenAddress = '0x03028D2F8B275695A1c6AFB69A4765e3666e36d9'; + +const aquaLendingAddress = '0x2f5d7A9D8D32c16e41aF811744DB9f15d853E0A5'; + +const aquaInfinityAddress = '0xddd0626BB795BdF9CfA925da5102eFA5E7008114'; + +const increase_in_voting = 5; //increase 5 times + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const erc20Multi = new Multicaller(network, provider, bep20Abi, { + blockTag + }); + + // returns user's aqua balance ofr their address + let score: any = erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + //returns user's shares in aqua auto comp vault + let usergAquaBalInAquaInfinityVault: any = multicall( + network, + provider, + aquaInfinityAbi, + addresses.map((address: any) => [ + aquaInfinityAddress, + 'getUserGtokenBal', + [address] + ]), + { blockTag } + ); + + // returns user's aqua balance in aqua-bnb pool + let usersNewAquaBnbVaultBalances: any = multicall( + network, + provider, + planetFinanceFarmAbi, + addresses.map((address: any) => [ + gammaFarmAddress, + 'userInfo', + ['2', address] + ]), + { blockTag } + ); + + //AQUA LENDING + let usersAquaInLending: any = multicall( + network, + provider, + aquaLendingAbi, + addresses.map((address: any) => [ + aquaLendingAddress, + 'getAccountSnapshot', + [address] + ]), + { blockTag } + ); + + const result = await Promise.all([ + score, + usergAquaBalInAquaInfinityVault, + usersNewAquaBnbVaultBalances, + usersAquaInLending + ]); + + score = result[0]; + usergAquaBalInAquaInfinityVault = result[1]; + usersNewAquaBnbVaultBalances = result[2]; + usersAquaInLending = result[3]; + + //AQUA-BNB + // total supply of aqua bnb lp token + erc20Multi.call('aquaBnbTotalSupply', aquaBnbLpTokenAddress, 'totalSupply'); + + // aqua balance of aqua bnb lp + erc20Multi.call('aquaBnbAquaBal', aquaAddress, 'balanceOf', [ + aquaBnbLpTokenAddress + ]); + + const erc20Result = await erc20Multi.execute(); + + const totalSupply = erc20Result.aquaBnbTotalSupply.toString(); + + const contractAquaBalance = erc20Result.aquaBnbAquaBal.toString(); + + const res = Object.fromEntries( + Object.entries(score).map((address: any, index) => { + return [ + address[0], + + address[1] + + (parseFloat( + formatUnits(usersNewAquaBnbVaultBalances[index]['0'].toString(), 18) + ) / + parseFloat(formatUnits(totalSupply, 18))) * + parseFloat(formatUnits(contractAquaBalance, 18)) + + parseFloat( + formatUnits(usergAquaBalInAquaInfinityVault[index].toString(), 18) + ) * + parseFloat(formatUnits(usersAquaInLending[index]['3'], 18)) * + increase_in_voting + + parseFloat(formatUnits(usersAquaInLending[index]['1'], 18)) * + parseFloat(formatUnits(usersAquaInLending[index]['3'], 18)) + ]; + }) + ); + return res; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/planet-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/planet-finance/examples.json new file mode 100644 index 00000000..a1d317dc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/planet-finance/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "planet-finance", + "params": { + "address": "0x72B7D61E8fC8cF971960DD9cfA59B8C829D91991", + "symbol": "AQUA", + "decimals": 18 + } + }, + "network": "56", + "addresses": ["0x3BDaDCbF17e24B2157d920E25407FF8cd4f5F54C"], + "snapshot": 13267571 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/planet-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/planet-finance/index.ts new file mode 100644 index 00000000..6b3e0c18 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/planet-finance/index.ts @@ -0,0 +1,221 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { Multicaller } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'planet-finance'; +export const version = '0.0.1'; + +const planetFinanceFarmAbi = [ + 'function poolInfo(uint256) returns (address want,uint256 allocPoint,uint256 lastRewardBlock,uint256 accAQUAPerShare,address strat)', + 'function stakedWantTokens(uint256 _pid, address _user) returns (uint256)' +]; + +const bep20Abi: any = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address) view returns (uint256)' +]; + +const aquaAutoCompAbi = [ + 'function balanceOf() view returns (uint256)', + 'function totalShares() view returns (uint256)', + 'function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime , uint256 cakeAtLastUserAction , uint256 lastUserActionTime)' +]; + +const aquaLendingAbi = [ + 'function getAccountSnapshot(address) view returns (uint256,uint256,uint256,uint256)' +]; + +const planetFinanceFarmContractAddress = + '0x0ac58Fd25f334975b1B61732CF79564b6200A933'; + +const gammaFarmAddress = '0xB87F7016585510505478D1d160BDf76c1f41b53d'; + +const aquaAutoCompPoolAddress = '0x8A53dAdF2564d030b41dB1c04fB3c4998dC1326e'; + +const aquaAddress = '0x72B7D61E8fC8cF971960DD9cfA59B8C829D91991'; + +const aquaBnbLpTokenAddress = '0x03028D2F8B275695A1c6AFB69A4765e3666e36d9'; + +const aquaGammaLpTokenAddress = '0xcCaF3fcE9f2D7A7031e049EcC65c0C0Cc331EE0D'; + +const aquaLendingAddress = '0xb7eD4A5AF620B52022fb26035C565277035d4FD7'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const erc20Multi = new Multicaller(network, provider, bep20Abi, { + blockTag + }); + + const autoCompMulti = new Multicaller(network, provider, aquaAutoCompAbi, { + blockTag + }); + + // returns user's aqua balance ofr their address + let score: any = erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + // returns user's aqua balance in aqua only vault + let usersAquaVaultBalances: any = multicall( + network, + provider, + planetFinanceFarmAbi, + addresses.map((address: any) => [ + planetFinanceFarmContractAddress, + 'stakedWantTokens', + ['1', address] + ]), + { blockTag } + ); + + //returns user's shares in aqua auto comp vault + let usersAquaAutoCompVaultBalances: any = multicall( + network, + provider, + aquaAutoCompAbi, + addresses.map((address: any) => [ + aquaAutoCompPoolAddress, + 'userInfo', + [address] + ]), + { blockTag } + ); + + // returns user's aqua balance in aqua-bnb vault + let usersAquaGammaVaultBalances: any = multicall( + network, + provider, + planetFinanceFarmAbi, + addresses.map((address: any) => [ + gammaFarmAddress, + 'stakedWantTokens', + ['0', address] + ]), + { blockTag } + ); + + // returns user's aqua balance in aqua-bnb vault + let usersNewAquaBnbVaultBalances: any = multicall( + network, + provider, + planetFinanceFarmAbi, + addresses.map((address: any) => [ + gammaFarmAddress, + 'stakedWantTokens', + ['1', address] + ]), + { blockTag } + ); + + //AQUA LENDING + let usersAquaInLending: any = multicall( + network, + provider, + aquaLendingAbi, + addresses.map((address: any) => [ + aquaLendingAddress, + 'getAccountSnapshot', + [address] + ]), + { blockTag } + ); + + const result = await Promise.all([ + score, + usersAquaVaultBalances, + usersAquaAutoCompVaultBalances, + usersAquaGammaVaultBalances, + usersNewAquaBnbVaultBalances, + usersAquaInLending + ]); + + score = result[0]; + usersAquaVaultBalances = result[1]; + usersAquaAutoCompVaultBalances = result[2]; + usersAquaGammaVaultBalances = result[3]; + usersNewAquaBnbVaultBalances = result[4]; + usersAquaInLending = result[5]; + + //AQUA-BNB + erc20Multi.call('aquaBnbTotalSupply', aquaBnbLpTokenAddress, 'totalSupply'); + + erc20Multi.call('aquaBnbAquaBal', aquaAddress, 'balanceOf', [ + aquaBnbLpTokenAddress + ]); + + //AQUA-GAMMA + erc20Multi.call( + 'aquaGammaTotalSupply', + aquaGammaLpTokenAddress, + 'totalSupply' + ); + + erc20Multi.call('aquaGammaAquaBal', aquaAddress, 'balanceOf', [ + aquaGammaLpTokenAddress + ]); + + const erc20Result = await erc20Multi.execute(); + + const totalSupply = erc20Result.aquaBnbTotalSupply.toString(); + + const contractAquaBalance = erc20Result.aquaBnbAquaBal.toString(); + + const totalSupplyAquaGamma = erc20Result.aquaGammaTotalSupply.toString(); + + const aquaGammaContractAquaBalance = erc20Result.aquaGammaAquaBal.toString(); + + //AQUA AUTO COMPOUNDING + autoCompMulti.call('aquaBalance', aquaAutoCompPoolAddress, 'balanceOf'); + autoCompMulti.call('totalShares', aquaAutoCompPoolAddress, 'totalShares'); + + const autoCompResult = await autoCompMulti.execute(); + + let aquaBalance = autoCompResult.aquaBalance.toString(); + aquaBalance = parseFloat(formatUnits(aquaBalance, 18)); + + let totalShares = autoCompResult.totalShares.toString(); + totalShares = parseFloat(formatUnits(totalShares, 18)); + + return Object.fromEntries( + Object.entries(score).map((address: any, index) => [ + address[0], + + address[1] + + parseFloat(formatUnits(usersAquaVaultBalances[index].toString(), 18)) + + (parseFloat( + formatUnits(usersNewAquaBnbVaultBalances[index].toString(), 18) + ) / + parseFloat(formatUnits(totalSupply, 18))) * + parseFloat(formatUnits(contractAquaBalance, 18)) + + (parseFloat( + formatUnits(usersAquaGammaVaultBalances[index].toString(), 18) + ) / + parseFloat(formatUnits(totalSupplyAquaGamma, 18))) * + parseFloat(formatUnits(aquaGammaContractAquaBalance, 18)) + + (parseFloat( + formatUnits( + usersAquaAutoCompVaultBalances[index]['shares'].toString(), + 18 + ) + ) / + totalShares) * + aquaBalance + + parseFloat(formatUnits(usersAquaInLending[index]['1'], 18)) * + parseFloat(formatUnits(usersAquaInLending[index]['3'], 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/README.md b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/README.md new file mode 100644 index 00000000..83d8344a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/README.md @@ -0,0 +1,17 @@ +# POAP (erc721) eventId with weight + +Each POAP is implemented as an erc721 with a max supply tokens. + +This strategy weights the vote with a specific ERC721 NFT with a given EventId according to the holdings of each POAP and relative scarcity of each NFT. + +Here is an example of parameters: + +```json +{ + "symbol": "POAP", + "eventIds": [ + { "id": "1213", "weight": 10 }, + { "id": "1293", "weight": 1 } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/examples.json new file mode 100644 index 00000000..5cdcc7a1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "poap-with-weight-v2", + "params": { + "symbol": "POAP", + "eventIds": [ + { "id": "1213", "weight": 10 }, + { "id": "1293", "weight": 1 } + ] + } + }, + "network": "1", + "addresses": [ + "0xab6f255dac71103ea6d57b320eaf0eec901b05aa", + "0x878aac6eeaf3e3207d11723b820d6eb1105fa892", + "0x837d21cfda71e93e5257f95ce2c49751675ebcb1" + ], + "snapshot": 13040844 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/index.ts new file mode 100644 index 00000000..74cc8a4a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight-v2/index.ts @@ -0,0 +1,94 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'gawainb'; +export const version = '2.1.0'; + +const POAP_API_ENDPOINT_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/poap-xyz/poap', + '100': 'https://api.thegraph.com/subgraphs/name/poap-xyz/poap-xdai' +}; +// subgraph query in filter has max length of 500 +const EVENT_IDS_LIMIT = 500; +const MAX_TOKENS_PER_PAGE = 1000; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + if (options.eventIds.length > EVENT_IDS_LIMIT) { + throw new Error(`Max number (${EVENT_IDS_LIMIT}) of event ids exceeded`); + } + + const eventIds = options.eventIds.map((eventId) => eventId.id); + const addressesMap = addresses.reduce((map, address) => { + map[getAddress(address)] = 0; + return map; + }, {}); + + const query = { + tokens: { + __args: { + where: { + event_: { + id_in: eventIds + }, + owner_in: addresses.map((a) => a.toLowerCase()) + }, + first: MAX_TOKENS_PER_PAGE, + skip: 0 + }, + event: { + id: true, + tokenCount: true + }, + id: true, + owner: { + id: true + } + } + }; + if (snapshot !== 'latest') { + query.tokens.__args['block'] = { number: snapshot }; + } + + while (true) { + const supplyResponse = await subgraphRequest( + POAP_API_ENDPOINT_URL[network], + query + ); + + if (supplyResponse && supplyResponse.tokens) { + const eventIdsWeightMap = options.eventIds.reduce( + (map, { id, weight }) => { + map[id] = weight; + return map; + }, + {} + ); + + supplyResponse.tokens.forEach((token) => { + const tokenOwnerId = getAddress(token.owner.id); + + if (addressesMap[tokenOwnerId] === undefined) return; + + addressesMap[tokenOwnerId] += + eventIdsWeightMap[token.event.id] * parseInt(token.event.tokenCount); + }); + } + + // if the number of tokens received is less than the max per page, + // then we have received all the tokens and can stop making requests + if (supplyResponse?.tokens?.length < MAX_TOKENS_PER_PAGE) { + break; + } + + query.tokens.__args.skip += MAX_TOKENS_PER_PAGE; + } + + return addressesMap; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/README.md b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/README.md new file mode 100644 index 00000000..a7cdf5e3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/README.md @@ -0,0 +1,18 @@ +# POAP (erc721) tokenid with weight + +Each POAP is implemented as an erc721 with a max supply tokens. + +This strategy weights the vote with a specific ERC721 NFT with a given TokenId according to the holdings of each POAP and relative scarcity of each NFT. + +Here is an example of parameters: + +```json +{ + "symbol": "POAP", + "tokenIds": [ + {"id":"100001", "weight": 100}, + {"id":"100002", "weight": 10}, + {"id":"1000", "weight": 1} + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/examples.json b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/examples.json new file mode 100644 index 00000000..dff8fd53 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "poap-with-weight", + "params": { + "symbol": "POAP", + "tokenIds": [ + { "id": "100001", "weight": 100 }, + { "id": "100002", "weight": 10 }, + { "id": "1000", "weight": 1 } + ] + } + }, + "network": "1", + "addresses": [ + "0x837d21cfda71e93e5257f95ce2c49751675ebcb1", + "0x00ac36c51500e900ab0f4e692fc1338cf70571b2", + "0xdd6f702c2907ce401888d993d7dc185e7a824466" + ], + "snapshot": 13040844 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/index.ts b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/index.ts new file mode 100644 index 00000000..3fca2933 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap-with-weight/index.ts @@ -0,0 +1,70 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; +import examplesFile from './examples.json'; + +export const author = 'gawainb'; +export const version = '1.1.0'; +export const examples = examplesFile; + +const POAP_API_ENDPOINT_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/poap-xyz/poap', + '100': 'https://api.thegraph.com/subgraphs/name/poap-xyz/poap-xdai' +}; + +const getTokenSupply = { + tokens: { + __args: { + block: undefined as undefined | { number: number }, + where: { + id_in: undefined + } + }, + event: { + tokenCount: true + }, + id: true, + owner: { + id: true + } + } +}; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const addressesMap = addresses.reduce((map, address) => { + map[getAddress(address)] = 0; + return map; + }, {}); + // Set TokenIds as arguments for GQL query + getTokenSupply.tokens.__args.where.id_in = options.tokenIds.map( + (token) => token.id + ); + if (snapshot !== 'latest') { + getTokenSupply.tokens.__args.block = { number: snapshot }; + } + const supplyResponse = await subgraphRequest( + POAP_API_ENDPOINT_URL[network], + getTokenSupply + ); + + if (supplyResponse && supplyResponse.tokens) { + const tokenIdsWeightMap = options.tokenIds.reduce((map, { id, weight }) => { + map[id] = weight; + return map; + }, {}); + supplyResponse.tokens.forEach((token: any) => { + const checksumAddress = getAddress(token.owner.id); + if (!addressesMap[checksumAddress]) addressesMap[checksumAddress] = 0; + addressesMap[checksumAddress] += + tokenIdsWeightMap[token.id] * parseInt(token.event.tokenCount); + }); + } + + return addressesMap; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap/README.md b/Implementations/API/backend/utils/snapshot/strategies/poap/README.md new file mode 100644 index 00000000..7b484eb2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap/README.md @@ -0,0 +1,14 @@ +# POAP (erc721) + +Each POAP is implemented as an erc721 with a max supply tokens. + +If no `eventIds` are passed, then this strategy returns the number of tokens owned by each account. Otherwise, it returns the number of tokens per account where the event id is included in `eventIds`. + +Here are some examples of parameters: + +```json +{ + "symbol": "POAP", + "eventIds": ["1213", "1293"] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/poap/examples.json new file mode 100644 index 00000000..e790ab40 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "poap", + "params": { + "symbol": "POAP", + "eventIds": ["1213", "1293"] + } + }, + "network": "100", + "addresses": [ + "0x837d21cfda71e93e5257f95ce2c49751675ebcb1", + "0x00ac36c51500e900ab0f4e692fc1338cf70571b2", + "0xdd6f702c2907ce401888d993d7dc185e7a824466" + ], + "snapshot": 25283078 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/poap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/poap/index.ts new file mode 100644 index 00000000..f088b83d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/poap/index.ts @@ -0,0 +1,105 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; +import { strategy as erc721Strategy } from '../erc721'; + +export const author = 'greenealexander'; +export const version = '1.2.0'; + +// subgraph query in filter has max length of 500 +const EVENT_IDS_LIMIT = 500; +const POAP_API_ENDPOINT_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/poap-xyz/poap', + '100': 'https://api.thegraph.com/subgraphs/name/poap-xyz/poap-xdai' +}; +// subgraph query in filter has max length of 500 +const MAX_ACCOUNTS_IN_QUERY = 500; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const shouldFilterForEvents = (options?.eventIds?.length ?? 0) > 0; + + if (!shouldFilterForEvents) { + const results = await erc721Strategy( + space, + network, + provider, + addresses, + { address: '0x22C1f6050E56d2876009903609a2cC3fEf83B415' }, + snapshot + ); + return addresses.reduce((map, address) => { + map[getAddress(address)] = results[address] ?? 0; + return map; + }, {}); + } + + if (shouldFilterForEvents && options.eventIds.length > EVENT_IDS_LIMIT) { + throw new Error(`Max number (${EVENT_IDS_LIMIT}) of event ids exceeded`); + } + + const addressesMap = addresses.reduce((map, address) => { + map[getAddress(address)] = 0; + return map; + }, {}); + const lowercaseAddresses = Object.keys(addressesMap).map((address) => + address.toLowerCase() + ); + + // batch addresses to query into slices of MAX_ACCOUNTS_IN_QUERY size + const lowercaseAddressBatches: string[][] = []; + for (let i = 0; i < lowercaseAddresses.length; i += MAX_ACCOUNTS_IN_QUERY) { + const slice = lowercaseAddresses.slice(i, i + MAX_ACCOUNTS_IN_QUERY); + lowercaseAddressBatches.push(slice); + } + + const query = { + accounts: { + __args: { + where: { + id_in: [] as string[] + } + }, + id: true, + tokens: { + __args: { + where: { + event_in: options.eventIds + } + }, + id: true + } + } + }; + if (snapshot !== 'latest') { + query.accounts.__args['block'] = { number: snapshot }; + } + + const results = await Promise.allSettled<{ + accounts: { id: string; tokens?: { id: string }[] }[]; + }>( + lowercaseAddressBatches.map((addresses) => { + query.accounts.__args.where.id_in = addresses; + return subgraphRequest(POAP_API_ENDPOINT_URL[network], query); + }) + ); + + for (const supplyResponse of results) { + if (supplyResponse.status === 'rejected') continue; + + for (const account of supplyResponse.value.accounts) { + const accountId = getAddress(account.id); + + if (addressesMap[accountId] === undefined) continue; + + addressesMap[accountId] = account.tokens?.length ?? 0; + } + } + + return addressesMap; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/pob-hash/examples.json b/Implementations/API/backend/utils/snapshot/strategies/pob-hash/examples.json new file mode 100644 index 00000000..9f63f709 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pob-hash/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "pob-hash", + "params": { + "symbol": "POB" + } + }, + "network": "1", + "addresses": [ + "0x0154d25120ed20a516fe43991702e7463c5a6f6e", + "0x0B7056e2D9064f2ec8647F1ae556BAcc06da6Db4", + "0xcc5Ddc8CCD5B1E90Bc42F998ec864Ead0090A12B", + "0x000000000000000000000000000000000000dead", + "0x721931508df2764fd4f70c53da646cb8aed16ace" + ], + "snapshot": 12208247 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/pob-hash/index.ts b/Implementations/API/backend/utils/snapshot/strategies/pob-hash/index.ts new file mode 100644 index 00000000..9931089a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pob-hash/index.ts @@ -0,0 +1,57 @@ +import { subgraphRequest } from '../../utils'; + +export const author = 'dave4506'; +export const version = '0.1.1'; + +export const SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/proofofbeauty/hash' +}; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + // ran into issues where returning the score map in lowercase wouldn't match the connected addresses hex string + const lowerCasedAddressToOriginalAddressMap = Object.fromEntries( + new Map(addresses.map((a) => [a.toLowerCase(), a])) + ); + const hashOwnersParams = { + hashOwners: { + __args: { + where: { + id_in: Object.keys(lowerCasedAddressToOriginalAddressMap) + }, + first: 1000 // IS THIS ENOUGH? + }, + id: true, + totalQuantity: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + hashOwnersParams.hashOwners.__args.block = { number: snapshot }; + } + + const result = await subgraphRequest(SUBGRAPH_URL[network], hashOwnersParams); + + const scoresMap: { [key: string]: number } = {}; + + if (result && result.hashOwners) { + result.hashOwners.forEach((ow) => { + const id = lowerCasedAddressToOriginalAddressMap[ow.id]; + if (scoresMap[id] == undefined) scoresMap[id] = 0; + scoresMap[id] = scoresMap[id] + parseInt(ow.totalQuantity); + }); + } else { + console.error('Subgraph request failed'); + } + const scores: [string, number][] = Object.entries(scoresMap).map( + ([address, score]) => [address, score] + ); + + return Object.fromEntries(scores); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/pod-leader/README.md b/Implementations/API/backend/utils/snapshot/strategies/pod-leader/README.md new file mode 100644 index 00000000..0ac19392 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pod-leader/README.md @@ -0,0 +1,36 @@ +# PodLeader pool balance + +Calculates the balance of either a uniswap pair in a Yield Yak/Pod leader style farm, or the amount of each individual token in that LP which is deposited into the farm. For example, if an LP pair has 100 token1 and 200 token2, one can isolate token2 for snapshot votes. One can also give weights to each of these tokens when combined with other strategies. + +## Accepted options: + +- chefAddress: Masterchef contract address +- pid: Mastechef pool id (starting with zero) + +- uniPairAddress: Address of a uniswap pair (or a sushi pair or any other with the same interface) + - If the uniPairAddress option is provided, converts staked LP token balance to base token balance + (based on the pair total supply and base token reserve) + - If uniPairAddress is null or undefined, returns staked token balance of the pool + +- tokenAddress: Address of a token for single token Pools. + - if the uniPairAddress is provided the tokenAddress is ignored. + +- weight: Integer multiplier of the result (for combining strategies with different weights, totally optional) +- weightDecimals: Integer value of number of decimal places to apply to the final result + +- token0.address: Address of the uniPair token 0. If defined, the strategy will return the result for the token0. + +- token0.weight: Integer multiplier of the result for token0 +- token0.weightDecimals: Integer value of number of decimal places to apply to the result of token0 + +- token1.address: Address of the uniPair token 1. If defined, the strategy will return the result for the token1. + +- token1.weight: Integer multiplier of the result for token1 +- token1.weightDecimal: Integer value of number of decimal places to apply to the result of token1 + + +- log: Boolean flag to enable or disable logging to the console (used for debugging purposes during development) + + + +Check the examples.json file for how to use the options. diff --git a/Implementations/API/backend/utils/snapshot/strategies/pod-leader/examples.json b/Implementations/API/backend/utils/snapshot/strategies/pod-leader/examples.json new file mode 100644 index 00000000..67d0c657 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pod-leader/examples.json @@ -0,0 +1,63 @@ +[ + { + "name": "Example query - Count of tokens in single token Pool", + "strategy": { + "name": "pod-leader", + "params": { + "symbol": "STAKE", + "chefAddress": "0x111E1E97435b57467E79d4930acc4B7EB3d478ad", + "uniPairAddress": "0x73e6CB72a79dEa7ed75EF5eD6f8cFf86C9128eF5", + "token0": { + "address": "0x8B1d98A91F853218ddbb066F20b8c63E782e2430", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "2", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "43114", + "addresses": ["0x9F8A5B35f5508071cf2304A670EAB0803F3737aa"], + "snapshot": 5475221 + }, + { + "name": "Example query - Count of tokens in single token Pool", + "strategy": { + "name": "pod-leader", + "params": { + "chefAddress": "0x111E1E97435b57467E79d4930acc4B7EB3d478ad", + "uniPairAddress": "0x1a9bd67c82c0e8e47c3ad2fa772fcb9b7a831a37", + "token1": { + "address": "0x8B1d98A91F853218ddbb066F20b8c63E782e2430", + "weight": 1, + "weightDecimals": 0 + }, + "pid": "0", + "weight": 1, + "weightDecimals": 0 + } + }, + "network": "43114", + "addresses": ["0x9F8A5B35f5508071cf2304A670EAB0803F3737aa"], + "snapshot": 5475221 + }, + + { + "name": "Example query - Tokens in single token Pool", + "strategy": { + "name": "pod-leader", + "params": { + "chefAddress": "0xA3654801Ba6FB21d5A984F9a857441395dDeccFb", + "tokenAddress": "0x8B1d98A91F853218ddbb066F20b8c63E782e2430", + "pid": "0", + "weight": 1, + "weightDecimals": 0, + "decimals": 0 + } + }, + "network": "43114", + "addresses": ["0x9F8A5B35f5508071cf2304A670EAB0803F3737aa"], + "snapshot": 5475221 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/pod-leader/index.ts b/Implementations/API/backend/utils/snapshot/strategies/pod-leader/index.ts new file mode 100644 index 00000000..4b448e50 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/pod-leader/index.ts @@ -0,0 +1,343 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'ursamaritimus'; +export const version = '0.3.0'; + +/* + * PodLeader pool balance. Accepted options: + * - chefAddress: Masterchef contract address + * - pid: Mastechef pool id (starting with zero) + * + * - uniPairAddress: Address of a uniswap pair (or a sushi pair or any other with the same interface) + * - If the uniPairAddress option is provided, converts staked LP token balance to base token balance + * (based on the pair total supply and base token reserve) + * - If uniPairAddress is null or undefined, returns staked token balance of the pool + * + * - tokenAddress: Address of a token for single token Pools. + * - if the uniPairAddress is provided the tokenAddress is ignored. + * + * - weight: Integer multiplier of the result (for combining strategies with different weights, totally optional) + * - weightDecimals: Integer value of number of decimal places to apply to the final result + * + * - token0.address: Address of the uniPair token 0. If defined, the strategy will return the result for the token0. + * + * - token0.weight: Integer multiplier of the result for token0 + * - token0.weightDecimals: Integer value of number of decimal places to apply to the result of token0 + * + * - token1.address: Address of the uniPair token 1. If defined, the strategy will return the result for the token1. + * + * - token1,weight: Integer multiplier of the result for token1 + * - token1.weightDecimal: Integer value of number of decimal places to apply to the result of token1 + * + * + * - log: Boolean flag to enable or disable logging to the console (used for debugging purposes during development) + * + + * + * Check the examples.json file for how to use the options. + */ + +const abi = [ + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardTokenDebt)', + 'function totalSupply() view returns (uint256)', + 'function getReserves() view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)', + 'function token0() view returns (address)', + 'function token1() view returns (address)', + 'function decimals() view returns (uint8)' +]; + +let log: string[] = []; +let _options; + +const getUserInfoCalls = (addresses: any[]) => { + const result: any[] = []; + + for (const address of addresses) { + result.push([_options.chefAddress, 'userInfo', [_options.pid, address]]); + } + + return result; +}; + +const getTokenCalls = () => { + const result: any[] = []; + + if (_options.uniPairAddress != null) { + result.push([_options.uniPairAddress, 'totalSupply', []]); + result.push([_options.uniPairAddress, 'getReserves', []]); + result.push([_options.uniPairAddress, 'token0', []]); + result.push([_options.uniPairAddress, 'token1', []]); + result.push([_options.uniPairAddress, 'decimals', []]); + + if (_options.token0?.address != null) { + result.push([_options.token0.address, 'decimals', []]); + } + + if (_options.token1?.address != null) { + result.push([_options.token1.address, 'decimals', []]); + } + } else if (_options.tokenAddress != null) { + result.push([_options.tokenAddress, 'decimals', []]); + } + + return result; +}; + +function arrayChunk(arr: T[], chunkSize: number): T[][] { + const result: T[][] = []; + + for (let i = 0, j = arr.length; i < j; i += chunkSize) { + result.push(arr.slice(i, i + chunkSize)); + } + + return result; +} + +async function processValues( + values: any[], + tokenValues: any[], + network: any, + provider: any, + blockTag: string | number +) { + log.push(`values = ${JSON.stringify(values, undefined, 2)}`); + log.push(`tokenValues = ${JSON.stringify(tokenValues, undefined, 2)}`); + printLog(); + + const poolStaked = values[0][0] as BigNumber; + const weight = BigNumber.from(_options.weight || 1); + const weightDecimals = BigNumber.from(10).pow( + BigNumber.from(_options.weightDecimals || 0) + ); + + let result = 0; + + if (_options.uniPairAddress == null) { + log.push(`poolStaked = ${poolStaked}`); + + if (_options.tokenAddress != null) { + const tokenDecimals = BigNumber.from(10).pow( + BigNumber.from(tokenValues[0][0]) + ); + + log.push(`tokenDecimals = ${tokenDecimals}`); + log.push(`decimals = ${_options.decimals}`); + printLog(); + + result = toFloat(poolStaked.div(tokenDecimals), _options.decimals); + } else { + printLog(); + result = toFloat(poolStaked, _options.decimals); + } + } else { + const uniTotalSupply = tokenValues[0][0]; + const uniReserve0 = tokenValues[1][0]; + const uniReserve1 = tokenValues[1][1]; + const uniPairDecimalsIndex: any = + _options.uniPairAddress != null ? 4 : null; + const uniPairDecimalsCount = tokenValues[uniPairDecimalsIndex][0]; + const uniPairDecimals = + uniPairDecimalsIndex != null + ? BigNumber.from(10).pow(BigNumber.from(uniPairDecimalsCount || 0)) + : BigNumber.from(1); + + const token0Address = tokenValues[2][0]; + const useToken0 = + _options.token0?.address != null && + _options.token0.address.toString().toLowerCase() == + token0Address?.toString().toLowerCase(); + + log.push(`useToken0 = ${useToken0}`); + + if (useToken0) { + const token0DecimalsIndex = 5; + + log.push(`token0DecimalsIndex = ${token0DecimalsIndex}`); + log.push(`tokenValues = ${JSON.stringify(tokenValues, undefined, 2)}`); + printLog(); + + result += await GetTokenValue( + network, + provider, + blockTag, + uniTotalSupply, + uniReserve0, + uniPairDecimals, + poolStaked, + tokenValues, + token0Address, + token0DecimalsIndex, + _options.token0?.weight, + _options.token0?.weightDecimals + ); + } + + const token1Address = tokenValues[3][0]; + const useToken1 = + _options.token1?.address != null && + _options.token1.address.toString().toLowerCase() == + token1Address?.toString().toLowerCase(); + + log.push(`useToken1 = ${useToken1}`); + + if (useToken1) { + const token1DecimalsIndex = _options.token0?.address != null ? 6 : 5; + + log.push(`token1DecimalsIndex = ${token1DecimalsIndex}`); + log.push(`tokenValues = ${JSON.stringify(tokenValues, undefined, 2)}`); + printLog(); + + result += await GetTokenValue( + network, + provider, + blockTag, + uniTotalSupply, + uniReserve1, + uniPairDecimals, + poolStaked, + tokenValues, + token1Address, + token1DecimalsIndex, + _options.token1?.weight, + _options.token1?.WeightDecimals + ); + } + + if (!useToken0 && !useToken1) { + log.push(`poolStaked = ${poolStaked}`); + log.push(`uniPairDecimals = ${uniPairDecimals}`); + printLog(); + + const tokenCount = poolStaked.toNumber() / 10 ** uniPairDecimalsCount; + + log.push(`tokenCount = ${tokenCount}`); + + result = tokenCount / 10 ** (_options.decimals || 0); + } + } + + log.push(`result = ${result}`); + printLog(); + + result *= weight.toNumber() / weightDecimals.toNumber(); + + log.push(`weight = ${weight}`); + log.push(`weightDecimals = ${weightDecimals}`); + log.push(`result = ${result}`); + printLog(); + + return result; +} + +function toFloat(value: BigNumber, decimals: any): number { + const decimalsResult = decimals === 0 ? 0 : decimals || 18; + + log.push(`toFloat value = ${value}`); + log.push(`toFloat decimals = ${decimals}`); + log.push(`toFloat decimalsResult = ${decimalsResult}`); + printLog(); + + return parseFloat(formatUnits(value.toString(), decimalsResult)); +} + +async function GetTokenValue( + network: any, + provider: any, + blockTag: string | number, + uniTotalSupply: any, + uniReserve: any, + uniPairDecimals: BigNumber, + poolStaked: BigNumber, + tokenValues: any[], + tokenAddress: any, + tokenDecimalsIndex: any, + tokenWeight: any, + tokenWeightDecimals: any +) { + const weightDecimals = BigNumber.from(10).pow( + BigNumber.from(tokenWeightDecimals || 0) + ); + const weight = BigNumber.from(tokenWeight || 1); + const tokensPerLp = uniReserve.mul(uniPairDecimals).div(uniTotalSupply); + + const tokenDecimals = + tokenDecimalsIndex != null + ? BigNumber.from(10).pow( + BigNumber.from(tokenValues[tokenDecimalsIndex][0] || 0) + ) + : BigNumber.from(1); + log.push(`tokenAddress = ${tokenAddress}`); + log.push(`tokenDecimals = ${tokenDecimals}`); + log.push(`poolStaked = ${poolStaked}`); + log.push(`uniReserve = ${uniReserve}`); + log.push(`uniPairDecimals = ${uniPairDecimals}`); + log.push(`uniTotalSupply = ${uniTotalSupply}`); + log.push(`tokensPerLp = ${tokensPerLp}`); + log.push(`tokenWeight = ${weight}`); + log.push(`tokenWeightDecimals = ${weightDecimals}`); + + printLog(); + + const tokenCount = poolStaked + .mul(tokensPerLp) + .div(tokenDecimals) + .mul(weight) + .div(weightDecimals); + + log.push(`tokenCount = ${tokenCount}`); + + return toFloat(tokenCount, _options.decimals); +} + +function printLog() { + if (_options.log || false) { + console.debug(log); + log = []; + } +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + _options = options; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const userInfoCalls = getUserInfoCalls(addresses); + const tokenCalls = getTokenCalls(); + const entries = new Map(); + + const userInfoResponse = await multicall( + network, + provider, + abi, + userInfoCalls, + { blockTag } + ); + + const userInfoChunks = arrayChunk(userInfoResponse, 1); + + const tokenResponse = await multicall(network, provider, abi, tokenCalls, { + blockTag + }); + + for (let i = 0; i < userInfoChunks.length; i++) { + const value = userInfoChunks[i]; + const score = await processValues( + value, + tokenResponse, + network, + provider, + blockTag + ); + + entries.set(addresses[i], score); + } + + return Object.fromEntries(entries); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/polis-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/polis-balance/examples.json new file mode 100644 index 00000000..6358eccf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/polis-balance/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Polis Balance", + "strategy": { + "name": "polis-balance", + "params": { + "validators": "0x636278bAd2D76AAC5E9bAcfd185687c8901063E9", + "staking": "0xf69e9147d88de50c4bd42f9b41cbce00841cc2dd", + "symbol": "POLIS", + "decimals": 18 + } + }, + "network": "333999", + "addresses": [ + "0xed86b5962a1a73afad7181f0b3e427a93ce8e552", + "0x78d60a7ccd14f637bc4637e4cfd71e39b38d6614", + "0x769354a23fcFEa505F49705cd4085C4e69aA7eCe", + "0x17286e3fBA160b68803eeA3C70716A89B2e62e49" + ], + "snapshot": 640029 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/polis-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/polis-balance/index.ts new file mode 100644 index 00000000..6ba9ba0d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/polis-balance/index.ts @@ -0,0 +1,150 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall, Multicaller } from '../../utils'; +import { BigNumberish } from '@ethersproject/bignumber'; +import networks from '@snapshot-labs/snapshot.js/src/networks.json'; + +export const author = 'eabz'; +export const version = '0.1.0'; + +const abi = [ + 'function getEthBalance(address addr) public view returns (uint256 balance)' +]; + +const validator_abi = [ + 'function idByStakingAddress(address addr) external view returns(uint256)' +]; + +const stake_getpools_abi = [ + 'function getDelegatorPools(address _delegator,uint256 _offset,uint256 _length) external view returns(uint256[] memory result)' +]; + +const stake_amount_abi = [ + 'function stakeAmount(uint256,address) external view returns(uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi_pool = new Multicaller(network, provider, validator_abi, { + blockTag + }); + + addresses.forEach((address) => + multi_pool.call(address, options.validators, 'idByStakingAddress', [ + address + ]) + ); + + const multi_delegated = new Multicaller( + network, + provider, + stake_getpools_abi, + { + blockTag + } + ); + + addresses.forEach((address) => + multi_delegated.call(address, options.staking, 'getDelegatorPools', [ + address, + 0, + 0 + ]) + ); + + const result_pools: Record = await multi_pool.execute(); + const result_delegated_pools: Record = + await multi_delegated.execute(); + + const multi_own_staked = new Multicaller( + network, + provider, + stake_amount_abi, + { + blockTag + } + ); + + const multi_staked = new Multicaller(network, provider, stake_amount_abi, { + blockTag + }); + + for (let i = 0; i < addresses.length; i++) { + const pool = result_pools[addresses[i]]; + if (pool.toString() !== '0') { + multi_own_staked.call(addresses[i], options.staking, 'stakeAmount', [ + pool, + '0x0000000000000000000000000000000000000000' + ]); + } + const pools = result_delegated_pools[addresses[i]]; + for (const pool of pools) { + multi_staked.call( + addresses[i] + '-' + pool.toString(), + options.staking, + 'stakeAmount', + [pool, addresses[i]] + ); + } + } + + const final_balances = {}; + + const result_pools_own: Record = + await multi_own_staked.execute(); + + const result_pools_staked: Record = + await multi_staked.execute(); + + Object.keys(result_pools_own).map((addr) => { + final_balances[addr] = parseFloat( + formatUnits(result_pools_own[addr].toString()) + ); + }); + + Object.keys(result_pools_staked).map((addr) => { + const address = addr.split('-'); + const addition = parseFloat( + formatUnits(result_pools_staked[addr].toString()) + ); + if (final_balances[address[0]]) { + final_balances[address[0]] += addition; + } else { + final_balances[address[0]] = addition; + } + }); + + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + networks[network].multicall, + 'getEthBalance', + [address] + ]), + { blockTag } + ); + + const balances = Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), 18)) + ]) + ); + + Object.keys(balances).map((account) => { + if (final_balances[account]) { + balances[account] += final_balances[account]; + } + }); + + return balances; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/README.md b/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/README.md new file mode 100644 index 00000000..86d2c26c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/README.md @@ -0,0 +1,3 @@ +# Posichain Staking Strategy + +Calculates vote of validators based on the total staking (includes self stake and total balance delegated to them). diff --git a/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/examples.json new file mode 100644 index 00000000..33a3de46 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "posichain-staking", + "params": { + "symbol": "POSI" + } + }, + "network": "900000", + "addresses": [ + "0xD3Ee1d798eCea22Fde4D29590eFF0c77bA7e6b9e", + "0xb065c1a5BC1B3c1b1dF8781D369cffc534B65731", + "0x7fd592690ec6A7300289B059ee8711f4f9BBfD8a" + ], + "snapshot": 3989273 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/index.ts new file mode 100644 index 00000000..7562ebf4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/posichain-staking/index.ts @@ -0,0 +1,41 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'dannyposi'; +export const version = '0.0.1'; + +type Params = { + symbol: string; + decimals: number; +}; + +export async function strategy( + _space: string, + _network: string, + provider: StaticJsonRpcProvider, + // adding a 0 value for addresses not in the result is not needed + // since they are dropped anyway in utils.ts + // https://github.com/snapshot-labs/snapshot-strategies/blob/02439eb120ed7c4cc0c493924b78d92d22006b40/src/utils.ts#L26 + _addresses: Array, + options: Params, + snapshot: number | string +) { + const blockTag: number | string = + typeof snapshot === 'number' ? snapshot : 'latest'; + const response: Record = await provider.send( + 'hmyv2_getValidatorsStakeByBlockNumber', + [blockTag] + ); + return Object.fromEntries( + Object.entries(response).map(([address, balance]) => [ + address, + parseFloat( + formatUnits( + BigNumber.from('0x' + balance.toString(16)), + options && options.decimals ? options.decimals : 18 + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/README.md new file mode 100644 index 00000000..6e0b441d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/README.md @@ -0,0 +1,3 @@ +# Posichain Total Balance Strategy + +Calculates vote of validators based on their balance, plus total staking (includes self stake and total balance delegated to them). diff --git a/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/examples.json new file mode 100644 index 00000000..e89b2fb5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "posichain-total-balance", + "params": { + "symbol": "POSI" + } + }, + "network": "900000", + "addresses": [ + "0xD3Ee1d798eCea22Fde4D29590eFF0c77bA7e6b9e", + "0xb065c1a5BC1B3c1b1dF8781D369cffc534B65731", + "0x7fd592690ec6A7300289B059ee8711f4f9BBfD8a" + ], + "snapshot": 3989273 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/index.ts new file mode 100644 index 00000000..3224bb33 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/posichain-total-balance/index.ts @@ -0,0 +1,72 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall } from '../../utils'; +import networks from '@snapshot-labs/snapshot.js/src/networks.json'; + +export const author = 'dannyposi'; +export const version = '0.0.1'; + +const abi = [ + 'function getEthBalance(address addr) public view returns (uint256 balance)' +]; + +type Params = { + symbol: string; + decimals: number; +}; + +export async function strategy( + _space: string, + network: string, + provider: StaticJsonRpcProvider, + addresses: Array, + options: Params, + snapshot: number | string +) { + const blockTag: number | string = + typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingResponse: Record = await provider.send( + 'hmyv2_getValidatorsStakeByBlockNumber', + [blockTag] + ); + const stakingBalances = Object.fromEntries( + Object.entries(stakingResponse) + .filter(([address]) => addresses.includes(address)) + .map(([address, balance]) => [ + address, + parseFloat( + formatUnits( + BigNumber.from('0x' + balance.toString(16)), + options && options.decimals ? options.decimals : 18 + ) + ) + ]) + ); + + const balanceResponse = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + networks[network].multicall, + 'getEthBalance', + [address] + ]), + { blockTag } + ); + const decimals = options.decimals || 18; + + const currentBalances = Object.fromEntries( + balanceResponse.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), decimals)) + ]) + ); + + return Object.entries(currentBalances).reduce( + (acc, [key, value]) => ({ ...acc, [key]: (acc[key] || 0) + value }), + { ...stakingBalances } + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/README.md new file mode 100644 index 00000000..149a18b8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/README.md @@ -0,0 +1,15 @@ +# position-governance-power + +Allows the tokens staked in the staking contracts, and casted NFT to be used to calculate voter scores. + +Here is an example of parameters: + +```json +{ + "address": "0x5ca42204cdaa70d5c773946e69de942b85ca6706", + "symbol": "POSI", + "decimals": 18, + "stakeManagerAddress": "0x0C54B0b7d61De871dB47c3aD3F69FEB0F2C8db0B", + "nftStakingPoolAddress": "0x6257229FA379AFDBb91732091B5DE32cdB759845" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/examples.json new file mode 100644 index 00000000..da75d0fc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "position-governance-power", + "params": { + "address": "0x5ca42204cdaa70d5c773946e69de942b85ca6706", + "symbol": "POSI", + "decimals": 18, + "stakeManagerAddress": "0x0C54B0b7d61De871dB47c3aD3F69FEB0F2C8db0B", + "nftStakingPoolAddress": "0x6257229FA379AFDBb91732091B5DE32cdB759845" + } + }, + "network": "56", + "addresses": [ + "0xA3772E9B69b5877dde7580D17aE9716d228aafde", + "0x694fC01E9A8A0E4814B15B15317Db42D777F8b2B", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad" + ], + "snapshot": 18482717 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/index.ts new file mode 100644 index 00000000..adfd0393 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/index.ts @@ -0,0 +1,74 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, multicall } from '../../utils'; + +export const author = 'JustinPosition'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +const stakeManagerAbi = [ + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +const nftStakingPoolAbi = [ + 'function balanceOf(address account) view returns (uint256)' +]; + +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) => { + return multi.call(address, options.address, 'balanceOf', [address]); + }); + + const nftStakingBalances = await multicall( + network, + provider, + nftStakingPoolAbi, + addresses.map((address) => [ + options.nftStakingPoolAddress, + 'balanceOf', + [address] + ]), + { blockTag } + ); + const stakeBalances = await multicall( + network, + provider, + stakeManagerAbi, + addresses.map((address: any) => [ + options.stakeManagerAddress, + 'userInfo', + ['0', address] + ]), + { blockTag } + ); + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance], index) => { + return [ + address, + parseFloat( + formatUnits(nftStakingBalances[index][0], options.decimals) + ) + + parseFloat( + formatUnits(stakeBalances[index].amount, options.decimals) + ) + + parseFloat(formatUnits(balance, options.decimals)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/schema.json b/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/schema.json new file mode 100644 index 00000000..847fa37a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/position-governance-power/schema.json @@ -0,0 +1,54 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + }, + "stakeManagerAddress": { + "type": "string", + "title": "Stake Manager Contract address", + "examples": ["e.g. 0x0C54B0b7d61De871dB47c3aD3F69FEB0F2C8db0B"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "nftStakingPoolAddress": { + "type": "string", + "title": "NFT Stake Contract address", + "examples": ["e.g. 0x6257229FA379AFDBb91732091B5DE32cdB759845"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": [ + "address", + "decimals", + "stakeManagerAddress", + "nftStakingPoolAddress" + ], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/potion/README.md b/Implementations/API/backend/utils/snapshot/strategies/potion/README.md new file mode 100644 index 00000000..ffeddee9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/potion/README.md @@ -0,0 +1,3 @@ +# potion + +This is a voting strategy specific for Potion DAO to vote with the Potion Unlock NFT secret length. diff --git a/Implementations/API/backend/utils/snapshot/strategies/potion/examples.json b/Implementations/API/backend/utils/snapshot/strategies/potion/examples.json new file mode 100644 index 00000000..c65c3d86 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/potion/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "potion", + "params": { + "address": "0xEDAC702337b70f176423175d2f30fDf9c7a613A4", + "symbol": "SECRET" + } + }, + "network": "1", + "addresses": [ + "0xDbaaE747346488f372F0168A2D515B63Eb6586ba", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x9C732F0ABd5961b2c7E23FF646cad187D2687e80", + "0xAF1bff74708098dB603e48aaEbEC1BBAe03Dcf11", + "0x149Df72C9B65E99897FFa6b988cEFcEb87fF0E91", + "0xB4ACa4d18C3F3AD9Bfddd0A0dc8F669d51798dEA", + "0x9aADd82015413781D4b030aB839E965b1Ae0b264", + "0x2a20D21eAf90F14c0662ec94b3f1a192bbA17991" + ], + "snapshot": 14275528 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/potion/index.ts b/Implementations/API/backend/utils/snapshot/strategies/potion/index.ts new file mode 100644 index 00000000..aa60a7c7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/potion/index.ts @@ -0,0 +1,86 @@ +import { getAddress } from '@ethersproject/address'; +import { Multicaller, subgraphRequest } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.1'; + +const SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/dapp-testing/potion-unlock'; + +const abi = [ + 'function rarityConfig(uint256) view returns (uint32 startTokenId, uint32 endTokenId, uint32 secretSegmentStart, uint32 secretSegmentLength, uint32 bytesPerPiece)' +]; + +function parseConfigs(configs) { + return Object.values(configs).map((config: any) => { + const supply = config[1] - config[0] + 1; + return { + startTokenId: config[0], + endTokenId: config[1], + secretSegmentLength: config[3], + supply, + vp: (config[3] / supply) * 1e3 + }; + }); +} + +function tokenIdToVP(tokenId, configs) { + const id = parseInt(tokenId); + let vp = 0; + configs.forEach((config) => { + if (id >= config.startTokenId && id <= config.endTokenId) vp = config.vp; + }); + return vp; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const rarityCount = [...Array(6).keys()]; + const multi = new Multicaller(network, provider, abi, { blockTag }); + rarityCount.forEach((rarity) => + multi.call(rarity, options.address, 'rarityConfig', [rarity]) + ); + const configs = parseConfigs(await multi.execute()); + + const pages = [...Array(6).keys()]; + const params = Object.fromEntries( + pages.map((page, i) => [ + `_${page}`, + { + __aliasFor: 'nfts', + __args: { + first: 1000, + skip: i * 1000, + orderBy: 'tokenId', + orderDirection: 'asc' + }, + tokenId: true, + owner: true + } + ]) + ); + if (snapshot !== 'latest') { + pages.forEach((page) => { + // @ts-ignore + params[`_${page}`].__args.block = { number: snapshot }; + }); + } + let result = await subgraphRequest(SUBGRAPH_URL, params); + result = Object.values(result).flat(); + const scores = {}; + addresses.forEach((address) => (scores[address] = 0)); + result.forEach((nft) => { + const owner = getAddress(nft.owner); + if (typeof scores[owner] === 'number') + scores[owner] += tokenIdToVP(nft.tokenId, configs); + }); + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/README.md b/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/README.md new file mode 100644 index 00000000..f090b977 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/README.md @@ -0,0 +1,48 @@ +# prePO Vesting Snapshot Strategy + +This strategy returns a voting score based on PPO under vesting from the [prePO Vesting contract](https://github.com/prepo-io/prepo-monorepo/blob/main/apps/smart-contracts/token/contracts/vesting/Vesting.sol). + +To use this strategy, your contract must contain 3 methods from the prePO [Vesting interface](https://github.com/prepo-io/prepo-monorepo/blob/main/apps/smart-contracts/token/contracts/vesting/interfaces/IVesting.sol): `getAmountAllocated`, `getClaimableAmount` and `getVestedAmount`. + +This strategy assumes that the vesting token has 18 decimals. + +### Calculation + +When contract is paused (as a proxy for a cliff): +`score = (unclaimedVestedBalance + unvestedBalance) * multiplier` + +Else: +`score = unclaimedVestedBalance + unvestedBalance * multiplier` + +where: + +- `unclaimedVestedBalance = getClaimableAmount` +- `unvestedBalance = getAmountAllocated - getVestedAmount` + +### Parameters + +The strategy takes three parameters: + +- `symbol`: Symbol of the strategy +- `address`: Address of contract that has all the methods mentioned above +- `multiplier`: A multiplier applied to the unvested balance + +Here is an example of parameters: + +```json +{ + "symbol": "PPO (Vesting)", + "address": "0xB1B74EA823bAd9AFb5e2caC578235EeeB329A245", + "multiplier": 0.5 +} +``` + +### Tests + +To test the strategy, run `yarn test --strategy=prepo-vesting --more=500` + +### Links + +- [prePO's Website](https://prepo.io/) +- [prePO's GitHub](https://github.com/prepo-io/prepo-monorepo/) +- [prePO's Snapshot Space](https://vote.prepo.io/) diff --git a/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/examples.json new file mode 100644 index 00000000..bb81fcaf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example Goerli query", + "strategy": { + "name": "prepo-vesting", + "params": { + "symbol": "PPO (Vesting)", + "address": "0xB1B74EA823bAd9AFb5e2caC578235EeeB329A245", + "multiplier": 0.5 + } + }, + "network": "5", + "addresses": [ + "0x1549920373edB37AE3a9Cffe1bE02844Df3127D0", + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 7343530 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/index.ts new file mode 100644 index 00000000..cb0f0766 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/index.ts @@ -0,0 +1,58 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'prepo-io'; +export const version = '1.0.0'; + +const abi = [ + 'function getAmountAllocated(address _recipient) external view returns (uint256)', + 'function getClaimableAmount(address _recipient) external view returns (uint256)', + 'function getVestedAmount(address _recipient) external view returns (uint256)', + 'function isPaused() external view returns (bool)' +]; + +type MulticallOutput = Record>; + +const convertBN = (amount: BigNumberish, unitName?: BigNumberish) => + parseFloat(formatUnits(amount, unitName)); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const { address, multiplier } = options; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + addresses.forEach((addr) => { + multi.call(`allocated.${addr}`, address, 'getAmountAllocated', [addr]); + multi.call(`claimable.${addr}`, address, 'getClaimableAmount', [addr]); + multi.call(`vested.${addr}`, address, 'getVestedAmount', [addr]); + multi.call(`isPaused`, address, 'isPaused', []); + }); + + const { allocated, claimable, vested, isPaused }: MulticallOutput = + await multi.execute(); + + const output = Object.fromEntries( + Object.entries(allocated).map(([address, amountAllocated]) => { + const unclaimedVestedBalance = convertBN(claimable[address], 18); + const unvestedBalance = convertBN( + BigNumber.from(amountAllocated).sub(vested[address]), + 18 + ); + const score = isPaused + ? (unclaimedVestedBalance + unvestedBalance) * multiplier + : unclaimedVestedBalance + unvestedBalance * multiplier; + return [address, score]; + }) + ); + return output; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/schema.json b/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/schema.json new file mode 100644 index 00000000..fa8fcf9a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/prepo-vesting/schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "prePO Vesting Strategy", + "type": "object", + "properties": { + "symbol": { + "type": "string", + "title": "Symbol", + "examples": ["e.g. PPO (Vesting)"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract Address", + "examples": ["e.g. 0xB1B74EA823bAd9AFb5e2caC578235EeeB329A245"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "multiplier": { + "type": "number", + "title": "Multiplier", + "examples": ["e.g. 0.5"] + } + }, + "required": ["address", "multiplier"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/printer-financial/examples.json b/Implementations/API/backend/utils/snapshot/strategies/printer-financial/examples.json new file mode 100644 index 00000000..5df0c39d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/printer-financial/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example printer-financial query", + "strategy": { + "name": "printer-financial", + "params": { + "symbol": "INK", + "lpPairAddress": "0xDECC75dBF9679d7A3B6AD011A98F05b5CC6A8a9d", + "lpPoolAddress": "0xF95AB2A261B0920f3d5aBc02A41dBe125eBA10aE", + "lpPoolId": "3", + "printerAddress": "0xb1E6B2a4e6c5717CDBf8F6b01e89455C920a3646", + "inkAddress": "0xFFAbb85ADb5c25D57343547a8b32B62f03814B12" + } + }, + "network": "250", + "addresses": [ + "0x659fc9f71f407764D6C961Cea4c1814409feb5eE", + "0x1CBC2226b57Ae4BDbF6BC5CE1908E9D1873553aa" + ], + "snapshot": 31153229 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/printer-financial/index.ts b/Implementations/API/backend/utils/snapshot/strategies/printer-financial/index.ts new file mode 100644 index 00000000..bed53ea8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/printer-financial/index.ts @@ -0,0 +1,79 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'printerfinancial'; +export const version = '0.1.1'; + +const abi = [ + 'function balanceOf(address) view returns (uint256 amount)', + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)', + 'function totalSupply() view returns (uint256)', + 'function strategy() view returns (address)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + let multi = new Multicaller(network, provider, abi, { blockTag }); + + multi = new Multicaller(network, provider, abi, { blockTag }); + + multi.call(`lp.inkBalance`, options.inkAddress, 'balanceOf', [ + options.lpPairAddress + ]); + multi.call(`lp.totalSupply`, options.lpPairAddress, 'totalSupply'); + + addresses.forEach((address: any) => { + multi.call(`ink.${address}`, options.inkAddress, 'balanceOf', [address]); + multi.call(`inkInPrinter.${address}`, options.printerAddress, 'balanceOf', [ + address + ]); + multi.call(`lpInPools.${address}`, options.lpPoolAddress, 'userInfo', [ + options.lpPoolId, + address + ]); + multi.call(`lp.${address}`, options.lpPairAddress, 'balanceOf', [address]); + }); + + const result = await multi.execute(); + + return Object.fromEntries( + addresses.map((address: any) => { + const inkInWallet = parseFloat(formatUnits(result.ink[address], 18)); + + const inkInLpInWallet = parseFloat( + formatUnits( + result.lp[address] + .mul(result.lp.inkBalance) + .div(result.lp.totalSupply), + 18 + ) + ); + + const inkInPrinter = parseFloat( + formatUnits(result.inkInPrinter[address], 18) + ); + + const inkInPools = parseFloat( + formatUnits( + result.lpInPools[address].amount + .mul(result.lp.inkBalance) + .div(result.lp.totalSupply), + 18 + ) + ); + + return [ + address, + inkInWallet + inkInLpInWallet + inkInPrinter + inkInPools + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/printer-financial/schema.json b/Implementations/API/backend/utils/snapshot/strategies/printer-financial/schema.json new file mode 100644 index 00000000..08edb901 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/printer-financial/schema.json @@ -0,0 +1,65 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "inkAddress": { + "type": "string", + "title": "INK contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "printerAddress": { + "type": "string", + "title": "Printer contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "lpPoolAddress": { + "type": "string", + "title": "LP Pool address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "lpPoolId": { + "type": "string", + "title": "LP Pool ID", + "examples": ["e.g. 0"], + "pattern": "^[0-9]{1}$", + "minLength": 1, + "maxLength": 1 + }, + "lpPairAddress": { + "type": "string", + "title": "INK LP Pair address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": [ + "inkAddress", + "printerAddress", + "lpPoolAddress", + "lpPairAddress" + ], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/README.md b/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/README.md new file mode 100644 index 00000000..f4853e1d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/README.md @@ -0,0 +1,11 @@ +# proof-of-humanity + +It checks if an address is registered with Proof of Humanity. + +It takes the address of the Proof of Humanity contract as a paramter: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/examples.json b/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/examples.json new file mode 100644 index 00000000..5db11926 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "proof-of-humanity", + "params": { + "address": "0xC5E9dDebb09Cd64DfaCab4011A0D5cEDaf7c9BDb" + } + }, + "network": "1", + "addresses": [ + "0x1d7b9b8234bb7d72ddb278977f63fcc304d6f5d4", + "0xad51219eed2068da73ef6a3a8f19b6144c328a9f" + ], + "snapshot": 14551594 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/index.ts b/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/index.ts new file mode 100644 index 00000000..57706abe --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/index.ts @@ -0,0 +1,32 @@ +import { Multicaller } from '../../utils'; + +export const author = 'jaybuidl'; +export const version = '0.1.0'; + +const abi = [ + 'function isRegistered(address _submissionID) external view returns (bool)' +]; + +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) => + multi.call(address, options.address, 'isRegistered', [address]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, registered]) => [ + address, + Number(registered) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/schema.json b/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/schema.json new file mode 100644 index 00000000..93145f2b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proof-of-humanity/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "address": { + "type": "string", + "title": "PoH Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["address"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/README.md new file mode 100644 index 00000000..ac298c14 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/README.md @@ -0,0 +1,15 @@ +# protofi-erc721-tier-weighted + +This strategy returns the voting power of a wallet given by the sum of the NFT token owned, weighted by the tier of the NFT. Works specifically for Protofi NFTs. +With the parameter "countUsed" you decide if taking into account used NFTs as voting power. + +Here is an example of parameters: + +```json +{ + "address": "0x1aDB6f30561116B4283169DdD1Ca16ed2A34355A", + "symbol": "PNFT", + "tierToWeight": [10,20,30,40,50], + "countUsed": true +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/examples.json new file mode 100644 index 00000000..baab1db4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "protofi-erc721-tier-weighted", + "params": { + "address": "0x1aDB6f30561116B4283169DdD1Ca16ed2A34355A", + "symbol": "PNFT", + "tierToWeight": [10, 20, 30, 40, 50], + "countUsed": false + } + }, + "network": "250", + "addresses": [ + "0x5bF13Edc7De3E95029ffDC0C65C193E9bBEBcead", + "0x6f3064d973C08Dd9c88D43080549F474E5827d71", + "0x7751f0B8CfbC18b8931Ae54E234F908014D6D46d", + "0x346F59ac0aF2C85DB1250322b2beD4eEb4c61313", + "0x8a347ec3cB809D9a53d2B8d74e23f08d908e19dc" + ], + "snapshot": 31508266 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/index.ts new file mode 100644 index 00000000..de84fa57 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/protofi-erc721-tier-weighted/index.ts @@ -0,0 +1,109 @@ +import { Multicaller } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'theothercrypto'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function tokenTier(uint256 index) external view returns (uint256)', + 'function tokenUsed(uint256 _id) public view virtual returns (bool)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // First, get the balance of the token + const callWalletToBalanceOf = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToBalanceOf.call(walletAddress, options.address, 'balanceOf', [ + walletAddress + ]); + } + const walletToBalanceOf: Record = + await callWalletToBalanceOf.execute(); + + // Second, get the tokenId's for each token + const callWalletToAddresses = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletAddress, count] of Object.entries(walletToBalanceOf)) { + for (let index = 0; index < count.toNumber(); index++) { + callWalletToAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToAddresses: Record = + await callWalletToAddresses.execute(); + + // Third, given the tokenIds for each token + const callWalletToTiers = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletAddress, tokenId] of Object.entries(walletIDToAddresses)) { + callWalletToTiers.call( + walletAddress.toString() + '-' + tokenId.toString(), + options.address, + 'tokenTier', + [tokenId] + ); + } + + const walletIDToTiers: Record = + await callWalletToTiers.execute(); + + // Third, given the tokenIds for each token get if the token is used + const callWalletToUsed = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletAddress, tokenId] of Object.entries(walletIDToAddresses)) { + callWalletToUsed.call( + walletAddress.toString() + '-' + tokenId.toString(), + options.address, + 'tokenUsed', + [tokenId] + ); + } + const walletIDToUsed: Record = + await callWalletToUsed.execute(); + + // Ultimately, sum the weights for each tokenId and assign votes based on the + // strategy parameters + const walletToLpBalance = {} as Record; + for (const [walletID, tokenTier] of Object.entries(walletIDToTiers)) { + const address = walletID.split('-')[0]; + const used = walletIDToUsed[walletID]; + if (!options.countUsed && used) { + // Its used and + continue; + } + + // Voting power given by the tier of NFTs owned + const tokenIdValue = options.tierToWeight[tokenTier - 1]; + + walletToLpBalance[address] = walletToLpBalance[address] + ? walletToLpBalance[address].add(BigNumber.from(tokenIdValue)) + : BigNumber.from(tokenIdValue); + } + + return Object.fromEntries( + Object.entries(walletToLpBalance).map(([address, balance]) => [ + address, + balance.toNumber() + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/README.md new file mode 100644 index 00000000..af84210f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/README.md @@ -0,0 +1,5 @@ +# Proxy ERC1155 + +This allows for a Proxy wallet to map to multiple wallets owned by the user. + +You would use the exact same parameters as erc1155-balance-of, but the signing wallet is now the proxy wallet. diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/examples.json new file mode 100644 index 00000000..dd62f202 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "proxyprotocol-erc1155-balance-of", + "params": { + "symbol": "ADI", + "address": "0x28472a58a490c5e09a238847f66a68a47cc76f0f", + "tokenId": "1", + "decimals": 0 + } + }, + "network": "1", + "addresses": [ + "0x346f1c338b38ef9cf18964695dd68e9956ca5d37", + "0xa164591f695b11e1c6b77925e326e20754521200" + ], + "snapshot": 15304592 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/index.ts new file mode 100644 index 00000000..38a211f6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc1155-balance-of/index.ts @@ -0,0 +1,60 @@ +import fetch from 'cross-fetch'; +import { strategy as erc1155BalanceOfStrategy } from '../erc1155-balance-of'; + +export const author = 'rawrjustin'; +export const version = '0.1.0'; + +const calculateVotingPower = (inputAddresses, addressScores, walletMap) => { + const userVotingPower = {}; + inputAddresses.forEach((input) => { + let count = 0.0; + walletMap[input.toLowerCase()].forEach((address) => { + count += addressScores[address]; + }); + userVotingPower[input] = count; + }); + return userVotingPower; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + // Get the wallet mapping from proxy wallets to actual wallets + const url = 'https://api.proxychat.xyz/external/v0/getProxyWalletMappings'; + const params = { + proxyAddresses: addresses + }; + const apiResponse = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params) + }); + const data = await apiResponse.json(); + + // Flatten the wallet mapping so it's an array of real wallets to query for tokens + const arrayOfProxyWallets = Object.keys(data).map(function (key) { + return data[key]; + }); + const flattenedWalletAddresses = [].concat.apply([], arrayOfProxyWallets); + + // Query for token holdings + const addressScores = await erc1155BalanceOfStrategy( + space, + network, + provider, + flattenedWalletAddresses, + options, + snapshot + ); + + // Calculate the voting power across all wallets and map it back to original Proxy wallets. + return calculateVotingPower(addresses, addressScores, data); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/README.md new file mode 100644 index 00000000..12ea50a1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/README.md @@ -0,0 +1,5 @@ +# Proxy ERC-20 + +This allows for a Proxy wallet to map to multiple wallets owned by the user. + +You would use the exact same parameters as erc20-balance-of, but the signing wallet is now the proxy wallet. diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/examples.json new file mode 100644 index 00000000..89db17a9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "proxyprotocol-erc20-balance-of", + "params": { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "symbol": "WETH", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x346f1c338b38ef9cf18964695dd68e9956ca5d37", + "0xa164591f695b11e1c6b77925e326e20754521200" + ], + "snapshot": 15304592 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/index.ts new file mode 100644 index 00000000..ba3fabe3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/index.ts @@ -0,0 +1,60 @@ +import fetch from 'cross-fetch'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'rawrjustin'; +export const version = '0.1.0'; + +const calculateVotingPower = (inputAddresses, addressScores, walletMap) => { + const userVotingPower = {}; + inputAddresses.forEach((input) => { + let count = 0.0; + walletMap[input.toLowerCase()].forEach((address) => { + count += addressScores[address]; + }); + userVotingPower[input] = count; + }); + return userVotingPower; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + // Get the wallet mapping from proxy wallets to actual wallets + const url = 'https://api.proxychat.xyz/external/v0/getProxyWalletMappings'; + const params = { + proxyAddresses: addresses + }; + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params) + }); + const data = await response.json(); + + // Flatten the wallet mapping so it's an array of real wallets to query for tokens + const arrayOfProxyWallets = Object.keys(data).map(function (key) { + return data[key]; + }); + const flattenedWalletAddresses = [].concat.apply([], arrayOfProxyWallets); + + // Query for token holdings + const addressScores = await erc20BalanceOfStrategy( + space, + network, + provider, + flattenedWalletAddresses, + options, + snapshot + ); + + // Calculate the voting power across all wallets and map it back to original Proxy wallets. + return calculateVotingPower(addresses, addressScores, data); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/schema.json b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/schema.json new file mode 100644 index 00000000..2113da8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc20-balance-of/schema.json @@ -0,0 +1,33 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/README.md new file mode 100644 index 00000000..bbe65e12 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/README.md @@ -0,0 +1,5 @@ +# Proxy ERC-721 + +This allows for a Proxy wallet to map to multiple wallets owned by the user. + +You would use the exact same parameters as erc721-balance-of, but the signing wallet is now the proxy wallet. diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/examples.json new file mode 100644 index 00000000..991cf382 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "proxyprotocol-erc721-balance-of", + "params": { + "address": "0x6826c4c51f4855d0280e99f646c5ef43edb3848e", + "symbol": "TYXMK" + } + }, + "network": "1", + "addresses": [ + "0x346f1c338b38ef9cf18964695dd68e9956ca5d37", + "0xa164591f695b11e1c6b77925e326e20754521200" + ], + "snapshot": 15304592 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/index.ts new file mode 100644 index 00000000..4a87c703 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/index.ts @@ -0,0 +1,60 @@ +import fetch from 'cross-fetch'; +import { strategy as erc721BalanceOfStrategy } from '../erc721'; + +export const author = 'rawrjustin'; +export const version = '0.1.0'; + +const calculateVotingPower = (inputAddresses, addressScores, walletMap) => { + const userVotingPower = {}; + inputAddresses.forEach((input) => { + let count = 0.0; + walletMap[input.toLowerCase()].forEach((address) => { + count += addressScores[address]; + }); + userVotingPower[input] = count; + }); + return userVotingPower; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + // Get the wallet mapping from proxy wallets to actual wallets + const url = 'https://api.proxychat.xyz/external/v0/getProxyWalletMappings'; + const params = { + proxyAddresses: addresses + }; + const apiResponse = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params) + }); + const data = await apiResponse.json(); + + // Flatten the wallet mapping so it's an array of real wallets to query for tokens + const arrayOfProxyWallets = Object.keys(data).map(function (key) { + return data[key]; + }); + const flattenedWalletAddresses = [].concat.apply([], arrayOfProxyWallets); + + // Query for token holdings + const addressScores = await erc721BalanceOfStrategy( + space, + network, + provider, + flattenedWalletAddresses, + options, + snapshot + ); + + // Calculate the voting power across all wallets and map it back to original Proxy wallets. + return calculateVotingPower(addresses, addressScores, data); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/schema.json b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/schema.json new file mode 100644 index 00000000..a63b9e00 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/proxyprotocol-erc721-balance-of/schema.json @@ -0,0 +1,28 @@ +{ + "$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. DOODLE"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["address"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/README.md new file mode 100644 index 00000000..68d1671e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/README.md @@ -0,0 +1,66 @@ +# psp-in-sepsp2-balance + +This is a strategy to get PSP balances staked in sePSP2 contract and multiply that by `options.multiplier`. + +It works like this: +1. Get BPT balance an account holds +```js +const sePSP_balance = BPT_balance = SPSP.PSPBalance(address) +``` + +2. Get tokens of the Balancer Pool +```js +const [tokens] = await Vault.getPoolTokens(poolId) +``` + +3. Construct an exit pool request that could be used to unstake 1 BPT balance +```js +const exitPoolRequest = { + assets: tokens, // Balancer Pools underlying tokens + minAmountsOut: [0,0], // minimal amounts received + userData, // endoded [1, 1e18], // ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT = 1 + toInternalBalance: false, // transfer tokens to recipient, as opposed to depositing to internal balance +} + ``` + +4. Find how many tokens you would receive by unstaking 1 BPT balance +```js +const [amountsOut] = await BalancerHelpers.callStatic.queryExit( + poolId, + Zero_account, // sender + Zero_account, // recipient + exitPoolRequest + ) +// sender & recipient don't matter as we only getting an estimate +``` +`amountsOut` is a representation of BPT balance in the Balancer Pool's underlying tokens. In the same order as `assets` + +5. One of the `amountsOut` is PSP portion of 1 BPT. +```js +const PSP_In_1_BPT = amountsOut[index_from_assets] +``` + +6. Multiply PSP_balance by score multiplier. +```js +const Vote_power = PSP_In_1_BPT * BPT_balance * 2.5 +``` + +Here is an example of parameters: + +```json +{ + "address": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "symbol": "PSP", + "decimals": 18, + "sePSP2": { + "address": "0x593F39A4Ba26A9c8ed2128ac95D109E8e403C485", + "decimals": 18 + }, + "balancer": { + "poolId": "0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a", + "BalancerHelpers": "0x5aDDCCa35b7A0D07C74063c48700C8590E87864E", + "Vault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + }, + "multiplier": 2.5 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/examples.json new file mode 100644 index 00000000..f77f7517 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/examples.json @@ -0,0 +1,47 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "psp-in-sepsp2-balance", + "params": { + "address": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "symbol": "PSP", + "decimals": 18, + "sePSP2": { + "address": "0x593F39A4Ba26A9c8ed2128ac95D109E8e403C485", + "decimals": 18 + }, + "balancer": { + "poolId": "0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a", + "BalancerHelpers": "0x5aDDCCa35b7A0D07C74063c48700C8590E87864E", + "Vault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + }, + "multiplier": 2.5 + } + }, + "network": "1", + "addresses": [ + "0x05182E579FDfCf69E4390c3411D8FeA1fb6467cf", + "0x0DDC793680FF4f5793849c8c6992be1695CbE72A", + "0x0edefa91e99da1eddd1372c1743a63b1595fc413", + "0xd37f7b32a541d9e423f759dff1dd63181651bd04", + "0xf9aa0da6e2fa01a17e2f69e878e45bb26c1b34b7", + "0xc570429a39a93fd267d1047b2363cfba07198ff7", + "0x4e8ffddb1403cf5306c6c7b31dc72ef5f44bc4f5", + "0x0ddc793680ff4f5793849c8c6992be1695cbe72a", + "0xd880507d359af862a5f8f318c8e934ab478ca818", + "0x510a7cd4ba40f7b6643f566a5d45ea55f5cd8d0e", + "0x1ff3c4bfa745b72f942c5cf2b769b3d8a6610a5e", + "0x5577933afc0522c5ee71115df61512f49da0543e", + "0x6eb8d6bccceb84832725dcf792468dd8ba088449", + "0xe768FF81990E7Ac73C18a2eCbf038815023599Dc", + "0xB9E11C28617D46866c1D7d95EaebAC3AC12CDAD3", + "0xB5714084eeF0f02eFDD145DFB3Fe2e3290591D7b", + "0xCC6B30531DE603787a4D0305FC7eD404374Cf771", + "0xcb492647CB51E243Fb2582C0300C4c7573acdEBf", + "0xB8f6f3cc7b162d7E5b9196140Fb1878cdA316ba0", + "0x584BaA4b71b0A3fA522658128f36a6A4AbeAC2ae" + ], + "snapshot": 16492220 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/index.ts new file mode 100644 index 00000000..08dc8745 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/psp-in-sepsp2-balance/index.ts @@ -0,0 +1,147 @@ +import { BigNumberish, BigNumber } from '@ethersproject/bignumber'; +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { Contract } from '@ethersproject/contracts'; +import { defaultAbiCoder } from '@ethersproject/abi'; +import { strategy as fetchERC20Balances } from '../erc20-balance-of'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'paraswap'; +export const version = '0.1.0'; + +const BalancerVaultAbi = [ + 'function getPoolTokens(bytes32 poolId) external view returns (address[] tokens, uint256[] balances, uint256 lastChangeBlock)' +]; +interface PoolTokensFromVault { + tokens: string[]; + balances: BigNumber[]; + lastChangeBlock: BigNumber; +} + +const BalancerHelpersAbi = [ + 'function queryExit(bytes32 poolId, address sender, address recipient, tuple(address[] assets, uint256[] minAmountsOut, bytes userData, bool toInternalBalance) request) returns (uint256 bptIn, uint256[] amountsOut)' +]; + +interface QueryExitResult { + bptIn: BigNumber; + amountsOut: BigNumber[]; +} + +interface StrategyOptions { + address: string; + symbol: string; + decimals: number; + sePSP2: { address: string; decimals: number }; + balancer: { + poolId: string; + BalancerHelpers: string; + Vault: string; + }; + multiplier: number; +} + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +export async function strategy( + space: string, + network: string, + provider, + addresses: string[], + options: StrategyOptions, + snapshot: number +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const account2BPTBalance = await fetchERC20Balances( + space, + network, + provider, + addresses, + options.sePSP2, + snapshot + ); + + const balancerVault = new Contract( + options.balancer.Vault, + BalancerVaultAbi, + provider + ); + + const { tokens: poolTokens }: PoolTokensFromVault = + await balancerVault.getPoolTokens(options.balancer.poolId, { blockTag }); + + const tokenLowercase = options.address.toLowerCase(); + const tokenIndex = poolTokens.findIndex( + (token) => token.toLowerCase() === tokenLowercase + ); + + if (tokenIndex === -1) { + throw new Error( + `Token ${options.address} doesn't belong to Balancer Pool ${options.balancer.poolId}` + ); + } + + const balancerHelpers = new Contract( + options.balancer.BalancerHelpers, + BalancerHelpersAbi, + provider + ); + + const exitPoolRequest = constructExitPoolRequest( + poolTokens, + // how much will get for 1 BPT + parseUnits('1', options.sePSP2.decimals) + ); + + const queryExitResult: QueryExitResult = + await balancerHelpers.callStatic.queryExit( + options.balancer.poolId, + ZERO_ADDRESS, + ZERO_ADDRESS, + exitPoolRequest, + { blockTag } + ); + + const pspFor1BPT = parseFloat( + formatUnits(queryExitResult.amountsOut[tokenIndex], options.decimals) + ); + + const address2PSPinSePSP2 = Object.fromEntries( + Object.entries(account2BPTBalance).map(([address, bptBalance]) => { + const pspBalance = pspFor1BPT * bptBalance; + + const checksummedAddress = getAddress(address); + + return [checksummedAddress, pspBalance * options.multiplier]; + }) + ); + + return address2PSPinSePSP2; +} + +interface ExitPoolRequest { + assets: string[]; + minAmountsOut: BigNumberish[]; + userData: string; + toInternalBalance: boolean; +} + +// ExitKind enum for BalancerHerlpers.queryExit call +const EXACT_BPT_IN_FOR_TOKENS_OUT = 1; + +export function constructExitPoolRequest( + assets: string[], + bptAmountIn: BigNumberish +): ExitPoolRequest { + const abi = ['uint256', 'uint256']; + const data = [EXACT_BPT_IN_FOR_TOKENS_OUT, bptAmountIn]; + const userData = defaultAbiCoder.encode(abi, data); + + const minAmountsOut = assets.map(() => 0); + + return { + assets, + minAmountsOut, + userData, + toInternalBalance: false + }; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/README.md new file mode 100644 index 00000000..ea0ebf70 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/README.md @@ -0,0 +1,24 @@ +# EPNS Governance: Voting + +This strategy weighs the vote using: +- Amount of `$PUSH` delegated to user +- Amount of `$PUSH` staked in Push staking pool +- Amount of `$PUSH-LP` staked in LP staking pool + +The voting power is calculated in terms of `$PUSH`, so `$PUSH-LP` tokens are converted to `$PUSH` using on chain data from Uniswap-V2 Router, WETH and USDT token contracts. + +- Example Parameters: +```JSON +{ + "pushTokenAddr": "0xf418588522d5dd018b425E472991E52EBBeEEEEE", + "pushLPTokenAddr": "0xaf31fd9c3b0350424bf96e551d2d1264d8466205", + "stakingAddr": "0xB72ff1e675117beDefF05a7D0a472c3844cfec85", + "symbol": "PUSH", + + "uniswapV2Router02": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "WETHAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "USDTAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7", + + "decimals": 18 +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/examples.json new file mode 100644 index 00000000..590d1ba7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/examples.json @@ -0,0 +1,31 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "push-voting-power", + "params": { + "pushTokenAddr": "0xf418588522d5dd018b425E472991E52EBBeEEEEE", + "pushLPTokenAddr": "0xaf31fd9c3b0350424bf96e551d2d1264d8466205", + "stakingAddr": "0xB72ff1e675117beDefF05a7D0a472c3844cfec85", + "uniswapV2Router02": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "symbol": "PUSH", + + "WETHAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "USDTAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7", + + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x554d29160f779Adf0a4328597cD33Ea1Df4D9Ee9", + "0xcB034160f7B45E41E6015ECEA09F31A66C144422", + "0x1c8c125C1Eac1C8d450979cbf0146985D75df55A", + "0x5913760160d245d0C9A05a8a956012694281bEE3", + "0x4b9D53246eD18db31f26Fc59b6e47a9efC3C1213", + "0x89c1151cA988ca5372b569A30d24964A94Ad05FD", + "0xE08fcc423566BF10B53e32E05d24C0429F91111A" + ], + "snapshot": 13626267 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/index.ts new file mode 100644 index 00000000..149fdf3b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/push-voting-power/index.ts @@ -0,0 +1,159 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall } from '../../utils'; + +export const author = 'mujtaba1747'; +export const version = '0.1.0'; + +const tokenBNtoNumber = (tokenBn) => { + return ( + tokenBn.div(BigNumber.from(10).pow(BigNumber.from(10))).toNumber() / + 100000000 + ); +}; + +const sharedABI = [ + 'function balanceOf(address user, address token) view returns (uint256)', + 'function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)' +]; + +const wethABI = ['function balanceOf(address) view returns (uint256)']; + +const epnsTokenABI = [ + 'function getCurrentVotes(address account) view returns (uint96)', + 'function balanceOf(address account) view returns (uint256)' +]; + +const epnsLpABI = ['function totalSupply() view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const responseEPNSToken = await multicall( + network, + provider, + epnsTokenABI, + [[options.pushTokenAddr, 'balanceOf', [options.pushLPTokenAddr]]].concat( + addresses.map((address: any) => [ + options.pushTokenAddr, + 'getCurrentVotes', + [address.toLowerCase()] + ]) + ), + { blockTag } + ); + + // Used .slice, as 2 different type of calls to the same contract were made in a + // single Multicall using .concat + // This was done because the number of Multicalls allowed is limited to 5 by Snapshot + + const responseDelegatedPUSH = responseEPNSToken.slice(1); + + const pushAmountReserve = tokenBNtoNumber( + responseEPNSToken.slice(0, 1)[0][0] + ); + + const responseStaked = await multicall( + network, + provider, + sharedABI, + + addresses + .map((address: any) => [ + options.stakingAddr, + 'balanceOf', + [address.toLowerCase(), options.pushTokenAddr] + ]) + .concat( + addresses.map((address: any) => [ + options.stakingAddr, + 'balanceOf', + [address.toLowerCase(), options.pushLPTokenAddr] + ]) + ), + + { blockTag } + ); + + const responseStakedPUSH = responseStaked.slice(0, addresses.length); + + const responseStakedLP = responseStaked.slice(addresses.length); + + const responseWETH = await multicall( + network, + provider, + wethABI, + [[options.WETHAddress, 'balanceOf', [options.pushLPTokenAddr]]], + { blockTag } + ); + + const wethAmountReserve = tokenBNtoNumber(responseWETH[0][0]); + + const responseLPConversion = await multicall( + network, + provider, + sharedABI, + [ + [ + options.uniswapV2Router02, + 'getAmountsOut', + [ + '1000000000000000000', + [options.pushTokenAddr, options.WETHAddress, options.USDTAddress] + ] + ], + + [ + options.uniswapV2Router02, + 'getAmountsOut', + ['1000000000000000000', [options.WETHAddress, options.USDTAddress]] + ] + ], + { blockTag } + ); + + // pushPrice and wethPrice are in terms of USDT + const pushPrice = responseLPConversion[0]['amounts'][2].toNumber() / 1e6; + + const wethPrice = responseLPConversion[1]['amounts'][1].toNumber() / 1e6; + + const responseEPNSLPToken = await multicall( + network, + provider, + epnsLpABI, + [[options.pushLPTokenAddr, 'totalSupply', []]], + { blockTag } + ); + + // Calculating price of EPNS-LP Tokens in terms of EPNS Tokens + const uniLpTotalSupply = tokenBNtoNumber(responseEPNSLPToken[0][0]); + + const uniLpPrice = + (pushAmountReserve * pushPrice + wethAmountReserve * wethPrice) / + uniLpTotalSupply; + + const lpToPushRatio = uniLpPrice / pushPrice; + + return Object.fromEntries( + responseDelegatedPUSH.map((value, i) => [ + addresses[i], + + // Voting Power = Delegated PUSH + Staked PUSH + Staked UNI-LP PUSH + parseFloat(formatUnits(value.toString(), options.decimals)) + + parseFloat( + formatUnits(responseStakedPUSH[i].toString(), options.decimals) + ) + + parseFloat( + formatUnits(responseStakedLP[i].toString(), options.decimals) + ) * + lpToPushRatio + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/README.md b/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/README.md new file mode 100644 index 00000000..7b720967 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/README.md @@ -0,0 +1,13 @@ +# radicle-community-tokens + +This strategy is used to calculate voting power from the Radicle funding subgraph with the entity NFT and field `amtPerSec`. Each voter can have multiple NFTs, each with specific `amtPerSec`. The voters' voting power would be the sum of all the `amtPerSec`. If a fundingProject `id` is provided the summation is done only over NFTs of that project. + +Here is an example of parameters: + +```json +{ + "symbol": "DAI", + "decimals": 18, + "fundingProject": "0x488fc5d6e43259f87f816556a7ab99dc78cfbab6" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/examples.json b/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/examples.json new file mode 100644 index 00000000..0ddc2f6c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "radicle-community-tokens", + "params": { + "symbol": "DAI", + "decimals": 18, + "fundingProject": "0x488fc5d6e43259f87f816556a7ab99dc78cfbab6" + } + }, + "network": "4", + "addresses": [ + "0xeCa823848221a1DA310E1a711E19D82F43101B07", + "0xb5bb9A125c2F67F1F2cd9d8992955bb209490aFE" + ], + "snapshot": 9601000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/index.ts b/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/index.ts new file mode 100644 index 00000000..d3aedb53 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/radicle-community-tokens/index.ts @@ -0,0 +1,86 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const FUNDING_SUBGRAPH_URL = { + '4': 'https://api.studio.thegraph.com/query/9578/funding-subgraph-v5/v0.0.1' // Rinkeby testnet +}; + +export const author = 'AmirSarraf'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + let params = {}; + const fundingProject = options.fundingProject; + const mainField: string = fundingProject ? 'fundingProjects' : 'nfts'; + + if (fundingProject) { + // parameters to query nfts belonging to the provided addresses in a certain fundingProject + params = { + fundingProjects: { + __args: { + id: fundingProject.toLowerCase() + }, + id: true, + nfts: { + __args: { + where: { + nftReceiver_in: addresses.map((address) => address.toLowerCase()) + } + }, + amtPerSec: true, + nftReceiver: true + } + } + }; + } else { + // parameters to query nfts belonging to the provided addresses + params = { + nfts: { + __args: { + where: { + nftReceiver_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + amtPerSec: true, + nftReceiver: true + } + }; + } + + if (snapshot !== 'latest') { + // @ts-ignore + params[mainField].__args.block = { number: snapshot }; + } + + let result = await subgraphRequest(FUNDING_SUBGRAPH_URL[network], params); + result = fundingProject + ? result?.fundingProjects?.find((proj) => proj.id == fundingProject) //double checking id + : result; + + const score: Record = {}; + if (result && result.nfts) { + result.nfts.forEach((nft) => { + const userAddress = getAddress(nft.nftReceiver); + const userScore = nft.amtPerSec; + if (!score[userAddress]) score[userAddress] = BigNumber.from(0); + score[userAddress] = score[userAddress].add(BigNumber.from(userScore)); + }); + } + + return Object.fromEntries( + Object.entries(score).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/README.md b/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/README.md new file mode 100644 index 00000000..5e5a70b1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/README.md @@ -0,0 +1,22 @@ +# rari-fuse + +Returns the voter's underlying collateral balances in a given Rari Fuse market (fToken). + +Here is an example of parameters: + +- `symbol` - (**Optional**, `string`) Symbol of the underlying ERC20 token +- `token` - (**Required**, `string`) Address of the underlying token. +- `fToken` - (**Required**, `string`) Address of the fToken (Rari Fuse market) + +```json +{ + "symbol": "BANK", + "token": "0x2d94AA3e47d9D5024503Ca8491fcE9A2fB4DA198", + "fToken": "0x250316B3E46600417654b13bEa68b5f64D61E609" +} +``` + +## Reference + +For details about exchange rate between fTokens and underlying tokens, see +https://docs.rari.capital/fuse/#interpreting-exchange-rates diff --git a/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/examples.json b/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/examples.json new file mode 100644 index 00000000..a733c48b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "rari-fuse", + "params": { + "symbol": "BANK", + "token": "0x2d94AA3e47d9D5024503Ca8491fcE9A2fB4DA198", + "fToken": "0x250316B3E46600417654b13bEa68b5f64D61E609" + } + }, + "network": "1", + "addresses": [ + "0x3839AcF1ee7699D1F46b1BE840D8aD8317FDf757", + "0x0cf861f96378dbd5194d74cbe6b0424fafaed940", + "0x0f1d41cc51e97dc9d0cad80dc681777eed3675e3", + "0xb6ac0341fcf3fb507a8208d34a97f13779e1393d", + "0x173ff4db38c3fcde0584f8ea7930c44969a29ba4" + ], + "snapshot": 14482171 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/index.ts b/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/index.ts new file mode 100644 index 00000000..8979152e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/index.ts @@ -0,0 +1,62 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'MantisClone'; +export const version = '0.1.0'; + +const abi = [ + 'function underlying() public view returns (address)', + 'function decimals() public view returns (uint8)', + 'function exchangeRateStored() public view returns (uint256)', + 'function balanceOf(address owner) external returns (uint256)' +]; + +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 }); + multi.call('underlying', options.fToken, 'underlying', []); + multi.call('tokenDecimals', options.token, 'decimals', []); + multi.call('fTokenDecimals', options.fToken, 'decimals', []); + multi.call('exchangeRate', options.fToken, 'exchangeRateStored', []); + addresses.forEach((address) => + multi.call(`fTokenBalances.${address}`, options.fToken, 'balanceOf', [ + address + ]) + ); + const result = await multi.execute(); + + const underlying: string = result.underlying; + const tokenDecimals: BigNumber = result.tokenDecimals; + const fTokenDecimals: BigNumber = result.fTokenDecimals; + const exchangeRate: BigNumber = result.exchangeRate; + const fTokenBalances: Record = result.fTokenBalances; + + if (options.token !== underlying) { + throw new Error( + `token parameter doesn't match fToken.underlying(). token=${options.token}, underlying=${underlying}` + ); + } + + const mantissa: BigNumber = BigNumber.from(18) + .add(tokenDecimals) + .sub(fTokenDecimals); + const divisor: BigNumber = BigNumber.from(10).pow(mantissa); + + return Object.fromEntries( + Object.entries(fTokenBalances).map(([address, balance]) => [ + address, + parseFloat( + formatUnits(balance.mul(exchangeRate).div(divisor), fTokenDecimals) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/schema.json b/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/schema.json new file mode 100644 index 00000000..71e3fecc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rari-fuse/schema.json @@ -0,0 +1,36 @@ +{ + "$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. BANK"], + "maxLength": 16 + }, + "token": { + "type": "string", + "title": "token", + "examples": ["e.g. 0x2d94AA3e47d9D5024503Ca8491fcE9A2fB4DA198"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "fToken": { + "type": "string", + "title": "fToken", + "examples": ["e.g. 0x250316B3E46600417654b13bEa68b5f64D61E609"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["token", "fToken"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/razor-network-voting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/razor-network-voting/examples.json new file mode 100644 index 00000000..e3adcd77 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/razor-network-voting/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "razor-network-voting", + "strategy": { + "name": "razor-network-voting", + "params": { + "symbol": "RAZOR" + } + }, + "network": "80001", + "snapshot": 665995, + "addresses": [ + "0xE79772bC4798fE17dABD3f033af63cc9f887B50F", + "0x6ee26af75a47e15ab040af73cbdc077740f4825c", + "0xfbf427e3c5456b8f3b2e0b98145fa13c16747369", + "0xe0add3053b836e1e1006540b4de959f2b4472a24", + "0xfBf3140A02C1E8fe08a39b86b97c50175B698111", + "0x26A98513240401567733253C6CdD16F006fcd9b1", + "0xA74b60E368c7C870EeB581dc899cFA8158058bb8" + ] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/razor-network-voting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/razor-network-voting/index.ts new file mode 100644 index 00000000..f83d3671 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/razor-network-voting/index.ts @@ -0,0 +1,135 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +export const author = 'razor-network'; +export const version = '0.1.0'; + +const RAZOR_NETWORK_SUBGRAPH_URL = + 'https://graph-indexer.razorscan.io/subgraphs/name/razor/razor'; + +// a method to calculate corresponding razor amount for delegators +function sRZR_to_RZR( + stake: BigNumber, + totalSupply: BigNumber, + amount: BigNumber +) { + try { + return stake.mul(amount).div(totalSupply); + } catch (err) { + return BigNumber.from(0); + // do nothing + } +} + +function wei_to_ether(amount: number) { + return amount / 10 ** 18; +} + +export async function strategy( + space: any, + network: any, + provider: any, + addresses: any[], + options: any, + //symbol: string, + snapshot: string +) { + const params = { + delegators: { + __args: { + where: { + delegatorAddress_in: addresses + } // delegatorAddress + }, // Amount_Delegated + staker: { + totalSupply: true, + stake: true, + staker: true + }, + delegatorAddress: true, + sAmount: true + }, + stakers: { + __args: { + where: { + staker_in: addresses // stakerAddress + } + }, + stake: true, + totalSupply: true, + staker: true, + sAmount: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.delegators.__args.block = { number: snapshot }; + } + + const score = {}; + + // subgraph request 1 : it fetches all the details of the stakers and delegators. + const result = await subgraphRequest(RAZOR_NETWORK_SUBGRAPH_URL, params); + if (result.delegators || result.stakers) { + result.delegators.forEach( + async (delegator: { + sAmount: string; + staker: { stake: string; totalSupply: string; staker: string }; + delegatorAddress: string; + }) => { + const razor_amount = sRZR_to_RZR( + BigNumber.from(delegator.sAmount), + BigNumber.from(delegator.staker.totalSupply), + BigNumber.from(delegator.staker.stake) + ); + if ( + getAddress(delegator.delegatorAddress) != + getAddress(delegator.staker.staker) + ) { + //if delegator has delegated to more than one staker, we need to add that amount also to calculate score. + if (!score[getAddress(delegator.delegatorAddress)]) { + //if score[delegator] has no score setup already we will put it as intial amount + score[getAddress(delegator.delegatorAddress)] = wei_to_ether( + Number(razor_amount) + ); + } else { + // update the score of delegator by adding new Stoken -> razor Value + score[getAddress(delegator.delegatorAddress)] += wei_to_ether( + Number(razor_amount) + ); + } + } + } + ); + + // for stakers + result.stakers.forEach( + async (Staker: { + staker: string; + stake: string; + sAmount: string | number; + totalSupply: string; + }) => { + const razor_amount = sRZR_to_RZR( + BigNumber.from(Staker.sAmount), + BigNumber.from(Staker.totalSupply), + BigNumber.from(Staker.stake) + ); + //score will be based on the current stake in the block Number + if (!score[getAddress(Staker.staker)]) { + //if score[delegator] has no score setup already we will put it as intial amount + score[getAddress(Staker.staker)] = wei_to_ether(Number(razor_amount)); + } else { + // update the score of delegator by adding new Stoken -> razor Value + score[getAddress(Staker.staker)] += wei_to_ether( + Number(razor_amount) + ); + } + } + ); + } + + // it returns the array of scores. + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/README.md b/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/README.md new file mode 100644 index 00000000..24620446 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/README.md @@ -0,0 +1,11 @@ +This strategy returns the total dLP of a user + +Here is an example of parameters: + +{ + "rdnt": "0x3082cc23568ea640225c2467653db90e9250aaa0", + "lpToken": "0x32DF62DC3AED2CD6224193052CE665DC18165841", + "lockingContract": "0x76ba3eC5f5adBf1C58c91e86502232317EeA72dE", + "balancerPoolId": "0x32df62dc3aed2cd6224193052ce665dc181658410002000000000000000003bd", + "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + } diff --git a/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/examples.json new file mode 100644 index 00000000..cffa2db0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/examples.json @@ -0,0 +1,40 @@ +[ + { + "name": "RDNT in dLP - Arbitrum", + "strategy": { + "name": "rdnt-capital-voting", + "params": { + "rdnt": "0x3082cc23568ea640225c2467653db90e9250aaa0", + "lpToken": "0x32DF62DC3AED2CD6224193052CE665DC18165841", + "lockingContract": "0x76ba3eC5f5adBf1C58c91e86502232317EeA72dE", + "balancerPoolId": "0x32df62dc3aed2cd6224193052ce665dc181658410002000000000000000003bd", + "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + } + }, + "network": "42161", + "addresses": [ + "0x2eAA7327e9B5Ff46bc2B7452acE9e44A1528eb84", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e" + ], + "snapshot": 75307012 + }, + { + "name": "RDNT in dLP - BSC", + "strategy": { + "name": "rdnt-capital-voting", + "params": { + "rdnt": "0xf7DE7E8A6bd59ED41a4b5fe50278b3B7f31384dF", + "lpToken": "0x346575fc7f07e6994d76199e41d13dc1575322e1", + "lockingContract": "0x4FD9F7C5ca0829A656561486baDA018505dfcB5E" + } + }, + "network": "56", + "addresses": [ + "0x2b4516253e984b30804356cB5be476ccfB45fa49", + "0x31d64d45dE2c5b0742635da913FCD587BBCd4C5A", + "0x2eAA7327e9B5Ff46bc2B7452acE9e44A1528eb84" + ], + "snapshot": 26920121 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/index.ts new file mode 100644 index 00000000..72ad21a4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rdnt-capital-voting/index.ts @@ -0,0 +1,139 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, multicall, call } from '../../utils'; + +export const author = 'JDoy99'; +export const version = '0.1.0'; + +const erc20Abi = [ + 'function balanceOf(address owner) external returns (uint256)' +]; +const lpTokenAbi = [ + 'function getReserves() view returns (uint112, uint112, uint32)', + 'function token0() view returns (address)', + 'function totalSupply() view returns (uint256)' +]; +const mfdAbi = [ + 'function lockedBalances(address) view returns (uint256, uint256, uint256, uint256, tuple(uint256,uint256,uint256,uint256)[])' +]; +const balancerVaultAbi = [ + 'function getPoolTokenInfo(bytes32,address) view returns (uint256)' +]; + +const toJsNum = (bn: BigNumberish) => { + return parseFloat(formatUnits(bn)); +}; + +const rdntPerBalancerLpToken = async (network, provider, options, blockTag) => { + const rdntInVault = await call(provider, balancerVaultAbi, [ + options.balancerVault, + 'getPoolTokenInfo', + [options.balancerPoolId, options.rdnt], + { blockTag } + ]); + const rdntInLp = toJsNum(rdntInVault); + + const [totalSupplyBn] = await multicall( + network, + provider, + lpTokenAbi, + [[options.lpToken, 'totalSupply']], + { blockTag } + ); + + const totalSupply = toJsNum(totalSupplyBn[0]); + return rdntInLp / totalSupply; +}; + +const rdntPerUniLpToken = async (network, provider, options, blockTag) => { + const [totalSupplyBn, token0s, reserves] = await multicall( + network, + provider, + lpTokenAbi, + [ + [options.lpToken, 'totalSupply'], + [options.lpToken, 'token0'], + [options.lpToken, 'getReserves'] + ], + { blockTag } + ); + + const totalSupply = toJsNum(totalSupplyBn[0]); + const [reserve0, reserve1] = reserves; + const [token0] = token0s; + + let rdntInLp; + if (token0.toLowerCase() === options.rdnt.toLowerCase()) { + rdntInLp = toJsNum(reserve0); + } else { + rdntInLp = toJsNum(reserve1); + } + + return rdntInLp / totalSupply; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Get RDNT per LP token (LP provider dependent) + let rdntPerLp; + if (network === '42161') { + rdntPerLp = await rdntPerBalancerLpToken( + network, + provider, + options, + blockTag + ); + } else if (network === '56') { + rdntPerLp = await rdntPerUniLpToken(network, provider, options, blockTag); + } + + // console.log(`RDNT per LP token: ${rdntPerLp}`); + + // Get non-locked LP balances + const lpBalanceMulticall = new Multicaller(network, provider, erc20Abi, { + blockTag + }); + addresses.forEach((address) => + lpBalanceMulticall.call(address, options.lpToken, 'balanceOf', [address]) + ); + const lpBalances: Record = + await lpBalanceMulticall.execute(); + + // Get locked LP balances + const mfdMulticall = new Multicaller(network, provider, mfdAbi, { + blockTag + }); + addresses.forEach((address) => { + mfdMulticall.call(address, options.lockingContract, 'lockedBalances', [ + address + ]); + }); + const lockedBalances = await mfdMulticall.execute(); + + // Combined locked & unlocked LP balances for all users + // TODO: better way of handling this accumulation w/ new typed result obj + Object.keys(lockedBalances).forEach(function (key) { + if (lpBalances.hasOwnProperty(key)) { + lpBalances[key] = + toJsNum(lpBalances[key]) + toJsNum(lockedBalances[key][2]); + } else { + lpBalances[key] = toJsNum(lockedBalances[key][2]); + } + }); + + // User's total LP balance * RDNT per LP token => total RDNT in their LP positions + return Object.fromEntries( + Object.entries(lpBalances).map(([address, balance]) => [ + address, + balance * rdntPerLp + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/rebased/examples.json b/Implementations/API/backend/utils/snapshot/strategies/rebased/examples.json new file mode 100644 index 00000000..bfc39d66 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rebased/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Rebased", + "strategy": { + "name": "rebased", + "params": { + "symbol": "REB", + "uniswap": "0x54f5f952cca8888227276581F26978F99FDBa64E", + "sharePool": "0x574D80f005B9f5a26e6D4E0bcbD379EABD7edEb0", + "token": "0x87f5f9ebe40786d49d35e1b5997b07ccaa8adbff", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xde389fb34f54bec6d09bd98152a69be15c43163f", + "0xbe80fd79e26ce0a605e5d2803e876f1b009d70cc", + "0x55c307cbe54a1c1c105838a9d0fd60b75d7ff951", + "0x01BaD7E976d59CE92295Dbacd5da7fc06FE05412", + "0x64be824f732312490224a67537d49b8580068abf" + ], + "snapshot": 11941425 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/rebased/index.ts b/Implementations/API/backend/utils/snapshot/strategies/rebased/index.ts new file mode 100644 index 00000000..97923be5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rebased/index.ts @@ -0,0 +1,103 @@ +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'codingsh'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'addr', + type: 'address' + } + ], + name: 'totalStakedFor', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + multi.call('uniswapBalance', options.token, 'balanceOf', [options.uniswap]); + multi.call('uniswapTotalSupply', options.uniswap, 'totalSupply'); + addresses.forEach((address) => { + multi.call( + `scores.${address}.totalStaked`, + options.sharePool, + 'totalStakedFor', + [address] + ); + multi.call(`scores.${address}.uniswap`, options.uniswap, 'balanceOf', [ + address + ]); + multi.call(`scores.${address}.balance`, options.token, 'balanceOf', [ + address + ]); + }); + + const result = await multi.execute(); + const rebasedPerLP = result.uniswapBalance; + + return Object.fromEntries( + Array(addresses.length) + .fill('') + .map((_, i) => { + const lpBalances = result.scores[addresses[i]].uniswap; + const stakedLpBalances = result.scores[addresses[i]].totalStaked; + const tokenBalances = result.scores[addresses[i]].balance; + const lpBalance = lpBalances.add(stakedLpBalances); + const rebasedLpBalance = lpBalance + .add(tokenBalances) + .mul(rebasedPerLP) + .div(parseUnits('1', 18)); + return [ + addresses[i], + parseFloat(formatUnits(rebasedLpBalance, options.decimals)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/recusal-list/README.md b/Implementations/API/backend/utils/snapshot/strategies/recusal-list/README.md new file mode 100644 index 00000000..bd22d205 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/recusal-list/README.md @@ -0,0 +1,22 @@ +# recusal-list + +This is strategy for disallowing certain address from voting due to conflict of interest or other reasons for recusal. +You can pass any strategy as an optionnal parameter to combine the recusal with another one. + +Below is an example of parameters. The address list renotes which addresses to restrict. The strategy and its params defines the strategy to use if needed. + +```json +{ + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7" + ], + "strategy": { + "name": "erc721", + "params": { + "address": "0x3B8CeB26f4FabACbD02b22caeceeb26D67E4013A", + "symbol": "MLZ" + } + } +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/recusal-list/examples.json b/Implementations/API/backend/utils/snapshot/strategies/recusal-list/examples.json new file mode 100644 index 00000000..5668ac31 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/recusal-list/examples.json @@ -0,0 +1,29 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "recusal-list", + "params": { + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7" + ], + "strategy": { + "name": "erc721", + "params": { + "address": "0x3B8CeB26f4FabACbD02b22caeceeb26D67E4013A", + "symbol": "MLZ" + } + } + } + }, + "network": "1", + "addresses": [ + "0xc9417BaeF2f55583e52dD2eC64B74AcafE8A05db", + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 15008926 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/recusal-list/index.ts b/Implementations/API/backend/utils/snapshot/strategies/recusal-list/index.ts new file mode 100644 index 00000000..9a2b4e5c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/recusal-list/index.ts @@ -0,0 +1,44 @@ +import strategies from '..'; + +export const author = 'bshyong'; +export const version = '0.2.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const recusalList = + options?.addresses.map((address) => address.toLowerCase()) || []; + + if (options.strategy?.name) { + const result: { string: number } = await strategies[ + options.strategy.name + ].strategy( + space, + network, + provider, + addresses.filter( + (address: any) => !recusalList.includes(address.toLowerCase()) + ), + options.strategy.params, + snapshot + ); + return Object.fromEntries( + Object.entries(result).map(([address, value]) => [ + address, + recusalList.includes(address.toLowerCase()) ? 0 : value + ]) + ); + } else { + return Object.fromEntries( + addresses.map((address) => [ + address, + recusalList.includes(address.toLowerCase()) ? 0 : 1 + ]) + ); + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/reliquary/README.md b/Implementations/API/backend/utils/snapshot/strategies/reliquary/README.md new file mode 100644 index 00000000..10ded2c7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/reliquary/README.md @@ -0,0 +1,40 @@ +# reliquary + +This strategy utilizes Relic NFTs from Reliquary to calculate voting power. The strategy can be configured to either +use the level number to weight the voting power or the multiplier assigned to each level. + +If we use the levels strategy, the formula to calculate the voting power is + +`votingPower = level / maxVotingLevel * amount` + +or if we use multiplier + +`votingPower = levelMultiplier / maxLevelMultiplier * amount` + +In other words, if the nft has reached max voting level the voting power is equal to the amount deposited. + +Since Relic levels only update on an interaction, we have to chose if we want to use the current 'actual' level, or the level which the relic would +have after an update. This can be done using the `useLevelOnUpdate` flag. + +Configuration: +| property | type | value | description | +|------|---|---|---| +| reliquaryAddress | string | 0x12345... | address of reliquary contract +| poolId | number | 0...n | pool ID used for voting +| minVotingLevel | number | 0...n | min level required to vote +| maxVotingLevel | number | 0...n | max voting level +| decimals | number | 6..18 | number of decimals of the token deposited into this pool +| strategy | string | 'level' / 'multiplier' | which strategy to use to weight voting power +| useLevelOnUpdate | boolean | true / false | use hypothetical level after update + +```json +{ + "reliquaryAddress": "0xb0fc43069089d0fa02baaa896ac2efcb596d7d05", + "poolId": 1, + "minVotingLevel": 1, + "maxVotingLevel": 7, + "decimals": 18, + "strategy": "multiplier", + "useLevelOnUpdate": false +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/reliquary/examples.json b/Implementations/API/backend/utils/snapshot/strategies/reliquary/examples.json new file mode 100644 index 00000000..d7a2b892 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/reliquary/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Example configuration", + "strategy": { + "name": "reliquary", + "params": { + "reliquaryAddress": "0x1ed6411670c709F4e163854654BD52c74E66D7eC", + "poolId": 1, + "minVotingLevel": 1, + "maxVotingLevel": 7, + "decimals": 18, + "strategy": "multiplier", + "useLevelOnUpdate": false + } + }, + "network": "250", + "addresses": [ + "0x4fbe899d37fb7514adf2f41B0630E018Ec275a0C", + "0x945d88011cAC5FDc3eAF7DbA51592bfA98aEe91A", + "0x1E243A85822E2CD42C81359E0ea42033000D02a4", + "0xf7Ee8A9d014E9046D007bD448AaE7C667eF91E98" + ], + "snapshot": 54393483 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/reliquary/index.ts b/Implementations/API/backend/utils/snapshot/strategies/reliquary/index.ts new file mode 100644 index 00000000..d288fb55 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/reliquary/index.ts @@ -0,0 +1,164 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, call } from '../../utils'; + +export const author = '0xSkly'; // coAuthor = 'franzns' +export const version = '0.1.0'; + +const abi = [ + 'function relicPositionsOfOwner(address owner) view returns (uint256[] relicIds, tuple(uint256 amount, uint256 rewardDebt, uint256 rewardCredit, uint256 entry, uint256 poolId, uint256 level)[] positionInfos)', + 'function getLevelInfo(uint256 pid) view returns (tuple(uint256[] requiredMaturities, uint256[] multipliers, uint256[] balance) levelInfo)', + 'function levelOnUpdate(uint256 relicId) view returns (uint256 level)' +]; + +export async function strategy( + space: string, + network: string, + provider: StaticJsonRpcProvider, + addresses: string[], + options: { + reliquaryAddress: string; + poolId: number; + minVotingLevel: number; + maxVotingLevel: number; + decimals?: number; + strategy: 'level' | 'multiplier'; + useLevelOnUpdate?: boolean; + }, + snapshot?: number | string +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + for (const address of addresses) { + multi.call(address, options.reliquaryAddress, 'relicPositionsOfOwner', [ + address + ]); + } + + // first we get all relics for each voter + const relicPositionsByOwner: Record< + string, + [ + BigNumber[], //relicIds + { + amount: BigNumber; + rewardDebt: BigNumber; + rewardCredit: BigNumber; + entry: BigNumber; + poolId: BigNumber; + level: BigNumber; + }[] // correlating positions + ] + > = await multi.execute(); + + // then we filter by the configured pool ID + const relevantRelicPositions = Object.entries(relicPositionsByOwner).flatMap( + ([owner, [relicIds, positions]]) => + positions + .map((position, index) => ({ + owner, + relicId: relicIds[index].toNumber(), + poolId: position.poolId.toNumber(), + amount: position.amount, + level: position.level.toNumber() + })) + .filter((position) => position.poolId === options.poolId) + ); + + // if the strategy should use the level on update, we override the level + if (options.useLevelOnUpdate) { + for (const relicPosition of relevantRelicPositions) { + multi.call( + `${relicPosition.owner}.${relicPosition.relicId}.level`, + options.reliquaryAddress, + 'levelOnUpdate', + [relicPosition.relicId] + ); + } + + const relicLevelByVoterAndRelic: { + [owner: string]: { + [relicId: string]: { level: BigNumber }; + }; + } = await multi.execute(); + + for (const relicPosition of relevantRelicPositions) { + relicPosition.level = + relicLevelByVoterAndRelic[relicPosition.owner][ + relicPosition.relicId + ].level.toNumber(); + } + } + + const userVotingPower: Record = {}; + + /* + if we use the level strategy, we just add the level as a multiplier in relation to the max level. + So the formula used is: relicAmount * level / maxLevel + */ + if (options.strategy === 'level') { + for (const relicPosition of relevantRelicPositions) { + const multiplier = + relicPosition.level >= options.minVotingLevel + ? Math.min(options.maxVotingLevel, relicPosition.level) + : 0; + const votingPower = parseFloat( + formatUnits( + relicPosition.amount.mul(multiplier).div(options.maxVotingLevel), + options.decimals ?? 18 + ) + ); + + if (relicPosition.owner in userVotingPower) { + userVotingPower[relicPosition.owner] += votingPower; + } else { + userVotingPower[relicPosition.owner] = votingPower; + } + } + + return userVotingPower; + } + /* + otherwise we use the level multiplier to weight the voting power. For this + we need to get the multipliers for each level for the configured pool. + The formula used is: relicAmount * levelMultiplier / maxMultiplier + + */ + + const poolLevelInfo: { + requiredMaturities: BigNumber[]; + multipliers: BigNumber[]; + balance: BigNumber[]; + } = await call( + provider, + abi, + [options.reliquaryAddress, 'getLevelInfo', [options.poolId]], + { blockTag } + ); + + const maxMultiplier = poolLevelInfo.multipliers[options.maxVotingLevel]; + + for (const relicPosition of relevantRelicPositions) { + const multiplier = + poolLevelInfo.multipliers[ + Math.min(options.maxVotingLevel, relicPosition.level) + ].toNumber(); + + const votingPower = parseFloat( + formatUnits( + relicPosition.amount.mul(multiplier).div(maxMultiplier), + options.decimals ?? 18 + ) + ); + + if (relicPosition.owner in userVotingPower) { + userVotingPower[relicPosition.owner] += votingPower; + } else { + userVotingPower[relicPosition.owner] = votingPower; + } + } + return userVotingPower; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ren-nodes/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ren-nodes/examples.json new file mode 100644 index 00000000..f6d47801 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ren-nodes/examples.json @@ -0,0 +1,40 @@ +[ + { + "name": "Example query for ren-nodes on Mainnet", + "strategy": { + "name": "ren-nodes", + "params": { + "symbol": "REN" + } + }, + "network": "1", + "addresses": [ + "0x6f911B394745979667C2d2b2d1aB4588d93353a2", + "0x89aa6d95FA5345B81F4A2FD627117A6d3694300a", + "0xFA79618B49db3d4f7c5184D238d13EED7276219b", + "0xCd7e8A1018F9B9f773f2c2faDD44F54Ca0D94cE3", + "0x7f1D49272843186A125d7661e8140d612beC1889", + "0xa032EC8f225d572d14DCba06b20c748a3A0A0cCc", + "0x613FC06a5802a13CdAbC6B5E98dd01C5680c8Cce", + "0xdC27DB9B1bAeb85D7f53a98ea106f5C12c2cd286", + "0xD91B0C425B80103DfB174b2aB4c76Aa784cB5109", + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + ], + "snapshot": 12094900 + }, + { + "name": "Example query for ren-nodes on Kovan", + "strategy": { + "name": "ren-nodes", + "params": {} + }, + "network": "42", + "addresses": [ + "0x8eDF844Ae6BDBf6C1DF929D7d9E98743472142ac", + "0x797522Fb74d42bB9fbF6b76dEa24D01A538d5D66", + "0xD5B5b26521665Cb37623DCA0E49c553b41dbF076", + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" + ], + "snapshot": 24028500 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ren-nodes/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ren-nodes/index.ts new file mode 100644 index 00000000..e9e36b53 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ren-nodes/index.ts @@ -0,0 +1,113 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'noiach'; +export const version = '0.1.0'; + +/* + * A strategy based on the number of RenVM nodes an address has, and how long + * each node has been registered for. + */ + +const RENVM_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/renproject/renvm', + '42': 'https://api.thegraph.com/subgraphs/name/renproject/renvm-testnet' +}; + +// A month in seconds. +const MONTH = 28 * 24 * 60 * 60; + +// Subgraph restricts the number of results of a query to 1000 entities. +const QUERY_LIMIT = 1000; + +const RENVM_SUBGRAPH_QUERY = { + darknodes: { + __args: { + first: QUERY_LIMIT, + // Updated each loop. + skip: 0, + where: { + // Skip nodes that have been deregistered. + registeredAt_gt: 0, + // Updated below. + registeredAt_lte: undefined + } + }, + registeredAt: true, + deregisteredAt: true, + operator: true + } +}; + +interface SubgraphDarknode { + registeredAt: number; + deregisteredAt: number; + operator: string; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + if (snapshot !== 'latest') { + // @ts-ignore + RENVM_SUBGRAPH_QUERY.darknodes.__args.block = { number: snapshot }; + } + + // Filter out nodes that are still pending registration. + const timestamp = (await provider.getBlock(snapshot)).timestamp; + RENVM_SUBGRAPH_QUERY.darknodes.__args.where.registeredAt_lte = timestamp; + + let nodes: SubgraphDarknode[] = []; + // Loop through to fetch all `darknode` entities. At the time of writing, two + // requests are required. + while (nodes.length < 10000) { + // Skip the number nodes already seen. + RENVM_SUBGRAPH_QUERY.darknodes.__args.skip = nodes.length; + const result = await subgraphRequest( + RENVM_SUBGRAPH_URL[network], + RENVM_SUBGRAPH_QUERY + ); + if (result && result.darknodes) { + nodes = nodes.concat(result.darknodes); + } else { + break; + } + + // If the number of results returned was less than QUERY_LIMIT, then there + // are no more results to fetch. + if (result.darknodes.length < QUERY_LIMIT) { + break; + } + } + + // Initialize scores to 0 for each address in `addresses`. + const scores = addresses.reduce( + (obj, address) => ({ ...obj, [getAddress(address)]: 0 }), + {} + ); + nodes.forEach((darknode) => { + // Skip operators that aren't in `addresses`. + const nodeOperator = getAddress(darknode.operator); + if (scores[nodeOperator] === undefined) { + return; + } + + // Check that the darknode isn't deregistered. + if (darknode.deregisteredAt > 0 && darknode.deregisteredAt < timestamp) { + return; + } + + // Take square root of the number of months (rounded up) the node has been + // registered for. + const timeRegistered = timestamp - darknode.registeredAt; + const score = Math.sqrt(Math.ceil(timeRegistered / MONTH)); + + scores[nodeOperator] += score; + }); + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/README.md b/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/README.md new file mode 100644 index 00000000..2924af73 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/README.md @@ -0,0 +1,52 @@ +# REP3 Badges + +The Proof of Contribution Protocol (or PoCP) is the beta version of rep3's credentialing protocol. The rep3 DAO tool (live at https://app.rep3.gg/) is one possible implementation of the protocol to facilitate better member and contribution management for DAOs. + +Using the protocol, DAOs can give various types of badges to their members. These badges are organised in a parent-child relationship -- first, all members receive a membership badge (which is the parent badge), and then members can receive contribution badges (representing their work) or any other type of badge as designed by the community (for example, one-off badges to acknowledge exemplary work). The contribution badges and the custom badges are children to the membership badge. + +This architecture lets communities track and visualise their member activity and, on the other hand, lets community members build their portfolio of work through on-chain credentials. These naturally have a higher signalling value than more traditional types of portfolios like resumés. + +Badges given through the rep3 platform are also fully interoperable with several web3 tools, especially those that are used for gating resources behind a token. These badges, at a contract level, are custom implmentations of ERC-721 tokens. This is a deliberate decision to balance the trade-off between maintaining the integrity of these badges (w.r.t. the work they represent) and permitting key rotation by users. + +The rest of this document details the technical specifications and integration process of our protocol. Please note that this document might be confusing to understand or even have some outdated parts. We are in the process of updating these as we begin to focus on protocol-level integrations in addition to focussing on tool adoption. + +As always, do not hesitate at all to reach out to any team member in case you have a question. We usually reply within 12h and will be more than happy to address any questions you may have. + +Have a great day ahead! + +```json +{ + "symbol": "REP-3", + "rep3ContractAddress": "0x959a3e7e2ea1ea56c9127ae9ed271ede495d145f", + "erc20Token": "0x6b175474e89094c44da98b954eedeac495271d0f", + "erc20Symbol": "DAI", + "erc20Decimal": 18, + "subgraphNetwork": 80001, + "specs": [ + { + "type": 0, + "level": 1, + "weight": 5 + }, + { + "type": 0, + "level": 2, + "weight": 10 + }, + { + "type": 0, + "level": 3, + "weight": 15 + }, + { + "type": 0, + "level": 4, + "weight": 20 + }, + { + "type": 1, + "weight": 2 + } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/examples.json b/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/examples.json new file mode 100644 index 00000000..bda5d22f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/examples.json @@ -0,0 +1,105 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "rep3-badges", + "params": { + "symbol": "REP-3", + "rep3ContractAddress": "0x959a3e7e2ea1ea56c9127ae9ed271ede495d145f", + "erc20Token": "0x6b175474e89094c44da98b954eedeac495271d0f", + "erc20Symbol": "DAI", + "erc20Decimal": 18, + "subgraphNetwork": 80001, + "specs": [ + { + "type": 0, + "level": 1, + "weight": 5 + }, + { + "type": 0, + "level": 2, + "weight": 10 + }, + { + "type": 0, + "level": 3, + "weight": 15 + }, + { + "type": 0, + "level": 4, + "weight": 20 + }, + { + "type": 1, + "weight": 2 + } + ] + } + }, + "network": "1", + "addresses": [ + "0x565CBd65Cb3e65445AfD14169003A528C985e9C7", + "0xD1fe5790AD50C9E8d2e9F03f4f42A09a9F59dec3", + "0x56d0b5ed3d525332f00c9bc938f93598ab16aaa7", + "0x49e4dbff86a2e5da27c540c9a9e8d2c3726e278f", + "0x4757ce43dc5429b8f1a132dc29ef970e55ae722b", + "0xd7539FCdC0aB79a7B688b04387cb128E75cb77Dc", + "0x6E33e22f7aC5A4b58A93C7f6D8Da8b46c50A3E20", + "0xC9dA7343583fA8Bb380A6F04A208C612F86C7701", + "0x2AC89522CB415AC333E64F52a1a5693218cEBD58" + ], + "snapshot": 15874502 + }, + { + "name": "Example query", + "strategy": { + "name": "rep3-badges", + "params": { + "symbol": "REP-3", + "rep3ContractAddress": "0x959a3e7e2ea1ea56c9127ae9ed271ede495d145f", + "subgraphNetwork": 80001, + "specs": [ + { + "type": 0, + "level": 1, + "weight": 5 + }, + { + "type": 0, + "level": 2, + "weight": 10 + }, + { + "type": 0, + "level": 3, + "weight": 15 + }, + { + "type": 0, + "level": 4, + "weight": 20 + }, + { + "type": 1, + "weight": 2 + } + ] + } + }, + "network": "1", + "addresses": [ + "0x565CBd65Cb3e65445AfD14169003A528C985e9C7", + "0xD1fe5790AD50C9E8d2e9F03f4f42A09a9F59dec3", + "0x56d0b5ed3d525332f00c9bc938f93598ab16aaa7", + "0x49e4dbff86a2e5da27c540c9a9e8d2c3726e278f", + "0x4757ce43dc5429b8f1a132dc29ef970e55ae722b", + "0xd7539FCdC0aB79a7B688b04387cb128E75cb77Dc", + "0x6E33e22f7aC5A4b58A93C7f6D8Da8b46c50A3E20", + "0xC9dA7343583fA8Bb380A6F04A208C612F86C7701", + "0x2AC89522CB415AC333E64F52a1a5693218cEBD58" + ], + "snapshot": 15864502 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/index.ts b/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/index.ts new file mode 100644 index 00000000..b36ed2d2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rep3-badges/index.ts @@ -0,0 +1,222 @@ +import { error } from 'console'; +import { subgraphRequest } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'eth-jashan'; +export const version = '1.0.0'; + +const REP3_SUBGRAPH_API_URLS_BY_CHAIN_ID = { + '80001': 'https://api.thegraph.com/subgraphs/name/eth-jashan/rep3-mumbai', + '137': 'https://api.thegraph.com/subgraphs/name/eth-jashan/rep3-matic' +}; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +// subgraph helper function to get Membership Badges +function fetchMembershipsForAddress( + network: string, + contractAddress: string, + + blockTag: number | string +): Promise { + const url = REP3_SUBGRAPH_API_URLS_BY_CHAIN_ID[network]; + + if (url == undefined) { + throw new error(`Unsupported network with id: ${network}`); + } + + const query = { + membershipNFTs: { + __args: { + where: { + contractAddress: contractAddress + }, + block: blockTag != 'latest' ? { number: blockTag } : null + }, + time: true, + level: true, + tokenID: true, + claimer: true + } + }; + + return subgraphRequest(url, query); +} + +// subgraph helper function to get Association Badges + +function fetchAssociationBadgesForAddress( + network: string, + contractAddress: string, + blockTag: number | string +): Promise { + const url = REP3_SUBGRAPH_API_URLS_BY_CHAIN_ID[network]; + + if (url == undefined) { + throw new error(`Unsupported network with id: ${network}`); + } + + const query = { + associationBadges: { + __args: { + where: { + contractAddress: contractAddress + }, + block: blockTag != 'latest' ? { number: blockTag } : null + }, + type: true, + tokenID: true, + claimer: true + } + }; + + return subgraphRequest(url, query); +} + +// Combining ERC20 Balances and Weights from Rep3 Badges + +function applyBadgeWeights(badges: any[], erc20Balance: any, options: any) { + const badgeWeights = {}; + badges.forEach((badge: any) => { + if (badge?.level) { + const levelWeight = options.specs.find( + (spec) => spec.level === parseInt(badge?.level) + ); + if (badgeWeights[getAddress(badge.claimer)]) { + badgeWeights[getAddress(badge.claimer)] = + badgeWeights[getAddress(badge.claimer)] + levelWeight.weight; + } else { + badgeWeights[getAddress(badge.claimer)] = levelWeight.weight; + } + } else if (badge?.type) { + const levelWeight = options.specs.find( + (spec) => spec.type === parseInt(badge?.type) + ); + if (badgeWeights[getAddress(badge.claimer)]) { + badgeWeights[getAddress(badge.claimer)] = + badgeWeights[getAddress(badge.claimer)] + levelWeight.weight; + } else { + badgeWeights[getAddress(badge.claimer)] = levelWeight.weight; + } + } + }); + if (options?.erc20Token) { + Object.keys(erc20Balance).forEach((key) => { + if (badgeWeights[key]) { + if (erc20Balance[key]) { + erc20Balance[key] = erc20Balance[key] * badgeWeights[key]; + } else { + erc20Balance[key] = badgeWeights[key]; + } + } + }); + } + + return options?.erc20Token ? erc20Balance : badgeWeights; +} + +// helper function to get ERC20 balances for addresses +async function getErc20Balance( + network, + provider, + abi, + blockTag, + addresses, + options +): Promise { + if (options.erc20Token) { + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + multi.call(address, options.erc20Token, 'balanceOf', [address]) + ); + try { + const result: any = await multi.execute(); + Object.keys(result).forEach((key) => { + result[key] = parseFloat( + formatUnits(result[key], options.erc20Decimal) + ); + }); + + return result; + } catch (error: any) { + return {}; + } + } else { + return {}; + } +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const erc20BalanceOf: any = await getErc20Balance( + network, + provider, + abi, + blockTag, + addresses, + options + ); + + let associationBadges: any[] = []; + + const allMembershipbadges = await fetchMembershipsForAddress( + options.subgraphNetwork, + options.rep3ContractAddress, + 'latest' + ); + const allAssociationBadges = await fetchAssociationBadgesForAddress( + options.subgraphNetwork, + options.rep3ContractAddress, + + 'latest' + ); + + const validMembershipNft: any[] = addresses.map((address: string) => { + const membershipNftForAddress = allMembershipbadges.membershipNFTs.filter( + (x) => getAddress(x.claimer) === address + ); + const associationBadgeForAddress = + allAssociationBadges.associationBadges.filter( + (x) => getAddress(x.claimer) === address + ); + if (associationBadgeForAddress.length > 0) { + associationBadges = associationBadges.concat(associationBadgeForAddress); + } + if (membershipNftForAddress.length > 1) { + const latestMembership = allMembershipbadges.membershipNFTs.sort( + (p1, p2) => (p1.time < p2.time ? 1 : p1.time > p2.time ? -1 : 0) + ); + return latestMembership[0]; + } else if (membershipNftForAddress.length === 1) { + return membershipNftForAddress[0]; + } + }); + + let allWeightableBadges = validMembershipNft.concat(associationBadges); + allWeightableBadges = allWeightableBadges.filter((x) => x !== undefined); + let badgeWeights = {}; + if (!allWeightableBadges) return badgeWeights; + + badgeWeights = applyBadgeWeights( + allWeightableBadges, + erc20BalanceOf, + options + ); + + return Object.fromEntries( + addresses.map((address: string) => [address, badgeWeights[address] || 0]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/README.md b/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/README.md new file mode 100644 index 00000000..9ef44e0e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/README.md @@ -0,0 +1,49 @@ +# Reverse Voting Escrow Strategy + +This strategy calculates voting power using the token contract address and a vesting contract. + +It is custom-built for [SeedClub](https://seedclub.xyz/). + +## Overview + +There are 3 states votable tokens can be in: +- In a users wallet +- Unclaimed in the vesting contract but vested +- Unvested in the vesting contract + +When a user connects to snapshot to vote on a Seed Club proposal their voting power should be calculated as follows. + +**Voting power formula** +```md +Voting power = tokens in wallet + unclaimed tokens in vesting contract + vesting tokens * 0.1 +``` + + +## Contract Details + +[$CLUB Contract](https://etherscan.io/address/0xf76d80200226ac250665139b9e435617e4ba55f9) +[Vesting Contract](https://etherscan.io/address/0xD46f00d9F1f6d2e65D9572F9ce283ba925FE591a) +[Vesting Backend](https://github.com/agoraxyz/club-backend#endpoints) (hosted at club.agora.space/api/ ) + +Vesting data is currently stored off-chain on a backend. All the data is encoded in a merkle tree and only the root is stored in the contract. When someone wants to claim, their data and proof are checked if they are a valid node of the tree. + + +## Contributing & Issues + +Contribute to this strategy on [Nascent's](https://nascent.xyz) Snapshot Strategies Github fork: [nascentxyz/snapshot-strategies](https://github.com/nascentxyz/snapshot-strategies). + +For technical assistance, reach out to [@andreasbigger](https://twitter.com/andreasbigger) on twitter or email `andreas@nascent.xyz`. + +## Custom Testing + +Install `ts-node` if it's not already installed on your machine: +```sh +npm install -g typescript +npm install -g ts-node +``` + +Then, run the test script from the root project directory: +```sh +ts-node src/strategies/reverse-voting-escrow/test.ts +``` + diff --git a/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/examples.json b/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/examples.json new file mode 100644 index 00000000..4d838750 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Reverse Voting Escrow Strategy Example", + "strategy": { + "name": "reverse-voting-escrow", + "params": { + "symbol": "RVE", + "club": "0xF76d80200226AC250665139B9E435617e4Ba55F9", + "vesting": "0xD46f00d9F1f6d2e65D9572F9ce283ba925FE591a", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x05e57688C639b0742ea3E940b4E9DC0fb69B1B88", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x135C21b2DA426760718E39DA954974c4572AE9f6" + ], + "snapshot": 14490773 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/index.ts b/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/index.ts new file mode 100644 index 00000000..b12ce4ee --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/reverse-voting-escrow/index.ts @@ -0,0 +1,183 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; +import fetch from 'cross-fetch'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'nascentxyz'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address owner) external view returns (uint256)', + 'function getClaimableAmount(bytes32 cohortId, uint256 index, address account, uint256 fullAmount) external view returns (uint256)', + 'function getClaimed(bytes32 cohortId, address account) public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // ** The full address -> balance mapping ** // + const reverseVotingBalance = {} as Record; + + // ** Extract `club` and `vesting` contract addresses ** // + // ** If not provided, default to hardcoded values ** // + const club = options.club + ? options.club + : '0xF76d80200226AC250665139B9E435617e4Ba55F9'; + const vesting = options.vesting + ? options.vesting + : '0xD46f00d9F1f6d2e65D9572F9ce283ba925FE591a'; + + // ** $CLUB balance + const callWalletToClubBalance = new Multicaller(network, provider, abi, { + blockTag + }); + for (const walletAddress of addresses) { + callWalletToClubBalance.call(walletAddress, club, 'balanceOf', [ + walletAddress + ]); + } + const walletToClubBalance: Record = + await callWalletToClubBalance.execute(); + + for (const [walletID, balance] of Object.entries(walletToClubBalance)) { + const address = walletID.split('-')[0]; + reverseVotingBalance[address] = reverseVotingBalance[address] + ? reverseVotingBalance[address].add(balance) + : balance; + } + + // ?? Available CLUB Agora API Endpoints ?? // + // ?? https://github.com/agoraxyz/club-backend#endpoints ?? // + // [GET] hello?name=World: a test endpoint with an optional parameter + // [GET] cohort/:cohortId: checks if a specific cohort with cohortId exists in the database or the contract. If both are true, returns it's data + // [GET] cohort-ids/:account: returns the IDs of the cohorts the account is in + // [GET] claim-data/:cohortId/:account: returns the claim data for a specific account in a specific cohort (cohortId) + // [GET] all-claim-data/:account: returns the claim data for a specific account from all the cohorts it is in + // [GET] all-data: returns all claim data + + const allData = await fetch(`https://club.agora.space/api/all-data`); + const allDataJSON = await allData.json(); + + // ** Claimed $CLUB tokens ** // + const getWalletToClaimedAmount = new Multicaller(network, provider, abi, { + blockTag + }); + + // ** Vested Mapping ** // + let walletToVestedAmount: any = {} as Record; + + // ** Loop over all claim data ** // + for (const [cohortId, cohortData] of Object.entries(allDataJSON)) { + // ** Claimable (vested) $CLUB tokens ** // + const getWalletToVestedAmount = new Multicaller(network, provider, abi, { + blockTag + }); + + // ** Iterate over addresses in the cohort with claims ** // + for (const [address, amountData] of Object.entries(cohortData as any)) { + if (addresses.includes(address)) { + // ** Extract the full amount of CLUB ** // + const amount: any = amountData; + const fullAmount: BigNumber = amount.amount + ? BigNumber.from(amount.amount) + : BigNumber.from(0); + const indexData: any = amountData; + const index: BigNumber = indexData.index + ? BigNumber.from(amount.index) + : BigNumber.from(0); + + // ** Create The Vested Amount Call ** // + getWalletToVestedAmount.call(address, vesting, 'getClaimableAmount', [ + cohortId, // cohortId + index, // index + address, // account + fullAmount // fullAmount + ]); + + // ** Create The Claimed Call ** // + getWalletToClaimedAmount.call(address, vesting, 'getClaimed', [ + cohortId, // cohortId + address // account + ]); + } + } + + // ** Execute the vested multicall ** // + try { + // This should return a mapping of wallet addresses to vested amounts + const tempWalletVestedAmounts: Record = + await getWalletToVestedAmount.execute(); + walletToVestedAmount = Object.assign( + walletToVestedAmount, + tempWalletVestedAmounts + ); + } catch (e) { + // !! IGNORE Multicall REVERTS !! // + } + } + + // ** Execute the claimed multicall ** // + const walletToClaimedAmount: Record = + await getWalletToClaimedAmount.execute(); + + // ** Map address to its full amount of claimable tokens ** // + const listOfFullAmounts = Object.entries(allDataJSON) + .map(([cohortId, cohortData]: any) => + Object.entries(cohortData).map(([address, data]: any) => [ + cohortId, + address, + data.amount, + data.index + ]) + ) + .reduce((prev, curr) => { + return prev.concat(curr); + }); + const fullAmounts: Map = new Map( + listOfFullAmounts.map(([, address, amount]) => [address, amount]) + ); + + // ** Iterate over the full amounts mapping from address -> total claim amount **// + fullAmounts.forEach((amount, address) => { + const vestedBalance: BigNumber = + walletToVestedAmount[address] || BigNumber.from(0); + const claimedBalance: BigNumber = + walletToClaimedAmount[address] || BigNumber.from(0); + + // ?? voting_power = current_balance + + // ?? vested_balance + + // ?? 0.1 * (fullAmount - vestedBalance - claimedBalance) + + const vestingPower = BigNumber.from(amount) + .sub(vestedBalance) + .sub(claimedBalance) + .div(BigNumber.from(10)); + const addedVotingPower = vestedBalance.add(vestingPower); + reverseVotingBalance[address] = reverseVotingBalance[address] + ? reverseVotingBalance[address].add(addedVotingPower) + : addedVotingPower; + }); + + // ** Return address, balance mapping ** // + const scores = Object.fromEntries( + addresses.map((address) => [ + address, + reverseVotingBalance[address] + ? parseFloat( + formatUnits( + reverseVotingBalance[address].toString(), + options.decimals + ) + ) + : 0 + ]) + ); + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/revest/examples.json b/Implementations/API/backend/utils/snapshot/strategies/revest/examples.json new file mode 100644 index 00000000..7bb16d30 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/revest/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "revest", + "params": { + "address": "0xe952bda8c06481506e4731C4f54CeD2d4ab81659", + "symbol": "RVST", + "tokenVault": "0xA81bd16Aa6F6B25e66965A2f842e9C806c0AA11F", + "staking": "0xbcbb435cf6f664caa5222c3ee01d1a377f12c428", + "token": "0x120a3879da835A5aF037bB2d1456beBd6B54d4bA", + "lpToken": "0x6490828Bd87Be38279A36F029f3b9Af8b4E14B49" + } + }, + "network": "1", + "addresses": [ + "0xd08b7e82942fac71d96fecaa99ed7323a95d9a79", + "0xD76F585b6B94202430875aE748fF8C038Dc64111", + "0x013040bcc92ca0bec2670d61f06da7c36678222a", + "0x9f75d69380a505a4e2AedB5C2dfdf41809E4D6C6", + "0xD43e2ca9Ff4653Ef024BD26df925524bE4498C0B", + "0x677b038Bc2DB99851be553673d0cBf8Cac6A0F3F", + "0xcf7434dBB0f4F61855ADF5e9A3c117d9B5fAe903", + "0xCedec45e06cBB9032625830C5FFB0CF3fAD1cF67", + "0x4D3f9F513FC366Aa075e53B592dCE9c14BF838A6", + "0x7E22AebC01ef48A2A6D39EaDF37b3BFac17C9649", + "0xa4e7f2a1edb5ad886baa09fb258f8aca7c934ba6", + "0xff80adc3eb2b549e9edf4b7109efe7487c805f85", + "0xd9d455a8b8b9aeda2da66c52b80c90ef423409df", + "0x000000000000000000000000000000000000dead" + ], + "snapshot": 14124450 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/revest/index.ts b/Implementations/API/backend/utils/snapshot/strategies/revest/index.ts new file mode 100644 index 00000000..c457dbd8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/revest/index.ts @@ -0,0 +1,163 @@ +import { defaultAbiCoder } from '@ethersproject/abi'; +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall } from '../../utils'; +import { subgraphRequest } from '../../utils'; + +export const author = 'RobAnon'; +export const version = '0.1.0'; + +export const SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/alexvorobiov/eip1155subgraph' +}; + +const abi2 = [ + 'function getDisplayValues(uint fnftId, uint) external view returns (bytes memory)', + 'function rewardsHandlerAddress() external view returns (address)', + 'function getAsset(uint fnftId) external view returns (address)' +]; + +const abi = [ + 'function getFNFT(uint fnftId) external view returns (tuple(address asset, address pipeToContract, uint depositAmount, uint depositMul, uint split, uint depositStopTime, bool maturityExtension, bool isMulti, bool nontransferrable))' +]; + +const abi3 = [ + 'function totalLPAllocPoint() external view returns (uint)', + 'function totalBasicAllocPoint() external view returns (uint)' +]; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const PRECISION = BigNumber.from('1000000000000000000000'); + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const eip1155OwnersParams = { + accounts: { + __args: { + where: { + id_in: addresses.map((a) => a.toLowerCase()) + } + }, + id: true, + balances: { + value: true, + token: { + registry: { + id: true + }, + identifier: true + } + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + eip1155OwnersParams.accounts.__args.block = { number: snapshot }; + } + const idsToCheck = new Set(); + + const result = await subgraphRequest( + SUBGRAPH_URL[network], + eip1155OwnersParams + ); + result.accounts.forEach((element) => { + element.relBal = element.balances.filter((balance) => { + const isRightAddress = + balance.token.registry.id.toLowerCase() === + options.address.toLowerCase() && balance.value != '0'; + if (isRightAddress) { + idsToCheck.add(balance.token.identifier); + } + return isRightAddress; + }); + }); + let ids = Array.from(idsToCheck); + const response = await multicall( + network, + _provider, + abi2, + ids.map((id: any) => [options.staking, 'getDisplayValues', [id, 0]]), + { blockTag } + ); + + const fnfts = Object.fromEntries( + response.map((value, i) => [ + ids[i], + defaultAbiCoder.decode(['uint', 'uint'], value[0])[0] + ]) + ); + Object.keys(fnfts).forEach((element) => { + if (fnfts[element].eq('0')) { + delete fnfts[element]; + } + }); + ids = Object.keys(fnfts); + const response2 = await multicall( + network, + _provider, + abi, + ids.map((id: any) => [options.tokenVault, 'getFNFT', [id]]), + { blockTag } + ); + const completeFNFTs = Object.fromEntries( + response2.map((value, i) => [ + ids[i], + { + allocPoints: fnfts[ids[i]], + isRVST: value[0].asset.toLowerCase() == options.token.toLowerCase() + } + ]) + ); + let rewards = await multicall( + network, + _provider, + abi2, + [''].map(() => [options.staking, 'rewardsHandlerAddress', []]), + { blockTag } + ); + rewards = rewards[0][0]; + let allocLP = await multicall( + network, + _provider, + abi3, + [ + [rewards, 'totalLPAllocPoint', []], + [rewards, 'totalBasicAllocPoint', []] + ], + { blockTag } + ); + const allocToken = allocLP[1][0]; + allocLP = allocLP[0][0]; + + //allocToken = allocToken[0][0]; + + const finalResult = {}; + result.accounts.forEach((account) => { + account.relBal.forEach((relBalEle) => { + if (completeFNFTs.hasOwnProperty(relBalEle.token.identifier)) { + const score = completeFNFTs[relBalEle.token.identifier].allocPoints + .mul(PRECISION) + .div( + completeFNFTs[relBalEle.token.identifier].isRVST + ? allocToken + : allocLP + ); + if (finalResult.hasOwnProperty(getAddress(account.id))) { + finalResult[getAddress(account.id)].add(score); + } else { + finalResult[getAddress(account.id)] = score; + } + } + }); + }); + const returnVals = {}; + Object.keys(finalResult).forEach((element) => { + returnVals[element] = parseInt(finalResult[element].toString(), 10); + }); + return returnVals; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/README.md b/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/README.md new file mode 100644 index 00000000..ebd32310 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/README.md @@ -0,0 +1,10 @@ +# riskharbor-underwriter + +This strategy allows underwriters in a given Risk Harbor vault to vote based on the shares issued to them across their various positions in that vault. This strategy works by querying the vault's subgraph to compute how many shares each user holds and divides that amount by the decimals in the underwriting asset. + +```json +{ + "SUBGRAPH_URL": "https://api.thegraph.com/subgraphs/name/some-protocol/v1-protocol", + "VAULT_ADDR": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/examples.json b/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/examples.json new file mode 100644 index 00000000..154fe0f7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "riskharbor-underwriter", + "params": { + "SUBGRAPH_URL": "https://api.thegraph.com/subgraphs/name/risk-harbor/v2-arbitrum", + "VAULT_ADDR": "0xbcA81A2118982182d897845571BE950aE94C619c" + } + }, + "network": "42161", + "addresses": [ + "0xD4D4e905d7F1Eb095769fAce2C2bE516865E4981", + "0xd401c5B54A079420C6C7D9405faFc9a10CD8a4ed", + "0x010dab3779810cf08ac213b9efa915821bb43e26", + "0x0dcdd4f4a70ebb2eaffd5a01bd6cacde14dae4f0", + "0xfc9bffa77c2b725add71f4ad88bbe228d5601eb3", + "0xc2e63f57958d5ad5ced8fc18a7b763d9c8327237" + ], + "snapshot": 22260293 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/index.ts b/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/index.ts new file mode 100644 index 00000000..a615a771 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/index.ts @@ -0,0 +1,79 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; + +export const author = 'dewpe'; +export const version = '0.1.1'; + +export async function strategy( + _space, + _network, + _provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const positionsQuery = { + underwriterPositions: { + __args: { + where: { + shares_not: '0', + vault: options.VAULT_ADDR.toLowerCase(), + user_in: addresses.map((addr: string) => addr.toLowerCase()) + }, + block: blockTag != 'latest' ? { number: blockTag } : null, + first: 1000 + }, + shares: true, + user: { + id: true + } + } + }; + + const decimalsQuery = { + vault: { + __args: { + id: options.VAULT_ADDR.toLowerCase(), + block: blockTag != 'latest' ? { number: blockTag } : null + }, + underwritingToken: { + decimals: true + } + } + }; + + const decimals = (await subgraphRequest(options.SUBGRAPH_URL, decimalsQuery)) + .vault.underwritingToken.decimals; + + const positions = ( + await subgraphRequest(options.SUBGRAPH_URL, positionsQuery) + ).underwriterPositions; + + // Go through each position and reduce it down to the form: + // userAddr: balance + const agUserBals: Record = {}; + positions.forEach((position) => { + const shares = BigNumber.from(position.shares); + if (shares.isZero()) return; + // If key already has a value, then increase it + if (agUserBals[position.user.id]) { + agUserBals[position.user.id] = ( + agUserBals[position.user.id] as BigNumber + ).add(shares); + } else { + agUserBals[position.user.id] = shares; + } + }); + + return Object.fromEntries( + Object.entries(agUserBals).map(([address, balance]) => [ + getAddress(address), + // Divide each bal by 1eDecimals + parseFloat(formatUnits(balance, decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/schema.json b/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/schema.json new file mode 100644 index 00000000..78f56d80 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/riskharbor-underwriter/schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "SUBGRAPH_URL": { + "type": "string", + "title": "Subgraph url", + "examples": [ + "https://api.thegraph.com/subgraphs/name/some-protocol/v1-protocol" + ] + }, + "VAULT_ADDR": { + "type": "string", + "title": "Vault address", + "examples": ["e.g. 0xbcA81A2118982182d897845571BE950aE94C619c"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["SUBGRAPH_URL", "VAULT_ADDR"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/rnbw-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/rnbw-balance/examples.json new file mode 100644 index 00000000..ea1368e2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rnbw-balance/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "halodao.eth", + "strategy": { + "name": "rnbw-balance", + "params": { + "token": "0x90ac0B8f17cE29Ed6A1CDb78eE2150927b3c0Cd5", + "symbol": "RNBW", + "decimals": 18 + } + }, + "network": "42", + "addresses": ["0x30Ca8F1e13978a7A819af012665CFa7E49F1c554"], + "snapshot": 25241929 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/rnbw-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/rnbw-balance/index.ts new file mode 100644 index 00000000..22943d57 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rnbw-balance/index.ts @@ -0,0 +1,72 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'mystbrent'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'getCurrentHaloHaloPrice', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + addresses.forEach((address) => { + multi.call(`scores.${address}.dsrtBalance`, options.token, 'balanceOf', [ + address + ]); + }); + + multi.call('dsrtPrice', options.token, 'getCurrentHaloHaloPrice'); + + const result = await multi.execute(); + const dsrtPrice = result.dsrtPrice; + + return Object.fromEntries( + Array(addresses.length) + .fill('') + .map((_, i) => { + const dsrtBalances = result.scores[addresses[i]].dsrtBalance; + return [ + addresses[i], + parseFloat(formatUnits(dsrtBalances.mul(dsrtPrice), 36)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/README.md b/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/README.md new file mode 100644 index 00000000..648d36a6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/README.md @@ -0,0 +1,13 @@ +# rocketpool-node-operator + +This is a strategy for staking node operators, it returns the half square rooted node effective stake balance given a node address. + +Here is an example of parameters: + +```json +{ + "address": "0xD33526068D116cE69F19A9ee46F0bd304F21A51f", + "symbol": "RPL", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/examples.json b/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/examples.json new file mode 100644 index 00000000..d46f8676 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/examples.json @@ -0,0 +1,33 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "rocketpool-node-operator", + "params": { + "address": "0xD33526068D116cE69F19A9ee46F0bd304F21A51f", + "symbol": "RPL", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x17Fa597cEc16Ab63A7ca00Fb351eb4B29Ffa6f46", + "0xca317A4ecCbe0Dd5832dE2A7407e3c03F88b2CdD", + "0x327260c50634136551bfE4e4eB082281555AAfAE", + "0x5d8172792a9e649053c07366E3a7C24a37F0C534", + "0x701F4dcEAD1049FA01F321d49F6dca525cF4A5A5", + "0xb8ed9ea221bf33d37360A76DDD52bA7b1E66AA5C", + "0xbfaf9BFa09F26EF8104A6d5FF09afdCC9300E5bc", + "0x174E0b45C03318B0C9bc03573028605B26764931", + "0x5f4cb66c9b1ed8a4758a059fdb10e0f72c307d8a", + "0x24609303b67051ef77735e34d671e2a13e3da35d", + "0xe35854cde18a3cc4706134b4850dd861a55b9a30", + "0x53938f795ab6c57070aad32905a70a2e5961a887", + "0xD6527Bd3d62f1Da520E6f74B89EBD8F8cD04564f", + "0xf8bFf17a1C9dfC632F6C905d12C404AfE451B16c", + "0x689c6853f3debac91b72f32bafa83200eec9613c", + "0xaebb400542598e6ee58b2fdf2e7425c07e8ba68d" + ], + "snapshot": 14490773 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/index.ts b/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/index.ts new file mode 100644 index 00000000..b687bb50 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rocketpool-node-operator/index.ts @@ -0,0 +1,48 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'rocket-pool'; +export const version = '0.1.1'; + +const rocketNodeStakingAddress = '0x3019227b2b8493e45Bf5d25302139c9a2713BF15'; +const rocketNodeStakingContractAbi = [ + 'function getNodeEffectiveRPLStake(address _nodeAddress) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const effectiveStake = new Multicaller( + network, + provider, + rocketNodeStakingContractAbi, + { blockTag } + ); + + addresses.forEach((address) => { + effectiveStake.call( + address, + rocketNodeStakingAddress, + 'getNodeEffectiveRPLStake', + [address] + ); + }); + + const effectiveStakeResponse: Record = + await effectiveStake.execute(); + + return Object.fromEntries( + Object.entries(effectiveStakeResponse).map(([address, balance]) => [ + address, + Math.sqrt(parseFloat(formatUnits(balance, options.decimals))) / 2 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/README.md b/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/README.md new file mode 100644 index 00000000..cc604911 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/README.md @@ -0,0 +1,39 @@ +# Rowdy Roos Voting Strategy + +This strategy can check the claimed balance and unclaimed balance of staking rewards, based off the Rowdy Roos staking contract. This means that it will include the balance that is still pending in the contract. + +The staking contract is expected to have the following two functions: +- `function getStakedTokens(address _owner) external view returns (uint16[] memory)` +- `function currentRewardsOf(uint16 _tokenId) public view returns (uint256)` + +The strategy returns the total of claimed and unclaimed rewards token. + +## Accepted options + +- **staking:** The address of the staking contract + +- **token:** The address of the rewards ERC20 token + +- **symbol:** The symbol of the rewards token + +- **decimals** Decimals for ERC-20 + +## Examples + +```JSON + +[ + { + "name": "Balance and Staking Unclaimed", + "strategy": { + "name": "balance-and-staking-unclaimed", + "params": { + "staking": "0xcC5CcdcbB9C4bc26e387052a94FA93b8890D5693", + "token": "0x2Af3cc814B0a10ABeD25C62b9bB679Da667E4bda", + "symbol": "BOBL", + "decimals": 18 + } + } + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/examples.json b/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/examples.json new file mode 100644 index 00000000..24deff43 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Rowdy Roos", + "strategy": { + "name": "rowdy-roos", + "params": { + "staking": "0xcC5CcdcbB9C4bc26e387052a94FA93b8890D5693", + "token": "0x2Af3cc814B0a10ABeD25C62b9bB679Da667E4bda", + "symbol": "BOBL", + "decimals": 18 + } + }, + "network": "1", + "addresses": ["0x9E2Ae5480ca4933149E6E1D834b3a880c4fC90Db"], + "snapshot": 14848961 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/index.ts b/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/index.ts new file mode 100644 index 00000000..7721695f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/rowdy-roos/index.ts @@ -0,0 +1,71 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'npbroo'; +export const version = '0.1.0'; + +const ERC20_ABI = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +const STAKING_ABI = [ + 'function getStakedTokens(address _owner) external view returns (uint16[] memory)', + 'function currentRewardsOf(uint16 _tokenId) public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPool = new Multicaller(network, provider, STAKING_ABI, { + blockTag + }); + + const tokenPool = new Multicaller(network, provider, ERC20_ABI, { + blockTag + }); + + addresses.forEach((address) => { + stakingPool.call(address, options.staking, 'getStakedTokens', [address]); + tokenPool.call(address, options.token, 'balanceOf', [address]); + }); + + const [stakingResponse, tokenResponse]: [ + Record, + Record + ] = await Promise.all([stakingPool.execute(), tokenPool.execute()]); + + addresses.forEach((address) => { + stakingResponse[address].forEach((id) => { + stakingPool.call(id, options.staking, 'currentRewardsOf', [id]); + }); + }); + + const stakedRewardsResponse: Record = + await stakingPool.execute(); + + return Object.fromEntries( + addresses.map((address) => { + const claimedCount = parseInt( + formatUnits(BigNumber.from(tokenResponse[address]), options.decimals) + ); + let total_staked_reward = 0; + stakingResponse[address].forEach((id) => { + total_staked_reward += parseInt( + formatUnits( + BigNumber.from(stakedRewardsResponse[id]), + options.decimals + ) + ); + }); + return [address, claimedCount + total_staked_reward]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-lp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-lp/examples.json new file mode 100644 index 00000000..1ceac068 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-lp/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "RULER-ETH SLP staked in rewards contract", + "strategy": { + "name": "ruler-staked-lp", + "params": { + "tokenAddress": "0x2aECCB42482cc64E087b6D2e5Da39f5A7A7001f8", + "sushiPoolAddress": "0xb1EECFea192907fC4bF9c4CE99aC07186075FC51", + "stakingAddress": "0x3423c8Af3a95D9FEE7Ec06c4e0E905D4fd559F89", + "symbol": "RULER", + "decimals": 18 + } + }, + "network": "1", + "addresses": ["0xDd79dc5B781B14FF091686961ADc5d47e434f4B0"], + "snapshot": 12065630 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-lp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-lp/index.ts new file mode 100644 index 00000000..c92c97b4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-lp/index.ts @@ -0,0 +1,125 @@ +import { multicall } from '../../utils'; + +export const author = 'vfatouros'; +export const version = '0.1.0'; + +const tokenAbi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '_lpToken', + type: 'address' + }, + { + internalType: 'address', + name: '_account', + type: 'address' + } + ], + name: 'getUser', + outputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256[]', + name: 'rewardsWriteoffs', + type: 'uint256[]' + } + ], + internalType: 'struct IBonusRewards.User', + name: '', + type: 'tuple' + }, + { + internalType: 'uint256[]', + name: '', + type: 'uint256[]' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const res = await multicall( + network, + provider, + tokenAbi, + [ + [options.sushiPoolAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.sushiPoolAddress]] + ].concat( + addresses.map((address: any) => [ + options.stakingAddress, + 'getUser', + [options.sushiPoolAddress, address] + ]) + ), + { blockTag } + ); + + const totalSupply = res[0]; + const tokenBalanceInLP = res[1]; + const tokensPerLP = + tokenBalanceInLP / 10 ** options.decimals / (totalSupply / 1e18); + + const response = res.slice(2); + + return Object.fromEntries( + response.map(([userInfo], i) => [ + addresses[i], + (userInfo.amount / 10 ** options.decimals) * tokensPerLP + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/README.md b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/README.md new file mode 100644 index 00000000..493bd525 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/README.md @@ -0,0 +1,14 @@ +# ruler-staked-token + +This strategy returns the balance of a specific ERC20 token staked in a ruler-style contract. + +Here is an example of parameters: + +```json +{ + "tokenAddress": "0x2aECCB42482cc64E087b6D2e5Da39f5A7A7001f8", + "stakingAddress": "0x3423c8Af3a95D9FEE7Ec06c4e0E905D4fd559F89", + "symbol": "RULER", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/examples.json new file mode 100644 index 00000000..3045a520 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ruler-staked-token", + "params": { + "tokenAddress": "0xb1EECFea192907fC4bF9c4CE99aC07186075FC51", + "stakingAddress": "0x3423c8Af3a95D9FEE7Ec06c4e0E905D4fd559F89", + "symbol": "RULER-WETH SLP", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x0291eb432CB4a2613a7415018933E3Db45Bcd769", + "0x7DE2bF548eaAd49588eB334696d7C2d4443C0575", + "0xBDbFdf3e82fC9d2bE1352e252aB1Ce2287fC2122", + "0x17EA85484cD4E97bE63fC02F20a196EDEAa937a9" + ], + "snapshot": 12777196 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/index.ts new file mode 100644 index 00000000..35aa25da --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ruler-staked-token/index.ts @@ -0,0 +1,61 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'drop-out-dev'; +export const version = '0.1.0'; + +const abi = [ + { + inputs: [ + { internalType: 'address', name: '_lpToken', type: 'address' }, + { internalType: 'address', name: '_account', type: 'address' } + ], + name: 'getUser', + outputs: [ + { + components: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'uint256[]', + name: 'rewardsWriteoffs', + type: 'uint256[]' + } + ], + internalType: 'struct IBonusRewards.User', + name: '', + type: 'tuple' + }, + { internalType: 'uint256[]', name: '', type: 'uint256[]' } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.stakingAddress, + 'getUser', + [options.tokenAddress, address] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map(([userInfo], i) => [ + addresses[i], + parseFloat(formatUnits(userInfo.amount, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/README.md b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/README.md new file mode 100644 index 00000000..d9ba0d12 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/README.md @@ -0,0 +1,3 @@ +# Saddle Finance Governance Strategy + +Custom strategy which includes unclaimed SDL tokens in the user's wallet, retroactive drop and team/advisor/investor vesting contracts. diff --git a/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/examples.json new file mode 100644 index 00000000..22ae6a20 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Saddle Finance", + "strategy": { + "name": "saddle-finance", + "params": { + "symbol": "SDL" + } + }, + "network": "1", + "addresses": [ + "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", + "0x664f45ed5084abcf2f8e1a95e320b06cc700591b", + "0xf4a0e9ad90f441670016742c8977f79553ee8ee8", + "0x243fFB6d39aD73327242D08329b8273B81ed0Ab0", + "0xcb10d759caaa8ec12a4d2e59f9d55018dd8b1c9a" + ], + "snapshot": 13754897 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/index.ts new file mode 100644 index 00000000..a6825242 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/index.ts @@ -0,0 +1,153 @@ +import fetch from 'cross-fetch'; +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { vestingContractAddrs } from './vestingContractAddrs'; + +export const author = 'saddle-finance'; +export const version = '0.1.0'; + +const SDLTokenAddress = '0xf1Dc500FdE233A4055e25e5BbF516372BC4F6871'; +const RetroRewardsContract = '0x5DCA270671935cf3dF78bd8373C22BE250198a03'; + +const abi = [ + 'function balanceOf(address) external view returns (uint256)', + 'function beneficiary() external view returns (address)', + 'function vestings(address) external view returns (bool isVerified, uint120 totalAmount, uint120 released)', + 'function vestedAmount() public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const remappedMerkleDataRes = await fetch( + 'https://gateway.pinata.cloud/ipfs/QmV73GEaijyiBFHu1vRdZBFffoCHaXYWG5SpurbEgr4VK6', + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + } + ); + const remappedMerkleData = await remappedMerkleDataRes.json(); + + const userWalletBalanceResponse = multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + SDLTokenAddress, + 'balanceOf', + [address.toLowerCase()] + ]), + { blockTag } + ); + + const beneficiaries = multicall( + network, + provider, + abi, + vestingContractAddrs.map((vestingContractAddress: any) => [ + vestingContractAddress.toLowerCase(), + 'beneficiary' + ]), + { blockTag } + ); + + const vestedAndUnclaimedAmountRes = multicall( + network, + provider, + abi, + vestingContractAddrs.map((vestingContractAddress: any) => [ + vestingContractAddress.toLowerCase(), + 'vestedAmount' + ]), + { blockTag } + ); + + const retroAddrs = Object.keys(remappedMerkleData); + + const userVestingsRes = multicall( + network, + provider, + abi, + retroAddrs.map((retroAddr: any) => [ + RetroRewardsContract, + 'vestings', + [retroAddr.toLowerCase()] + ]), + { blockTag } + ); + + const balances = await Promise.all([ + userWalletBalanceResponse, + vestedAndUnclaimedAmountRes, + beneficiaries, + userVestingsRes + ]); + + const retroUserBalances = {}; + retroAddrs.forEach((addr, i) => { + const userVesting = balances[3][i]; + if (userVesting?.isVerified) { + retroUserBalances[addr.toLowerCase()] = parseFloat( + formatUnits( + userVesting.totalAmount.sub(userVesting.released).toString(), + 18 + ) + ); + } else { + retroUserBalances[addr.toLowerCase()] = parseFloat( + formatUnits(remappedMerkleData[addr].amount, 18) + ); + } + }); + + const mappedBeneficiariesToUnclaimedAmount = balances[2].reduce( + (acc, addr, i) => ({ + ...acc, + [addr]: parseFloat(formatUnits(balances[1][i][0].toString(), 18)) + }), + {} + ); + + const userWalletBalances = balances[0].map((amount, i) => { + return [ + addresses[i].toLowerCase(), + parseFloat(formatUnits(amount.toString(), 18)) + ]; + }); + + const userTotal = {}; + // loop through user, investor/advisor/team-member, and airdrop wallets to calculate total. + userWalletBalances.forEach(([address, amount]) => { + const addr = address.toLowerCase(); + if (userTotal[addr]) userTotal[addr] += amount; + else userTotal[addr] = amount; + }); + for (const [address, amount] of Object.entries(retroUserBalances)) { + const addr = address.toLowerCase(); + if (userTotal[addr]) userTotal[addr] += amount; + else userTotal[addr] = amount; + } + for (const [address, amount] of Object.entries( + mappedBeneficiariesToUnclaimedAmount + )) { + const addr = address.toLowerCase(); + if (userTotal[addr]) userTotal[addr] += amount; + else userTotal[addr] = amount; + } + + const finalUserBalances = Object.fromEntries( + addresses.map((addr) => [addr, userTotal[addr.toLowerCase()]]) + ); + + return finalUserBalances; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/vestingContractAddrs.ts b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/vestingContractAddrs.ts new file mode 100644 index 00000000..5034e7a7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance-v2/vestingContractAddrs.ts @@ -0,0 +1,74 @@ +export const vestingContractAddrs = [ + '0x5dfbceea7a5f6556356c7a66d2a43332755d68a5', + '0xa440423cc4731909d21cda5b80cdf4a0e998a046', + '0x1e82992cd3f1f495827b545fa1d0845316c3404d', + '0xafc5d02588035124273291e35cacc11ce4249295', + '0xd17c31796d3cb41d9d211904780320c4be286172', + '0x85f99b73d0edd9cdb3462c94ebe4c5758684bdf1', + '0x92ff688d17504ff04f6551150ff34de61cf6f772', + '0x3f2763cace9b48f0cdbe84e049b5695ce3cf7d7e', + '0xb960fafebb589ca3500eb9350eea503548bccfc2', + '0xc7b2f1a2d0838370f88a2fd5c2e3f64d8af89a18', + '0x85c77d06f326381390b619ef202fe8fb9ce40679', + '0xcdec570c3eff689d97eaf3ad9eb31993dcf04f51', + '0xeed792fda7bd79398d4f3cc28f02bb65bfb7700f', + '0x878a65846a37b8cb117662bfdda596ed99b50f0d', + '0x5f4d8017ab0b5476a7177b2f1200f1ccf23f396d', + '0x8e0b95b6040188ac4a51da2eaf11ef93cc9af89f', + '0xa382a5427b387a8ea419d7259496d5b5d8930d43', + '0xcd57f671c59e32af35258c19ed112bac6c5db48b', + '0xf8264afe6483e7149ad9bc9d27759e37ce03f0ed', + '0x32b58b1bc7d10d5313b87b4e45c17d9cd342dcf6', + '0x7024716497d385ad9e5762a17e5d91893af5a47b', + '0xb8196a14c3318eb39518bc1977b99ea000e02f66', + '0xfdc134499b7de70ee88f4594761b8f6acf9c64a0', + '0xf5d69f455474f1f78654b08138178622dc20651a', + '0x64cac463ac033534bdbc94b9da06193b95cf779f', + '0x2a611277d378b475ba7bad5d601a94d19f6a5eb3', + '0x12ce3e43d2d6d793f2af61ff8e8ae7df88704b32', + '0xb5c81597f982dedf7452aecfe9ea0d2317d0a6cb', + '0x3ce780be5cfa346f60d1919451ec0dc9df316a12', + '0xa5a5f2cdefbbec9b107032edaa737c0b947acc9c', + '0xf1dfa2b7331f31317d15d74121485068589e0d8d', + '0x8a81e676d2f32c9cbaa0f5ea48d36ef7172eda97', + '0x94fcefc941ef42510e166746c9a8ab8fe4933cbc', + '0xb6fa5b81f6898b9acfa2d5af352b3ae25105028b', + '0x6672fbe9793970fd762ed7a48cbae81db7bb0a5e', + '0x7a1c42297c00823736fd91e3d0f2cc7ca848e98e', + '0xc896e23a786b51a55fe0c2d5091faa4bf2ee0896', + '0xa0e5ca644b026377f8f280e35438bf8acb0b5790', + '0x5c17b22a49ac26305d9a001fdc41733e59d868d0', + '0xdf239a0397c4f1d39b4cf414a1a06aa0f3797fba', + '0x76cb506fc99c10000145796b7e5e00d91b06829b', + '0x338179f237eecc39d3c0ad1a776ad02b1bf3761a', + '0xe1aeca359b91eadcd9934b3584b39fefff4c3b16', + '0xcfb49d2b349d389c41e5f915d1250e36a4eb42ca', + '0xee08c493a458876520813b256e9688eaede6a91a', + '0xdddf8b5c211fd97967eda1b7ad6330b9066bdee4', + '0x971b5eda88a400974556ac82d37389de8f140543', + '0xd6c29b1a8106584dc21ac3db4f4863e3caa47a60', + '0x5412a79e9cae0bad06bd9dd33f97ae2e196519e1', + '0x3e5f69698628b92e0a47f9c2c9e14ab892216096', + '0xe68319e9389554af7fe3f7ed41ff1901632634b6', + '0x493ecf1ece448ee83f72098cff0e196fb2948cb9', + '0x3d2ab86da84b2496168e5cf841d42a4ac27511d3', + '0x3bbfb974fc85286ca6db8162c312459a92f3e302', + '0xfde9512c0c4d7b092674229a42ab4aea5f743da1', + '0x597e475a5ddd90b3eb2135ac47319bd866f685d8', + '0xb39c77901054766662b77e3269d3b622e7cacab4', + '0x0622927ecf00406d48d05c39134bcd53ed396cb4', + '0xdcae005fcf34cf3e2b12b662dded94d0f7bf2977', + '0xdc0e9a031e9fc09681495a5ae5912954cbd858e4', + '0xbe3e1228d471fc747d7a4c68823910153ee552a5', + '0x45545bd03ccb1cd84d3c8f000a7d6c709d84720d', + '0x1ac13ff6e1bbca5b49c3f12468689289fb93c388', + '0x3cfd17f9cf57164ed64b91d25f72c2c6dfaeab48', + '0xaf8094420749b0131200b8e85f5018688261f110', + '0x039827df17ae5449b31162ec579bbbbe72300188', + '0xc7b3dbc8424a11255cf895c2916f24e0063dcdc3', + '0x2f246c27ed9f4839dff70233ce250a3f6024f484', + '0xa663ae21db74048e50401f542703e0802a3afeb9', + '0xf8b70d8cf29ee045acc5623ebe61037b33228fd1', + '0x5c85b43468da23f86016f508f14ca927bfd8a737', + '0x41092b4ecf2c4db719ec5ab67dbd0c66f095ee97' +]; diff --git a/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/README.md b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/README.md new file mode 100644 index 00000000..d9ba0d12 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/README.md @@ -0,0 +1,3 @@ +# Saddle Finance Governance Strategy + +Custom strategy which includes unclaimed SDL tokens in the user's wallet, retroactive drop and team/advisor/investor vesting contracts. diff --git a/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/examples.json new file mode 100644 index 00000000..22ae6a20 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Saddle Finance", + "strategy": { + "name": "saddle-finance", + "params": { + "symbol": "SDL" + } + }, + "network": "1", + "addresses": [ + "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", + "0x664f45ed5084abcf2f8e1a95e320b06cc700591b", + "0xf4a0e9ad90f441670016742c8977f79553ee8ee8", + "0x243fFB6d39aD73327242D08329b8273B81ed0Ab0", + "0xcb10d759caaa8ec12a4d2e59f9d55018dd8b1c9a" + ], + "snapshot": 13754897 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/index.ts new file mode 100644 index 00000000..44a22533 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/index.ts @@ -0,0 +1,153 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import fetch from 'cross-fetch'; +import { vestingContractAddrs } from './vestingContractAddrs'; + +export const author = 'saddle-finance'; +export const version = '0.1.0'; + +const SDLTokenAddress = '0xf1Dc500FdE233A4055e25e5BbF516372BC4F6871'; +const RetroRewardsContract = '0x5DCA270671935cf3dF78bd8373C22BE250198a03'; + +const abi = [ + 'function balanceOf(address) external view returns (uint256)', + 'function beneficiary() external view returns (address)', + 'function vestings(address) external view returns (bool isVerified, uint120 totalAmount, uint120 released)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const remappedMerkleDataRes = await fetch( + 'https://gateway.pinata.cloud/ipfs/QmV73GEaijyiBFHu1vRdZBFffoCHaXYWG5SpurbEgr4VK6', + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + } + ); + const remappedMerkleData = await remappedMerkleDataRes.json(); + + const userWalletBalanceResponse = multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + SDLTokenAddress, + 'balanceOf', + [address.toLowerCase()] + ]), + { blockTag } + ); + + const vestingAddrsBalanceRes = multicall( + network, + provider, + abi, + vestingContractAddrs.map((vestingContractAddress: any) => [ + SDLTokenAddress, + 'balanceOf', + [vestingContractAddress.toLowerCase()] + ]), + { blockTag } + ); + + const beneficiaries = multicall( + network, + provider, + abi, + vestingContractAddrs.map((vestingContractAddress: any) => [ + vestingContractAddress.toLowerCase(), + 'beneficiary' + ]), + { blockTag } + ); + + const retroAddrs = Object.keys(remappedMerkleData); + + const userVestingsRes = multicall( + network, + provider, + abi, + retroAddrs.map((retroAddr: any) => [ + RetroRewardsContract, + 'vestings', + [retroAddr.toLowerCase()] + ]), + { blockTag } + ); + + const balances = await Promise.all([ + userWalletBalanceResponse, + vestingAddrsBalanceRes, + beneficiaries, + userVestingsRes + ]); + + const retroUserBalances = {}; + retroAddrs.forEach((addr, i) => { + const userVesting = balances[3][i]; + if (userVesting?.isVerified) { + retroUserBalances[addr.toLowerCase()] = parseFloat( + formatUnits( + userVesting.totalAmount.sub(userVesting.released).toString(), + 18 + ) + ); + } else { + retroUserBalances[addr.toLowerCase()] = parseFloat( + formatUnits(remappedMerkleData[addr].amount, 18) + ); + } + }); + + const mappedBeneficiariesToVestingContract = balances[2].reduce( + (acc, addr, i) => ({ + ...acc, + [addr]: parseFloat(formatUnits(balances[1][i][0].toString(), 18)) + }), + {} + ); + + const userWalletBalances = balances[0].map((amount, i) => { + return [ + addresses[i].toLowerCase(), + parseFloat(formatUnits(amount.toString(), 18)) + ]; + }); + + const userTotal = {}; + // loop through user, investor/advisor/team-member, and airdrop wallets to calculate total. + userWalletBalances.forEach(([address, amount]) => { + const addr = address.toLowerCase(); + if (userTotal[addr]) userTotal[addr] += amount; + else userTotal[addr] = amount; + }); + for (const [address, amount] of Object.entries(retroUserBalances)) { + const addr = address.toLowerCase(); + if (userTotal[addr]) userTotal[addr] += amount; + else userTotal[addr] = amount; + } + for (const [address, amount] of Object.entries( + mappedBeneficiariesToVestingContract + )) { + const addr = address.toLowerCase(); + if (userTotal[addr]) userTotal[addr] += amount; + else userTotal[addr] = amount; + } + + const finalUserBalances = Object.fromEntries( + addresses.map((addr) => [addr, userTotal[addr.toLowerCase()]]) + ); + + return finalUserBalances; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/vestingContractAddrs.ts b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/vestingContractAddrs.ts new file mode 100644 index 00000000..5034e7a7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saddle-finance/vestingContractAddrs.ts @@ -0,0 +1,74 @@ +export const vestingContractAddrs = [ + '0x5dfbceea7a5f6556356c7a66d2a43332755d68a5', + '0xa440423cc4731909d21cda5b80cdf4a0e998a046', + '0x1e82992cd3f1f495827b545fa1d0845316c3404d', + '0xafc5d02588035124273291e35cacc11ce4249295', + '0xd17c31796d3cb41d9d211904780320c4be286172', + '0x85f99b73d0edd9cdb3462c94ebe4c5758684bdf1', + '0x92ff688d17504ff04f6551150ff34de61cf6f772', + '0x3f2763cace9b48f0cdbe84e049b5695ce3cf7d7e', + '0xb960fafebb589ca3500eb9350eea503548bccfc2', + '0xc7b2f1a2d0838370f88a2fd5c2e3f64d8af89a18', + '0x85c77d06f326381390b619ef202fe8fb9ce40679', + '0xcdec570c3eff689d97eaf3ad9eb31993dcf04f51', + '0xeed792fda7bd79398d4f3cc28f02bb65bfb7700f', + '0x878a65846a37b8cb117662bfdda596ed99b50f0d', + '0x5f4d8017ab0b5476a7177b2f1200f1ccf23f396d', + '0x8e0b95b6040188ac4a51da2eaf11ef93cc9af89f', + '0xa382a5427b387a8ea419d7259496d5b5d8930d43', + '0xcd57f671c59e32af35258c19ed112bac6c5db48b', + '0xf8264afe6483e7149ad9bc9d27759e37ce03f0ed', + '0x32b58b1bc7d10d5313b87b4e45c17d9cd342dcf6', + '0x7024716497d385ad9e5762a17e5d91893af5a47b', + '0xb8196a14c3318eb39518bc1977b99ea000e02f66', + '0xfdc134499b7de70ee88f4594761b8f6acf9c64a0', + '0xf5d69f455474f1f78654b08138178622dc20651a', + '0x64cac463ac033534bdbc94b9da06193b95cf779f', + '0x2a611277d378b475ba7bad5d601a94d19f6a5eb3', + '0x12ce3e43d2d6d793f2af61ff8e8ae7df88704b32', + '0xb5c81597f982dedf7452aecfe9ea0d2317d0a6cb', + '0x3ce780be5cfa346f60d1919451ec0dc9df316a12', + '0xa5a5f2cdefbbec9b107032edaa737c0b947acc9c', + '0xf1dfa2b7331f31317d15d74121485068589e0d8d', + '0x8a81e676d2f32c9cbaa0f5ea48d36ef7172eda97', + '0x94fcefc941ef42510e166746c9a8ab8fe4933cbc', + '0xb6fa5b81f6898b9acfa2d5af352b3ae25105028b', + '0x6672fbe9793970fd762ed7a48cbae81db7bb0a5e', + '0x7a1c42297c00823736fd91e3d0f2cc7ca848e98e', + '0xc896e23a786b51a55fe0c2d5091faa4bf2ee0896', + '0xa0e5ca644b026377f8f280e35438bf8acb0b5790', + '0x5c17b22a49ac26305d9a001fdc41733e59d868d0', + '0xdf239a0397c4f1d39b4cf414a1a06aa0f3797fba', + '0x76cb506fc99c10000145796b7e5e00d91b06829b', + '0x338179f237eecc39d3c0ad1a776ad02b1bf3761a', + '0xe1aeca359b91eadcd9934b3584b39fefff4c3b16', + '0xcfb49d2b349d389c41e5f915d1250e36a4eb42ca', + '0xee08c493a458876520813b256e9688eaede6a91a', + '0xdddf8b5c211fd97967eda1b7ad6330b9066bdee4', + '0x971b5eda88a400974556ac82d37389de8f140543', + '0xd6c29b1a8106584dc21ac3db4f4863e3caa47a60', + '0x5412a79e9cae0bad06bd9dd33f97ae2e196519e1', + '0x3e5f69698628b92e0a47f9c2c9e14ab892216096', + '0xe68319e9389554af7fe3f7ed41ff1901632634b6', + '0x493ecf1ece448ee83f72098cff0e196fb2948cb9', + '0x3d2ab86da84b2496168e5cf841d42a4ac27511d3', + '0x3bbfb974fc85286ca6db8162c312459a92f3e302', + '0xfde9512c0c4d7b092674229a42ab4aea5f743da1', + '0x597e475a5ddd90b3eb2135ac47319bd866f685d8', + '0xb39c77901054766662b77e3269d3b622e7cacab4', + '0x0622927ecf00406d48d05c39134bcd53ed396cb4', + '0xdcae005fcf34cf3e2b12b662dded94d0f7bf2977', + '0xdc0e9a031e9fc09681495a5ae5912954cbd858e4', + '0xbe3e1228d471fc747d7a4c68823910153ee552a5', + '0x45545bd03ccb1cd84d3c8f000a7d6c709d84720d', + '0x1ac13ff6e1bbca5b49c3f12468689289fb93c388', + '0x3cfd17f9cf57164ed64b91d25f72c2c6dfaeab48', + '0xaf8094420749b0131200b8e85f5018688261f110', + '0x039827df17ae5449b31162ec579bbbbe72300188', + '0xc7b3dbc8424a11255cf895c2916f24e0063dcdc3', + '0x2f246c27ed9f4839dff70233ce250a3f6024f484', + '0xa663ae21db74048e50401f542703e0802a3afeb9', + '0xf8b70d8cf29ee045acc5623ebe61037b33228fd1', + '0x5c85b43468da23f86016f508f14ca927bfd8a737', + '0x41092b4ecf2c4db719ec5ab67dbd0c66f095ee97' +]; diff --git a/Implementations/API/backend/utils/snapshot/strategies/safe-vested/README.md b/Implementations/API/backend/utils/snapshot/strategies/safe-vested/README.md new file mode 100644 index 00000000..be8d5ba0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/safe-vested/README.md @@ -0,0 +1,33 @@ +# safe-vested + +Custom strategy to compute voting power from vested tokens. Originally created for the Safe allocations. Vesting smart contract code can be found [here](https://github.com/safe-global/safe-token/blob/81e0f3548033ca9916f38444f2e62e5f3bb2d3e1/contracts/VestingPool.sol). + +## strategy parameters + +The following parameters can be used to configure the strategy. + +### allocationsSource + +This parameter is mandatory. It expects a JSON using the following structure providing at least the example parameters. +```json +[ + [ + { + "account": "ACCOUNT_ADDRESS", + "contract": "ALLOCATIONS_CONTRACT", + "vestingId": "VESTING_HASH", + "amount": "VESTED_AMOUNT" + } + ] +] +``` + +### claimDateLimit + +This is an optional parameter. A date limit to claim the vesting. Since that moment the vesting won't be considered unless the account already claimed some amount. + +Needs to follow [ISO Date format](https://www.w3schools.com/js/js_date_formats.asp). +```js +"2022-12-04T11:00:00Z" +``` + diff --git a/Implementations/API/backend/utils/snapshot/strategies/safe-vested/examples.json b/Implementations/API/backend/utils/snapshot/strategies/safe-vested/examples.json new file mode 100755 index 00000000..3924bb74 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/safe-vested/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "safe-vested", + "params": { + "allocationsSource": "https://safe-claiming-app-data.gnosis-safe.io/allocations/1/snapshot-allocations-data.json", + "claimDateLimit": "2022-12-28T11:00:00Z" + } + }, + "network": "1", + "snapshot": 15800000, + "addresses": [ + "0x9970dcab40e29a84D1020DeaEa443dCE1d8471b7", + "0x1230B3d59858296A31053C1b8562Ecf89A2f888b", + "0x121Cf7457171bD7a93232807CBa65f7Bd7a48a2d", + "0xda2242a6DbDa924bf7D7DB8aC9482d4763EB211C", + "0xf2565317F3Ae8Ae9EA98E9Fe1e7FADC77F823cbD", + "0x37b828802FAeA4244d176Da386CF632F5Bbc414F", + "0xA8FFD6B87388F8d5FACfDa0147d9B0Da511539b6", + "0xd7539FCdC0aB79a7B688b04387cb128E75cb77Dc" + ] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/safe-vested/index.ts b/Implementations/API/backend/utils/snapshot/strategies/safe-vested/index.ts new file mode 100755 index 00000000..6398d6c0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/safe-vested/index.ts @@ -0,0 +1,110 @@ +import fetch from 'cross-fetch'; +import { formatUnits, parseUnits } from '@ethersproject/units'; + +import { Multicaller } from '../../utils'; + +export const author = 'dasanra'; +export const version = '0.2.0'; + +// https://github.com/safe-global/safe-token/blob/81e0f3548033ca9916f38444f2e62e5f3bb2d3e1/contracts/VestingPool.sol +const abi = [ + 'function vestings(bytes32) view returns (address account, uint8 curveType, bool managed, uint16 durationWeeks, uint64 startDate, uint128 amount, uint128 amountClaimed, uint64 pausingDate, bool cancelled)' +]; + +type AllocationDetails = { + account: string; + contract: string; + vestingId: string; + amount: string; +}; + +type Options = { + allocationsSource: string; + claimDateLimit: string | undefined; +}; + +const canStillClaim = (claimDateLimit: string | undefined): boolean => { + // if a claim date limit is set we check if it's still possible to claim + if (claimDateLimit) { + const now = new Date(); + const limitDate = new Date(claimDateLimit); + return now.getTime() < limitDate.getTime(); + } + + // if not date limit is set can always claim. + return true; +}; + +export async function strategy( + space: string, + network: string, + provider, + addresses: string[], + options: Options, + snapshot: number | string = 'latest' +) { + const response = await fetch(options.allocationsSource, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }); + const allocationsList: [[AllocationDetails]] = await response.json(); + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const multi = new Multicaller(network, provider, abi, { + blockTag, + limit: 250 + }); + + // Get current vesting state from smart contract using the vestingId + addresses.forEach((address) => { + const addressAllocations = allocationsList.find( + (allocations: AllocationDetails[]) => allocations[0].account === address + ); + + if (addressAllocations) { + addressAllocations.forEach(({ contract, vestingId }) => { + multi.call(vestingId, contract, 'vestings', [vestingId]); + }); + } + }); + + const vestings = await multi.execute(); + + const flatAllocationsList = allocationsList.flat(); + // Check vesting state, consider only unclaimed amounts and group allocations to the same account + return Object.keys(vestings).reduce((acc, key) => { + const { account, amount } = flatAllocationsList.find( + ({ vestingId }) => vestingId === key + ) as AllocationDetails; + + const hasAlreadyClaimed = vestings[key].account === account; + let currentVestingAmount; + if (hasAlreadyClaimed) { + // If account already claimed only count the pending amount + currentVestingAmount = vestings[key].amount.sub( + vestings[key].amountClaimed + ); + } else { + // Else nothing claimed yet so consider the full allocation + // or none if the claim date limit was set and reached. + currentVestingAmount = canStillClaim(options.claimDateLimit) + ? amount + : '0'; + } + + const previousAmount = acc[account]; + // If account received multiple allocations sum them + // Else we just return the currentAmount + const pendingVestedAmount = previousAmount + ? parseUnits(previousAmount.toString()).add(currentVestingAmount) + : currentVestingAmount; + + return { + ...acc, + [account]: parseFloat(formatUnits(pendingVestedAmount)) + }; + }, {}); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/README.md new file mode 100644 index 00000000..eafa910a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/README.md @@ -0,0 +1,18 @@ +# staked-psp-balance + +This strategy computes the voting power of a staker relative to one token involved in a Aave like safety module that accepts arbitrary balancer LP token as staked token. +It uses balancer-pool-id strategy. + +To simplify async flow, it requires to pass couple of parameters: +- balancer pool id +- safety module (address, decimals) +- voting token (address, decimals) + + +This strategy works under 2 different regimes: + +1/ if voting_token matches reward_token of safety module +-> count for staked tokens and unclaimed rewards + +2/ else +-> count for staked tokens only \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/examples.json new file mode 100644 index 00000000..a562035d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/examples.json @@ -0,0 +1,37 @@ +[ + { + "name": "Example safety module bpt query", + "strategy": { + "name": "safety-module-bpt-power", + "params": { + "symbol": "PSP", + "balancerPoolId": "0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a", + "safetyModule": { + "address": "0xC8DC2Ec5f5e02bE8b37A8444a1931F02374A17ab", + "decimals": 18 + }, + "votingToken": { + "address": "0xcAfE001067cDEF266AfB7Eb5A286dCFD277f3dE5", + "decimals": 18 + } + } + }, + "network": "1", + "addresses": [ + "0x0ddc793680ff4f5793849c8c6992be1695cbe72a", + "0x9c0d72f2ac26420cb7eeb155bf401b672840e87b", + "0x7494eb2916cad8649f4f91eb1db6e20be605dad6", + "0xb7b65acf585c29070b9926c089fcfa1eb9983d3d", + "0x9824697f7c12cabada9b57842060931c48dea969", + "0x5b52e503c9e1440b47991bc0a64599c1c916084c", + "0x3ce06981bc523f950e3df346878216365b24b6fe", + "0xf2078c58d6c38c893af4e40d7b09843ec3b7d26c", + "0xcff61382e659603046358f86a119efd127d5bb48", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 14215928 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/index.ts new file mode 100644 index 00000000..4cbc5ea8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/index.ts @@ -0,0 +1,227 @@ +import { strategy as balancerPoolIdStrategy } from '../balancer-poolid'; +import { BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'mwamedacen'; +export const version = '0.1.0'; + +interface Options { + balancerPoolId: string; + safetyModule: { + address: string; + decimals: number; + }; + votingToken: { + address: string; + decimals: number; + }; +} + +type FetchSafetyModuleScoreOutput = Promise; + +async function fetchSafetyModuleScore( + space: string, + network: string, + provider, + addresses: string[], + options: Options, + snapshot: number +): FetchSafetyModuleScoreOutput { + const scores = await balancerPoolIdStrategy( + space, + network, + provider, + [options.safetyModule.address], + { + poolId: options.balancerPoolId, + token: options.votingToken.address + }, + snapshot + ); + + return parseFloat(scores[options.safetyModule.address]); +} + +const SafetyModuleMinABI = [ + 'function totalSupply() external view returns (uint256)', + 'function STAKED_TOKEN() external view returns (address)', + 'function REWARD_TOKEN() external view returns (address)', + 'function decimals() view returns (uint8)', + 'function balanceOf(address account) external view returns (uint256)', + 'function getTotalRewardsBalance(address staker) view returns (uint256)' +]; + +const TOTAL_SUPPLY_ATTR = 'totalSupply'; +const STAKED_TOKEN_ATTR = 'stakedToken'; +const REWARD_TOKEN_ATTR = 'rewardToken'; +const BALANCE_OF_ATTR = 'balanceOf'; +const REWARDS_OF_ATTR = 'totalRewardsBalance'; + +type FetchAccountsSafetyModuleStakesAndRewardsOuput = Promise<{ + [address: string]: { + [BALANCE_OF_ATTR]: BigNumberish; + [REWARD_TOKEN_ATTR]: BigNumberish; + }; +}>; + +async function fetchAccountsSafetyModuleStakesAndRewards( + space: string, + network: string, + provider, + addresses: string[], + options: Options, + snapshot: number +): FetchAccountsSafetyModuleStakesAndRewardsOuput { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, SafetyModuleMinABI, { + blockTag + }); + + addresses.forEach((address) => { + multi.call( + `${BALANCE_OF_ATTR}_${address}`, + options.safetyModule.address, + 'balanceOf', + [address] + ); + multi.call( + `${REWARDS_OF_ATTR}_${address}`, + options.safetyModule.address, + 'getTotalRewardsBalance', + [address] + ); + }); + + const result: Record = await multi.execute(); + + return Object.entries(result).reduce((acc, [key, value]) => { + const [attr, addr] = key.split('_'); + + if (!acc[addr]) { + acc[addr] = {}; + } + + acc[addr][attr] = value; + + return acc; + }, {}); +} + +type FetchSafetyModuleGlobalStateOutput = Promise<{ + [TOTAL_SUPPLY_ATTR]: BigNumberish; + [REWARD_TOKEN_ATTR]: string; + [STAKED_TOKEN_ATTR]: string; +}>; + +async function fetchSafetyModuleGlobalState( + space: string, + network: string, + provider, + addresses: string[], + options: Options, + snapshot: number +): FetchSafetyModuleGlobalStateOutput { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, SafetyModuleMinABI, { + blockTag + }); + + multi.call(STAKED_TOKEN_ATTR, options.safetyModule.address, 'STAKED_TOKEN'); + multi.call(REWARD_TOKEN_ATTR, options.safetyModule.address, 'REWARD_TOKEN'); + multi.call(TOTAL_SUPPLY_ATTR, options.safetyModule.address, 'totalSupply'); + + const result: { + [STAKED_TOKEN_ATTR]: string; + [REWARD_TOKEN_ATTR]: string; + [TOTAL_SUPPLY_ATTR]: BigNumberish; + } = await multi.execute(); + + return result; +} + +export async function strategy( + space: string, + network: string, + provider, + addresses: string[], + options: Options, + snapshot: number +) { + const [safetyModuleScore, accountsStakesAndRewards, safetyModuleGlobalState] = + await Promise.all( + [ + fetchSafetyModuleScore, + fetchAccountsSafetyModuleStakesAndRewards, + fetchSafetyModuleGlobalState + ].map((fn) => + fn(space, network, provider, addresses, options, snapshot) + ) as [ + FetchSafetyModuleScoreOutput, + FetchAccountsSafetyModuleStakesAndRewardsOuput, + FetchSafetyModuleGlobalStateOutput + ] + ); + + const safetyModuleStakedToken = safetyModuleGlobalState[STAKED_TOKEN_ATTR]; + + if ( + safetyModuleStakedToken.toLowerCase() !== + options.balancerPoolId.substring(0, 42).toLowerCase() + ) { + throw new Error( + `safety-module-bpt-power, safety module's staken token ${safetyModuleStakedToken} doesn't match balancer pool ${options.balancerPoolId}` + ); + } + + const safetyModuleRewardsToken = safetyModuleGlobalState[REWARD_TOKEN_ATTR]; + + const votingAndRewardTokenMatching = + safetyModuleRewardsToken.toLowerCase() === + options.votingToken.address.toLowerCase(); + + const safetyModuleTotalSupply = parseFloat( + formatUnits( + safetyModuleGlobalState[TOTAL_SUPPLY_ATTR], + options.safetyModule.decimals + ) + ); + + const scores = Object.fromEntries( + Object.entries(accountsStakesAndRewards).map( + ([address, accountStakesAndRewards]) => { + const accountSafetyModuleBalance = parseFloat( + formatUnits( + accountStakesAndRewards[BALANCE_OF_ATTR], + options.safetyModule.decimals + ) + ); + + const accountSharePercent = + accountSafetyModuleBalance / safetyModuleTotalSupply; + + const accountStakedScore = accountSharePercent * safetyModuleScore; + + if (!votingAndRewardTokenMatching) { + return [address, accountStakedScore]; + } + + const accountRewardsScore = parseFloat( + formatUnits( + accountStakesAndRewards[REWARDS_OF_ATTR], + options.votingToken.decimals + ) + ); + + const accountStakedAndRewardsScore = + accountStakedScore + accountRewardsScore; + + return [address, accountStakedAndRewardsScore]; + } + ) + ); + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/schema.json b/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/schema.json new file mode 100644 index 00000000..565093bc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/safety-module-bpt-power/schema.json @@ -0,0 +1,66 @@ +{ + "$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. PSP"], + "maxLength": 16 + }, + "balancerPoolId": { + "type": "string", + "title": "BalancerPoolId", + "examples": [ + "e.g. 0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a" + ], + "pattern": "^0x[a-fA-F0-9]{64}$", + "minLength": 66, + "maxLength": 66 + }, + "safetyModule": { + "type": "object", + "title": "SafetyModule", + "properties": { + "address": { + "type": "string", + "title": "Address", + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + } + }, + "votingToken": { + "type": "object", + "title": "VotingToken", + "properties": { + "address": { + "type": "string", + "title": "Address", + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + } + } + }, + "required": ["balancerPoolId", "safetyModule", "votingToken"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/README.md b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/README.md new file mode 100644 index 00000000..3430850a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/README.md @@ -0,0 +1,94 @@ +# Saffron Finance V2 Strategy + +This strategy scans all the pools defined in the SaffronStakingV2 counting up staked SFI. Voting scores are increased +by 10% as a default. The `multiplier` option can be set to numbers such as `1.1` (10% increase) and `1.2` (20% increase). + +This strategy does not include counting an account's balances of either [SFI](https://etherscan.io/token/0xb753428af26e81097e7fd17f40c88aaa3e04902c) +or the [SFI Hodl Contract](https://etherscan.io/address/0x4e5ee20900898054e998fd1862742c28c651bf5d). Those are counted by the +`saffron-finance` strategy. + +## Testing Saffron Finance strategies + +To run Snapshot tests: + +```bash +yarn test --strategy=saffron-finance-v2 --more=500 +``` + +## Strategy Parameters + +Example strategy params: + +```json +{ + "symbol": "SAFF_STAK_V2", + "decimals": 18, + "multiplier": 1.1, + "stakingPool": "0x4eB4C5911e931667fE1647428F38401aB1661763", + "singleAssets": [ "0xb753428af26E81097e7fD17f40c88aaA3E04902c", "0xf34960d9d60be18cc1d5afc1a6f012a723a28811"] +} +``` + +| Parameter | Description | +|----------------|--------------------------------------------------------------------------------------------| +| symbol | Label for the strategy configuration | +| decimals | Number of decimals used by the asset used to calculate voting. Default: 18. | +| multiplier | Voting score is multiplied by this value to provide additional voting power. Default: 1.1. | +| stakingPool | The address of the SaffronStakingV2 contract. | +| | Default: "0x4eB4C5911e931667fE1647428F38401aB1661763" (_SaffronStakingV2 Contract_). | +| singleAssets | Array of contract addresses of single asset stakings. | +| | Default: [ "0xb753428af26E81097e7fD17f40c88aaA3E04902c" ]. (_SFI address_). | + + +To count SFI and Hodl Contract balances, use the strategy, `saffron-finance`: + +```json +{ + "symbol": "SFI", + "votingSchemes": [ + { + "name": "oneToOne", + "type": "DirectBoostScheme", + "multiplier": 1.0 + }, + { + "name": "staking", + "type": "DirectBoostScheme", + "multiplier": 1.1 + }, + { + "name": "uniswap", + "type": "LPReservePairScheme", + "multiplier": 1.1 + }, + { + "name": "sushiswap", + "type": "LPReservePairScheme", + "multiplier": 1.1 + } + ], + "dexLpTypes": [ + { + "name": "uniswap", + "lpToken": "0xC76225124F3CaAb07f609b1D147a31de43926cd6" + }, + { + "name": "sushiswap", + "lpToken": "0x23a9292830Fc80dB7f563eDb28D2fe6fB47f8624" + } + ], + "contracts": [ + { + "votingScheme": "oneToOne", + "label": "SFI", + "tokenAddress": "0xb753428af26e81097e7fd17f40c88aaa3e04902c" + }, + { + "votingScheme": "oneToOne", + "label": "TEAM_HODL_TOKEN", + "tokenAddress": "0x4e5ee20900898054e998fd1862742c28c651bf5d" + } + ] +} + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/examples.json new file mode 100644 index 00000000..f322d6b4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/examples.json @@ -0,0 +1,44 @@ +[ + { + "name": "Example Saffron V2", + "strategy": { + "name": "saffron-finance-v2", + "params": { + "symbol": "SAFF_STAK_V2", + "decimals": 18, + "multiplier": 1.1, + "stakingPool": "0x4eB4C5911e931667fE1647428F38401aB1661763", + "singleAssets": [ + "0xb753428af26E81097e7fD17f40c88aaA3E04902c", + "0xf34960d9d60be18cc1d5afc1a6f012a723a28811" + ] + } + }, + "network": "1", + "addresses": [ + "0x5AE6eb5fE2195885fDBf62285936A3e89F92e17a", + "0xD90B866039E8820c2Cd082840fceeD81Cef691F8", + "0x905D6a479C4be28aF08364CE1c8e02eBC9c4bdA8", + "0x64eacbcdbc6123bcc8b90a5fde8dd099aadb0e56", + "0x9cbadd5ce7e14742f70414a6dcbd4e7bb8712719", + "0xA43Cfc82083cd5EdaC9ABf13059bDb2447A10a80", + "0x854cd0603e0c72c4758798ebeb110e882973239f", + "0xcef2d0c7d89c3dcc7a8e8af561b0294bcd6e9ebd", + "0x91dca37856240e5e1906222ec79278b16420dc92", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x3478697c64578D3D8092925EE365168CcabfeB66", + "0x905D6a479C4be28aF08364CE1c8e02eBC9c4bdA8", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C" + ], + "snapshot": 14394748 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/index.ts new file mode 100644 index 00000000..4273d85a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance-v2/index.ts @@ -0,0 +1,179 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'turpintinz'; +export const version = '2.0.1'; + +const SAFF_STAKING_V2 = '0x4eB4C5911e931667fE1647428F38401aB1661763'; +const SFI = '0xb753428af26E81097e7fD17f40c88aaA3E04902c'; +const SFI_DECIMALS = 18; +const SINGLE_ASSETS_DEFAULT = [SFI]; +const tenTo18 = BigNumber.from(10).pow(18); +const STAKING_VOTE_BOOST_DEFAULT = 1.1; + +// ============ Needed contract ABI ============ +const abi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function getReserves() view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast)', + 'function poolLength() view returns (uint256)', + 'function poolInfo(uint256) view returns (address lpToken, uint256 allocPoint, uint256 lastRewardBlock, uint256 accSFIPerShare)', + 'function totalSupply() view returns (uint256)', + 'function token0() view returns (address)', + 'function token1() view returns (address)', + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +type PoolInfo = { + id: BigNumber; + lpToken: LpSfiPair; +}; + +type LpSfiPair = { + lpAddress: string; + token0: string; + token1: string; + sfiReserve: BigNumber; + totalSupply: BigNumber; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const multi = new Multicaller(network, provider, abi, { blockTag }); + const pools = new Array(); + const stakingPool = options.stakingPool + ? options.stakingPool + : SAFF_STAKING_V2; + const singleAssets = options.singleAssets + ? options.singleAssets + : SINGLE_ASSETS_DEFAULT; + const votingDecimals = options.decimals ? options.decimals : SFI_DECIMALS; + + // ========== Prepare pools' lp token data ========== + multi.call('poolLength', stakingPool, 'poolLength', []); + const poolLenResult: Record = await multi.execute(); + + const poolLength = BigNumber.from(poolLenResult.poolLength).toNumber(); + for (let i = 0; i < poolLength; i++) { + const path = `poolInfo[${i}]`; + multi.call(path, stakingPool, 'poolInfo', [BigNumber.from(i)]); + } + const poolInfoResults = await multi.execute(); + for (let i = 0; i < poolLength; i++) { + const lp: LpSfiPair = { + lpAddress: poolInfoResults.poolInfo[i].lpToken, + token0: '', + token1: '', + sfiReserve: BigNumber.from(0), + totalSupply: BigNumber.from(0) + }; + const pool: PoolInfo = { + id: BigNumber.from(i), + lpToken: lp + }; + pools.push(pool); + } + + for (let i = 0; i < poolLength; i++) { + const lpAddress = pools[i].lpToken.lpAddress; + if ( + singleAssets.find( + (item) => item.toLowerCase() === lpAddress.toLowerCase() + ) + ) { + multi.call(`reserves[${i}]`, lpAddress, 'balanceOf', [stakingPool]); + multi.call(`supply[${i}]`, lpAddress, 'totalSupply', []); + } else { + multi.call(`token0[${i}]`, lpAddress, 'token0', []); + multi.call(`token1[${i}]`, lpAddress, 'token1', []); + multi.call(`reserves[${i}]`, lpAddress, 'getReserves', []); + multi.call(`supply[${i}]`, lpAddress, 'totalSupply', []); + } + } + const reservesResult = await multi.execute(); + for (let i = 0; i < poolLength; i++) { + const pool = pools[i]; + if ( + singleAssets.find( + (item) => item.toLowerCase() === pool.lpToken.lpAddress.toLowerCase() + ) + ) { + pool.lpToken.sfiReserve = reservesResult.reserves[i]; + } else { + let sfiReserve = BigNumber.from(0); + if ( + reservesResult.token0[i] === SFI || + reservesResult.token1[i] === SFI + ) { + sfiReserve = + reservesResult.token0[i] === SFI + ? reservesResult.reserves[i]._reserve0 + : reservesResult.reserves[i]._reserve1; + } + pool.lpToken.token0 = reservesResult.token0[i]; + pool.lpToken.token1 = reservesResult.token1[i]; + pool.lpToken.sfiReserve = sfiReserve; + } + pool.lpToken.totalSupply = reservesResult.supply[i]; + } + + // ====== retrieve user info for each pool ============ + addresses.forEach((address) => { + pools.forEach((pool) => + multi.call( + `userInfo.${address}[${pool.id.toNumber()}]`, + stakingPool, + 'userInfo', + [pool.id, address] + ) + ); + }); + const userInfoResult = await multi.execute(); + + // =========== Calculate voting power for each user account ========== + const result = new Map(); + + addresses.forEach((address) => { + let total = BigNumber.from(0); + pools.forEach((pool) => { + const userInfo = + userInfoResult.userInfo[`${address}`][pool.id.toNumber()]; + + let poolTotal; + if ( + singleAssets.find( + (item) => item.toLowerCase() === pool.lpToken.lpAddress.toLowerCase() + ) + ) { + poolTotal = userInfo.amount; + } else { + poolTotal = userInfo.amount + .mul(tenTo18) + .mul(pools[pool.id.toNumber()].lpToken.sfiReserve) + .div(pools[pool.id.toNumber()].lpToken.totalSupply) + .div(tenTo18); + } + + total = total.add(poolTotal); + }); + result.set(address, total); + }); + + const formatted = new Map(); + const votingMultiplier = + 'multiplier' in options ? options.multiplier : STAKING_VOTE_BOOST_DEFAULT; + result.forEach((balance, address) => { + const rawVote = parseFloat(formatUnits(balance, votingDecimals)); + const calculatedVote = rawVote * votingMultiplier; + formatted.set(address, calculatedVote); + }); + + return Object.fromEntries(formatted); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/saffron-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance/examples.json new file mode 100644 index 00000000..f8a8036b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance/examples.json @@ -0,0 +1,217 @@ +[ + { + "name": "saffron-finance", + "strategy": { + "name": "saffron-finance", + "params": { + "symbol": "SFI", + "votingSchemes": [ + { + "name": "oneToOne", + "type": "DirectBoostScheme", + "multiplier": 1.0 + }, + { + "name": "staking", + "type": "DirectBoostScheme", + "multiplier": 1.1 + }, + { + "name": "uniswap", + "type": "LPReservePairScheme", + "multiplier": 1.1 + }, + { + "name": "sushiswap", + "type": "LPReservePairScheme", + "multiplier": 1.1 + } + ], + "dexLpTypes": [ + { + "name": "uniswap", + "lpToken": "0xC76225124F3CaAb07f609b1D147a31de43926cd6" + }, + { + "name": "sushiswap", + "lpToken": "0x23a9292830Fc80dB7f563eDb28D2fe6fB47f8624" + } + ], + "contracts": [ + { + "votingScheme": "oneToOne", + "label": "SFI", + "tokenAddress": "0xb753428af26e81097e7fd17f40c88aaa3e04902c" + }, + { + "votingScheme": "oneToOne", + "label": "TEAM_HODL_TOKEN", + "tokenAddress": "0x4e5ee20900898054e998fd1862742c28c651bf5d" + }, + { + "votingScheme": "staking", + "label": "E2_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0x1A9aA87F180A502930c22361e2a746137Ba74750" + }, + { + "votingScheme": "staking", + "label": "E3_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0xEd4eaeB6e20d899143b74a5b4130322418d87765" + }, + { + "votingScheme": "staking", + "label": "E4_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0x11942800A0596D3fe9641a116eeaeD387b638c1A" + }, + { + "votingScheme": "staking", + "label": "E5_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0x3b3570c445a7Eb359dedD91F8348dC746223A87D" + }, + { + "votingScheme": "staking", + "label": "E6_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0xbAD5Cc4fFA06e16e367a6D492ADd8Ca04aEAe4A2" + }, + { + "votingScheme": "staking", + "label": "E7_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0x03B41d734D3dcd23F1B3f1AFF65270Bf6eB233eA" + }, + { + "votingScheme": "staking", + "label": "E8_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0xceF561E639b53e04aB9E82653fdfacAe135A0Ad0" + }, + { + "votingScheme": "staking", + "label": "E9_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0x3C3105CDbC01350C9A303352C163216A8fb2180f" + }, + { + "votingScheme": "staking", + "label": "E10_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0x96B45C15bB1AE5DD175a4Bc721868B28b8AD2291" + }, + { + "votingScheme": "staking", + "label": "E11_SFISTAK_LP_PRINCIPAL", + "tokenAddress": "0x77B2914Fe065b5bf38553D1CF3f3717f32B7C4c8" + }, + { + "votingScheme": "uniswap", + "label": "E2_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0xF489fF098BFC862F09ec583c01bCFD2D4C43c589" + }, + { + "votingScheme": "uniswap", + "label": "E3_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0x68b03AbC0b41Bc2F113d103CffC39bD9aD850f8f" + }, + { + "votingScheme": "sushiswap", + "label": "E3_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x531B49EFd42775788f72a470a64E6b54d198f0be" + }, + { + "votingScheme": "uniswap", + "label": "E4_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0x531B49EFd42775788f72a470a64E6b54d198f0be" + }, + { + "votingScheme": "sushiswap", + "label": "E4_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x898932Fd99355953DC46cb6Aa47F76a183ACb381" + }, + { + "votingScheme": "uniswap", + "label": "E5_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0x2E44c39a205BD8F807d1f1AE97B921F0DA32f225" + }, + { + "votingScheme": "sushiswap", + "label": "E5_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x32c93305FF2c79D139e344B913a6202572c67cA4" + }, + { + "votingScheme": "uniswap", + "label": "E6_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0x9DaB689F26688C6da25869CE414E4BDcCfD0289F" + }, + { + "votingScheme": "sushiswap", + "label": "E6_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x740b3e7dD42D7ff4769c2dE4Cb3C968E4e0aa6B6" + }, + { + "votingScheme": "uniswap", + "label": "E7_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0x7972790bADE77686852309F60d9C60912b899C39" + }, + { + "votingScheme": "sushiswap", + "label": "E7_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x7DF684a871fAF58579f210CBcC001CB02b5D1b7F" + }, + { + "votingScheme": "uniswap", + "label": "E8_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0x20A1ad2122B145F1629713b41af3A8C15aDBF499" + }, + { + "votingScheme": "sushiswap", + "label": "E8_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x20A1ad2122B145F1629713b41af3A8C15aDBF499" + }, + { + "votingScheme": "uniswap", + "label": "E9_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0xe4B9FfC784A0cD9e5411B5880e2066E6D6E436c3" + }, + { + "votingScheme": "sushiswap", + "label": "E9_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x063E86d5A10cadA77d4a7385F93A09a1A5d2178B" + }, + { + "votingScheme": "uniswap", + "label": "E10_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0xff1c79Ef1b3096314a3Bed7F0bD71f1D9A422eC6" + }, + { + "votingScheme": "sushiswap", + "label": "E10_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x9D128c1a98a229670A5D4FDa1DCbdB33Ee54cB7d" + }, + { + "votingScheme": "uniswap", + "label": "E11_SFIETH_UNI_LP_PRINCIPAL", + "tokenAddress": "0x386DbC8Aae0B53dE186D1a9AaC0a4AD48B4Ac624" + }, + { + "votingScheme": "sushiswap", + "label": "E11_SFIETH_SUSHI_LP_PRINCIPAL", + "tokenAddress": "0x016b05626510c1c599c3F5dA3C290e3b1c734884" + } + ] + } + }, + "network": "1", + "addresses": [ + "0xc8482e19c3fcd56b499d376ab1aad069442ba0cb", + "0xa0019248a3e80225c9c552c5796ea890bbbd3ce0", + "0x9cbadd5ce7e14742f70414a6dcbd4e7bb8712719", + "0xc8c2b727d864cc75199f5118f0943d2087fb543b", + "0xd51d953b017e6f2fc76446b0e6ba26e271305952", + "0x86282134ce9fc4ac724b84b93d6d5b41ce59a40d", + "0x5cfa9d7ed59d6ba0ea17626ac15f949d005a2239", + "0x1c012b03f1c2dea274d2eeeb566b0eeabfe3af1a", + "0x49531b794aecf2c823c1426a4c9990c4c45df501", + "0x988abfbb8743cc440e7ff7da6d2f4c60e301501f", + "0x69e0e2b3d523d3b247d798a49c3fa022a46dd6bd", + "0x47edf911b3c4915c947dc2b66bec977b8db65ab0", + "0x0a53e28f2f7b27971e18a6305c2c74a449badd2e" + ], + "snapshot": 12206854 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/saffron-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance/index.ts new file mode 100644 index 00000000..fad38fa4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/saffron-finance/index.ts @@ -0,0 +1,340 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { multicall } from '../../utils'; + +export const author = 'saffron.finance'; +export const version = '0.1.0'; + +const BIG18 = BigNumber.from('1000000000000000000'); +const VOTE_BOOST_DIV_1000 = BigNumber.from(1000); +const DECIMALS = 18; +const QUERIES_PER_DEX_LP_PAIR = 2; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'getReserves', + outputs: [ + { + internalType: 'uint112', + name: '_reserve0', + type: 'uint112' + }, + { + internalType: 'uint112', + name: '_reserve1', + type: 'uint112' + }, + { + internalType: 'uint32', + name: '_blockTimestampLast', + type: 'uint32' + } + ], + stateMutability: 'view', + type: 'function', + constant: true + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +// VotingScheme is an interface that provides one method for invoking a concrete implementation of different +// ways to calculate a score when given a raw balance from a Saffron LP token. +interface VotingScheme { + doAlgorithm(balance: BigNumber): BigNumber; +} + +// DirectBoostScheme provides logic to apply a multiplier, or boost, to a raw balance value. This gives +// Saffron Finance the ability to adjust the voting power a token holder has depending external configuration. +// +// name ... unique string that identifies the instance of the VotingScheme +// multiplier ... the raw balance value is multiplied by the multiplier such that: score = (multiplier)(balance). +// If this value is 1.0, it is equivalent to score = balance. +// If this value is less than 1.0 and greater than 0.0, then the token holder's voting power is reduced. +// If this value is 0.0, then the token holder has no voting power. +class DirectBoostScheme implements VotingScheme { + // private name: string; + private multiplier: number; + + constructor(name: string, multiplier) { + // this.name = name; + this.multiplier = multiplier; + } + + public doAlgorithm(balance: BigNumber): BigNumber { + const voteBoost1000 = BigNumber.from(this.multiplier * 1000); + return balance.mul(voteBoost1000).div(VOTE_BOOST_DIV_1000); + } +} + +// LPReservePairScheme provides logic to apply a voting score to only the SFI side of a Uniswap or Sushiswap LP token +// pair. +// +// name ... unique string that identifies the instance of the VotingScheme +// multiplier ... the raw balance value is multiplied by the multiplier such that: score = (multiplier)(balance). +// If this value is 1.0, it is equivalent to score = balance. +// If this value is less than 1.0 and greater than 0.0, then the token holder's voting power is reduced. +// If this value is 0.0, then the token holder has no voting power. +// saffLpToSfi_E18 ... Conversion of the Saffron LP Pair Token holding to SFI value with expected value to be in wei. +class LPReservePairScheme implements VotingScheme { + // private name: string; + private multiplier: number; + private saffLpToSfi_E18: BigNumber; + + constructor(name: string, multiplier: number, saffLpToSfi_E18: BigNumber) { + // this.name = name; + this.multiplier = multiplier; + this.saffLpToSfi_E18 = saffLpToSfi_E18; + } + + doAlgorithm(balance: BigNumber): BigNumber { + const voteMult1000 = BigNumber.from(this.multiplier * 1000); + const calculatedScore = balance.mul(this.saffLpToSfi_E18).div(BIG18); + return calculatedScore.mul(voteMult1000).div(VOTE_BOOST_DIV_1000); + } +} + +// VoteScorer acts as the context to invoke the relevant VotingScheme by way of its calculateScore method. +// It assumes all VotingScheme's are created by the createVotingScheme method prior to invocation of calculateScore. +// +// votingSchemes ... A Map that provides keyed access to a VotingScheme instance. +// dexReserveData ... An Array that holds necessary Uniswap and Sushiswap LP Pair Token data for LPReservePairScheme. +class VoteScorer { + private votingSchemes: Map = new Map< + string, + VotingScheme + >(); + private dexReserveData: Array = + new Array(); + + constructor(dexReserveData: Array) { + this.dexReserveData = dexReserveData; + this.votingSchemes.set('default', new DirectBoostScheme('default', 1.0)); + } + + public createVotingScheme(name: string, type: string, multiplier: number) { + let votingScheme: VotingScheme = new DirectBoostScheme( + 'no-vote-scheme', + 0.0 + ); + if (type === 'DirectBoostScheme') { + votingScheme = new DirectBoostScheme(name, multiplier); + } else if (type === 'LPReservePairScheme') { + const lpReservePairData = this.dexReserveData.find( + (e) => e.name === name + ); + if (lpReservePairData === undefined) { + throw Error(`Failed to locate token LP Pair data for ${name}.`); + } + votingScheme = new LPReservePairScheme( + name, + multiplier, + lpReservePairData.saffLpToSFI_E18 + ); + } else { + throw new Error(`Unsupported voting scheme type, ${type}.`); + } + this.votingSchemes.set(name, votingScheme); + } + + public calculateScore(schemeName: string, balance: BigNumber) { + const votingScheme = this.votingSchemes.get(schemeName); + if (votingScheme === undefined) { + throw new Error( + `Failed to locate voting scheme, ${schemeName}. Check initialization of votingSchemes.` + ); + } + return votingScheme.doAlgorithm(balance); + } +} + +// Batch represents a batch record that tracks a grouping of calls made by Multicall. The qIdxStart and qIdxEnd +// indicate the beginning and ending indices of the callQueries array. +type Batch = { + tag: string; + votingScheme: string; + qIdxStart: number; + qIdxEnd: number; +}; + +// DexReserveSupply is a single record that holds the queries for the values of a LP Pair token's reserves and +// supply. It also acts as a record for the calculated value for converting a Saffron LP toke holding to SFI. +type DexReserveSupply = { + name: string; + reservesQuery: string[]; + reserveQueryIdx: number; + reserve: BigNumber; + supplyQuery: string[]; + supplyQueryIdx: number; + supply: BigNumber; + saffLpToSFI_E18: BigNumber; +}; + +// VotingScore is a single voting score for the address. Each address from addresses will have one VotingScore for +// each contract in options.contracts. +type VotingScore = { + address: string; + score: number; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const callQueries: Array = new Array(); + let callResponses: Array = new Array(); + const holdersQueryBatches: Array = new Array(); + const votingScores: Array = new Array(); + let callQueryIndex = 0; + + // ================ LP Pair Token Reserve and Total Supply ================== + const dexReserveData = new Array(); + options.dexLpTypes.forEach((dexToken) => { + const d: DexReserveSupply = { + name: dexToken.name, + reservesQuery: [dexToken.lpToken, 'getReserves'], + reserveQueryIdx: 0, + reserve: BigNumber.from(0), + supplyQuery: [dexToken.lpToken, 'totalSupply'], + supplyQueryIdx: 0, + supply: BigNumber.from(0), + saffLpToSFI_E18: BigNumber.from(0) + }; + callQueries.push(d.reservesQuery); + d.reserveQueryIdx = callQueryIndex++; + callQueries.push(d.supplyQuery); + d.supplyQueryIdx = callQueryIndex++; + dexReserveData.push(d); + }); + + // ============= Multicall queries ============== + options.contracts.forEach((contract) => { + const queries = addresses.map((address: any) => { + return [contract.tokenAddress, 'balanceOf', [address]]; + }); + const queriesLength = callQueries.push(...queries); + const batch = { + tag: contract.label, + votingScheme: contract.votingScheme, + qIdxStart: callQueryIndex, + qIdxEnd: queriesLength + }; + callQueryIndex = queriesLength; + holdersQueryBatches.push(batch); + }); + + // Run queries + callResponses = await multicall(network, provider, abi, callQueries, { + blockTag + }); + + // ========== Extract and process query responses ========== + dexReserveData.forEach((drd) => { + drd.reserve = callResponses[drd.reserveQueryIdx][0]; + drd.supply = callResponses[drd.supplyQueryIdx][0]; + drd.saffLpToSFI_E18 = drd.reserve.mul(BIG18).div(drd.supply); + }); + + // ========== Build the voting schemes and calculate individual scores ============ + const voteScorer: VoteScorer = new VoteScorer(dexReserveData); + options.votingSchemes.forEach((scheme) => { + voteScorer.createVotingScheme(scheme.name, scheme.type, scheme.multiplier); + }); + + // Push empty Voting Score elements to the votingScores array. This allows Batch.qIdxStart to + // correspond correctly to votingScores. + const emptyVotingScoreCountToAdd = + dexReserveData.length * QUERIES_PER_DEX_LP_PAIR; + const emptyVote: VotingScore = { address: '0x00', score: 0.0 }; + for (let i = 0; i < emptyVotingScoreCountToAdd; i++) { + votingScores.push(emptyVote); + } + + options.contracts.forEach((contract) => { + const batch = holdersQueryBatches.find((e) => e.tag === contract.label); + if (batch === undefined) { + throw new Error( + `Failed to locate tag, ${contract.label}, in queryBatches.` + ); + } + const idxStart = batch.qIdxStart; + const batchScores = addresses.map((address: any, index: number) => { + return { + address: address, + score: voteScorer.calculateScore( + contract.votingScheme, + callResponses[idxStart + index][0] + ) + }; + }); + votingScores.push(...batchScores); + }); + + // ================ Sum up everything ================= + const addressVotingScore = addresses.map( + (address: any, addressIndex: number) => { + let total = BigNumber.from(0); + holdersQueryBatches.forEach((batch: Batch) => { + const votingScore = votingScores[batch.qIdxStart + addressIndex]; + if (votingScore === undefined) { + throw new Error( + `Expected a votingScore at batch.qIdxStart: ${batch.qIdxStart}, addressIndex: ${addressIndex}` + ); + } + if (votingScore.address === address) { + total = total.add(votingScore.score); + } else { + throw new Error( + `${batch.tag} expected address, ${address}, found ${votingScore.address}` + ); + } + }); + + // Return single record { address, score } where score should have exponent of 18 + return { address: address, score: total }; + } + ); + + return Object.fromEntries( + addressVotingScore.map((addressVote) => { + return [ + addressVote.address, + parseFloat(formatUnits(addressVote.score, DECIMALS)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/README.md new file mode 100644 index 00000000..79ecccba --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/README.md @@ -0,0 +1,21 @@ +# samurailegends-generals-balance + +A strategy that calculates the amount of general NFTs a user owns (NFT's with ids under 5000), which gives the voting power score. + +Here is an example of parameters: + +```json +{ + "batchAddress": "0x197352D6738011f2df1c3bB487a64aB075d1153A", + "nftAddress": "0x14a3Ee3771845cee9EA2D49Fcca8DDA58f5D5D8b", + "multiplier": 400, + "treshold": 500 +} +``` + +Parameter explanation: + +- **batchAddress** The address of the batch balance contract +- **nftAddress** The address of the nft collection that should be fetched +- **multiplier** The voting multiplier to use +- **treshold** The treshold for which the NFTs should be counted (e.g. if 500, all under 500 are counted) diff --git a/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/examples.json new file mode 100644 index 00000000..83f8b419 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example Samurai Legends Generals balance query", + "strategy": { + "name": "samurailegends-generals-balance", + "params": { + "batchAddress": "0x7442bf183d95d6ef452375ee6d021b84c5bc46a9", + "nftAddress": "0x14a3Ee3771845cee9EA2D49Fcca8DDA58f5D5D8b", + "multiplier": 3, + "treshold": 500 + } + }, + "network": "97", + "addresses": ["0x77a8e03bbA3F669A56C3F5e0194654b96C0d8449"], + "snapshot": 18684485 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/index.ts new file mode 100644 index 00000000..cfaa864a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/samurailegends-generals-balance/index.ts @@ -0,0 +1,44 @@ +import { Multicaller } from '../../utils'; + +export const author = 'Samurai-Legends'; +export const version = '0.2.0'; + +const abi = [ + 'function erc721BatchOwnerOf(address nftAddress, uint idMin, uint idMax) external view returns (address[] memory)' +]; + +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 }); + + multi.call('batch.owners', options.batchAddress, 'erc721BatchOwnerOf', [ + options.nftAddress, + '0', + options.treshold.toString() + ]); + const result = await multi.execute(); + + const balances = result.batch.owners.reduce( + (prev: Record, curr: string) => { + if (curr in prev) prev[curr] += 1; + else prev[curr] = 1; + return prev; + }, + {} + ); + + return Object.fromEntries( + addresses.map((address: string) => { + const balance = balances[address] || 0; + return [address, balance * options.multiplier]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/README.md b/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/README.md new file mode 100644 index 00000000..6a57b8e2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/README.md @@ -0,0 +1,62 @@ + +# SandManDAO + + In Sandman Finance, NFTs are the backbone of the platform ownership. And the way to participate in this DAO. + +Our NFTs are composed by various skills, as shown [here](https://docs.death.sandman.finance/nfts/nft-cards-composition). +One of the skill is "Experience", that starts at 0. And get increased as each community member uses this card. + +This SandmanDAO strategy consists in reading the "Experience" skill of each NFT that the wallet owner has, and the following logic: +1. If the NFTs has 0 "Experience", then it's vote value is 1. +2. IF the NFTs has more than 0 "Experience", then the vote value equals the 1 + and extra value of the experience. +3. IF the wallet has multiple NFTs. then the vote value is the sum of all NFTs. + + +## Strategy Parameters + + + +Example strategy params: + + + +```json + +[ + +{ + +"name": "DAO sandman.finance", + +"strategy": { + +"name": "sandManDAO", + +"params": { + +"address": "0x743F554f6AcCd4E452AF6C96c48B78E849d87316", + +"symbol": "TheEndless" + +} + +}, + +"network": "137", + +"addresses": [ + +"0xA34f2b833753bCDb6e652A87B0d363FF8f1eE9c5", + +"0x6fEc079288329553F4d4512be33d05d5793e1f31" + +], + +"snapshot": 24110041 + +} + +] +``` + +##### DAO SANDMAN.FINANCE ENHANCED BY NFT \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/examples.json new file mode 100644 index 00000000..f2083091 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "dao sandman.finance", + "strategy": { + "name": "sandman-dao", + "params": { + "address": "0x743F554f6AcCd4E452AF6C96c48B78E849d87316", + "symbol": "TheEndless" + } + }, + "network": "137", + "addresses": [ + "0xA34f2b833753bCDb6e652A87B0d363FF8f1eE9c5", + "0x6fEc079288329553F4d4512be33d05d5793e1f31" + ], + "snapshot": 24110041 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/index.ts new file mode 100644 index 00000000..d1083ca2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sandman-dao/index.ts @@ -0,0 +1,91 @@ +import { Multicaller } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'sandmanfinance'; +export const version = '0.0.1'; + +const factoryNftABI = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function getCharacterOverView(uint256 tokenId) external view returns (string memory, uint256, uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // First, get the balance of nft + const callWalletToBalanceOf = new Multicaller( + network, + provider, + factoryNftABI, + { + blockTag + } + ); + for (const walletAddress of addresses) { + callWalletToBalanceOf.call(walletAddress, options.address, 'balanceOf', [ + walletAddress + ]); + } + const walletToBalanceOf: Record = + await callWalletToBalanceOf.execute(); + + // Second, get the tokenId's for each nft + const callWalletToAddresses = new Multicaller( + network, + provider, + factoryNftABI, + { + blockTag + } + ); + for (const [walletAddress, count] of Object.entries(walletToBalanceOf)) { + for (let index = 0; index < count.toNumber(); index++) { + callWalletToAddresses.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToAddresses: Record = + await callWalletToAddresses.execute(); + + // Third, get skil's for each tokenId + const callTokenToSkill = new Multicaller(network, provider, factoryNftABI, { + blockTag + }); + + for (const [walletID, tokenId] of Object.entries(walletIDToAddresses)) { + callTokenToSkill.call(walletID, options.address, 'getCharacterOverView', [ + tokenId + ]); + } + const walletIDToSkills: Record = + await callTokenToSkill.execute(); + + const results = {} as Record; + for (const [walletID, values] of Object.entries(walletIDToSkills)) { + const address = walletID.split('-')[0]; + const currentExperience = values[1] / 1e18; + + let extraBoosted = 1; + if (currentExperience > 0) { + extraBoosted = currentExperience / 100; + } + + results[address] = (results[address] || 0) + extraBoosted; + } + + return Object.fromEntries( + Object.entries(results).map(([address, weight]) => [address, weight]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/README.md b/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/README.md new file mode 100644 index 00000000..2cbcb284 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/README.md @@ -0,0 +1,21 @@ +# sd-boost-twavp + +This strategy is used by StakeDAO to vote with sdToken with Time Weigthed Averaged Voting Power system and veSDT voting boost. +_sampleSize is in days_ +_sampleStep is the number of block for TWAVP_ +_avgBlockTime is in seconds_ +Here is an example of parameters: + +```json +{ + "sdToken": "0xD1b5651E55D4CeeD36251c61c50C889B36F6abB5", + "veToken": "0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2", + "lockerToken": "0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6", + "gauge": "0x7f50786A0b15723D741727882ee99a0BF34e3466", + "symbol": "sdToken", + "decimals": 18, + "sampleSize": 30, + "sampleStep": 5, + "avgBlockTime": 13.91 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/examples.json new file mode 100644 index 00000000..78be18a2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "sd-boost-twavp", + "params": { + "sdToken": "0xD1b5651E55D4CeeD36251c61c50C889B36F6abB5", + "veToken": "0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2", + "lockerToken": "0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6", + "gauge": "0x7f50786A0b15723D741727882ee99a0BF34e3466", + "symbol": "sdToken", + "decimals": 18, + "sampleSize": 15, + "sampleStep": 5, + "avgBlockTime": 13.91 + } + }, + "network": "1", + "addresses": [ + "0xa7888f85bd76deef3bd03d4dbcf57765a49883b3", + "0x8d9f950c23b73edf79ce52f74c6fb589cd2cbd90" + ], + "snapshot": 15000000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/index.ts new file mode 100644 index 00000000..dc4a937a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-boost-twavp/index.ts @@ -0,0 +1,188 @@ +import { multicall } from '../../utils'; + +export const author = 'clement-ux'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)', + 'function working_supply() external view returns (uint256)' +]; + +const veSDT = '0x0C30476f66034E11782938DF8e4384970B6c9e8a'; + +const F1 = 0.4; +const F2 = 0.6; + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + // Maximum of 5 multicall + if (options.sampleStep > 5) { + throw new Error('maximum of 5 call'); + } + + // About the blockList + const av_blockEmission = options.avgBlockTime; + const lastBlock = await provider.getBlockNumber(); + let blockTag = typeof snapshot === 'number' ? snapshot : lastBlock; + const nbrsEmittedBlock = Math.floor( + (options.sampleSize * 60 * 60 * 24) / av_blockEmission + ); + const blockTagList: number[] = []; + for (let i = 1; i < options.sampleStep + 1; i++) { + blockTagList.push( + blockTag - + Math.floor( + (nbrsEmittedBlock * (options.sampleStep - i)) / options.sampleStep + ) + ); + } + //console.log('Used block List: ', blockTagList); + + // Query + const veSDTBalanceQuery = addresses.map((address: any) => [ + veSDT, + 'balanceOf', + [address] + ]); + const sdTokenBalanceQuery = addresses.map((address: any) => [ + options.gauge, + 'balanceOf', + [address] + ]); + const responseBlockRef = await multicall( + network, + provider, + abi, + [ + [options.veToken, 'balanceOf', [options.lockerToken]], + [veSDT, 'totalSupply'], + [options.gauge, 'totalSupply'], + [options.gauge, 'working_supply'], + ...veSDTBalanceQuery, + ...sdTokenBalanceQuery + ], + { + blockTag + } + ); + const responseOtherBlock: number[] = []; + for (let i = options.sampleStep - 1; i > 0; i--) { + blockTag = blockTagList[i - 1]; + responseOtherBlock.push( + await multicall(network, provider, abi, [...sdTokenBalanceQuery], { + blockTag + }) + ); + } + + // Constant + // Get LLVotingPower : total Liquid Locker voting power + const LLVotingPower = responseBlockRef[0]; + // Get veSDTSupply : total veSDT supply + const veSDTSupply = responseBlockRef[1]; + // Get gaugeSupply : total balance of staked sdToken + const gaugeSupply = responseBlockRef[2]; + // Get Working supply on the gauge + const workingSupply = responseBlockRef[3]; + + const responseClean = responseBlockRef.slice(4, responseBlockRef.length); + const chunks = chunk(responseClean, addresses.length); + + // Variable per address + // Get veSDTBalance : user veSDT balance + const veSDTBalance = chunks[0]; + // Get sdTokenBalance : user balance of staked sdToken + const sdTokenBalance = chunks[1]; + //console.log(sdTokenBalance) + + // Get adjustedBalance = user adjusted balance of staked sdToken + const adjustedBalance = Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + const veSDTRatio = veSDTBalance[i][0] / veSDTSupply[0]; + // Address : RefBlock : Value + //console.log( + // addresses[i], + // 'block: ', + // blockTagList[blockTagList.length - 1], + // 'value: ', + // Number(sdTokenBalance[i]) + //); + let sumSdTokenBalance = Number(sdTokenBalance[i]); + for (let j = 0; j < options.sampleStep - 1; j++) { + // Address : Block : Value + //console.log( + // addresses[i], + // 'block: ', + // blockTagList[options.sampleStep - 2 - j], + // 'value: ', + // Number(responseOtherBlock[j][i]) + //); + sumSdTokenBalance += Number(responseOtherBlock[j][i]); + } + const avgSdTokenBalance = sumSdTokenBalance / options.sampleStep; + //console.log(addresses[i]," average sdToken Balance",avgSdTokenBalance); + const derived_i = + avgSdTokenBalance * F1 + gaugeSupply * veSDTRatio * F2; + const adjustedBalance_i = Math.min(derived_i, avgSdTokenBalance); + + // Print statements variable per address + //console.log(`==================${addresses[i]}==================`); + //console.log( + // 'Balance veSDT: ', + // (veSDTBalance[i][0] / 10 ** DECIMALS).toString() + //); + //console.log( + // 'Ratio balance/supply veSDT: ', + // (veSDTRatio * 100).toString(), + // '%' + //); + //console.log( + // 'Calcul inside adjusted balance: ', + // (derived_i / 10 ** DECIMALS).toString() + //); + //console.log( + // 'AdjustedBalance: ', + // (adjustedBalance_i / 10 ** DECIMALS).toString() + //); + //console.log( + // 'LLVotingPower: ', + // (LLVotingPower / 10 ** DECIMALS).toString() + //); + //console.log('veSDTSupply: ', (veSDTSupply / 10 ** DECIMALS).toString()); + //console.log('gaugeSupply: ', (gaugeSupply / 10 ** DECIMALS).toString()); + //console.log( + // 'Working Supply: ', + // (workingSupply / 10 ** DECIMALS).toString() + //); + // console.log(``);``` + return [addresses[i], adjustedBalance_i / 10 ** options.decimals]; + }) + ); + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + // Get votingPower : user voting power + const votingPower = + workingSupply > 0 + ? (adjustedBalance[addresses[i]] * LLVotingPower) / workingSupply + : 0; + return [addresses[i], votingPower]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-boost/README.md b/Implementations/API/backend/utils/snapshot/strategies/sd-boost/README.md new file mode 100644 index 00000000..866704ff --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-boost/README.md @@ -0,0 +1,15 @@ +# sd-boost + +This strategy is used by StakeDAO to vote with sdToken with veSDT voting boost +Here is an example of parameters: + +```json +{ + "sdToken": "0x402F878BDd1f5C66FdAF0fabaBcF74741B68ac36", + "veToken": "0xc8418aF6358FFddA74e09Ca9CC3Fe03Ca6aDC5b0", + "lockerToken": "0xCd3a267DE09196C48bbB1d9e842D7D7645cE448f", + "gauge": "0xF3C6e8fbB946260e8c2a55d48a5e01C82fD63106", + "symbol": "sdToken", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-boost/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sd-boost/examples.json new file mode 100644 index 00000000..a5944649 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-boost/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "sd-boost", + "params": { + "sdToken": "0x402F878BDd1f5C66FdAF0fabaBcF74741B68ac36", + "veToken": "0xc8418aF6358FFddA74e09Ca9CC3Fe03Ca6aDC5b0", + "lockerToken": "0xCd3a267DE09196C48bbB1d9e842D7D7645cE448f", + "gauge": "0xF3C6e8fbB946260e8c2a55d48a5e01C82fD63106", + "symbol": "sdToken", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xb7BFcDC3a2AA2aF3Fe653C9E8a19830977E1993C", + "0x806346b423dDB4727C1f5dC718886430aA7CE9cF" + ], + "snapshot": 14476645 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-boost/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sd-boost/index.ts new file mode 100644 index 00000000..15791454 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-boost/index.ts @@ -0,0 +1,139 @@ +import { multicall } from '../../utils'; + +export const author = 'clement-ux'; +export const version = '0.0.3'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)', + 'function working_supply() external view returns (uint256)' +]; + +const veSDT = '0x0C30476f66034E11782938DF8e4384970B6c9e8a'; + +const F1 = 0.4; +const F2 = 0.6; + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const veSDTBalanceQuery = addresses.map((address: any) => [ + veSDT, + 'balanceOf', + [address] + ]); + const sdTokenBalanceQuery = addresses.map((address: any) => [ + options.gauge, + 'balanceOf', + [address] + ]); + + const response = await multicall( + network, + provider, + abi, + [ + [options.veToken, 'balanceOf', [options.lockerToken]], + [veSDT, 'totalSupply'], + [options.gauge, 'totalSupply'], + [options.gauge, 'working_supply'], + ...veSDTBalanceQuery, + ...sdTokenBalanceQuery + ], + { + blockTag + } + ); + + // Constant + // Get LLVotingPower : total Liquid Locker voting power + const LLVotingPower = response[0]; + // Get veSDTSupply : total veSDT supply + const veSDTSupply = response[1]; + // Get gaugeSupply : total balance of staked sdToken + const gaugeSupply = response[2]; + // Get Working supply on the gauge + const workingSupply = response[3]; + + const responseClean = response.slice(4, response.length); + const chunks = chunk(responseClean, addresses.length); + + // Variable per address + // Get veSDTBalance : user veSDT balance + const veSDTBalance = chunks[0]; + // Get sdTokenBalance : user balance of staked sdToken + const sdTokenBalance = chunks[1]; + + // Get adjustedBalance = user adjusted balance of staked sdToken + const adjustedBalance = Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + const veSDTRatio = veSDTBalance[i][0] / veSDTSupply[0]; + const derived_i = + sdTokenBalance[i][0] * F1 + gaugeSupply * veSDTRatio * F2; + const adjustedBalance_i = Math.min(derived_i, sdTokenBalance[i][0]); + + // Print statements variable per address + //console.log(`==================${addresses[i]}==================`); + //console.log( + // 'Balance veSDT: ', + // (veSDTBalance[i][0] / 10 ** DECIMALS).toString() + //); + //console.log( + // 'Balance of sdToken: ', + // (sdTokenBalance[i][0] / 10 ** DECIMALS).toString() + //); + //console.log( + // 'Ratio balance/supply veSDT: ', + // (veSDTRatio * 100).toString(), + // '%' + //); + //console.log( + // 'Calcul inside adjusted balance: ', + // (derived_i / 10 ** DECIMALS).toString() + //); + //console.log( + // 'AdjustedBalance: ', + // (adjustedBalance_i / 10 ** DECIMALS).toString() + //); + //console.log( + // 'LLVotingPower: ', + // (LLVotingPower / 10 ** DECIMALS).toString() + //); + //console.log('veSDTSupply: ', (veSDTSupply / 10 ** DECIMALS).toString()); + //console.log('gaugeSupply: ', (gaugeSupply / 10 ** DECIMALS).toString()); + //console.log( + // 'Working Supply: ', + // (workingSupply / 10 ** DECIMALS).toString() + //); + // console.log(``);``` + return [addresses[i], adjustedBalance_i / 10 ** options.decimals]; + }) + ); + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + // Get votingPower : user voting power + const votingPower = + workingSupply > 0 + ? (adjustedBalance[addresses[i]] * LLVotingPower) / workingSupply + : 0; + return [addresses[i], votingPower]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/README.md b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/README.md new file mode 100644 index 00000000..8b15abd2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/README.md @@ -0,0 +1,26 @@ +# sd-vote-boost-twavp + +This strategy is used by Stake DAO to vote with sdToken using Time Weigthed Averaged Voting Power (TWAVP) system and adapted for veSDT boost delegation. + +``` +VotingPower(user) = veToken.balanceOf(liquidLocker) * (average.sdTokenGauge.working_balances(user) / sdTokenGauge.working_supply) +``` + +>_sampleSize: in days_ +>_sampleStep: the number of block for `average` calculation (max 5)_ +>_avgBlockTime: in seconds_ + +Here is an example of parameters: + +```json +{ + "veToken": "0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2", + "liquidLocker": "0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6", + "sdTokenGauge": "0x7f50786A0b15723D741727882ee99a0BF34e3466", + "symbol": "sdToken", + "decimals": 18, + "sampleSize": 30, + "sampleStep": 5, + "avgBlockTime": 12.0 +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/examples.json new file mode 100644 index 00000000..9be59268 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "sd-vote-boost-twavp", + "params": { + "veToken": "0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2", + "liquidLocker": "0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6", + "sdTokenGauge": "0x7f50786A0b15723D741727882ee99a0BF34e3466", + "symbol": "sdToken", + "decimals": 18, + "sampleSize": 30, + "sampleStep": 5, + "avgBlockTime": 12.0 + } + }, + "network": "1", + "addresses": [ + "0xa7888f85bd76deef3bd03d4dbcf57765a49883b3", + "0x8d9f950c23b73edf79ce52f74c6fb589cd2cbd90", + "0x4af79ffcabb09083af6ccc3b2c20fe989519f6d7", + "0xa429ac8bd9382e7f69645cc30bd718b5d33d674b", + "0x7c9f43215D3B3D5055433a9d7B12bCBf9F4be442" + ], + "snapshot": 15725810 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/index.ts new file mode 100644 index 00000000..e9ab6810 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost-twavp/index.ts @@ -0,0 +1,105 @@ +import { multicall } from '../../utils'; + +export const author = 'clement-ux'; +export const version = '0.0.1'; +export const dependOnOtherAddress = false; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function working_supply() external view returns (uint256)', + 'function working_balances(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + // Maximum of 5 multicall + if (options.sampleStep > 5) { + throw new Error('maximum of 5 call'); + } + + // About the blockList + const av_blockEmission = options.avgBlockTime; + const lastBlock = await provider.getBlockNumber(); + let blockTag = typeof snapshot === 'number' ? snapshot : lastBlock; + const nbrsEmittedBlock = Math.floor( + (options.sampleSize * 60 * 60 * 24) / av_blockEmission + ); + const blockTagList: number[] = []; + for (let i = 1; i < options.sampleStep + 1; i++) { + blockTagList.push( + blockTag - + Math.floor( + (nbrsEmittedBlock * (options.sampleStep - i)) / options.sampleStep + ) + ); + } + //console.log('Used block List: ', blockTagList); + + // Query + const workingBalanceQuery = addresses.map((address: any) => [ + options.sdTokenGauge, + 'working_balances', + [address] + ]); + + const response: number[] = []; + + for (let i = 0; i < options.sampleStep; i++) { + blockTag = blockTagList[i]; + response.push( + await multicall( + network, + provider, + abi, + [ + [options.sdTokenGauge, 'working_supply'], + [options.veToken, 'balanceOf', [options.liquidLocker]], + ...workingBalanceQuery + ], + { + blockTag + } + ) + ); + } + + // Constant + // Get Working supply on the gauge + const workingSupply = response[response.length - 1][0]; + const votingPowerLiquidLocker = response[response.length - 1][1]; + + const averageWorkingBalance = Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + let sum = Number(0); + //console.log(`==================${addresses[i]}==================`); + for (let j = 0; j < response.length; j++) { + sum += Number(response[j][i + 2]); + //console.log(Number(response[j][i+2])) + } + //console.log("Adjusted Balance : ", sum/(response.length*10**options.decimals)) + return [addresses[i], sum / response.length]; + }) + ); + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + // Get votingPower : user voting power + const votingPower = + workingSupply > 0 + ? (averageWorkingBalance[addresses[i]] * votingPowerLiquidLocker) / + (workingSupply * 10 ** options.decimals) + : 0; + return [addresses[i], votingPower]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/README.md b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/README.md new file mode 100644 index 00000000..769e9932 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/README.md @@ -0,0 +1,18 @@ +# sd-vote-boost + +This strategy is used by StakeDAO to vote with sdToken adapted for veSDT boost delegation (without TWAVP). +``` +VotingPower(user) = veToken.balanceOf(liquidLocker) * (sdTokenGauge.working_balances(user) / sdTokenGauge.working_supply) +``` + +Here is an example of parameters: + +```json +{ + "veToken": "0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2", + "liquidLocker": "0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6", + "sdTokenGauge": "0x7f50786A0b15723D741727882ee99a0BF34e3466", + "symbol": "sdToken", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/examples.json new file mode 100644 index 00000000..9e471543 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "sd-vote-boost", + "params": { + "veToken": "0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2", + "liquidLocker": "0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6", + "sdTokenGauge": "0x7f50786A0b15723D741727882ee99a0BF34e3466", + "symbol": "sdToken", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xa7888f85bd76deef3bd03d4dbcf57765a49883b3", + "0x8d9f950c23b73edf79ce52f74c6fb589cd2cbd90", + "0x4af79ffcabb09083af6ccc3b2c20fe989519f6d7", + "0xa429ac8bd9382e7f69645cc30bd718b5d33d674b", + "0x7c9f43215D3B3D5055433a9d7B12bCBf9F4be442" + ], + "snapshot": 15725810 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/index.ts new file mode 100644 index 00000000..70b5dffe --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sd-vote-boost/index.ts @@ -0,0 +1,62 @@ +import { multicall } from '../../utils'; + +export const author = 'clement-ux'; +export const version = '0.0.1'; +export const dependOnOtherAddress = false; +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function working_supply() external view returns (uint256)', + 'function working_balances(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + // BlockTag + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Query + const workingBalanceQuery = addresses.map((address: any) => [ + options.sdTokenGauge, + 'working_balances', + [address] + ]); + + // Multicall + const response = await multicall( + network, + provider, + abi, + [ + [options.sdTokenGauge, 'working_supply'], + [options.veToken, 'balanceOf', [options.liquidLocker]], + ...workingBalanceQuery + ], + { + blockTag + } + ); + + // Constant + const workingSupply = response[0]; // working supply on gauge + const votingPowerLiquidLocker = response[1]; // balanceOf veCRV LiquidLocker + + // Return + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + const votingPower = + workingSupply > 0 + ? (response[i + 2] * votingPowerLiquidLocker) / + (workingSupply * 10 ** options.decimals) + : 0; + return [addresses[i], votingPower]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/selfswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/selfswap/README.md new file mode 100644 index 00000000..42c76488 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/selfswap/README.md @@ -0,0 +1,9 @@ +# selfswap + +Fetches [SELF](https://bscscan.com/address/0x7a364484303b38bce7b0ab60a20da8f2f4370129) balance from the following sources: + +- Wallet +- SELF-BNB LP Farm +- SELF Pool +- SELF Vault +- Pools that were active at the time of the snapshot diff --git a/Implementations/API/backend/utils/snapshot/strategies/selfswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/selfswap/examples.json new file mode 100644 index 00000000..82b20d8c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/selfswap/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "selfswap", + "params": { + "symbol": "SELF" + } + }, + "network": "56", + "addresses": [ + "0x26e82CB17cfd4ef12096f38f3ba0DAD6ea5B5035", + "0x21fF20E7e1B820020415707298b92299CF0951fE", + "0x2b3D1D31ac5C053cf89a92EE9c94dbF3774D6366", + "0x273e3fD65450032a44AC6CA36F6551D74A459B6A" + ], + "snapshot": 16308675 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/selfswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/selfswap/index.ts new file mode 100644 index 00000000..76531aa0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/selfswap/index.ts @@ -0,0 +1,224 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { strategy as masterChefPoolBalanceStrategy } from '../masterchef-pool-balance'; +import { formatEther } from '@ethersproject/units'; +import { Zero, WeiPerEther } from '@ethersproject/constants'; +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest, Multicaller } from '../../utils'; + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); + +const PAGE_SIZE = 1000; + +export const author = 'Cr3k'; +export const version = '0.0.1'; + +const SELF_ADDRESS = '0x7a364484303b38bce7b0ab60a20da8f2f4370129'; +const SELF_VAULT_ADDRESS = '0xeb4f1307DE7DF263E8e54d083fE7db1e281e710D'; +const SELF_BNB_LP_ADDRESS = '0x9C6FF656A563Ec9057460D8a400E2AC7c2AE0a1C'; + +const MASTER_CHEF_ADDRESS = { + v1: '0x3d03d12F95Bdc4509804f9Bcee4139b7789DC516' +}; + +const vaultAbi = [ + 'function getPricePerFullShare() view returns (uint256)', + 'function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime, uint256 selfAtLastUserAction, uint256 lastUserActionTime)' +]; + +const smartChefUrl = 'https://api.thegraph.com/subgraphs/name/cr3k/smartchef'; + +async function getPools(provider, snapshot: any) { + let blockNumber = snapshot; + if (blockNumber === 'latest') { + blockNumber = await provider.getBlockNumber(); + } + + const params = { + smartChefs: { + __args: { + where: { + stakeToken: SELF_ADDRESS.toLowerCase(), + endBlock_gte: blockNumber, + startBlock_lt: blockNumber + } + }, + id: true + } + }; + + const pools = await subgraphRequest(smartChefUrl, params); + + return pools.smartChefs; +} + +async function getSmartChefStakedSELFAmount( + snapshot: any, + poolAddresses: string[], + addresses: string[] +) { + const addressChunks = chunk(addresses, 1500); + let results: any[] = []; + + for (const addressChunk of addressChunks) { + const params = { + users: { + __args: { + where: { + pool_in: poolAddresses.map((addr) => addr.toLowerCase()), + address_in: addressChunk.map((addr) => addr.toLowerCase()), + stakeAmount_gt: '0' + }, + first: PAGE_SIZE + }, + address: true, + stakeAmount: true + } + }; + + let page = 0; + let triedBlockNumber = false; + + while (true) { + // @ts-ignore + params.users.__args.skip = page * PAGE_SIZE; + if (snapshot !== 'latest' && !triedBlockNumber) { + // @ts-ignore + params.users.__args.block = { number: snapshot }; + } else { + // @ts-ignore + delete params.users.__args.block; + } + let result; + try { + result = await subgraphRequest(smartChefUrl, params); + } catch (error) { + if (!triedBlockNumber) { + triedBlockNumber = true; + continue; + } else { + throw error; + } + } + if (!Array.isArray(result.users) && !triedBlockNumber) { + triedBlockNumber = true; + continue; + } + results = results.concat(result.users); + page++; + if (result.users.length < PAGE_SIZE) break; + } + } + + return results.reduce>((acc, user) => { + if (acc[user.address]) { + acc[user.address] = (acc[user.address] as BigNumber).add( + user.stakeAmount + ); + } else { + acc[user.address] = BigNumber.from(user.stakeAmount); + } + return acc; + }, {}); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const pools = await getPools(provider, snapshot); + + const userPoolBalance = await getSmartChefStakedSELFAmount( + snapshot, + pools.map((p) => p.id), + addresses + ); + + const blockTag = snapshot; + + const erc20Balance = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + { + address: SELF_ADDRESS, + symbol: 'SELF', + decimals: 18 + }, + snapshot + ); + + const selfBnbLpBalance = await masterChefPoolBalanceStrategy( + space, + network, + provider, + addresses, + { + chefAddress: MASTER_CHEF_ADDRESS.v1, + uniPairAddress: SELF_BNB_LP_ADDRESS, + pid: '251', + symbol: 'SELF-BNB LP', + tokenIndex: 0 + }, + snapshot + ); + + const selfVaultBalance = await getVaultBalance( + network, + provider, + addresses, + blockTag + ); + + return Object.fromEntries( + addresses.map((address) => [ + address, + erc20Balance[address] + + selfBnbLpBalance[address] + + parseFloat( + formatEther( + (userPoolBalance[address.toLowerCase()] || Zero).add( + selfVaultBalance[address] || Zero + ) + ) + ) + ]) + ); +} + +async function getVaultBalance(network, provider, addresses, blockTag) { + const vaultMulti = new Multicaller(network, provider, vaultAbi, { blockTag }); + + vaultMulti.call( + SELF_VAULT_ADDRESS, + SELF_VAULT_ADDRESS, + 'getPricePerFullShare' + ); + + addresses.forEach((address) => + vaultMulti.call( + `${SELF_VAULT_ADDRESS}-${address}`, + SELF_VAULT_ADDRESS, + 'userInfo', + [address] + ) + ); + + const vaultMultiRes = await vaultMulti.execute(); + + return Object.fromEntries( + addresses.map((address) => [ + address, + (vaultMultiRes[SELF_VAULT_ADDRESS] || Zero) + .mul(vaultMultiRes[`${SELF_VAULT_ADDRESS}-${address}`]?.shares || Zero) + .div(WeiPerEther) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/README.md b/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/README.md new file mode 100644 index 00000000..b64b32d1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/README.md @@ -0,0 +1,12 @@ +# single-staking-autocompound-balanceof + +Used for fetching the staked token balance in an autocompounding single staking pool + +Here is an example of parameters: + +```json +{ + "stakingPoolAddress": "0xC122d1F67bbC3e40aec198C54B9eC13E1b0990eC", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/examples.json b/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/examples.json new file mode 100644 index 00000000..996fdfcc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Single Staking Amount from Autocompounding Vault", + "strategy": { + "name": "single-staking-autocompound-balanceof", + "params": { + "stakingPoolAddress": "0xC122d1F67bbC3e40aec198C54B9eC13E1b0990eC", + "symbol": "FUZZ", + "decimals": 18 + } + }, + "network": "1666600000", + "addresses": [ + "0x12e49d93588e0056bd25530C3B1E8AAc68F4B70a", + "0x8C612B03b3358C1E535706695c635C360034B968", + "0x24d19f100ba142543a863fc2294b188e35ab55b9" + ], + "snapshot": 32877171 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/index.ts b/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/index.ts new file mode 100644 index 00000000..b1c445e3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-autocompound-balanceof/index.ts @@ -0,0 +1,48 @@ +/* eslint-disable prettier/prettier */ +import { formatUnits } from '@ethersproject/units'; +import { multicall, Multicaller } from '../../utils'; + +export const author = 'michaelotis'; +export const version = '0.1.0'; +export const dependOnOtherAddress = false; + +const abi = [ + 'function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime, uint256 fuzzAtLastUserAction, uint256 lastUserActionTime)', + 'function getPricePerFullShare() view returns (uint256)' +]; + +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) => + multi.call(address, options.stakingPoolAddress, 'userInfo', [address]) + ); + + const [[[getPricePerFullShare]]] = await Promise.all([ + multicall( + network, + provider, + abi, + [[options.stakingPoolAddress, 'getPricePerFullShare', []]], + { blockTag } + ) + ]); + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, userInfo]) => [ + address, + parseFloat(formatUnits(userInfo.shares, options.decimals)) * parseFloat(formatUnits(getPricePerFullShare, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/README.md b/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/README.md new file mode 100644 index 00000000..94bda2f1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/README.md @@ -0,0 +1,14 @@ +# single-staking-pools-balanceof + +Used for fetching the staked token balance in a single staking pool + +Here is an example of parameters: + +```json +{ + "stakingPoolAddress": [ + "0x081Ffa6Fa76e738531B3717301F4B636efAe1F1e" + ], + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/examples.json b/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/examples.json new file mode 100644 index 00000000..0a066d7f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "single-staking-pools-balanceof", + "params": { + "stakingPoolAddresses": ["0x081Ffa6Fa76e738531B3717301F4B636efAe1F1e"], + "symbol": "DAI", + "decimals": 18 + } + }, + "network": "43114", + "addresses": ["0xa598710E9EdA808dF224E14748f6eba374043715"], + "snapshot": 4565540 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/index.ts b/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/index.ts new file mode 100644 index 00000000..0a390744 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-pools-balanceof/index.ts @@ -0,0 +1,39 @@ +/* eslint-disable prettier/prettier */ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'PolySwift'; +export const version = '0.1.0'; + + +const singleStakingPoolAbi = [ + 'function userInfo(address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, singleStakingPoolAbi, { blockTag }); + + options.stakingPoolAddresses.forEach(stakingPoolAddress => { + addresses.forEach((address) => + multi.call(address, stakingPoolAddress, 'userInfo', [address]) + ); + }) + + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, userInfo]) => [ + address, + parseFloat(formatUnits(userInfo.amount, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/README.md b/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/README.md new file mode 100644 index 00000000..a7c2d76d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/README.md @@ -0,0 +1,13 @@ +# single-staking-vault-balanceof + +Used for fetching the staked token balance in a single staking vault + +The only parameter is the vault address. The vault must +have the function call `wantLockedTotal(address)` which should +return the amount of tokens in the vault. + +```json +{ + "vaultAddress": "0xA68E643e1942fA8635776b718F6EeD5cEF2a3F15" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/examples.json b/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/examples.json new file mode 100644 index 00000000..5a4bbb5e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "single-staking-vault-balanceof", + "params": { + "symbol": "STAKE", + "vaultAddress": "0xA68E643e1942fA8635776b718F6EeD5cEF2a3F15" + } + }, + "network": "1666600000", + "addresses": [ + "0xD20B976584bF506BAf5cC604D1f0A1B8D07138dA", + "0x4ff9B7C1424b9E4375BbbDF3357a318412c02E0c", + "0x57B7713c0E013cfbEC0E4C6c8B264dAf7598ebA9", + "0xB989B490F9899a5AD56a4255A3C84457040B59dc" + ], + "snapshot": 18263021 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/index.ts b/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/index.ts new file mode 100644 index 00000000..729c3b7a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/single-staking-vault-balanceof/index.ts @@ -0,0 +1,39 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'foxthefarmer'; +export const version = '0.0.1'; + +const vaultAbi = ['function wantLockedTotal(address) view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const vaultBalancesCalls: any = multicall( + network, + provider, + vaultAbi, + addresses.map((address: any) => [ + options.vaultAddress, + 'wantLockedTotal', + [address] + ]), + { blockTag } + ); + + const vaultBalances = await Promise.all([vaultBalancesCalls]); + + return Object.fromEntries( + Object.entries(addresses).map((address: any, index) => [ + address[1], + parseFloat(formatUnits(vaultBalances[0][index].toString(), 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/README.md new file mode 100644 index 00000000..c47b88c5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/README.md @@ -0,0 +1,16 @@ +# skale-delegation-weighted + +This strategy allow SKL tokens holders to participate in vote, where the weight of the vote is an amount of delegated tokens. +Holder can delegate directly or with Escrow contract(provided by SKALE) + +Required params in `example.json`: + - addressSKL - address of SKL token + - addressAllocator - address of Allocator contract + +```json +{ + "addressSKL": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "SKL", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/examples.json new file mode 100644 index 00000000..bb63dd6a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/examples.json @@ -0,0 +1,31 @@ +[ + { + "name": "Directly delegated amount of SKL token or delegated with Escrow ", + "strategy": { + "name": "skale-delegation-weighted", + "params": { + "addressSKL": "0x00c83aeCC790e8a4453e5dD3B0B4b3680501a7A7", + "addressAllocator": "0xB575c158399227b6ef4Dcfb05AA3bCa30E12a7ba", + "symbol": "SKL", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0xD6f4196EDe7a5e83e4819bf77784F8b24D9475BD", + "0xB4FA5AA4073E43C8b8E780df971856fc9e94F7c8", + "0x4AeD8A87544fFAe057354A4a762E70c38f5D6bc7", + "0x6A3B29FdfC7F4752851451fA20ECdCFeb1bA2Fd0", + "0xF2cCBcF4Ac3e021C4041F39F751A0b46b1C8aa14", + "0x9c738ed8D50B283B7884DA4e69400a178158e42e", + "0x2B3C7D1eF5FDfC0557934019c531d3E70D6200AE", + "0x80F41289795F122C82b83D8C3A760E01FDBF5C76", + "0x0BC34C33880a45d7Aa3bfDafE37Fd157E1Dca9bb", + "0x864521f4A31f1C893f8414697dFb6D3A2d949AC5", + "0xFdD2245Fa2B7881AB78C171Cc84F088e520450E2", + "0xd753854eA19B204E6Ee9b5544239Fcc7d40f932A", + "0xfFd22b84fB1d46ef74Ed6530b2635BE61340f347" + ], + "snapshot": 16048934 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/index.ts new file mode 100644 index 00000000..9ed2f69b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/skale-delegation-weighted/index.ts @@ -0,0 +1,72 @@ +import { BigNumberish, BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, multicall } from '../../utils'; + +export const author = 'payvint'; +export const version = '1.0.0'; + +const abi = [ + 'function getAndUpdateDelegatedAmount(address wallet) external returns (uint)', + 'function getEscrowAddress(address beneficiary) external view returns (address)' +]; + +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) => { + multi.call(address, options.addressSKL, 'getAndUpdateDelegatedAmount', [ + address + ]); + }); + const resultAccounts: Record = await multi.execute(); + + console.log(resultAccounts); + + const escrowAddressCallsQuery = addresses.map((address: any) => [ + options.addressAllocator, + 'getEscrowAddress', + [address] + ]); + + const escrowAddressesFromAccount = await multicall( + network, + provider, + abi, + [...escrowAddressCallsQuery], + { + blockTag + } + ); + + const addressToEscrow = new Map(); + addresses.forEach((address: any, index: number) => { + addressToEscrow[address] = escrowAddressesFromAccount[index][0]; + }); + + addresses.forEach((address: any) => { + multi.call(address, options.addressSKL, 'getAndUpdateDelegatedAmount', [ + addressToEscrow[address] + ]); + }); + + const resultEscrows: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(resultAccounts).map(([address, balance]) => [ + address, + parseFloat( + formatUnits( + BigNumber.from(balance).add(BigNumber.from(resultEscrows[address])) + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/README.md b/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/README.md new file mode 100644 index 00000000..770f7772 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/README.md @@ -0,0 +1,15 @@ +# erc20-balance-of + +This strategy returns the yield-farming pool balances of the voters for the SingularityNET project. + +Here is an example of parameters: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18, + "farmingAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", + "farmingPoolId": 0 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/examples.json b/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/examples.json new file mode 100644 index 00000000..efc76d24 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "snet-farmers", + "params": { + "address": "0xa1e841e8f770e5c9507e2f8cfd0aa6f73009715d", + "symbol": "AGIX", + "decimals": 8, + "farmingAddress": "0x8C89C50D3986e9e310df034554DEb63dDf7dB538", + "farmingPoolId": 2 + } + }, + "network": "3", + "addresses": [ + "0x88bc4235d23af66a268098e4a4dddeec9362682a", + "0xabd2ccb3828b4428bbde6c2031a865b0fb272a5a" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/index.ts b/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/index.ts new file mode 100644 index 00000000..301f7572 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-farmers/index.ts @@ -0,0 +1,56 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'Vivek205'; +export const version = '0.1.0'; + +type UserInfoResponse = { + amount: BigNumber; + rewardDebt: BigNumber; +}; + +type FarmResult = Record; + +const farmingAbi = [ + 'function userInfo(uint256 poolid, address account) external view returns (uint256 amount, int256 rewardDebt)' +]; + +const parseNumber = (value: BigNumberish): BigNumber => BigNumber.from(value); + +const multiCallerFactory = (network, provider, blockTag) => (abi) => + new Multicaller(network, provider, abi, { blockTag }); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const { farmingAddress, farmingPoolId } = options; + + const initMultiCaller = multiCallerFactory(network, provider, blockTag); + const farmingInfoCaller = initMultiCaller(farmingAbi); + + addresses.forEach((address) => { + farmingInfoCaller.call(address, farmingAddress, 'userInfo', [ + farmingPoolId, + address + ]); + }); + + const farmingInfoResult: FarmResult = await farmingInfoCaller.execute(); + + return Object.fromEntries( + addresses.map((address) => { + const farmingBalance = parseNumber(farmingInfoResult[address].amount); + return [ + address, + parseFloat(formatUnits(farmingBalance, options.decimals)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/README.md b/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/README.md new file mode 100644 index 00000000..7bd5450d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/README.md @@ -0,0 +1,14 @@ +# erc20-balance-of + +This strategy returns the liquidity pool balances of the voters for the SingularityNET project. + +Here is an example of parameters: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "lpDecimals": 18, + "lpAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/examples.json b/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/examples.json new file mode 100644 index 00000000..acbcf9f9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "snet-liquidity-providers", + "params": { + "address": "0xa1e841e8f770e5c9507e2f8cfd0aa6f73009715d", + "symbol": "AGIX", + "decimals": 8, + "lpAddress": "0x5318855ad173220e446002c01d5ee5f940502e70" + } + }, + "network": "3", + "addresses": [ + "0x8c89c50d3986e9e310df034554deb63ddf7db538", + "0x4e61d78b75c0bc64dc053cf93cfe31c912922b5a", + "0x46ef7d49aaa68b29c227442bdbd18356415f8304", + "0xe4ce53c10f2f84b17020152e5d8a0996687c9a03", + "0x3d09d2ac0ba857d48130fb243ccfa0387b7a167f", + "0x8c9b16489d8264bab6b9ddfb6ee3914392e973ea" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/index.ts b/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/index.ts new file mode 100644 index 00000000..0d35a93b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-liquidity-providers/index.ts @@ -0,0 +1,88 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, call } from '../../utils'; + +export const author = 'Vivek205'; +export const version = '0.1.0'; + +type FinalResult = [ + Record, + Record, + BigNumberish +]; + +const erc20Abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint)' +]; + +const parseNumber = (value: BigNumberish): BigNumber => BigNumber.from(value); + +const computeTokenContribution = ( + lpBalance: BigNumber, + lpTotalSupply: BigNumber, + contractTokenBalance: BigNumber +) => { + lpTotalSupply = lpTotalSupply.isZero() ? BigNumber.from(1) : lpTotalSupply; + const tokenContribution = lpBalance + .mul(contractTokenBalance) + .div(lpTotalSupply); + return tokenContribution; +}; + +const multiCallerFactory = (network, provider, blockTag) => (abi) => + new Multicaller(network, provider, abi, { blockTag }); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const { address: tokenAddress, lpAddress } = options; + + const initMultiCaller = multiCallerFactory(network, provider, blockTag); + + const lpBalanceCaller = initMultiCaller(erc20Abi); + const lpTotalSupplyCaller = initMultiCaller(erc20Abi); + + addresses.forEach((address) => { + lpBalanceCaller.call(address, lpAddress, 'balanceOf', [address]); + lpTotalSupplyCaller.call(address, lpAddress, 'totalSupply', []); + }); + + const contractBalanceCall = () => + call(provider, erc20Abi, [tokenAddress, 'balanceOf', [lpAddress]]); + + const [ + lpBalanceResult, + lpTotalSupplyResult, + contractBalanceResult + ]: FinalResult = await Promise.all([ + lpBalanceCaller.execute(), + lpTotalSupplyCaller.execute(), + contractBalanceCall() + ]); + + const contractTokenBalance = parseNumber(contractBalanceResult); + + return Object.fromEntries( + addresses.map((address) => { + const lpBalance = parseNumber(lpBalanceResult[address]); + const lpTotalSupply = parseNumber(lpTotalSupplyResult[address]); + const senderTokenShare = computeTokenContribution( + lpBalance, + lpTotalSupply, + contractTokenBalance + ); + + return [ + address, + parseFloat(formatUnits(senderTokenShare, options.lpDecimals)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/README.md b/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/README.md new file mode 100644 index 00000000..85618377 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/README.md @@ -0,0 +1,15 @@ +# erc20-balance-of + +This strategy returns the staking pool balances of the voters for the SingularityNET project. + + +Here is an example of parameters: + +```json +{ + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18, + "stakingAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/examples.json b/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/examples.json new file mode 100644 index 00000000..8f0424c9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/examples.json @@ -0,0 +1,28 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "snet-stakers", + "params": { + "address": "0xa1e841e8f770e5c9507e2f8cfd0aa6f73009715d", + "symbol": "AGIX", + "decimals": 8, + "stakingAddress": "0xF153Aec75e1A8B2f5647bEfaC783b31a5c3Fba22", + "stakeMapIndex": 26 + } + }, + "network": "3", + "addresses": [ + "0xC4f3BFE7D69461B7f363509393D44357c084404c", + "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", + "0x520b1A5B90E103eDE987663CB18CB7a4311EC17e", + "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b", + "0xd03ea8624C8C5987235048901fB614fDcA89b117", + "0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC", + "0x749BaAE8b4dF2883B98F33616C97563fAE81E05C", + "0x2943144193B00Da6f1d4055383Bf0fBc99805322", + "0x03F2eFdcb4BF042EDceBe7c2A22c8fCDB7E0aF76" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/index.ts b/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/index.ts new file mode 100644 index 00000000..dab06fa2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snet-stakers/index.ts @@ -0,0 +1,67 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'Vivek205'; +export const version = '0.1.0'; + +type StakeInfoResponse = { + found: boolean; + approvedAmount: BigNumber; + pendingForApprovalAmount: BigNumber; + rewardComputeIndex: BigNumber; + claimableAmount: BigNumber; +}; + +type StakeResult = Record; + +const stakingAbi = [ + 'function getStakeInfo(uint256 stakeMapIndex, address staker) public view returns (bool found, uint256 approvedAmount, uint256 pendingForApprovalAmount, uint256 rewardComputeIndex, uint256 claimableAmount)' +]; + +const parseNumber = (value: BigNumberish): BigNumber => BigNumber.from(value); + +const parseStakeInfo = (value: StakeInfoResponse) => ({ + approvedAmount: parseNumber(value.approvedAmount), + pendingApprovalAmount: parseNumber(value.pendingForApprovalAmount) +}); + +const computeStakeBalance = (value: StakeInfoResponse) => { + const { approvedAmount, pendingApprovalAmount } = parseStakeInfo(value); + return approvedAmount.add(pendingApprovalAmount); +}; + +const multiCallerFactory = (network, provider, blockTag) => (abi) => + new Multicaller(network, provider, abi, { blockTag }); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const { stakingAddress, stakeMapIndex } = options; + + const initMultiCaller = multiCallerFactory(network, provider, blockTag); + + const stakingInfoCaller = initMultiCaller(stakingAbi); + + addresses.forEach((address) => { + stakingInfoCaller.call(address, stakingAddress, 'getStakeInfo', [ + stakeMapIndex, + address + ]); + }); + + const stakingInfoResult: StakeResult = await stakingInfoCaller.execute(); + + return Object.fromEntries( + addresses.map((address) => { + const stakeBalance = computeStakeBalance(stakingInfoResult[address]); + return [address, parseFloat(formatUnits(stakeBalance, options.decimals))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/snowswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/snowswap/README.md new file mode 100644 index 00000000..8a83fd7c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snowswap/README.md @@ -0,0 +1,12 @@ +Snowswap + +Checks for the number of SNOW's staked in Frosty’s pool (https://etherscan.io/address/0x7d2c8b58032844f222e2c80219975805dce1921c) and adds it to the balance of the voters FLAME ERC20 token + +Here is an example of parameters: + +{ + "address": "0xfe9A29aB92522D14Fc65880d817214261D8479AE", + "symbol": "SNOW", + "decimals": 18, + "snowStakingAddress": "0x7d2c8b58032844f222e2c80219975805dce1921c" +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/snowswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/snowswap/examples.json new file mode 100644 index 00000000..2b75c277 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snowswap/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Snowswap", + "strategy": { + "name": "snowswap", + "params": { + "address": "0xfe9A29aB92522D14Fc65880d817214261D8479AE", + "symbol": "SNOW", + "decimals": 18, + "snowStakingAddress": "0x7d2c8b58032844f222e2c80219975805dce1921c" + } + }, + "network": "1", + "addresses": [ + "0x109763295D1636D6b311ab690c63e2A7A4606bC7", + "0x7d2c8b58032844f222e2c80219975805dce1921c" + ], + "snapshot": 13087619 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/snowswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/snowswap/index.ts new file mode 100644 index 00000000..bcccaa60 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/snowswap/index.ts @@ -0,0 +1,51 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'jairsnowswap'; +export const version = '0.1.0'; + +const stakedAbi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const { snowStakingAddress, decimals } = options; + + const snowBalances = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const stakedTokenBalances = new Multicaller(network, provider, stakedAbi, { + blockTag + }); + + addresses.forEach((address: string) => + stakedTokenBalances.call(address, snowStakingAddress, 'balanceOf', [ + address + ]) + ); + const result: Record = + await stakedTokenBalances.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, output]) => [ + address, + parseFloat(formatUnits(output, decimals)) + snowBalances[address] + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/README.md b/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/README.md new file mode 100644 index 00000000..04e7da0c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/README.md @@ -0,0 +1,14 @@ +# solv-voucher-claimable + +This strategy is to let owners of [Solv vesting vouchers](https://solv.finance/) vote with the voting power equal to their claimable token amount in the voucher at the time of the snapshot. + +This can be combined with `erc20-balance-of` strategy to give the voting power of "amount held in wallet" + "amount available in the vesting voucher". + +The parameters are the `address` of the vesting voucher contract and the `symbol` of the underlying token. Here is an example of parameters: + +```json +{ + "address": "0x522f8fe415e08b600b8bd6c1db74a1b696845d0d", + "symbol": "PDT" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/examples.json b/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/examples.json new file mode 100644 index 00000000..95c270db --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/examples.json @@ -0,0 +1,42 @@ +[ + { + "name": "PDT Vesting Voucher Claimable Amounts", + "strategy": { + "name": "solv-voucher-claimable", + "params": { + "address": "0x522f8fe415e08b600b8bd6c1db74a1b696845d0d", + "symbol": "PDT" + } + }, + "network": "1", + "addresses": [ + "0x0d5f507074db8ead56f5875219dbf1ff73bcd429", + "0xf96225d26fa257b200420696092e02cc141cf3d8", + "0xd3be4499f28ef91a6b6dd5ab6beb0588039d72ba", + "0x661c3a6db7241f2a7b3b1c1d73fe98198d089fc2", + "0xceeab2af38e6b086cdce120c49f93b65f0b92b76", + "0xc40fc1c553737b2aa8572fdb036986510219f233" + ], + "snapshot": 14955800 + }, + { + "name": "VERA Vesting Voucher Claimable Amounts", + "strategy": { + "name": "solv-voucher-claimable", + "params": { + "address": "0x928b35660f8388042d871e82eb40234901461354", + "symbol": "VERA" + } + }, + "network": 56, + "addresses": [ + "0x0d5f507074db8ead56f5875219dbf1ff73bcd429", + "0xf96225d26fa257b200420696092e02cc141cf3d8", + "0xd3be4499f28ef91a6b6dd5ab6beb0588039d72ba", + "0x661c3a6db7241f2a7b3b1c1d73fe98198d089fc2", + "0xceeab2af38e6b086cdce120c49f93b65f0b92b76", + "0xc40fc1c553737b2aa8572fdb036986510219f233" + ], + "snapshot": 19287600 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/index.ts b/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/index.ts new file mode 100644 index 00000000..32fb035c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/index.ts @@ -0,0 +1,78 @@ +import { BigNumber, FixedNumber } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'mitesh-mutha'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function tokenURI(uint256 tokenId) external view returns (string)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Fetch the balanceOf the addresses i.e. how many vouchers do they hold? + const balanceOfMulti = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + balanceOfMulti.call(address, options.address, 'balanceOf', [address]) + ); + const ownedCounts: Record = await balanceOfMulti.execute(); + + // Fetch the voucher token IDs held for each address + const tokenIdsMulti = new Multicaller(network, provider, abi, { blockTag }); + addresses.map((address) => { + let ownedCount = ownedCounts[address]; + while (ownedCount.gt(0)) { + const index = ownedCount.sub(1); + tokenIdsMulti.call( + `${address}-${index.toString()}`, + options.address, + 'tokenOfOwnerByIndex', + [address, index.toNumber()] + ); + ownedCount = index; + } + }); + const ownerTokenIds: Record = await tokenIdsMulti.execute(); + + // Fetch the voucher data for each voucher held by an address among the address + const tokenURIMulti = new Multicaller(network, provider, abi, { blockTag }); + Object.entries(ownerTokenIds).map(([addressWithIndex, tokenId]) => { + tokenURIMulti.call(`${addressWithIndex}`, options.address, `tokenURI`, [ + tokenId + ]); + }); + const ownerTokenURIs: Record = await tokenURIMulti.execute(); + + // Go through the list of results and sum up claimable values + const claimableVotingPower: Record = {}; + Object.entries(ownerTokenURIs).map(([addressWithIndex, tokenURI]) => { + const address = addressWithIndex.split('-')[0]; + if (tokenURI.split(',')[0] == 'data:application/json') { + const tokenData = JSON.parse(tokenURI.slice(22)); + const claimableAmount = tokenData['properties']['claimableAmount']; + if (!claimableVotingPower[address]) + claimableVotingPower[address] = FixedNumber.from(0); + claimableVotingPower[address] = claimableVotingPower[address].addUnsafe( + FixedNumber.fromString(claimableAmount) + ); + } + }); + + // Return the computed values + return Object.fromEntries( + Object.entries(claimableVotingPower).map(([address, votingPower]) => [ + address, + votingPower.toUnsafeFloat() + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/schema.json b/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/schema.json new file mode 100644 index 00000000..7d01484d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/solv-voucher-claimable/schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "symbol": { + "type": "string", + "title": "Underlying Token Symbol", + "examples": ["e.g. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Vesting Voucher Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["address"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/spaceid/README.md b/Implementations/API/backend/utils/snapshot/strategies/spaceid/README.md new file mode 100644 index 00000000..bdd0e29c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spaceid/README.md @@ -0,0 +1 @@ +This is SPACEID strategy, it returns the voting power of the voters. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/spaceid/examples.json b/Implementations/API/backend/utils/snapshot/strategies/spaceid/examples.json new file mode 100644 index 00000000..21cf9991 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spaceid/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "spaceid query on ethers", + "strategy": { + "name": "spaceid", + "params": {} + }, + "network": "56", + "addresses": [ + "0xad3d542ACD90A3492B41AF49399BAeb755A11e59", + "0xb2C2325B93b1a3C882fe47C453622f1D75801D72", + "0x5d8CeEDdf7881f962a9354Ab5c187655fC3dec3F" + ], + "snapshot": 28831785 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/spaceid/index.ts b/Implementations/API/backend/utils/snapshot/strategies/spaceid/index.ts new file mode 100644 index 00000000..60a64ae7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spaceid/index.ts @@ -0,0 +1,95 @@ +import { strategy as UniswapV3Strategy } from '../uniswap-v3'; +import { subgraphRequest } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; +import { WeiPerEther } from '@ethersproject/constants'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'SID-Marcus'; +export const version = '0.0.1'; + +const pancakeV3Subgraph = + 'https://api.thegraph.com/subgraphs/name/messari/pancakeswap-v3-bsc'; + +const UNISWAP_ID_USDC_PAIR = '0x6ac6b053a2858bea8ad758db680198c16e523184'; +const PANCAKE_ID_USDT_PAIR = '0x4e1f9aDf96dBA6Dc09c973228c286568F1315ea8'; + +async function getLpTokenOnBsc(addresses, snapshot) { + const params = { + accounts: { + __args: { + where: { + id_in: addresses + }, + block: snapshot !== 'latest' ? { number: snapshot } : { number_gte: 0 } + }, + id: true, + withdraws: { + __args: { + where: { pool: PANCAKE_ID_USDT_PAIR } + }, + inputTokenAmounts: true, + timestamp: true + }, + deposits: { + __args: { + where: { pool: PANCAKE_ID_USDT_PAIR } + }, + inputTokenAmounts: true, + timestamp: true + } + } + }; + + const pools = await subgraphRequest(pancakeV3Subgraph, params); + + const pancakeIDLPScore = {}; + for (const account of pools.accounts) { + account.id = getAddress(account.id); + let IdLPToken: BigNumber = BigNumber.from(0); + for (const withdraw of account.withdraws) { + IdLPToken = IdLPToken.add(BigNumber.from(withdraw.inputTokenAmounts[0])); + } + for (const deposit of account.deposits) { + IdLPToken = IdLPToken.sub(BigNumber.from(deposit.inputTokenAmounts[0])); + } + pancakeIDLPScore[account.id] = IdLPToken.div(WeiPerEther).toNumber(); + pancakeIDLPScore[account.id] < 0 && (pancakeIDLPScore[account.id] = 0); + } + return pancakeIDLPScore; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + let LPScore = {}; + switch (network) { + case '1': + LPScore = await UniswapV3Strategy( + space, + network, + provider, + addresses, + { + poolAddress: UNISWAP_ID_USDC_PAIR, + tokenReserve: 0 + }, + snapshot + ); + break; + case '56': + LPScore = await getLpTokenOnBsc(addresses, snapshot); + break; + } + return Object.fromEntries( + addresses.map((address) => { + const addressScore = LPScore[address] ?? 0; + // console.log(address, LPScore[address], delegationPower[address]); + return [getAddress(address), addressScore]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/spaceid/schema.json b/Implementations/API/backend/utils/snapshot/strategies/spaceid/schema.json new file mode 100644 index 00000000..0a7e6079 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spaceid/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "SPACEID", + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/spacey2025/README.md b/Implementations/API/backend/utils/snapshot/strategies/spacey2025/README.md new file mode 100644 index 00000000..5f440298 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spacey2025/README.md @@ -0,0 +1,15 @@ +Spacey2025 + +This strategy implements the voting power rules for blockchain game SpaceY2025(https://spacey2025.com). Voting power rules are as below: +- SPAY x1 +- Ticket x1500 +- Land x600 +- Tower x6 + + +Here is an example of parameters: + +{ + "address": "0x230185C3B02b897B89cb1e62717AD7772b8319DA", + "symbol": "SPACEY NFT" +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/spacey2025/examples.json b/Implementations/API/backend/utils/snapshot/strategies/spacey2025/examples.json new file mode 100644 index 00000000..76a09aa2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spacey2025/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "spacey2025", + "params": { + "address": "0x230185C3B02b897B89cb1e62717AD7772b8319DA", + "symbol": "NFT" + } + }, + "network": "56", + "addresses": [ + "0xBAB298D0Dcb2589a1c24B6c88fb10BD08eFe3265", + "0x71c06f42637489844cdb9238e9f6e6e5e0be0fc7", + "0xcb02df5930d55f8ad9fdb37c0c24509ccb378f06" + ], + "snapshot": 14713766 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/spacey2025/index.ts b/Implementations/API/backend/utils/snapshot/strategies/spacey2025/index.ts new file mode 100644 index 00000000..50579c5b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spacey2025/index.ts @@ -0,0 +1,82 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'chuang39'; +export const version = '0.1.0'; + +const SPACEY2025_MARKETPLACE_SUBGRAPH_URL = { + '56': 'https://api.thegraph.com/subgraphs/name/blockfishio/marketplacebsc' +}; + +enum Category { + LAND = 'land', + BOARDINGPASS = 'boardingpass', + BUILDING = 'building', + TOWER = 'tower', + TRAP = 'trap' +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const params = { + nfts: { + __args: { + where: { + owner_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000, + skip: 0 + }, + owner: { + id: true + }, + category: {} + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.nfts.__args.block = { number: snapshot }; + } + + const score = {}; + let hasNext = true; + while (hasNext) { + const result = await subgraphRequest( + SPACEY2025_MARKETPLACE_SUBGRAPH_URL[network], + params + ); + + const nfts = result && result.nfts ? result.nfts : []; + for (const nft of nfts) { + let vp = 0; + switch (nft.category) { + case Category.TOWER: + case Category.BUILDING: + case Category.TRAP: + vp += 6; + break; + case Category.BOARDINGPASS: + vp += 1500; + break; + case Category.LAND: + vp += 600; + break; + default: + break; + } + const userAddress = getAddress(nft.owner.id); + score[userAddress] = (score[userAddress] || 0) + vp; + } + + params.nfts.__args.skip += params.nfts.__args.first; + hasNext = nfts.length === params.nfts.__args.first; + } + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/spookyswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/spookyswap/examples.json new file mode 100644 index 00000000..9ee20ca8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spookyswap/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Example spookyswap query", + "strategy": { + "name": "spookyswap", + "params": { + "symbol": "SPOOKY", + "boo": { + "numerator": 3, + "denominator": 1 + }, + "lp": { + "numerator": 5, + "denominator": 1 + }, + "vaultTokens": [ + { + "symbol": "BOO in beefy", + "address": "0xEe3a7c885Fd3cc5358FF583F2DAB3b8bC473316f", + "numerator": 1, + "denominator": 1, + "decimals": 18 + } + ] + } + }, + "network": "250", + "addresses": [ + "0x1F0C5a9046f0db0e8b651Cd9E8e23ba4Efe4B86d", + "0x95478C4F7D22D1048F46100001c2C69D2BA57380" + ], + "snapshot": 8070436 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/spookyswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/spookyswap/index.ts new file mode 100644 index 00000000..9ec40154 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spookyswap/index.ts @@ -0,0 +1,152 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'ooGwei'; +export const version = '0.1.0'; + +const FARM_ADDRESS = '0x2b2929E785374c651a81A63878Ab22742656DcDd'; +const LP_TOKEN_ADDRESS = '0xEc7178F4C41f346b2721907F5cF7628E388A7a58'; +const BOO_TOKEN_ADDRESS = '0x841FAD6EAe12c286d1Fd18d1d525DFfA75C7EFFE'; + +const abi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + }, + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const multi = new Multicaller(network, provider, abi, { blockTag }); + + addresses.forEach((address: any) => { + multi.call(`boo.${address}`, BOO_TOKEN_ADDRESS, 'balanceOf', [address]); + multi.call(`lpInFarm.${address}`, FARM_ADDRESS, 'userInfo', ['0', address]); + multi.call(`lp.${address}`, LP_TOKEN_ADDRESS, 'balanceOf', [address]); + options.vaultTokens.forEach((token: any) => { + multi.call( + `vaultTokens.${address}.${token.address}`, + token.address, + 'balanceOf', + [address] + ); + }); + }); + multi.call(`lp.totalSupply`, LP_TOKEN_ADDRESS, 'totalSupply', []); + multi.call(`lp.boo`, BOO_TOKEN_ADDRESS, 'balanceOf', [LP_TOKEN_ADDRESS]); + + const result = await multi.execute(); + + return Object.fromEntries( + addresses.map((address) => [ + address, + parseFloat( + formatUnits( + result.boo[address] + .mul(options.boo.numerator) + .div(options.boo.denominator), + 18 + ) + ) + + parseFloat( + formatUnits( + result.lpInFarm[address][0] + .mul(result.lp.boo) + .div(result.lp.totalSupply) + .mul(options.lp.numerator) + .div(options.lp.denominator), + 18 + ) + ) + + parseFloat( + formatUnits( + result.lp[address] + .mul(result.lp.boo) + .div(result.lp.totalSupply) + .mul(options.lp.numerator) + .div(options.lp.denominator), + 18 + ) + ) + + options.vaultTokens.reduce( + (prev: number, token: any, idx: number) => + prev + + parseFloat( + formatUnits( + result.vaultTokens[address][token.address] + .mul(options.vaultTokens[idx].numerator) + .div(options.vaultTokens[idx].denominator), + options.vaultTokens[idx].decimal + ) + ), + 0 + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/README.md b/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/README.md new file mode 100644 index 00000000..6ab92fc4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/README.md @@ -0,0 +1,22 @@ +# Spreadsheet + +Define voting power in Google Sheets. + +### Getting started + +- Check the template here: https://docs.google.com/spreadsheets/d/13aJrpOUtajyMLvmjvEqutnEP0FkuH3Z-LPH9m9Mbhis/edit?usp=sharing +- Select: File > Make a copy +- On the newly created file select: File > Share > Publish to web +- On the modal "Publish to web" click "Publish" button +- Copy the long id within the URL and use it as "sheetId" parameter for the strategy +- And use the id after "pub?gid=" as "gid" parameter +- You are ready! + +Here is an example of the strategy parameters: + +```json +{ + "sheetId": "2PACX-1vTmam7vShzrscNDQ0zy0IC11jJztebhWrK5_5kFT6N_WsEJXDXStmxrOlNmHt9IRf9g7VenegU8vGzh", + "gid": "0" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/examples.json b/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/examples.json new file mode 100644 index 00000000..b60fdb33 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "spreadsheet", + "params": { + "sheetId": "2PACX-1vTmam7vShzrscNDQ0zy0IC11jJztebhWrK5_5kFT6N_WsEJXDXStmxrOlNmHt9IRf9g7VenegU8vGzh", + "gid": "0" + } + }, + "network": "1", + "addresses": [ + "0x5BC928BF0DAb1e4A2ddd9e347b0F22e88026D76c", + "0x4C7909d6F029b3a5798143C843F4f8e5341a3473", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0xdD36Aa96D9BD4b49DA6E6734fF18Cc69F90F9435" + ], + "snapshot": 12857715 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/index.ts b/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/index.ts new file mode 100644 index 00000000..eb3ca106 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/index.ts @@ -0,0 +1,55 @@ +import fetch from 'cross-fetch'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; +export const dependOnOtherAddress = false; + +function csvToJson(csv) { + const lines = csv.split('\n'); + const keys = lines[0].split(',').map((key) => key.trim()); + return lines.slice(1).map((line) => + line.split(',').reduce((acc, cur, i) => { + const toAdd = {}; + toAdd[keys[i]] = cur.trim(); + return { ...acc, ...toAdd }; + }, {}) + ); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const block = await provider.getBlock(snapshot); + const ts = block.timestamp; + + const res = await fetch( + `https://docs.google.com/spreadsheets/d/e/${options.sheetId}/pub?gid=${ + options.gid || '0' + }&single=true&output=csv` + ); + const text = await res.text(); + const csv = (csvToJson(text) || []).map((item) => ({ + address: item.address, + vp: parseFloat(item['voting power'] || '0'), + ts: parseInt(item.timestamp || '0') + })); + + return Object.fromEntries( + addresses.map((address) => { + const items = csv + .filter( + (item) => + item.address.toLowerCase() === address.toLowerCase() && + item.ts <= ts + ) + .sort((a, b) => a.ts - b.ts); + const vp = (items.pop() || {}).vp || 0; + return [address, vp]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/schema.json b/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/schema.json new file mode 100644 index 00000000..b8bfd544 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/spreadsheet/schema.json @@ -0,0 +1,30 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "sheetId": { + "type": "string", + "title": "Spreadsheet id", + "examples": ["e.g. 2PACX-1vTmam7vShzrscND..."] + }, + "gid": { + "type": "string", + "title": "gid", + "examples": ["0"] + } + }, + "required": ["sheetId"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/squadz-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/squadz-power/README.md new file mode 100644 index 00000000..295cd266 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/squadz-power/README.md @@ -0,0 +1,15 @@ +Strategy for using the "power" stat of Squadz collections for voting power. + +Power factors in: +- if an address has a currently active membership +- how many memberships an address has been minted total + +Parameters should look like: +``` +{ + "symbol": "SQDZ", // the token symbol for your collection + "collectionAddress": "0xd5746787be995887c59eff90611778b9cb67f0db", // the address for your collection +} +``` + +RARE: If your collection was forked onto the Squadz engine (see heyshell.xyz), you will also need to include the `forkNumber` in the parameters object. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/squadz-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/squadz-power/examples.json new file mode 100644 index 00000000..f27dbe1a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/squadz-power/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example squadz-power query goerli", + "strategy": { + "name": "squadz-power", + "params": { + "symbol": "SQDZ", + "collectionAddress": "0xd5746787be995887c59eff90611778b9cb67f0db" + } + }, + "network": "5", + "addresses": [ + "0x4171160db0e7e2c75a4973b7523b437c010dd9d4", + "0xd50fc49ff389558d23a76cf246da147ff53d8df8" + ], + "snapshot": 6679906 + }, + { + "name": "Example squadz-power query polygon", + "strategy": { + "name": "squadz-power", + "params": { + "symbol": "SQDZ", + "collectionAddress": "0xe56a303d9494bc55bd4ea570af6fb69efbd1aa63" + } + }, + "network": "137", + "addresses": [ + "0xd50fc49ff389558d23a76cf246da147ff53d8df8", + "0xc8d61fe5db0ef7b4512ce4d086c9c1c3f091fb75", + "0x4171160db0e7e2c75a4973b7523b437c010dd9d4", + "0x57421dea5152997c5ca37c3c6a5891c2c0217078" + ], + "snapshot": 26893958 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/squadz-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/squadz-power/index.ts new file mode 100644 index 00000000..7b388de9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/squadz-power/index.ts @@ -0,0 +1,50 @@ +import { multicall } from '../../utils'; + +export const author = 'EzraWeller'; +export const version = '0.0.1'; + +const engineAddresses: { [network: string]: string } = { + '5': '0x7beaa4e60e0faab603e99813f0f2330704b53086', + '80001': '0x39235b78626d8fa4ef6a81ba5616c58708ba4ea5', + '137': '0xb4a1a96ffa514b295b9a0de127288ec7d09e4e7c', + '4': '0xbeea7483aef24502a27eb7a35aad55280f8e2ebc' +}; + +const engineAbi = [ + 'function getMemberInfo(address, uint256, address) view returns (uint256, uint256, uint256, bool, bool, uint256, uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + if (!Object.keys(engineAddresses).includes(network)) + throw new Error( + 'Invalid network:' + network + 'not in' + Object.keys(engineAddresses) + ); + + const engineAddress = engineAddresses[network]; + const forkNumber = options.forkNumber ?? 0; + + const response = await multicall( + network, + provider, + engineAbi, + addresses.map((member) => [ + engineAddress, + 'getMemberInfo', + [options.collectionAddress, forkNumber, member] + ]), + { blockTag } + ); + + return Object.fromEntries( + response.map((value, i) => [addresses[i], parseInt(value[5])]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/squid-dao/README.md b/Implementations/API/backend/utils/snapshot/strategies/squid-dao/README.md new file mode 100644 index 00000000..c2a989d2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/squid-dao/README.md @@ -0,0 +1,10 @@ +# squid-dao + +Combines SquidDAO vewsSquid holders and NFT holders, returning a combined voting power as a percentage of the total. + +```json +{ + "decimals": 4, + "symbol": "SQDV" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/squid-dao/examples.json b/Implementations/API/backend/utils/snapshot/strategies/squid-dao/examples.json new file mode 100644 index 00000000..51389663 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/squid-dao/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "squid-dao", + "params": { + "symbol": "SQDV", + "decimals": 4 + } + }, + "network": "1", + "addresses": [ + "0xfd4f24676ed4588928213f37b126b53c07186f45", + "0x0f763341b448bb0f02370f4037fe4a2c84c9283f", + "0x020ca66c30bec2c4fe3861a94e4db4a498a35872" + ], + "snapshot": 13837434 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/squid-dao/index.ts b/Implementations/API/backend/utils/snapshot/strategies/squid-dao/index.ts new file mode 100644 index 00000000..a982c6ca --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/squid-dao/index.ts @@ -0,0 +1,118 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, call } from '../../utils'; + +export const author = 'zencephalon'; +export const version = '0.0.0'; + +const vewsSquidContractAddress = '0x58807e624b9953c2279e0efae5edcf9c7da08c7b'; +const nftContractAddress = '0x7136ca86129e178399b703932464df8872f9a57a'; +const sSquidContractAdress = '0x9d49bfc921f36448234b0efa67b5f91b3c691515'; + +const vewsSquidContractAbi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function supply() external view returns (uint256)' +]; +const nftContractAbi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; +const sSquidContractAbi = ['function index() external view returns (uint256)']; + +type MultiCallResult = Record; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const callIndex = () => { + return call(provider, sSquidContractAbi, [ + sSquidContractAdress, + 'index', + [] + ]); + }; + + const callNftSupply = () => { + return call(provider, nftContractAbi, [ + nftContractAddress, + 'totalSupply', + [] + ]); + }; + + const callVewsSquidSupply = () => { + return call(provider, vewsSquidContractAbi, [ + vewsSquidContractAddress, + 'supply', + [] + ]); + }; + + const makeMulticaller = (abi, contractAddress) => { + const multiCaller = new Multicaller(network, provider, abi, { + blockTag + }); + addresses.forEach((address) => + multiCaller.call(address, contractAddress, 'balanceOf', [address]) + ); + return multiCaller; + }; + + const vewsSquidMulti = makeMulticaller( + vewsSquidContractAbi, + vewsSquidContractAddress + ); + const nftMulti = makeMulticaller(nftContractAbi, nftContractAddress); + + const [index, nftSupply, vewsSquidSupply, vewsResults, nftResults]: [ + BigNumber, + BigNumber, + BigNumber, + MultiCallResult, + MultiCallResult + ] = await Promise.all([ + callIndex(), + callNftSupply(), + callVewsSquidSupply(), + vewsSquidMulti.execute(), + nftMulti.execute() + ]); + + const scores: Record = {}; + const nftMultiplier = BigNumber.from(10).pow(18); + + for (const address of addresses) { + const vewsScore = BigNumber.from(vewsResults[address] || 0) + .mul(index) + .div(1e9); + const nftScore = BigNumber.from(nftResults[address] || 0).mul( + nftMultiplier + ); + scores[address] = vewsScore.add(nftScore); + } + + const totalScore = nftSupply + .mul(nftMultiplier) + .add(vewsSquidSupply.mul(index).div(1e9)); + + const dec_multi = BigNumber.from(10).pow(options.decimals); + + return Object.fromEntries( + Object.entries(scores).map(([address, balance]) => [ + address, + parseFloat( + formatUnits( + balance.mul(100).mul(dec_multi).div(totalScore), + options.decimals + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/squid-dao/schema.json b/Implementations/API/backend/utils/snapshot/strategies/squid-dao/schema.json new file mode 100644 index 00000000..de512e1d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/squid-dao/schema.json @@ -0,0 +1,25 @@ +{ + "$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. SQDV"], + "maxLength": 16 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 4"] + } + }, + "required": ["decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/stablexswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/stablexswap/README.md new file mode 100644 index 00000000..a9143f67 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stablexswap/README.md @@ -0,0 +1,11 @@ +# Contract call strategy + +Allows the tokens staked in chef contracts to be used to calculate voter scores. In the JSON, weights can be specified + +## Usage + +To use locally we need to add Object.fromEntries: + +"npm i -D polyfill-object.fromentries" + +Then add "require('polyfill-object.fromentries')" in test/index.ts and run "npm run test --strategy=stablexswap" diff --git a/Implementations/API/backend/utils/snapshot/strategies/stablexswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/stablexswap/examples.json new file mode 100644 index 00000000..0f04e664 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stablexswap/examples.json @@ -0,0 +1,42 @@ +[ + { + "name": "stablexswap", + "strategy": { + "name": "stablexswap", + "params": { + "symbol": "SXSWAP", + "stax": { + "address": "0x0da6ed8b13214ff28e9ca979dd37439e8a88f6c4", + "decimals": "18" + }, + "masterchef": { + "address": "0xc80991f9106e26e43bf1c07c764829a85f294c71", + "decimals": "18" + }, + "stakingchef": { + "address": "0x0c0c475e32212b748c328e451ab3862ffe07369e", + "decimals": "18", + "weightage": "8" + }, + "pools": [ + { + "poolId": "0", + "weightage": "25" + }, + { + "poolId": "4", + "weightage": "4" + } + ] + } + }, + "network": "56", + "addresses": [ + "0xd8fa0957b8e3c3b07210a9b1c14da07572be3a69", + "0xecae266b38fa64ddb443ab1db44602ff61ff2f3b", + "0xa3E56A7d3C97681D443dE712e3d397D8d8D8A077", + "0x3232d9fbb5f1937c90361c6bee938e0b8f98c71d" + ], + "snapshot": 9242933 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/stablexswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/stablexswap/index.ts new file mode 100644 index 00000000..f5d7cbb1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stablexswap/index.ts @@ -0,0 +1,131 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'stablexswap'; +export const version = '0.0.1'; + +const abi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + }, + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'rewardDebt', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'poolsInfo', + outputs: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + addresses.forEach((address: any) => { + multi.call(`stax.${address}`, options.stax.address, 'balanceOf', [address]); + multi.call( + `stakingChef.${address}`, + options.stakingchef.address, + 'poolsInfo', + [address] + ); + options.pools.forEach((pool: any) => { + multi.call( + `masterChef.${address}.pool_${pool.poolId}`, + options.masterchef.address, + 'userInfo', + [pool.poolId, address] + ); + }); + }); + + const result = await multi.execute(); + + const parseRes = (elem, decimals) => { + return parseFloat(formatUnits(elem, decimals)); + }; + + return Object.fromEntries( + addresses.map((address) => [ + address, + parseRes(result.stax[address], options.stax.decimals) + + parseRes(result.stakingChef[address], options.stakingchef.decimals) * + options.stakingchef.weightage + + +options.pools.reduce( + (prev: number, pool: any) => + prev + + parseRes( + result.masterChef[address][`pool_${pool.poolId}`][0], + options.masterchef.decimals + ) * + pool.weightage, + 0 + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-balancer/examples.json b/Implementations/API/backend/utils/snapshot/strategies/staked-balancer/examples.json new file mode 100644 index 00000000..9d90396d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-balancer/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Equivalent amount of token held in a staked balancer LP contract", + "strategy": { + "name": "staked-balancer", + "params": { + "tokenAddress": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "balancerPoolId": "0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a", + "stakingContract": "0xc8dc2ec5f5e02be8b37a8444a1931f02374a17ab", + "symbol": "PSP" + } + }, + "network": "1", + "addresses": [ + "0x3CE06981bC523f950E3dF346878216365B24b6Fe", + "0x956a64f1028de63b3984637ddd10ae96662b3acd", + "0x3ce06981bc523f950e3df346878216365b24b6fe", + "0x5bb9d7c8e5b4767fdc754f67e6e701a55d3db1f9", + "0x9c0d72f2ac26420cb7eeb155bf401b672840e87b" + ], + "snapshot": 13976779 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-balancer/index.ts b/Implementations/API/backend/utils/snapshot/strategies/staked-balancer/index.ts new file mode 100644 index 00000000..9818bde2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-balancer/index.ts @@ -0,0 +1,115 @@ +import { multicall } from '../../utils'; +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +export const author = 'jo-chemla'; +export const version = '0.1.0'; + +const erc20ABI = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; + +const BALANCER_SUBGRAPH_URL_ROOT = + 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer'; + +const NETWORK_KEY = { + '1': '', + '42': '-kovan', + '137': '-polygon', + '42161': '-arbitrum' +}; + +function buildBalancerSubgraphUrl(chainId, version) { + const networkString = NETWORK_KEY[chainId]; + const versionString = version == 2 ? '-v2' : ''; + return `${BALANCER_SUBGRAPH_URL_ROOT}${networkString}${versionString}`; +} + +const params = { + pool: { + __args: { id: '' }, + totalShares: true, + tokens: { + __args: { + where: { address: '' } + }, + balance: true, + address: true + }, + shares: { + __args: { + where: { + userAddress_in: [], + balance_gt: 0 + } + }, + userAddress: { + id: true + }, + balance: true + } + } +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + // @ts-ignore + params.pool.__args.id = options.balancerPoolId; + params.pool.tokens.__args.where.address = options.tokenAddress; + // @ts-ignore + params.pool.shares.__args.where.userAddress_in = [options.stakingContract]; + + if (snapshot !== 'latest') { + // @ts-ignore + params.pool.__args.block = { number: snapshot }; + } + + // iterate through Balancer V1 & V2 Subgraphs + const score = {}; + for (let version = 1; version <= 2; version++) { + // Skip attempt to query subgraph on networks where V1 isn't deployed + if (network != 1 && network != 42 && version === 1) continue; + + // @ts-ignore + const url = buildBalancerSubgraphUrl(network, version); + const result = await subgraphRequest(url, params); + if (result && result.pool) { + const pool = result.pool; + // Since we did the filter where token address, we are sure tokens[0] is what we are looking for + // Idem, first share of pool.shares is that of staking contract + const tokensPerStkLP = + pool.tokens[0].balance * (pool.shares[0].balance / pool.totalShares); + + // Call totalSupply stakingContract and balanceOf stakingContract token of every address + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const res = await multicall( + network, + provider, + erc20ABI, + [[options.stakingContract, 'totalSupply', []]].concat( + addresses.map((address: any) => [ + options.stakingContract, + 'balanceOf', + [address] + ]) + ), + { blockTag } + ); + const stkTotalSupply = res[0].toString(10); + const response = res.slice(1); + + response.forEach((value, i) => { + const userAddress = getAddress(addresses[i]); + score[userAddress] = (value / stkTotalSupply) * tokensPerStkLP; + }); + } + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/README.md b/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/README.md new file mode 100644 index 00000000..d7a1d493 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/README.md @@ -0,0 +1,15 @@ +# staked-psp-balance + +This is a strategy to get DAO Maker Staked in DAO . + +It calls `DAO.stakeCount(address)` for the DAO Smart contract `smartcontract` +For each `stakeCount` it gets the dao staked using `stakeLists` + +```json +{ + "address": "0x0f51bb10119727a7e5ea3538074fb341f56b09ad", + "symbol": "DAO", + "decimals": 18, + "smartcontract": "0xd07e86f68C7B9f9B215A3ca3E79E74Bf94D6A847" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/examples.json b/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/examples.json new file mode 100644 index 00000000..1cf6c495 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "staked-daomaker", + "strategy": { + "name": "staked-daomaker", + "params": { + "address": "0x0f51bb10119727a7e5ea3538074fb341f56b09ad", + "symbol": "DAO", + "decimals": 18, + "smartcontract": "0xd07e86f68C7B9f9B215A3ca3E79E74Bf94D6A847" + } + }, + "network": "1", + "addresses": [ + "0x8097A41aE28a1aE6e333A8dEfC9d59E35a5f8945", + "0x0018fc9ca03e305f312bb91c1893ce7874e630c5", + "0x0050a84030dda38f929f8bf893e33c0dd7cb40d8", + "0x005624c04a39abccf2a66fde732fe5aa4dd26aab", + "0x005e1ecfafe45d0887428b8f6c5db978ec72296a", + "0x008a696e9855aa357f4ff7bf94674eb640bb2d2a", + "0x00a739ba820c0f98ac54ffd8284bfa12bca9194b", + "0x00b97fa8b11433f9b93bb7bcf438f98a2b77a3cc", + "0x00c5f023e6eea1e81170dd7d8c5293dac792a75c", + "0x00c7969c285559f3b21fe48622fa6e51c104c153", + "0x0218ea910135779376cf06be2d1d5c19c641aa0c", + "0x0216ed3aabfdaa50e263600e5a39082121ceed9f", + "0x0236f0869c0420b05eed924d4c5b559adec930f7", + "0x029cbfe5fa5ea8ed04f0d4e8e7051400238b0b61", + "0x02bc449b248f5c81b5a748db0b45402cd5ffa6b2", + "0x02c2fba4f75f9e5e60546ba83c03277ad547b179", + "0x03194d3df4e646b490b82a8acb4c720c32b31524" + ], + "snapshot": 13672525 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/index.ts b/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/index.ts new file mode 100644 index 00000000..e656bbc9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-daomaker/index.ts @@ -0,0 +1,92 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'paraswap'; +export const version = '0.1.0'; + +const abi = [ + 'function stakeCount(address stakerAddr) view returns (uint256)', + 'function stakeLists(address stakerAddr, uint256 stakeid) view returns (uint128,uint128,uint40,uint16,uint16,uint16)' +]; + +interface StrategyOptions { + address: string; + symbol: string; + decimals: number; + smartcontract: []; +} + +export async function strategy( + space: string, + network: string, + provider, + addresses: string[], + options: StrategyOptions, + snapshot: number +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const stakeCountByWallet = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.smartcontract, + 'stakeCount', + [address.toLowerCase()] + ]), + { blockTag } + ); + + const stakeAmountByWallet: any[] = []; + + // preparing second array for multicall + const arrayForMultiCall: any = []; + for (const i in stakeCountByWallet) { + const num = Number(stakeCountByWallet[i] + ''); + stakeAmountByWallet.push({ wallet: addresses[i], amt: BigNumber.from(0) }); + + if (num > 0) { + const arr = Array.from(Array(num).keys()); + for (const j in arr) { + arrayForMultiCall.push({ + wallet: addresses[i].toLowerCase(), + stakeId: arr[j] + }); + } + } + } + + const stakeAmountByStakeId = await multicall( + network, + provider, + abi, + arrayForMultiCall.map((r: any) => [ + options.smartcontract, + 'stakeLists', + [r.wallet, r.stakeId] + ]), + { blockTag } + ); + for (const i in arrayForMultiCall) { + for (const j in stakeAmountByWallet) { + if ( + arrayForMultiCall[i].wallet.toLowerCase() === + stakeAmountByWallet[j].wallet.toLowerCase() + ) { + stakeAmountByWallet[j].amt = stakeAmountByWallet[j].amt.add( + stakeAmountByStakeId[i][0] + ); + } + } + } + + return Object.fromEntries( + stakeAmountByWallet.map((info, i) => { + return [ + stakeAmountByWallet[i].wallet, + parseFloat(formatUnits(stakeAmountByWallet[i].amt.toString(), 18)) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/ABI/openStakingABI.json b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/ABI/openStakingABI.json new file mode 100644 index 00000000..9c5d284d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/ABI/openStakingABI.json @@ -0,0 +1,26 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "id", + "type": "address" + }, + { + "internalType": "address", + "name": "staker", + "type": "address" + } + ], + "name": "stakeOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/ABI/standardStakingABI.json b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/ABI/standardStakingABI.json new file mode 100644 index 00000000..48265808 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/ABI/standardStakingABI.json @@ -0,0 +1,21 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "stakeOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/README.md new file mode 100644 index 00000000..f4df5edc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/README.md @@ -0,0 +1,40 @@ +staked-defi-balance +=================== + +This custom strategy returns the score of addresses based on their staked token balance in a specific staking pool contract. It works with staking contracts built and deployed by Ferrum Network. You can use this strategy to track the cumulative balance of staking contracts deployed on multiple EVM chains by adding it to your space multiple times with the network and contract configuration. + +Here is an example of parameters: + +```json + +{ + "minStakedBalance": "1", + "contracts": [ + { + "tokenContractAddress": "0xaf329a957653675613D0D98f49fc93326AeB36Fc", + "symbol": "cFRM", + "decimals": 18, + "stakingPoolContractAddress": "0x35e15ff9ebb37d8c7a413fd85bad515396dc8008", + "stakingType": "open" + } + ] +} +``` + +Parameters +---------- + +- `minStakedBalance`: The minimum staked balance required for a user to be included in the score calculation. The value should be in wei. +- `contracts`: An array of contract configurations where each configuration contains: + - `tokenContractAddress`: The address of the token contract for the staked token. + - `symbol`: The symbol of the staked token. + - `decimals`: The number of decimals for the staked token. + - `stakingPoolContractAddress`: The address of the staking pool contract. + - `stakingType`: The type of staking pool contract. Supported values are "open" and "standard". + +Examples +-------- + +To use this strategy in your Snapshot configuration, you can refer to the following example: + +image diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/examples.json new file mode 100644 index 00000000..912c3de1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/examples.json @@ -0,0 +1,30 @@ +[ + { + "name": "Staked Defi Balance", + "strategy": { + "name": "staked-defi-balance", + "params": { + "minStakedBalance": "300000", + "contracts": [ + { + "tokenContractAddress": "0xaf329a957653675613D0D98f49fc93326AeB36Fc", + "symbol": "cFRM", + "decimals": 18, + "stakingPoolContractAddress": "0x35e15ff9ebb37d8c7a413fd85bad515396dc8008", + "stakingType": "open" + } + ] + } + }, + "network": "56", + "addresses": [ + "0x4c4180bf5ec78af9025bdd935ed69e29c2f6cbae", + "0x5785530b6f0ea72b0dba474d55b43e1af1182cad", + "0x468b83D6c8941115E9c61385aff8b71ADD5B8cE8", + "0xdd4ebebe197ca16dc2042414e3b243ab265b0d9a", + "0x697e8e42a50d9a759a238baab25919b177defb89", + "0x6479f7157f06e6610174b1029388B8D4193c00A0" + ], + "snapshot": 27877333 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/index.ts new file mode 100644 index 00000000..a4ac9eb1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/index.ts @@ -0,0 +1,94 @@ +// src/strategies/staked-defi-balance/index.ts + +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import { multicall, getProvider } from '../../utils'; +import openStakingAbi from './ABI/openStakingABI.json'; +import standardStakingAbi from './ABI/standardStakingABI.json'; +import { ABI } from './types'; + +export const author = 'taha-abbasi'; +export const version = '1.3.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const maxContractsPerStrategy = 5; + if (options.contracts.length > maxContractsPerStrategy) { + throw new Error( + 'Maximum of 5 contracts allowed per strategy, see details: https://github.com/snapshot-labs/snapshot-strategies#code' + ); + } + const addressScores = {}; + + for (const params of options.contracts) { + const paramNetwork = network.toString(); + const paramSnapshot = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPoolContractAddress = params.stakingPoolContractAddress; + let abi: ABI; + switch (params.stakingType) { + case 'open': + abi = openStakingAbi[0] as ABI; + break; + case 'standard': + abi = standardStakingAbi[0] as ABI; + break; + default: + throw new Error(`Invalid stakingType: ${params.stakingType}`); + } + + const stakingCalls = addresses.map((address) => { + const inputs = abi.inputs.map((input) => { + if (input.name === 'id') { + return params.tokenContractAddress; + } else if (input.name === 'staker' || input.name === 'account') { + return address; + } + }); + return [stakingPoolContractAddress, abi.name, inputs]; + }); + + const stakes = await multicall( + paramNetwork, + getProvider(paramNetwork), + [abi], + stakingCalls, + { blockTag: paramSnapshot } + ); + + const stakesMapped = {}; + for (let i = 0; i < addresses.length; i++) { + stakesMapped[getAddress(addresses[i])] = stakes[i][0]; + } + + addresses.forEach((address) => { + const normalizedAddress = getAddress(address); + const stakedBalance = stakesMapped[normalizedAddress]; + const formattedStakedBalance = parseFloat( + formatUnits(stakedBalance, params.decimals) + ); + + if (!addressScores[normalizedAddress]) { + addressScores[normalizedAddress] = 0; + } + + addressScores[normalizedAddress] += formattedStakedBalance; + }); + } + + const minStakedBalance = parseFloat(options.minStakedBalance); + + Object.keys(addressScores).forEach((address) => { + if (addressScores[address] < minStakedBalance) { + delete addressScores[address]; + } + }); + + return addressScores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/schema.json b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/schema.json new file mode 100644 index 00000000..7e4fb094 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Staked Defi Balance", + "type": "object", + "properties": { + "minStakedBalance": { + "type": "string" + }, + "contracts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tokenContractAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "symbol": { + "type": "string" + }, + "decimals": { + "type": "integer" + }, + "stakingPoolContractAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "stakingType": { + "oneOf": [ + { + "type": "string", + "enum": ["standard"] + }, + { + "type": "string", + "enum": ["open"] + } + ] + } + }, + "required": [ + "tokenContractAddress", + "symbol", + "decimals", + "stakingPoolContractAddress", + "stakingType" + ] + } + } + }, + "required": ["minStakedBalance", "contracts"] +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/types.ts b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/types.ts new file mode 100644 index 00000000..f172d95b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-defi-balance/types.ts @@ -0,0 +1,17 @@ +// src/strategies/staked-defi-balance/types.ts + +export interface ABI { + inputs: { + internalType: string; + name: 'id' | 'staker' | 'account'; + type: string; + }[]; + name: string; + outputs: { + internalType: string; + name: string; + type: string; + }[]; + stateMutability: string; + type: string; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-keep/README.md b/Implementations/API/backend/utils/snapshot/strategies/staked-keep/README.md new file mode 100644 index 00000000..4d9a137b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-keep/README.md @@ -0,0 +1,11 @@ +## Explanation +This strategy just gets the amount of KEEP that an address has staked in the operators that it owns. + +## Parameters +This strategy doesn't take any parameters. + +## Testing +The numbers can be cross-checked against [All The Keeps](https://allthekeeps.com/operators). + +## Notes +Some of the addresses provided in `examples.json` don't have any operators associated with them and thus don't have any staked KEEP. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-keep/examples.json b/Implementations/API/backend/utils/snapshot/strategies/staked-keep/examples.json new file mode 100644 index 00000000..7b41c94c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-keep/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Staked Keep Example Query", + "strategy": { + "name": "staked-keep", + "params": { + "symbol": "SKEEP" + } + }, + "network": "1", + "addresses": [ + "0x61912E0A8CB64061B9034B324E5ADbEB8dDDc673", + "0x8540F80Fab2AFCAe8d8FD6b1557B1Cf943A0999b", + "0xad32A8e6220741182940c5aBF610bDE99E737b2D", + "0xB8A3AE209d153560993BFd8178E60F09B1c682E8", + "0x3d24D77bEC08549D7Ea86c4e9937204C11E153f1" + ], + "snapshot": 11668787 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-keep/index.ts b/Implementations/API/backend/utils/snapshot/strategies/staked-keep/index.ts new file mode 100644 index 00000000..a0a9e05f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-keep/index.ts @@ -0,0 +1,45 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const KEEP_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/miracle2k/all-the-keeps' +}; + +export const author = 'corollari'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + _options, + snapshot +) { + const params = { + operators: { + __args: { + where: { + owner_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + owner: true, + stakedAmount: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.operators.__args.block = { number: snapshot }; + } + const result = await subgraphRequest(KEEP_SUBGRAPH_URL[network], params); + const score = {}; + if (result && result.operators) { + result.operators.forEach((op) => { + const userAddress = getAddress(op.owner); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + Number(op.stakedAmount); + }); + } + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/README.md new file mode 100644 index 00000000..a44d7422 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/README.md @@ -0,0 +1,21 @@ +# staked-psp-balance + +This is a strategy to get PSP balances staked in SPSP contracts. + +It calls `SPSP.PSPBalance(address)` for each SPSP staking contract for each user address and sums up PSP balances by user address. +Here is an example of parameters: + +```json +{ + "address": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "symbol": "PSP", + "decimals": 18, + "SPSPs": [ + "0x55A68016910A7Bcb0ed63775437e04d2bB70D570", + "0xea02DF45f56A690071022c45c95c46E7F61d3eAb", + "0x6b1D394Ca67fDB9C90BBd26FE692DdA4F4f53ECD", + "0x37b1E4590638A266591a9C11d6f945fe7A1adAA7", + "0xC3359DbdD579A3538Ea49669002e8E8eeA191433" + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/examples.json new file mode 100644 index 00000000..45b351a6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/examples.json @@ -0,0 +1,42 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "staked-psp-balance", + "params": { + "address": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "symbol": "PSP", + "decimals": 18, + "SPSPs": [ + "0x55A68016910A7Bcb0ed63775437e04d2bB70D570", + "0xea02DF45f56A690071022c45c95c46E7F61d3eAb", + "0x6b1D394Ca67fDB9C90BBd26FE692DdA4F4f53ECD", + "0x37b1E4590638A266591a9C11d6f945fe7A1adAA7", + "0xC3359DbdD579A3538Ea49669002e8E8eeA191433" + ] + } + }, + "network": "1", + "addresses": [ + "0x7494Eb2916CAD8649F4f91eB1DB6E20bE605DAd6", + "0xD17Ff37a8047f3520d0271B548f9a1066A8737df", + "0xe768FF81990E7Ac73C18a2eCbf038815023599Dc", + "0xB9E11C28617D46866c1D7d95EaebAC3AC12CDAD3", + "0xB5714084eeF0f02eFDD145DFB3Fe2e3290591D7b", + "0xCC6B30531DE603787a4D0305FC7eD404374Cf771", + "0xcb492647CB51E243Fb2582C0300C4c7573acdEBf", + "0xB8f6f3cc7b162d7E5b9196140Fb1878cdA316ba0", + "0xcff61382E659603046358f86a119EFd127D5BB48", + "0xD4B23B54314F2E038D089D6Fa6e7614415927DBC", + "0x584BaA4b71b0A3fA522658128f36a6A4AbeAC2ae", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 13672525 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/index.ts new file mode 100644 index 00000000..0ca662cc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-psp-balance/index.ts @@ -0,0 +1,60 @@ +import { BigNumberish, BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'paraswap'; +export const version = '0.1.0'; + +const abi = [ + 'function PSPBalance(address _account) view returns (uint256 pspAmount_)' +]; + +interface StrategyOptions { + address: string; + symbol: string; + decimals: number; + SPSPs: string[]; +} + +export async function strategy( + space: string, + network: string, + provider, + addresses: string[], + options: StrategyOptions, + snapshot: number +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + options.SPSPs.forEach((SPSP) => { + addresses.forEach((address) => { + const path = `${SPSP}_${address}`; + // calls SPSP.PSPBalance(address) + // and puts into {`${SPSP}_${address}`: balance} in result + return multi.call(path, SPSP, 'PSPBalance', [address]); + }); + }); + const result: Record = await multi.execute(); + + const pspByAddress = Object.entries(result).reduce>( + (accum, [path, balance]) => { + const [, address] = path.split('_'); + + if (!accum[address]) { + accum[address] = BigNumber.from(0); + } + accum[address] = accum[address].add(balance); + + return accum; + }, + {} + ); + + return Object.fromEntries( + Object.entries(pspByAddress).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/README.md b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/README.md new file mode 100644 index 00000000..c079aa5b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/README.md @@ -0,0 +1,57 @@ +# Staked Uniswap Modifiable + +This stategy is a fork of the staked-uniswap strategy that allows users to customise the function to get the total staked amount of uniswap-lp tokens in the contract. + +This strategy returns a score that is equivalent to the token amount provided in the uniswap liqudity pool +## Accepted options + +- **tokenAddress:** Token Address + +- **uniswapAddress:** Uniswap LP address + +- **stakingAddress:** LP staking address + +- **methodABI** ABI method to get total LP tokens staked in contract + +- **symbol** Token of ERC-20 + +- **decimals** Decimals for ERC-20 + +## Examples + +```JSON +[ + { + "name": "Tokens staked in LP staking contract", + "strategy": { + "name": "staked-uniswap-modifiable", + "params": { + "tokenAddress": "0x2a2550e0a75acec6d811ae3930732f7f3ad67588", + "uniswapAddress": "0x87051936dc0669460951d612fbbe93df88942229", + "stakingAddress": "0xCF2026d955E6686B8582765BF0c5D2Ec05996796", + "symbol": "PATH", + "decimals": 18, + "methodABI": { + "name": "getTotalDeposit", + "type": "function", + "inputs": [ + { + "name": "_account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + } + } + } + } +] +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/examples.json b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/examples.json new file mode 100644 index 00000000..eea08035 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/examples.json @@ -0,0 +1,37 @@ +[ + { + "name": "Tokens staked in LP staking contract", + "strategy": { + "name": "staked-uniswap-modifiable", + "params": { + "tokenAddress": "0x2a2550e0a75acec6d811ae3930732f7f3ad67588", + "uniswapAddress": "0x87051936dc0669460951d612fbbe93df88942229", + "stakingAddress": "0xCF2026d955E6686B8582765BF0c5D2Ec05996796", + "symbol": "PATH", + "decimals": 18, + "methodABI": { + "name": "getTotalDeposit", + "type": "function", + "inputs": [ + { + "name": "_account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + } + } + }, + "network": "1", + "addresses": ["0x29c0b49931A18CB8F343b8474D2fb103DBa1e294"], + "snapshot": 14022650 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/index.ts b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/index.ts new file mode 100644 index 00000000..606209da --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap-modifiable/index.ts @@ -0,0 +1,52 @@ +import { multicall } from '../../utils'; + +export const author = 'doodley1'; +export const version = '0.1.0'; + +const tokenAbi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function totalSupply() view returns (uint256)' +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const tokenMethodAbi = tokenAbi.concat(options.methodABI); + const methodName = options.methodABI['name']; + const res = await multicall( + network, + provider, + tokenMethodAbi, + [ + [options.uniswapAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.uniswapAddress]] + ].concat( + addresses.map((address: any) => [ + options.stakingAddress, + methodName, + [address] + ]) + ), + { blockTag } + ); + + const totalSupply = res[0]; + const tokenBalanceInUni = res[1]; + const tokensPerUni = + tokenBalanceInUni / 10 ** options.decimals / (totalSupply / 1e18); + + const response = res.slice(2); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + (value / 10 ** options.decimals) * tokensPerUni + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap/examples.json new file mode 100644 index 00000000..30cf17cc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Build-ETH LP staked in rewards contract", + "strategy": { + "name": "staked-uniswap", + "params": { + "tokenAddress": "0x6e36556b3ee5aa28def2a8ec3dae30ec2b208739", + "uniswapAddress": "0xdf6b861b4fbcfaffb62dd1906fcd3a863955704b", + "stakingAddress": "0xfd15657341492d1918e3a8b7421e9627d52056e9", + "symbol": "BUILD", + "decimals": 18 + } + }, + "network": "1", + "addresses": ["0x87616fA850c87a78f307878f32D808dad8f4d401"], + "snapshot": 11270000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap/index.ts new file mode 100644 index 00000000..1d670799 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staked-uniswap/index.ts @@ -0,0 +1,84 @@ +import { multicall } from '../../utils'; + +export const author = 'vfatouros'; +export const version = '0.1.0'; + +const tokenAbi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const res = await multicall( + network, + provider, + tokenAbi, + [ + [options.uniswapAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.uniswapAddress]] + ].concat( + addresses.map((address: any) => [ + options.stakingAddress, + 'balanceOf', + [address] + ]) + ), + { blockTag } + ); + + const totalSupply = res[0]; + const tokenBalanceInUni = res[1]; + const tokensPerUni = + tokenBalanceInUni / 10 ** options.decimals / (totalSupply / 1e18); + + const response = res.slice(2); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + (value / 10 ** options.decimals) * tokensPerUni + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/README.md b/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/README.md new file mode 100644 index 00000000..1fd27719 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/README.md @@ -0,0 +1,18 @@ +# stakedao-governance-update + +This strategy is used by StakeDAO to vote for the governance on the protocol. +Here is an example of parameters: + +```json +{ + "SDT_ETHEREUM": "0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F", + "SDT_POLYGON": "0x361A5a4993493cE00f61C32d4EcCA5512b82CE90", + "SDT_RARI_ETHEREUM": "0x1066AB47a342152C564AF62D179aA4B659a11F7d", + "xSDT_RARI_ETHEREUM": "0x806323188117b73315fC9EB3FAa3a48A8D080376", + "veSDT_ETHEREUM": "0x0C30476f66034E11782938DF8e4384970B6c9e8a", + "network_ETHEREUM": "1", + "network_POLYGON": "137", + "symbol": "sdToken", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/examples.json b/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/examples.json new file mode 100644 index 00000000..9c5fb510 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "stakedao-governance-update", + "params": { + "SDT_ETHEREUM": "0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F", + "SDT_POLYGON": "0x361A5a4993493cE00f61C32d4EcCA5512b82CE90", + "SDT_RARI_ETHEREUM": "0x1066AB47a342152C564AF62D179aA4B659a11F7d", + "xSDT_RARI_ETHEREUM": "0x806323188117b73315fC9EB3FAa3a48A8D080376", + "veSDT_ETHEREUM": "0x0C30476f66034E11782938DF8e4384970B6c9e8a", + "network_ETHEREUM": "1", + "network_POLYGON": "137", + "symbol": "sdToken", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x983ec6d045713d1b87f110a7edab9fc7996eefc0", + "0x6d75ffbffd1e63e5072f0ffbf6c4eefa16043967", + "0xbd2471B4150619a42093fFBA3a7AF35335ceC5B6" + ], + "snapshot": 14316000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/index.ts b/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/index.ts new file mode 100644 index 00000000..49f13778 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakedao-governance-update/index.ts @@ -0,0 +1,135 @@ +import { multicall } from '../../utils'; +import { getProvider } from '../../utils'; + +export const author = 'clement-ux'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)', + 'function locked(address arg0) external view returns (int128,uint256)' +]; + +const F = 4; // veSDT vote multiplicator + +const chunk = (arr, size) => + Array.from({ length: Math.ceil(arr.length / size) }, (v, i) => + arr.slice(i * size, i * size + size) + ); +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // *** Query *** // + + // Ethereum Side + const SDT_Query_ETH = addresses.map((address: any) => [ + options.SDT_ETHEREUM, + 'balanceOf', + [address] + ]); + + const SDT_Locked_Query_ETH = addresses.map((address: any) => [ + options.veSDT_ETHEREUM, + 'locked', + [address] + ]); + const veSDT_Query_ETH = addresses.map((address: any) => [ + options.veSDT_ETHEREUM, + 'balanceOf', + [address] + ]); + + const SDT_RARI_Query_ETH = addresses.map((address: any) => [ + options.SDT_RARI_ETHEREUM, + 'balanceOf', + [address] + ]); + const xSDT_RARI_Query_ETH = addresses.map((address: any) => [ + options.xSDT_RARI_ETHEREUM, + 'balanceOf', + [address] + ]); + // Polygon Side + const SDT_Query_POLYGON = addresses.map((address: any) => [ + options.SDT_POLYGON, + 'balanceOf', + [address] + ]); + + // *** Response *** // + const responseETH = await multicall( + options.network_ETHEREUM, + getProvider(options.network_ETHEREUM), + abi, + [ + ...SDT_Query_ETH, + ...SDT_Locked_Query_ETH, + ...SDT_RARI_Query_ETH, + ...xSDT_RARI_Query_ETH, + ...veSDT_Query_ETH + ], + { + blockTag + } + ); + const responsePOLYGON = await multicall( + options.network_POLYGON, + getProvider(options.network_POLYGON), + abi, + [...SDT_Query_POLYGON], + { + blockTag + } + ); + + const responseCleanETH = responseETH.slice(0, responseETH.length); + const responseCleanPOLYGON = responsePOLYGON.slice(0, responsePOLYGON.length); + const chunksETH = chunk(responseCleanETH, addresses.length); + const chunksPOLY = chunk(responseCleanPOLYGON, addresses.length); + + const SDT_ETH = chunksETH[0]; + const SDTLocked_ETH = chunksETH[1]; + const fSDT = chunksETH[2]; + const fxSDT = chunksETH[3]; + const veSDT = chunksETH[4]; + const SDT_POLY = chunksPOLY[0]; + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + const SDT_ETHi = SDT_ETH[i][0]; + const SDTLocked_ETHi = SDTLocked_ETH[i][0]; + const veSDTi = veSDT[i][0]; + const fSDTi = fSDT[i][0]; + const fxSDTi = fxSDT[i][0]; + const SDT_POLYi = SDT_POLY[i][0]; + + // Print statements + //console.log(`==================${addresses[i]}==================\n`); + //console.log(`${SDT_ETHi / 10 ** 18} SDT_ETH`); + //console.log(`${SDTLocked_ETHi / 10 ** 18} SDTLocked_ETH`); + //console.log(`${veSDTi / 10 ** 18} veSDT`); + //console.log(`${fSDTi / 10 ** 18} fSDT`); + //console.log(`${fxSDTi / 10 ** 18} fxSDT`); + //console.log(`${SDT_POLYi / 10 ** 18} SDT_POLY`); + + return [ + addresses[i], + SDT_ETHi.add(SDTLocked_ETHi) + .add(veSDTi.mul(F)) + .add(fSDTi) + .add(fxSDTi) + .add(SDT_POLYi) / + 10 ** 18 + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/README.md b/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/README.md new file mode 100644 index 00000000..001c6ea2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/README.md @@ -0,0 +1,19 @@ +# stakedotlink-vesting + +Returns the vesting amount of tokens within a stake.link [DelegatorPool](https://github.com/stakedotlink/contracts/blob/main/contracts/core/DelegatorPool.sol). + +To calculate the amount of vesting tokens, the strategy will perform `totalBalanceOf - balanceOf`, returning the amount +tokens currently vesting at any given point in time. + +- `totalBalanceOf`: Total balance of account including vesting tokens as per `VestingSchedule` +- `balanceOf`: Balance of account excluding vesting tokens as per `VestingSchedule` + +Here is an example of parameters: + +```json +{ + "address": "0xAEF186611EC96427d161107fFE14bba8aA1C2284", + "symbol": "stSDL", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/examples.json new file mode 100644 index 00000000..39d1080a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "stakedotlink-vesting", + "params": { + "address": "0xAEF186611EC96427d161107fFE14bba8aA1C2284", + "symbol": "stSDL", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x427a9957d3a131EE969a3BB5537070C6aEf03Ea4", + "0xF2aD781cFf42E1f506b78553DA89090C65b1A847", + "0xc316276f87019e5adbc3185A03e23ABF948A732D", + "0xE2b7cBA5E48445f9bD17193A29D7fDEb4Effb078" + ], + "snapshot": 16478298 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/index.ts new file mode 100644 index 00000000..dd192276 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/index.ts @@ -0,0 +1,42 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'stakedotlink'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOf(address _account) public view returns (uint256)', + 'function totalBalanceOf(address _account) public view returns (uint256)' +]; + +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) => + multi.call(address, options.address, 'balanceOf', [address]) + ); + const balanceResult: Record = await multi.execute(); + addresses.forEach((address) => + multi.call(address, options.address, 'totalBalanceOf', [address]) + ); + const totalBalanceResult: Record = + await multi.execute(); + + return Object.fromEntries( + Object.entries(balanceResult).map(([address, balance]) => { + const vestingBalance = + parseFloat(formatUnits(totalBalanceResult[address], options.decimals)) - + parseFloat(formatUnits(balance, options.decimals)); + return [address, vestingBalance]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/schema.json b/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/schema.json new file mode 100644 index 00000000..2113da8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakedotlink-vesting/schema.json @@ -0,0 +1,33 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/README.md b/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/README.md new file mode 100644 index 00000000..8f4a4bc0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/README.md @@ -0,0 +1,34 @@ +# Stakers and holders strategy + +This strategy return the balances of the voters from both staking pool and ERC721 NFT. + +## Accepted options + +- **staking:** Staking pool address. + +- **token:** ERC721 NFT address. + +## Examples + +```JSON +[ + { + "name": "Stakers and Holders", + "strategy": { + "name": "stakers-and-holders", + "params": { + "staking": "0x611D4fe3773606C7680020D1a59a2e8c5D43e682", + "token": "0x79104Beca59CAe0EeeDd5ecB9fbc1AAD90cA40FE", + "symbol": "HBC", + "decimals": 0 + } + }, + "network": "137", + "addresses": [ + "0x5a0270Cf694a5b1e645dd5812f8d497B6bb87e07", + "0x4C766160428cF0EE82b4dD677be90824A58E4855" + ], + "snapshot": 24178477 + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/examples.json b/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/examples.json new file mode 100644 index 00000000..345bac6d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Stakers and Holders", + "strategy": { + "name": "stakers-and-holders", + "params": { + "staking": "0x611D4fe3773606C7680020D1a59a2e8c5D43e682", + "token": "0x79104Beca59CAe0EeeDd5ecB9fbc1AAD90cA40FE", + "symbol": "HBC", + "decimals": 0 + } + }, + "network": "137", + "addresses": [ + "0x5a0270Cf694a5b1e645dd5812f8d497B6bb87e07", + "0x4C766160428cF0EE82b4dD677be90824A58E4855", + "0x8Ba7789Bb5B4389904cEa64aB4942F55c464C2c5", + "0x351A859bBA58C0E0589310D76E009F8d62Ae225A", + "0xEe44B0dC609A670f99629eC400B27D61489b7bb1", + "0x17af5cED4B2bA9F95E66EF22372900d367351A41" + ], + "snapshot": 24249987 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/index.ts b/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/index.ts new file mode 100644 index 00000000..27ae0e42 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/stakers-and-holders/index.ts @@ -0,0 +1,49 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'tony31913'; +export const version = '0.1.0'; + +const stakingAbi = [ + 'function depositsOf(address account) public view returns (uint256[] memory)' +]; + +const tokenAbi = [ + 'function balanceOf(address owner) public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPool = new Multicaller(network, provider, stakingAbi, { + blockTag + }); + const tokenPool = new Multicaller(network, provider, tokenAbi, { + blockTag + }); + + addresses.forEach((address) => { + stakingPool.call(address, options.staking, 'depositsOf', [address]); + tokenPool.call(address, options.token, 'balanceOf', [address]); + }); + + const [stakingResponse, tokenResponse]: [ + Record, + Record + ] = await Promise.all([stakingPool.execute(), tokenPool.execute()]); + + return Object.fromEntries( + addresses.map((address) => { + const stakingCount = stakingResponse[address].length; + const tokenCount = BigNumber.from(tokenResponse[address]).toNumber(); + return [address, stakingCount + tokenCount]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/README.md b/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/README.md new file mode 100644 index 00000000..7ca04e06 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/README.md @@ -0,0 +1,39 @@ +# Staking Claimed & Unclaimed + +This strategy can check the claimed and unclaimed balance of staking rewards, based off the Mutant Cat staking contract. This means that it will include the balance that is still pending in the contract. + +The staking contract is expected to have the following two functions: +- `depositsOf(address account) public view returns (uint256[] memory)` +- `calculateRewards(address account, uint256[] tokenIds) public view returns (uint256[] memory)` + +The strategy returns the total of claimed and unclaimed rewards token. + +## Accepted options + +- **staking:** The address of the staking contract + +- **token:** The address of the rewards ERC20 token + +- **symbol:** The symbol of the rewards token + +- **decimals** Decimals for ERC-20 + +## Examples + +```JSON + +[ + { + "name": "Staking Claimed and Unclaimed", + "strategy": { + "name": "staking-claimed-unclaimed", + "params": { + "staking": "0x9b561710fEED0B8d829de89C82F80Bb7B2B364B8", + "token": "0xceb726e6383468dd8ac0b513c8330cc9fb4024a8", + "symbol": "WORMS", + "decimals": 0 + } + } + } +] +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/examples.json b/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/examples.json new file mode 100644 index 00000000..78f9d756 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Staking Claimed and Unclaimed", + "strategy": { + "name": "staking-claimed-unclaimed", + "params": { + "staking": "0x9b561710fEED0B8d829de89C82F80Bb7B2B364B8", + "token": "0xceb726e6383468dd8ac0b513c8330cc9fb4024a8", + "symbol": "WORMS", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x55975a6da3a41c3dd81e95673f6ded1856d34711", + "0x0Fddf683A5F3a2B51704aAE4e828Ec9A35DEE5e5", + "0xdeA8554F1ec3496e0c81219f01D22515A265eCd0", + "0x5c86410bB3CfAA05a62802bcA20cCCbeEdF8f556" + ], + "snapshot": 14126302 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/index.ts b/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/index.ts new file mode 100644 index 00000000..7ac2e80d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/staking-claimed-unclaimed/index.ts @@ -0,0 +1,69 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; +import { formatEther } from '@ethersproject/units'; + +export const author = 'ZombieDAODev'; +export const version = '0.1.0'; + +const stakingAbi = [ + 'function depositsOf(address account) public view returns (uint256[] memory)', + 'function calculateRewards(address account, uint256[] tokenIds) public view returns (uint256[] memory)' +]; + +const tokenAbi = [ + 'function balanceOf(address owner) public view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingPool = new Multicaller(network, provider, stakingAbi, { + blockTag + }); + + const tokenPool = new Multicaller(network, provider, tokenAbi, { + blockTag + }); + + addresses.forEach((address) => { + stakingPool.call(address, options.staking, 'depositsOf', [address]); + tokenPool.call(address, options.token, 'balanceOf', [address]); + }); + const [stakingResponse, tokenResponse]: [ + Record, + Record + ] = await Promise.all([stakingPool.execute(), tokenPool.execute()]); + + addresses.forEach((address) => { + const tokenIds = stakingResponse[address].map((tokenId) => + BigNumber.from(tokenId).toNumber() + ); + stakingPool.call(address, options.staking, 'calculateRewards', [ + address, + tokenIds + ]); + }); + + const rewardsResponse: Record = + await stakingPool.execute(); + + return Object.fromEntries( + addresses.map((address) => { + const claimedCount = formatEther(BigNumber.from(tokenResponse[address])); + const unclaimedCount = formatEther( + rewardsResponse[address].reduce( + (prev, count) => BigNumber.from(prev).add(BigNumber.from(count)), + BigNumber.from('0') + ) + ); + return [address, parseInt(claimedCount) + parseInt(unclaimedCount)]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/README.md b/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/README.md new file mode 100644 index 00000000..d16f7a15 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/README.md @@ -0,0 +1,13 @@ +# Starcatchers Top Wallet Window Strategy + +Fetches Starcatchers Intergalactic Alliance subgraph to calculate top delegated +wallets. It then uses a block based window of time to determine the active +delegates in the selected cycle. + +## Parameters + +| Parameter | Desc | +| ---------------- | -------------------------------------------------- | +| origin | At what block height does Intergalactic time begin | +| delegateLimit | Max delegates allowed to vote | +| delegateDuration | How many blocks do delegates last | diff --git a/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/examples.json b/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/examples.json new file mode 100644 index 00000000..51bfef25 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "starcatchers-top-window", + "params": { + "origin": 14277684, + "delegateLimit": 1, + "delegateDuration": 91000, + "symbol": "STAR" + } + }, + "network": "1", + "addresses": [ + "0x370f75a63F438186DbfECfD27cD75a5023bEa737", + "0x239f4b81f1B54Ee89a506C98b34a4FF6b012f8E7", + "0xB0B60594c97D73A16233B112f7fFa6aB470B1790" + ], + "snapshot": 14278000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/index.ts b/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/index.ts new file mode 100644 index 00000000..ab8ffe43 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starcatchers-top-window/index.ts @@ -0,0 +1,67 @@ +import { subgraphRequest } from '../../utils'; + +export const author = 'sunshinekitty'; +export const version = '0.0.1'; + +const SC_GRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/sunshinekitty/starcatchers-ia1'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + if (!options.origin) { + options.origin = 14277684; // Block at 2/25/22 2:22pm PT (DST) + } + if (!options.delegateLimit) { + options.delegateLimit = 35; + } + if (!options.delegateDuration) { + const bpd = 6500; // Estimated blocks produced per day. + options.delegateDuration = 14 * bpd; // 2w + } + // Based on snapshot or current block height, calculates delegate cycles that + // have passed, then determines block height for current delegate cycle. + const blockNumber: number = + typeof snapshot === 'number' ? snapshot : await provider.getBlockNumber(); + const cyclesPassed: number = + ((blockNumber - Number(options.origin)) / + Number(options.delegateDuration)) | + 0; + const voteBlock: number = + Number(options.origin) + cyclesPassed * Number(options.delegateDuration); + + const query = { + voteWeights: { + __args: { + first: Number(options.delegateLimit), + orderBy: 'weight', + orderDirection: 'desc', + block: { + number: voteBlock + } + }, + id: true, + weight: true + } + }; + const results = await subgraphRequest(SC_GRAPH_URL, query); + if (!results) { + return; + } + + const delegates = {}; + addresses.forEach((address: string) => { + delegates[address] = 0; + for (let i = 0; i < Number(options.delegateLimit); i++) { + if (results.voteWeights[i].id == address.toLowerCase()) { + delegates[address] = 1; + } + } + }); + return delegates; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/README.md b/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/README.md new file mode 100644 index 00000000..43da74da --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/README.md @@ -0,0 +1,13 @@ +# starlay-ve-balance-of-locker-id + +This is the strategy for starlay's Voting Escrow, it returns the balances of the voters for veLAY token. + +Here is an example of parameters: + +```json +{ + "address": "0xDf32D28c1BdF25c457E82797316d623C2fcB29C8", + "symbol": "veLAY", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/examples.json b/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/examples.json new file mode 100644 index 00000000..762f19c5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "starlay-ve-balance-of-locker-id", + "params": { + "address": "0xDf32D28c1BdF25c457E82797316d623C2fcB29C8", + "symbol": "veLAY", + "decimals": 18 + } + }, + "network": "592", + "addresses": [ + "0x3Ff1aD046Cf6B5F5fC035b909e0E4482186b8354", + "0x50414Ac6431279824df9968855181474c919a94B", + "0x00aa74a272603c502503b1F470F81F4D441A6413", + "0xa9Fc75f34c338f658bC0BeAA57f14D675301185f", + "0xCdfc500F7f0FCe1278aECb0340b523cD55b3EBbb", + "0x8114c31Cd93141DD6229553F622b85b06624957e", + "0xCB163C7A7D299c54F71144D02870eC0c9A82AB29", + "0x61709f32808EBCD44b0D7c9271a4A16D167A7AC4", + "0x145a89758F3f9e26ffb96a4eD853d4fEDfa3a87B", + "0x1b40f4f72618530cA1e560b201F0a608C736E43e", + "0x02E7b714fae84e4BA80f3CDa5508553e7CF5042A", + "0xb3ff54b7F24950945bA8f1778F52835a7aD30A7F", + "0x0Ba1Aa8e98257EFb07cCa9bDCC410a38056897d1", + "0x244C76c039Cc412826F9e4427Ce600DcFC013a6c", + "0x332d9CDf9fDA07C32622B9C7eDE52Eb1a851eC53", + "0x7457667ee63BCa3c134f246af687091e7E259Bc2", + "0xab4382aAdae5bF4b37B90b9dc07FC72D52e2a28A", + "0x94c74DFA070500E317DddE4d64007FE26ae8aB9A" + ], + "snapshot": 3642724 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/index.ts b/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/index.ts new file mode 100644 index 00000000..80ca7100 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/index.ts @@ -0,0 +1,40 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'wolfwarrier14'; +export const version = '0.1.0'; + +const abi = [ + 'function ownerToId(address) view returns (uint256)', + 'function balanceOfLockerId(uint256 _lockerId) view returns (uint256)' +]; + +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) => + multi.call(address, options.address, 'ownerToId', [address]) + ); + const locker_id_result: Record = await multi.execute(); + + Object.entries(locker_id_result).map(([address, locker_id]) => + multi.call(address, options.address, 'balanceOfLockerId', [locker_id]) + ); + const balance_result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(balance_result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/schema.json b/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/schema.json new file mode 100644 index 00000000..2113da8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starlay-ve-balance-of-locker-id/schema.json @@ -0,0 +1,33 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/starsharks/README.md b/Implementations/API/backend/utils/snapshot/strategies/starsharks/README.md new file mode 100644 index 00000000..cefc0bca --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starsharks/README.md @@ -0,0 +1,3 @@ +# StarSharks + +StarSharks voting power strategy. diff --git a/Implementations/API/backend/utils/snapshot/strategies/starsharks/examples.json b/Implementations/API/backend/utils/snapshot/strategies/starsharks/examples.json new file mode 100644 index 00000000..21e4e9c6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starsharks/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "starsharks", + "params": { + "symbol": "VP", + "chainId": 56 + } + }, + "network": "56", + "addresses": [ + "0x413134Dd97857F103964ed85d96D04ED744B9fc6", + "0x82acB2ad3EF6A61c923D27b922bCaf89A97AC5A4" + ], + "snapshot": 15364151 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/starsharks/index.ts b/Implementations/API/backend/utils/snapshot/strategies/starsharks/index.ts new file mode 100644 index 00000000..15bc0e8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/starsharks/index.ts @@ -0,0 +1,82 @@ +import fetch from 'cross-fetch'; +import { formatUnits } from '@ethersproject/units'; +const { getAddress } = require('@ethersproject/address'); + +export const author = 'starsharks'; +export const version = '0.1.0'; + +const API_URLS = { + 56: 'https://www.starsharks.com/go/api/stake/vote-weight', + 97: 'https://develop.sharkshake.net/go/api/stake/vote-weight' +}; + +type DepositListRequest = { + addresses: string[]; + block_id: number; + network: number; +}; + +type Deposit = { + vesss_amount: string; + begin_at: number; + end_at: number; +}; + +type DepositListResponse = Record; + +async function getAddressesDespoits({ + addresses, + block_id, + network +}: DepositListRequest): Promise { + const res = await fetch(API_URLS[network], { + method: 'POST', + headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + accounts: addresses, + block_id + }) + }); + const data = (await res.json()) as any; + return data.data; +} + +function calcPowerByDeposits(deposits: Deposit[], blockTime: number) { + let power = 0; + for (const deposit of deposits) { + const { vesss_amount, begin_at, end_at } = deposit; + if (begin_at < blockTime && end_at > blockTime) { + const remainTimePercent = (end_at - blockTime) / (end_at - begin_at); + const increment = Number(formatUnits(vesss_amount)) * remainTimePercent; + power += increment; + } + } + return power; +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const block_id = typeof snapshot === 'number' ? snapshot : 0; + const block = await provider.getBlock('latest'); + const blockTime = block.timestamp; + const { chainId } = options; + const depositsMap = await getAddressesDespoits({ + addresses, + block_id, + network: chainId ?? network + }); + + const result = {}; + for (const address in depositsMap) { + const deposits = depositsMap[address]; + result[getAddress(address)] = calcPowerByDeposits(deposits, blockTime); + } + + return result; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/README.md b/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/README.md new file mode 100644 index 00000000..40fc546a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/README.md @@ -0,0 +1,16 @@ +# Contract call strategy + +Allows the tokens locked in marinate contract to be used to calculate voter scores. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "address": "0xe6d557d416ff5640235119369c7e26AA18a906D7", + "marinateLevels": [0, 1, 2, 3], + "symbol": "sUMAMI", + "marinateAddress": "0x190a6b6E8e4D9B8324E1F97127c588C5b082d94b", + "decimals": 9 +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/examples.json new file mode 100644 index 00000000..57093d9c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/examples.json @@ -0,0 +1,37 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "sumami-holders", + "params": { + "address": "0xe6d557d416ff5640235119369c7e26AA18a906D7", + "marinateLevels": [0, 1, 2, 3], + "symbol": "sUMAMI", + "marinateAddress": "0x190a6b6E8e4D9B8324E1F97127c588C5b082d94b", + "decimals": 9 + } + }, + "network": "42161", + "addresses": [ + "0x541D67bEdBfe820b4E58712bf032C7250548D733", + "0x93615705c74bfd7d7c0b0d9cfd5d04ee00d90471", + "0xb9d24761ebb3e3a88b8cf20e77f56ccf026a2044", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0xb2273e70eda1cb2e16bdfd7a15c031d12400f7ca", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x9a32b1a10504489f9b88106acec79c672630158c" + ], + "snapshot": 4178460 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/index.ts new file mode 100644 index 00000000..0c8226da --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sumami-holders/index.ts @@ -0,0 +1,70 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'arugulo'; +export const version = '0.1.0'; + +// Merged ABI for sUMAMI and Marinate contracts +const abi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function stakedBalance(address account, uint32 level) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + if (options.marinateLevels.length > 4) { + return []; + } + const sUmamiBalances = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const marinateBalances = await Promise.all( + options.marinateLevels.map((level: number) => + multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.marinateAddress, + 'stakedBalance', + [address, level], + { blockTag } + ]), + { blockTag } + ) + ) + ); + + const totalMarinateBalances = marinateBalances.reduce( + //@ts-ignore + (prev: any, cur: any) => + cur.map( + (balance, idx) => + (prev[idx] || 0) + + parseFloat(formatUnits(balance.toString(), options.decimals)) + ), + [] + ); + + return Object.fromEntries( + Object.entries(sUmamiBalances).map((address, index) => [ + address[0], + //@ts-ignore + address[1] + totalMarinateBalances[index] + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sunder/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sunder/examples.json new file mode 100644 index 00000000..3827e91e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sunder/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "sunder", + "params": { + "token": "0xaF5A2E9E0BCf9Ec3bf447F511F8Ac028F5959077", + "symbol": "sudner (token)", + "decimals": 18, + "balanceOfAdds": "0x20a183539DF9dE2223991329FE68d5cCc5c8C0D8" + } + }, + "network": "42", + "addresses": [ + "0x02Ec1090D59cbAA9D58EAeBb3328dF56A6dEd2D3", + "0x714AfdEB0c24127ec03149005AD59F80bbFD71d7" + ], + "snapshot": 26570329 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sunder/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sunder/index.ts new file mode 100644 index 00000000..4a5501e4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sunder/index.ts @@ -0,0 +1,37 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'krotos-arch'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOfDToken(address _token, address _account) public returns (uint256 _balance)' +]; + +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) => + multi.call(address, options.balanceOfAdds, 'balanceOfDToken', [ + options.token, + address + ]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/README.md b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/README.md new file mode 100644 index 00000000..831fcb55 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/README.md @@ -0,0 +1,15 @@ +# Sunrise Gaming Staking Token + +This strategy returns balances of the underlying token in Sunrise Staking pools + +Here is an example of parameters: + +```json +{ + "symbol": "SUNC", + "decimals": 18, + "tokenAddress": "0x692accdd8b86692427e0aa4752ae917df01cc56f", + "stakingAddress": "0x7dbE40ac6bB41A5FE4Fa2C74f31d7DEFBC793B58", + "poolIndex": 0 +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/examples.json new file mode 100644 index 00000000..cb25122f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Sunrise Staked token", + "strategy": { + "name": "sunrisegaming-staking", + "params": { + "symbol": "SUNC", + "decimals": 18, + "tokenAddress": "0x692accdd8b86692427e0aa4752ae917df01cc56f", + "stakingAddress": "0x7dbE40ac6bB41A5FE4Fa2C74f31d7DEFBC793B58", + "poolIndex": 0 + } + }, + "network": "1", + "addresses": [ + "0x7dbe40ac6bb41a5fe4fa2c74f31d7defbc793b58", + "0x26533020822f9b28c801e6beb772ef74dfe0c81e", + "0x75bd5989ce8f962a1864d8a0af8bfb5130805722", + "0x7505afcd576bb995577a944bccdb5c54c7d5ec45", + "0x930af93baa5f2c2f71bafd5a2fd7b1d47a61c352", + "0xbc444050d78107872c0262452fff004da9208258", + "0x26533020822f9b28c801e6beb772ef74dfe0c81e", + "0x45e049241b69df70cead33ef5a41d2d0a128ce01" + ], + "snapshot": 13229798 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/index.ts new file mode 100644 index 00000000..0c9cf1ed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-staking/index.ts @@ -0,0 +1,49 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'sunrisedao'; +export const version = '0.1.0'; + +const masterChefAbi = [ + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Get staked LP in staking constract + const stakedRes = await multicall( + network, + provider, + masterChefAbi, + [ + ...addresses.map((address: any) => [ + options.stakingAddress, + 'userInfo', + [options.poolIndex, address] + ]) + ], + { blockTag } + ); + + return Object.fromEntries( + stakedRes.map((stakedInfo, i) => { + const parsedAmount = parseFloat( + formatUnits(bn(stakedInfo.amount), options.decimal) + ); + return [addresses[i], parsedAmount]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/README.md b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/README.md new file mode 100644 index 00000000..b79cd426 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/README.md @@ -0,0 +1,16 @@ +# Sunrise Gaming Uniswap LP + +This strategy returns balances of the underlying token in Sunrise LP pools + +Here is an example of parameters: + +```json +{ + "symbol": "SUNC", + "decimals": 18, + "tokenAddress": "0x692accdd8b86692427e0aa4752ae917df01cc56f", + "lpTokenAddress": "0xaf5a7469Cf2571b973AEee9AE2f8aad00e1337d2", + "stakingAddress": "0x7dbE40ac6bB41A5FE4Fa2C74f31d7DEFBC793B58", + "poolIndex": 1 +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/examples.json new file mode 100644 index 00000000..ac12ecfc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Staked Univ2 LP for token", + "strategy": { + "name": "sunrisegaming-univ2-lp", + "params": { + "symbol": "SUNC", + "decimals": 18, + "tokenAddress": "0x692accdd8b86692427e0aa4752ae917df01cc56f", + "lpTokenAddress": "0xaf5a7469Cf2571b973AEee9AE2f8aad00e1337d2", + "stakingAddress": "0x7dbE40ac6bB41A5FE4Fa2C74f31d7DEFBC793B58", + "poolIndex": 1 + } + }, + "network": "1", + "addresses": [ + "0x7dbe40ac6bb41a5fe4fa2c74f31d7defbc793b58", + "0x26533020822f9b28c801e6beb772ef74dfe0c81e", + "0x75bd5989ce8f962a1864d8a0af8bfb5130805722", + "0x7505afcd576bb995577a944bccdb5c54c7d5ec45", + "0x930af93baa5f2c2f71bafd5a2fd7b1d47a61c352", + "0xbc444050d78107872c0262452fff004da9208258", + "0x26533020822f9b28c801e6beb772ef74dfe0c81e" + ], + "snapshot": 13227497 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/index.ts new file mode 100644 index 00000000..48c6c631 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sunrisegaming-univ2-lp/index.ts @@ -0,0 +1,82 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'sunrisedao'; +export const version = '0.1.0'; + +const erc20Abi = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address account) view returns (uint256)' +]; + +const masterChefAbi = [ + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)' +]; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Get LP balances + let res = await multicall( + network, + provider, + erc20Abi, + [ + [options.lpTokenAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.lpTokenAddress]], + ...addresses.map((address: any) => [ + options.lpTokenAddress, + 'balanceOf', + [address] + ]) + ], + { blockTag } + ); + + const lpTokenTotalSupply = bn(res[0]); // decimal: 18 + const totalTokensInPool = bn(res[1]); // decimal: options.decimal + + res = res.slice(2); + + // Get staked LP in staking constract + const stakedRes = await multicall( + network, + provider, + masterChefAbi, + [ + ...addresses.map((address: any) => [ + options.stakingAddress, + 'userInfo', + [options.poolIndex, address] + ]) + ], + { blockTag } + ); + + // How much tokens user has from LP tokens + const usersTokensFromLp = res.slice(0, addresses.length).map((amount, i) => { + const totalLp = bn(amount).add(bn(stakedRes[i].amount)); // decimal: 18 + + // (LP + StakedLP) x token.balanceOf(LPToken) / LPToken.totalSupply() + return totalLp.mul(totalTokensInPool).div(lpTokenTotalSupply); // decimal: options.decimal + }); + + return Object.fromEntries( + usersTokensFromLp.map((sum, i) => { + const parsedSum = parseFloat(formatUnits(sum, options.decimal)); + return [addresses[i], parsedSum]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/sushiswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/sushiswap/README.md new file mode 100644 index 00000000..ab726f15 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sushiswap/README.md @@ -0,0 +1,17 @@ +# Sushiswap + +This strategy returns balances of the underlying token in Sushiswap LP pools + +Here is an example of parameters: + +```json +{ + "address": "0x0Ae055097C6d159879521C384F1D2123D1f195e6", + "useStakedBalances": "true", + "masterchefVersion": "v1" +} +``` + +- *address* - the underlying token +- *useStakedBalances* - if **true** it will also return the token balances from the MasterChef LP Staking Pool +- *masterchefVersion* - if **v2** it will return the token balances from the MasterChef v2 LP Staking Pool instead of MasterChef v1. Defaults to v1 diff --git a/Implementations/API/backend/utils/snapshot/strategies/sushiswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sushiswap/examples.json new file mode 100644 index 00000000..94bc729d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sushiswap/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "sushiswap", + "params": { + "address": "0xcafe001067cdef266afb7eb5a286dcfd277f3de5", + "symbol": "PSP", + "useStakedBalances": "true", + "masterchefVersion": "v2" + } + }, + "network": "1", + "addresses": [ + "0x058476edacb23e9507cff379e7dd8cf4dee4d2db", + "0x5ba47f8c64fcf55e986e2f37860b91b501d1b1ed" + ], + "snapshot": 14080593 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sushiswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sushiswap/index.ts new file mode 100644 index 00000000..c840f3fa --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sushiswap/index.ts @@ -0,0 +1,279 @@ +import { getAddress } from '@ethersproject/address'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; + +// Minichef: https://github.com/sushiswap/sushiswap-interface/blob/master/src/services/graph/fetchers/masterchef.ts +// Exchange: https://github.com/sushiswap/sushiswap-interface/blob/master/src/services/graph/fetchers/exchange.ts + +const theGraph_baseUrl = 'https://api.thegraph.com/subgraphs/name/'; +const SUSHISWAP_SUBGRAPH_URL = { + exchange: { + '1': 'sushiswap/exchange', + '100': 'sushiswap/xdai-exchange', + '137': 'sushiswap/matic-exchange', + '250': 'sushiswap/fantom-exchange', + '42161': 'sushiswap/arbitrum-exchange', + '1285': 'sushiswap/moonriver-exchange', + '42220': 'jiro-ono/sushitestsubgraph', + '122': 'sushiswap/fuse-exchange', + '1666600000': 'sushiswap/harmony-exchange', + '56': 'sushiswap/bsc-exchange', + '43114': 'sushiswap/avalanche-exchange', + '66': 'okex-exchange/oec', + '128': 'heco-exchange/heco' + }, + masterChef: { + '1': 'sushiswap/master-chef', + '100': 'matthewlilley/xdai-minichef', // Could be replaced by 'sushiswap/xdai-minichef' + '137': 'sushiswap/matic-minichef', + '250': 'sushiswap/fantom-minichef', + '42161': 'matthewlilley/arbitrum-minichef', + '1285': 'sushiswap/moonriver-minichef', + '42220': 'sushiswap/celo-minichef-v2', + '122': 'sushiswap/fuse-minichef', + '1666600000': 'sushiswap/harmony-minichef' + }, + masterChefV2: { + '1': 'sushiswap/master-chefv2' + } +}; + +const PAGE_SIZE = 1000; + +export const author = 'maxaleks'; +export const version = '0.2.0'; + +async function getPairs(network, snapshot, token) { + const getParams = (tokenId, page) => { + const params = { + pairs: { + __args: { + where: { + [tokenId]: token.toLowerCase() + }, + first: PAGE_SIZE, + skip: page * PAGE_SIZE + }, + id: true, + totalSupply: true, + reserve0: true, + reserve1: true, + token0: { + id: true, + decimals: true + }, + token1: { + id: true, + decimals: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.pairs.__args.block = { number: snapshot }; + } + return params; + }; + const requestPairs = async (tokenId) => { + let pairs = []; + let page = 0; + while (true) { + const result = await subgraphRequest( + theGraph_baseUrl + SUSHISWAP_SUBGRAPH_URL.exchange[network], + getParams(tokenId, page) + ); + pairs = pairs.concat(result.pairs); + page++; + if (result.pairs.length < PAGE_SIZE) break; + } + return pairs; + }; + const [pairs1, pairs2] = await Promise.all( + ['token0', 'token1'].map((tokenId) => requestPairs(tokenId)) + ); + return Object.fromEntries( + pairs1.concat(pairs2).map((pair: any) => { + const isToken0 = pair.token0.id == token.toLowerCase(); + const rate = isToken0 + ? +pair.reserve0 / +pair.totalSupply + : +pair.reserve1 / +pair.totalSupply; + const decimals = isToken0 ? pair.token0.decimals : pair.token1.decimals; + return [pair.id, { rate, decimals }]; + }) + ); +} + +async function getPools(network, snapshot, token, masterChefUrl) { + const pairs = await getPairs(network, snapshot, token); + const params = { + pools: { + __args: { + where: { + pair_in: Object.keys(pairs) + }, + first: PAGE_SIZE + }, + id: true, + pair: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.pools.__args.block = { number: snapshot }; + } + let pools = []; + let page = 0; + while (true) { + // @ts-ignore + params.pools.__args.skip = page * PAGE_SIZE; + const result = await subgraphRequest(masterChefUrl, params); + pools = pools.concat(result.pools); + page++; + if (result.pools.length < PAGE_SIZE) break; + } + return Object.fromEntries( + pools.map((pool: any) => [pool.id, pairs[pool.pair]]) + ); +} + +async function getStakedBalances(network, snapshot, options, addresses) { + const token = options.address; + // Only allow the masterChefVersion key to be v2 if on mainnet, otherwise fallback to v1 + const masterchefSuffix = + network == '1' && options.masterchefVersion == 'v2' + ? SUSHISWAP_SUBGRAPH_URL.masterChefV2[network] + : SUSHISWAP_SUBGRAPH_URL.masterChef[network]; + const masterChefUrl = theGraph_baseUrl + masterchefSuffix; + const pools = await getPools(network, snapshot, token, masterChefUrl); + const params = { + users: { + __args: { + where: { + pool_in: Object.keys(pools), + address_in: addresses.map((addr) => addr.toLowerCase()) + }, + first: PAGE_SIZE + }, + address: true, + amount: true, + pool: { + id: true + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.__args.block = { number: snapshot }; + } + + let users = []; + let page = 0; + while (true) { + // @ts-ignore + params.users.__args.skip = page * PAGE_SIZE; + const result = await subgraphRequest(masterChefUrl, params); + users = users.concat(result.users); + page++; + if (result.users.length < PAGE_SIZE) break; + } + + return users.map((user: any) => { + const pool = pools[user.pool.id]; + const amount = + parseFloat(formatUnits(user.amount, pool.decimals)) * pool.rate; + return { + address: user.address, + amount + }; + }); +} + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + liquidityPositions: { + __args: { + where: { + liquidityTokenBalance_gt: 0 + } + }, + liquidityTokenBalance: true, + pair: { + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.liquidityPositions.__args.block = { number: snapshot }; + } + const tokenAddress = options.address.toLowerCase(); + const result = await subgraphRequest( + theGraph_baseUrl + SUSHISWAP_SUBGRAPH_URL.exchange[network], + params + ); + const score = {}; + if (result && result.users) { + result.users.forEach((u) => { + u.liquidityPositions + .filter( + (lp) => + lp.pair.token0.id == tokenAddress || + lp.pair.token1.id == tokenAddress + ) + .forEach((lp) => { + const token0perUni = lp.pair.reserve0 / lp.pair.totalSupply; + const token1perUni = lp.pair.reserve1 / lp.pair.totalSupply; + const userScore = + lp.pair.token0.id == tokenAddress + ? token0perUni * lp.liquidityTokenBalance + : token1perUni * lp.liquidityTokenBalance; + + const userAddress = getAddress(u.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScore; + }); + }); + } + if ( + options.useStakedBalances === 'true' && + SUSHISWAP_SUBGRAPH_URL.masterChef[network] + ) { + const stakedBalances = await getStakedBalances( + network, + snapshot, + options, + addresses + ); + stakedBalances.forEach((balance) => { + const userAddress = getAddress(balance.address); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] += balance.amount; + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/svs-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/svs-staking/examples.json new file mode 100644 index 00000000..27cea55e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/svs-staking/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "SVS stakers and holders", + "strategy": { + "name": "svs-staking", + "params": { + "symbol": "SVS", + "decimals": 0, + "tokenAddress": "0x219B8aB790dECC32444a6600971c7C3718252539", + "stakingAddress": "0x12753244901f9E612A471c15C7E5336e813D2e0B" + } + }, + "network": "1", + "addresses": [ + "0x24d19f100ba142543a863fc2294b188e35ab55b9", + "0xC0d6d28811df8466E4BAC33CfC9Ed6e745900a07", + "0xFdA9F18221d14F5FAD1ff8a0dbA92A716Ba8144D", + "0x43522a5abA7c0FEad84b6614C208B5162A1b4ADF", + "0xcA50e4211B330295212FF9C1160F5DF3E0Ccd9D8" + ], + "snapshot": 13449843 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/svs-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/svs-staking/index.ts new file mode 100644 index 00000000..480b014e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/svs-staking/index.ts @@ -0,0 +1,55 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'fsjuhl'; +export const version = '0.1.0'; + +const stakingAbi = [ + 'function getVampsBuried(address burier) view returns (uint256[])' +]; + +const tokenAbi = ['function balanceOf(address owner) view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const stakersResponse = await multicall( + network, + provider, + stakingAbi, + addresses.map((address: any) => [ + options.stakingAddress, + 'getVampsBuried', + [address] + ]), + { blockTag } + ); + + const holdersResponse = await multicall( + network, + provider, + tokenAbi, + addresses.map((address: any) => [ + options.tokenAddress, + 'balanceOf', + [address] + ]), + { blockTag } + ); + + return Object.fromEntries( + stakersResponse.map((value, i) => [ + addresses[i], + value[0].length + + parseFloat( + formatUnits(holdersResponse[i][0].toString(), options.decimals) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/swapr/README.md b/Implementations/API/backend/utils/snapshot/strategies/swapr/README.md new file mode 100644 index 00000000..4d486c7b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/swapr/README.md @@ -0,0 +1,13 @@ +# Swapr + +This strategy returns balances of the underlying token in Swapr pairs (including the liquidity staked on the farming campaigns offered by the platform). + +Here is an example of parameters: + +```json +{ + "address": "0xdE903E2712288A1dA82942DDdF2c20529565aC30" +} +``` + +- _address_ - the underlying token diff --git a/Implementations/API/backend/utils/snapshot/strategies/swapr/commons.ts b/Implementations/API/backend/utils/snapshot/strategies/swapr/commons.ts new file mode 100644 index 00000000..b2b64f6f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/swapr/commons.ts @@ -0,0 +1,15 @@ +export const SWAPR_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/luzzif/swapr-mainnet-v2', + '100': 'https://api.thegraph.com/subgraphs/name/luzzif/swapr-xdai-v2', + '42161': + 'https://api.thegraph.com/subgraphs/name/luzzif/swapr-arbitrum-one-v2' +}; + +export const mergeBalanceMaps = ( + outputMap: { [address: string]: number }, + inputMap: { [address: string]: number } +) => { + Object.entries(inputMap).forEach(([account, balance]) => { + outputMap[account] = (outputMap[account] || 0) + balance; + }); +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/swapr/examples.json b/Implementations/API/backend/utils/snapshot/strategies/swapr/examples.json new file mode 100644 index 00000000..ef699268 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/swapr/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "swapr", + "params": { + "symbol": "SWPR", + "address": "0xdE903E2712288A1dA82942DDdF2c20529565aC30" + } + }, + "network": "42161", + "addresses": ["0xa5A29f81EEE450eC189b2F8B4562af1785595D69"], + "snapshot": 2867713 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/swapr/index.ts b/Implementations/API/backend/utils/snapshot/strategies/swapr/index.ts new file mode 100644 index 00000000..bf4ae8cf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/swapr/index.ts @@ -0,0 +1,55 @@ +import { getSwaprLiquidityProvidersBalance } from './swapr-lps'; +import { strategy as erc20BalanceOfStartegy } from '../erc20-balance-of'; +import { mergeBalanceMaps } from './commons'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'luzzif'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const lpData = await getSwaprLiquidityProvidersBalance( + network, + addresses, + options, + snapshot + ); + + let i = 0; + const PAGE_SIZE = 250; + let rawErc20HoldersData: { [address: string]: number } = {}; + while (true) { + const pageData = await erc20BalanceOfStartegy( + space, + network, + provider, + addresses.slice(PAGE_SIZE * i, PAGE_SIZE * i + PAGE_SIZE), + options, + snapshot + ); + rawErc20HoldersData = { ...rawErc20HoldersData, ...pageData }; + if (Object.keys(pageData).length < PAGE_SIZE) break; + i++; + } + + // make sure the addresses have the correct casing before + // merging the balance maps + const erc20HoldersData = Object.entries(rawErc20HoldersData).reduce( + (accumulator, [address, balance]) => { + accumulator[getAddress(address)] = balance; + return accumulator; + }, + {} + ); + + const score: { [address: string]: number } = {}; + mergeBalanceMaps(score, lpData); + mergeBalanceMaps(score, erc20HoldersData); + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/swapr/swapr-lps.ts b/Implementations/API/backend/utils/snapshot/strategies/swapr/swapr-lps.ts new file mode 100644 index 00000000..2ccf51db --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/swapr/swapr-lps.ts @@ -0,0 +1,211 @@ +import { Decimal } from 'decimal.js-light'; +import { getAddress } from '@ethersproject/address'; +import { mergeBalanceMaps, SWAPR_SUBGRAPH_URL } from './commons'; +import { subgraphRequest } from '../../utils'; + +interface StandardLiquidityPosition { + id: string; + user: { id: string }; + liquidityTokenBalance: string; + pair: { + totalSupply: string; + reserve0: string; + reserve1: string; + }; +} + +interface StakedLiquidityPosition { + id: string; + user: { id: string }; + stakedAmount: string; + targetedPair: { + totalSupply: string; + reserve0: string; + reserve1: string; + }; +} + +const mergeStandardAndStakedPositions = ( + standardPositions: StandardLiquidityPosition[], + stakedPositions: StakedLiquidityPosition[] +) => { + return stakedPositions.reduce( + (accumulator: StandardLiquidityPosition[], stakedPosition) => { + const index = accumulator.findIndex( + (p) => p.user.id.toLowerCase() === stakedPosition.user.id.toLowerCase() + ); + if (index >= 0) + accumulator[index].liquidityTokenBalance = new Decimal( + accumulator[index].liquidityTokenBalance + ) + .plus(stakedPosition.stakedAmount) + .toString(); + else + accumulator.push({ + ...stakedPosition, + pair: stakedPosition.targetedPair, + liquidityTokenBalance: stakedPosition.stakedAmount + }); + return accumulator; + }, + standardPositions + ); +}; + +const getPositions = async ( + network, + addresses, + options, + snapshot +): Promise<{ + positionsByToken0: StandardLiquidityPosition[]; + positionsByToken1: StandardLiquidityPosition[]; +}> => { + const wantedTokenAddress = options.address; + const swaprSubgraphUrl = SWAPR_SUBGRAPH_URL[network]; + + const [token0Query, token1Query] = ['token0', 'token1'].map((key) => ({ + pairs: { + __args: { + where: { + [key]: wantedTokenAddress.toLowerCase() + }, + first: 1000 + }, + id: true + } + })); + if (snapshot !== 'latest') { + // @ts-ignore + token0Query.pairs.__args.block = { number: snapshot }; + // @ts-ignore + token1Query.pairs.__args.block = { number: snapshot }; + } + const swprPairsByToken0 = await subgraphRequest( + swaprSubgraphUrl, + token0Query + ); + const swprPairsByToken1 = await subgraphRequest( + swaprSubgraphUrl, + token1Query + ); + + const [liquidityPositionsByToken0Query, liquidityPositionsByToken1Query] = [ + swprPairsByToken0, + swprPairsByToken1 + ].map((wrappedPairs) => ({ + liquidityPositions: { + __args: { + where: { + user_in: addresses.map((address) => address.toLowerCase()), + pair_in: wrappedPairs.pairs.map((pair) => pair.id), + liquidityTokenBalance_gt: 0 + }, + first: 1000 + }, + user: { + id: true + }, + liquidityTokenBalance: true, + pair: { + totalSupply: true, + reserve0: true, + reserve1: true + } + } + })); + const liquidityPositionsByToken0 = await subgraphRequest( + swaprSubgraphUrl, + liquidityPositionsByToken0Query + ); + const liquidityPositionsByToken1 = await subgraphRequest( + swaprSubgraphUrl, + liquidityPositionsByToken1Query + ); + + const [ + liquidityMiningPositionsByToken0Query, + liquidityMiningPositionsByToken1Query + ] = [swprPairsByToken0, swprPairsByToken1].map((wrappedPairs) => ({ + liquidityMiningPositions: { + __args: { + where: { + user_in: addresses.map((address) => address.toLowerCase()), + targetedPair_in: wrappedPairs.pairs.map((pair) => pair.id), + stakedAmount_gt: 0 + }, + first: 1000 + }, + user: { + id: true + }, + stakedAmount: true, + targetedPair: { + totalSupply: true, + reserve0: true, + reserve1: true + } + } + })); + + const liquidityMiningPositionsByToken0 = await subgraphRequest( + swaprSubgraphUrl, + liquidityMiningPositionsByToken0Query + ); + const liquidityMiningPositionsByToken1 = await subgraphRequest( + swaprSubgraphUrl, + liquidityMiningPositionsByToken1Query + ); + + return { + positionsByToken0: mergeStandardAndStakedPositions( + liquidityPositionsByToken0.liquidityPositions, + liquidityMiningPositionsByToken0.liquidityMiningPositions + ), + positionsByToken1: mergeStandardAndStakedPositions( + liquidityPositionsByToken1.liquidityPositions, + liquidityMiningPositionsByToken1.liquidityMiningPositions + ) + }; +}; + +const lpDataToBalanceMap = ( + positions: StandardLiquidityPosition[], + useToken0Data: boolean +) => { + return positions.reduce( + (accumulator: { [address: string]: number }, position) => { + const userLpTokenBalance = new Decimal(position.liquidityTokenBalance); + const pairTotalSupply = new Decimal(position.pair.totalSupply); + const userPoolPercentage = userLpTokenBalance.dividedBy(pairTotalSupply); + const userHolding = new Decimal( + useToken0Data ? position.pair.reserve0 : position.pair.reserve1 + ).mul(userPoolPercentage); + const userAddress = getAddress(position.user.id); + accumulator[userAddress] = + (accumulator[userAddress] || 0) + userHolding.toNumber(); + return accumulator; + }, + {} + ); +}; + +export const getSwaprLiquidityProvidersBalance = async ( + network, + addresses, + options, + snapshot +): Promise<{ + [address: string]: number; +}> => { + const { positionsByToken0, positionsByToken1 } = await getPositions( + network, + addresses, + options, + snapshot + ); + const balanceMap: { [address: string]: number } = {}; + mergeBalanceMaps(balanceMap, lpDataToBalanceMap(positionsByToken0, true)); + mergeBalanceMaps(balanceMap, lpDataToBalanceMap(positionsByToken1, false)); + return balanceMap; +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/README.md b/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/README.md new file mode 100644 index 00000000..646e743c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/README.md @@ -0,0 +1,22 @@ +# sybil-protection + +This is the most common strategy, it returns the balances of the voters for a specific ERC20 token. + +Here is an example of parameters: + +```json +{ + "strategy": { + "name": "erc20-balance-of", + "params": { + "address": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + "symbol": "GRT", + "decimals": 18 + } + }, + "sybil": { + "poh": "0xC5E9dDebb09Cd64DfaCab4011A0D5cEDaf7c9BDb", + "brightId": "v5" + } +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/examples.json b/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/examples.json new file mode 100644 index 00000000..1bab98e6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/examples.json @@ -0,0 +1,46 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "sybil-protection", + "params": { + "strategy": { + "name": "erc20-balance-of", + "params": { + "address": "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + "symbol": "GRT", + "decimals": 18 + } + }, + "sybil": { + "poh": "0xC5E9dDebb09Cd64DfaCab4011A0D5cEDaf7c9BDb", + "brightId": "v5" + } + } + }, + "network": "1", + "addresses": [ + "0x7A38760C295f1ea086005214a279fb1280010483", + "0xF78108c9BBaF466dd96BE41be728Fe3220b37119", + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x8d5F05270da470e015b67Ab5042BDbE2D2FEFB48", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 15068173 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/index.ts b/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/index.ts new file mode 100644 index 00000000..a0f4108b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/sybil-protection/index.ts @@ -0,0 +1,66 @@ +import strategies from '..'; +import { strategy as proofOfHumanityStrategy } from '../proof-of-humanity'; +import { strategy as brightIdStrategy } from '../brightid'; + +export const author = 'samuveth'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + async function getProofOfHumanity() { + if (!options?.sybil?.poh) return {}; + return await proofOfHumanityStrategy( + space, + '1', + provider, + addresses, + { address: options.sybil.poh }, + snapshot + ); + } + + async function getBrightId() { + if (!options?.sybil?.brightId) return {}; + return await brightIdStrategy( + space, + network, + provider, + addresses, + { registry: options.sybil.brightId }, + snapshot + ); + } + + const [scores, proofOfHumanity, brightId] = await Promise.all([ + await strategies[options.strategy.name].strategy( + space, + network, + provider, + addresses, + options.strategy.params, + snapshot + ), + getProofOfHumanity(), + getBrightId() + ]); + + // Reduce voting power of address to zero if cybil check doesn't pass + const cybilScores = Object.keys(scores).reduce((acc, key) => { + if (proofOfHumanity?.[key] === 1) { + acc[key] = scores[key]; + } else if (brightId?.[key] === 1) { + acc[key] = scores[key]; + } else { + acc[key] = 0; + } + return acc; + }, {}); + + return cybilScores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/README.md new file mode 100644 index 00000000..94348421 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/README.md @@ -0,0 +1,14 @@ +# Synthetic Nouns + +This strategy allows you to determine if an address is eligible to vote by checking if it holds the unique NFT claimed by it originally. + +It uses the Zora API to find the minting event associated with the user address on the Synthetic Nouns contract and then checks if the NFT is still owned by the user. + +Here is an example of parameters: + +```json +{ + "address": "0x8761b55af5a703d5855f1865db8fe4dd18e94c53", + "symbol": "sNOUN" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/examples.json new file mode 100644 index 00000000..b72dc492 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query for Synthetic Nouns that are owned by the original claiming address", + "strategy": { + "name": "synthetic-nouns-with-claimer", + "params": { + "address": "0x8761b55af5a703d5855f1865db8fe4dd18e94c53", + "symbol": "sNOUN" + } + }, + "network": "1", + "addresses": [ + "0x8d25687829d6b85d9e0020b8c89e3ca24de20a89", + "0x368f593244f04c599b3328a23493680b23818fa6", + "0x6ab075abfA7cdD7B19FA83663b1f2a83e4A957e3", + "0x7f16D5c969380E3420E17B4c3456A3844745A578" + ], + "snapshot": 15486783 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/index.ts new file mode 100644 index 00000000..914cf302 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetic-nouns-with-claimer/index.ts @@ -0,0 +1,75 @@ +import { Multicaller } from '../../utils'; +import fetch from 'cross-fetch'; +import { getAddress } from '@ethersproject/address'; +export const author = 'stephancill'; +export const version = '0.1.0'; + +const abi = ['function ownerOf(uint256 index) external view returns (address)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const checksummedAddresses = addresses.map((address) => getAddress(address)); + + // Get the minter from zora api + const mints = await fetch('https://api.zora.co/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: `{ + mints(where: {collectionAddresses: "${ + options.address + }", minterAddresses: ${JSON.stringify( + addresses + )}}, pagination: {limit: 500}) { + nodes { + mint { + tokenId, + toAddress + } + } + } + }` + }) + }).then((res) => res.json()); + + const mintsByAddress = mints.data.mints.nodes.reduce((acc, node) => { + const address = getAddress(node.mint.toAddress); + const tokenId = node.mint.tokenId; + if (!acc[address]) { + acc[address] = undefined; + } + acc[address] = tokenId; + return acc; + }, {}); + + // Use multicall to check if the token is owned by the address + const multicaller = new Multicaller(network, provider, abi, { + blockTag + }); + checksummedAddresses.forEach((address) => { + if (mintsByAddress[address]) { + multicaller.call(address, options.address, 'ownerOf', [ + mintsByAddress[address] + ]); + } + }); + + const response = await multicaller.execute(); + + const scores = Object.fromEntries( + checksummedAddresses.map((address) => [ + address, + response[address] && getAddress(response[address]) === address ? 1 : 0 + ]) + ); + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/README.md new file mode 100644 index 00000000..5aaf2215 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/README.md @@ -0,0 +1,15 @@ +# Non-Quadratic Debt Percentage Strategy + +Calculates the weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["synthetix-non-quadratic"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/examples.json new file mode 100644 index 00000000..937bf7e1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "synthetix-non-quadratic", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD" + } + }, + "network": "1", + "addresses": [ + "0x78b037B39704e88a82DD23CFBE1f57f6AeF8EBC5", + "0x0bc3668d2AaFa53eD5E5134bA13ec74ea195D000", + "0xcAc59F91E4536Bc0E79aB816a5cD54e89f10433C", + "0x6dc88B231Cd04Dd1b1e525161162993F47140006", + "0x935D2fD458fdf41B6F7B62471f593797866a3Ce6", + "0x24e445fe7708Bf4bC2ae8d4df1694C98Af8BDE4F", + "0x49be88f0fcc3a8393a59d3688480d7d253c37d2a", + "0x27Cc4d6bc95b55a3a981BF1F1c7261CDa7bB0931" + ], + "snapshot": 13228907 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/index.ts new file mode 100644 index 00000000..0d42dd08 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic/index.ts @@ -0,0 +1,184 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract } from '@ethersproject/contracts'; +import { Provider } from '@ethersproject/providers'; +import { + subgraphRequest + // ipfsGet +} from '../../utils'; +import { + DebtCacheABI, + debtL1, + debtL2, + returnGraphParams, + SNXHoldersResult, + SynthetixStateABI +} from '../synthetix/helper'; + +export const author = 'andytcf'; +export const version = '1.0.0'; + +const MED_PRECISE_UNIT = 1e18; + +// @TODO: check if most-up-to-date version (using https://contracts.synthetix.io/SynthetixState) +const SynthetixStateContractAddress = + '0x4b9Ca5607f1fF8019c1C6A3c2f0CC8de622D5B82'; +// @TODO: check if most-up-to-date version (using http://contracts.synthetix.io/DebtCache) +const DebtCacheContractAddress = '0xe92B4c7428152052B0930c81F4c687a5F1A12292'; + +const defaultGraphs = { + '1': 'https://api.thegraph.com/subgraphs/name/killerbyte/synthetix', + '10': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/optimism-issuance' +}; + +// @TODO: update with the latest ovm snapshot +// const ovmSnapshotJSON = 'QmNwvhq4By1Mownjycg7bWSXqbJWMVyAWRZ1K4mjxuvGXg'; + +const loadLastDebtLedgerEntry = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + SynthetixStateContractAddress, + SynthetixStateABI, + provider + ); + + const lastDebtLedgerEntry = await contract.lastDebtLedgerEntry({ + blockTag: snapshot + }); + + return BigNumber.from(lastDebtLedgerEntry); +}; + +const loadL1TotalDebt = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + DebtCacheContractAddress, + DebtCacheABI, + provider + ); + + const currentDebtObject = await contract.currentDebt({ + blockTag: snapshot + }); + + return Number(currentDebtObject.debt) / MED_PRECISE_UNIT; +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _, + snapshot +) { + const score = {}; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + /* Global Constants */ + + const totalL1Debt = await loadL1TotalDebt(_provider, snapshot); // (high-precision 1e18) + const lastDebtLedgerEntry = await loadLastDebtLedgerEntry( + _provider, + snapshot + ); + + /* EDIT THESE FOR OVM */ + + // @TODO update the currentDebt for the snapshot from (https://contracts.synthetix.io/ovm/DebtCache) + const totalL2Debt = 22617610; + // @TODO update the lastDebtLedgerEntry from (https://contracts.synthetix.io/ovm/SynthetixState) + const lastDebtLedgerEntryL2 = 20222730523217499684984991; + // @TODO update the comparison between OVM:ETH c-ratios at the time of snapshot + const normalisedL2CRatio = 600 / 450; + // @TODO update the L2 block number to use + const L2BlockNumber = 1770186; + + const scaledTotalL2Debt = totalL2Debt * normalisedL2CRatio; + + /* --------------- */ + + /* Using the subgraph, we get the relevant L1 calculations */ + + const l1Results = (await subgraphRequest( + defaultGraphs[1], + returnGraphParams(blockTag, _addresses) + )) as SNXHoldersResult; + + if (l1Results && l1Results.snxholders) { + for (let i = 0; i < l1Results.snxholders.length; i++) { + const holder = l1Results.snxholders[i]; + const vote = await debtL1( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntry, + false + ); + score[getAddress(holder.id)] = vote; + } + } + + /* Using the subgraph, we get the relevant L2 calculations */ + + const l2Results = (await subgraphRequest( + defaultGraphs[10], + returnGraphParams(L2BlockNumber, _addresses) + )) as SNXHoldersResult; + + // @notice fallback for when subgraph is down + /* + const OVMSnapshot = await ipfsGet('gateway.pinata.cloud', ovmSnapshotJSON); + const array = Object.assign( + {}, + ...OVMSnapshot.data.snxholders.map((key) => ({ + [getAddress(key.id)]: { + initialDebtOwnership: key.initialDebtOwnership, + debtEntryAtIndex: key.debtEntryAtIndex + } + })) + ); + for (let k = 0; k < _addresses.length; k++) { + const address = _addresses[k]; + if (array[getAddress(address)]) { + score[getAddress(address)] += await quadraticWeightedVoteL2( + array[getAddress(address)].initialDebtOwnership, + array[getAddress(address)].debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntryL2 + ); + } else { + continue; + } + } + */ + + if (l2Results && l2Results.snxholders) { + for (let i = 0; i < l2Results.snxholders.length; i++) { + const holder = l2Results.snxholders[i]; + + const vote = await debtL2( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntryL2, + false + ); + + if (score[getAddress(holder.id)]) { + score[getAddress(holder.id)] += vote; + } else { + score[getAddress(holder.id)] = vote; + } + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/README.md new file mode 100644 index 00000000..c84c8327 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/README.md @@ -0,0 +1,15 @@ +# Non-Quadratic Debt Percentage Strategy + +Calculates the weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["synthetix-non-quadratic_1"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/examples.json new file mode 100644 index 00000000..60f4a95a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "synthetix-non-quadratic_1", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD", + "totalL2Debt": 48646913, + "lastDebtLedgerEntryL2": 9773647546760863848975891, + "L2BlockNumber": 919219 + } + }, + "network": "1", + "addresses": [ + "0xcAc59F91E4536Bc0E79aB816a5cD54e89f10433C", + "0x935D2fD458fdf41B6F7B62471f593797866a3Ce6" + ], + "snapshot": 13764062 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/index.ts new file mode 100644 index 00000000..5bd81ab8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_1/index.ts @@ -0,0 +1,153 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract } from '@ethersproject/contracts'; +import { Provider } from '@ethersproject/providers'; +import { subgraphRequest } from '../../utils'; +import { + DebtCacheABI, + debtL1, + debtL2, + returnGraphParams, + SNXHoldersResult, + SynthetixStateABI +} from '../synthetix/helper'; + +export const author = 'andytcf'; +export const version = '1.0.0'; + +const MED_PRECISE_UNIT = 1e18; + +// @TODO: check if most-up-to-date version (using https://contracts.synthetix.io/SynthetixState) +const SynthetixStateContractAddress = + '0x4b9Ca5607f1fF8019c1C6A3c2f0CC8de622D5B82'; +// @TODO: check if most-up-to-date version (using http://contracts.synthetix.io/DebtCache) +const DebtCacheContractAddress = '0x9D5551Cd3425Dd4585c3E7Eb7E4B98902222521E'; + +const defaultGraphs = { + '1': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/synthetix', + '10': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/optimism-main' +}; + +const loadLastDebtLedgerEntry = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + SynthetixStateContractAddress, + SynthetixStateABI, + provider + ); + + const lastDebtLedgerEntry = await contract.lastDebtLedgerEntry({ + blockTag: snapshot + }); + + return BigNumber.from(lastDebtLedgerEntry); +}; + +const loadL1TotalDebt = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + DebtCacheContractAddress, + DebtCacheABI, + provider + ); + + const currentDebtObject = await contract.currentDebt({ + blockTag: snapshot + }); + + return Number(currentDebtObject.debt) / MED_PRECISE_UNIT; +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _options, + snapshot +) { + const score = {}; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + /* Global Constants */ + + const totalL1Debt = await loadL1TotalDebt(_provider, snapshot); // (high-precision 1e18) + const lastDebtLedgerEntry = await loadLastDebtLedgerEntry( + _provider, + snapshot + ); + + /* EDIT THESE FOR OVM */ + + // @TODO update the currentDebt for the snapshot from (https://contracts.synthetix.io/ovm/DebtCache) + // const totalL2Debt = 48646913; + const totalL2Debt = _options.totalL2Debt; + // @TODO update the lastDebtLedgerEntry from (https://contracts.synthetix.io/ovm/SynthetixState) + // const lastDebtLedgerEntryL2 = 9773647546760863848975891; + const lastDebtLedgerEntryL2 = _options.lastDebtLedgerEntryL2; + // @TODO update the comparison between OVM:ETH c-ratios at the time of snapshot + const normalisedL2CRatio = 500 / 400; + // @TODO update the L2 block number to use + // const L2BlockNumber = 919219; + const L2BlockNumber = _options.L2BlockNumber; + + const scaledTotalL2Debt = totalL2Debt * normalisedL2CRatio; + + /* --------------- */ + + /* Using the subgraph, we get the relevant L1 calculations */ + + const l1Results = (await subgraphRequest( + defaultGraphs[1], + returnGraphParams(blockTag, _addresses) + )) as SNXHoldersResult; + + if (l1Results && l1Results.snxholders) { + for (let i = 0; i < l1Results.snxholders.length; i++) { + const holder = l1Results.snxholders[i]; + const vote = await debtL1( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntry, + false + ); + score[getAddress(holder.id)] = vote; + } + } + + /* Using the subgraph, we get the relevant L2 calculations */ + + const l2Results = (await subgraphRequest( + defaultGraphs[10], + returnGraphParams(L2BlockNumber, _addresses) + )) as SNXHoldersResult; + + if (l2Results && l2Results.snxholders) { + for (let i = 0; i < l2Results.snxholders.length; i++) { + const holder = l2Results.snxholders[i]; + + const vote = await debtL2( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntryL2, + false + ); + + if (score[getAddress(holder.id)]) { + score[getAddress(holder.id)] += vote; + } else { + score[getAddress(holder.id)] = vote; + } + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/README.md new file mode 100644 index 00000000..1d849ec8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/README.md @@ -0,0 +1,15 @@ +# Non-Quadratic Debt Percentage Strategy + +Calculates the weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["synthetix-non-quadratic_2"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/examples.json new file mode 100644 index 00000000..c18fd691 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "synthetix-non-quadratic_2", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD", + "totalL2Debt": 40201854, + "lastDebtLedgerEntryL2": 10909822163955610660349890, + "L2BlockNumber": 4750287 + } + }, + "network": "1", + "addresses": [ + "0x78b037B39704e88a82DD23CFBE1f57f6AeF8EBC5", + "0x0bc3668d2AaFa53eD5E5134bA13ec74ea195D000", + "0xcAc59F91E4536Bc0E79aB816a5cD54e89f10433C", + "0x6dc88B231Cd04Dd1b1e525161162993F47140006", + "0x935D2fD458fdf41B6F7B62471f593797866a3Ce6", + "0x24e445fe7708Bf4bC2ae8d4df1694C98Af8BDE4F", + "0x49be88f0fcc3a8393a59d3688480d7d253c37d2a", + "0x27Cc4d6bc95b55a3a981BF1F1c7261CDa7bB0931" + ], + "snapshot": 14443440 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/index.ts new file mode 100644 index 00000000..bcc5609f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-non-quadratic_2/index.ts @@ -0,0 +1,153 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract } from '@ethersproject/contracts'; +import { Provider } from '@ethersproject/providers'; +import { subgraphRequest } from '../../utils'; +import { + DebtCacheABI, + debtL1, + debtL2, + returnGraphParams, + SNXHoldersResult, + SynthetixStateABI +} from '../synthetix/helper'; + +export const author = 'andytcf'; +export const version = '1.0.0'; + +const MED_PRECISE_UNIT = 1e18; + +// @TODO: check if most-up-to-date version (using https://contracts.synthetix.io/SynthetixState) +const SynthetixStateContractAddress = + '0x4b9Ca5607f1fF8019c1C6A3c2f0CC8de622D5B82'; +// @TODO: check if most-up-to-date version (using http://contracts.synthetix.io/DebtCache) +const DebtCacheContractAddress = '0x1620Aa736939597891C1940CF0d28b82566F9390'; + +const defaultGraphs = { + '1': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/synthetix', + '10': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/optimism-main' +}; + +const loadLastDebtLedgerEntry = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + SynthetixStateContractAddress, + SynthetixStateABI, + provider + ); + + const lastDebtLedgerEntry = await contract.lastDebtLedgerEntry({ + blockTag: snapshot + }); + + return BigNumber.from(lastDebtLedgerEntry); +}; + +const loadL1TotalDebt = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + DebtCacheContractAddress, + DebtCacheABI, + provider + ); + + const currentDebtObject = await contract.currentDebt({ + blockTag: snapshot + }); + + return Number(currentDebtObject.debt) / MED_PRECISE_UNIT; +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _options, + snapshot +) { + const score = {}; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + /* Global Constants */ + + const totalL1Debt = await loadL1TotalDebt(_provider, snapshot); // (high-precision 1e18) + const lastDebtLedgerEntry = await loadLastDebtLedgerEntry( + _provider, + snapshot + ); + + /* EDIT THESE FOR OVM */ + + // @TODO update the currentDebt for the snapshot from (https://contracts.synthetix.io/ovm/DebtCache) + // const totalL2Debt = 40201854; + const totalL2Debt = _options.totalL2Debt; + // @TODO update the lastDebtLedgerEntry from (https://contracts.synthetix.io/ovm/SynthetixState) + // const lastDebtLedgerEntryL2 = 10909822163955610660349890; + const lastDebtLedgerEntryL2 = _options.lastDebtLedgerEntryL2; + // @TODO update the comparison between OVM:ETH c-ratios at the time of snapshot + const normalisedL2CRatio = 500 / 400; + // @TODO update the L2 block number to use + // const L2BlockNumber = 4750287; + const L2BlockNumber = _options.L2BlockNumber; + + const scaledTotalL2Debt = totalL2Debt * normalisedL2CRatio; + + /* --------------- */ + + /* Using the subgraph, we get the relevant L1 calculations */ + + const l1Results = (await subgraphRequest( + defaultGraphs[1], + returnGraphParams(blockTag, _addresses) + )) as SNXHoldersResult; + + if (l1Results && l1Results.snxholders) { + for (let i = 0; i < l1Results.snxholders.length; i++) { + const holder = l1Results.snxholders[i]; + const vote = await debtL1( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntry, + false + ); + score[getAddress(holder.id)] = vote; + } + } + + /* Using the subgraph, we get the relevant L2 calculations */ + + const l2Results = (await subgraphRequest( + defaultGraphs[10], + returnGraphParams(L2BlockNumber, _addresses) + )) as SNXHoldersResult; + + if (l2Results && l2Results.snxholders) { + for (let i = 0; i < l2Results.snxholders.length; i++) { + const holder = l2Results.snxholders[i]; + + const vote = await debtL2( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntryL2, + false + ); + + if (score[getAddress(holder.id)]) { + score[getAddress(holder.id)] += vote; + } else { + score[getAddress(holder.id)] = vote; + } + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/README.md new file mode 100644 index 00000000..6010c985 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/README.md @@ -0,0 +1,15 @@ +# Quadratic Debt Percentage Strategy + +Calculates the quadratic weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["synthetix-quadratic"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/examples.json new file mode 100644 index 00000000..2b525a0a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "synthetix-quadratic", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD" + } + }, + "network": "1", + "addresses": [ + "0x78b037B39704e88a82DD23CFBE1f57f6AeF8EBC5", + "0x0bc3668d2AaFa53eD5E5134bA13ec74ea195D000", + "0xcAc59F91E4536Bc0E79aB816a5cD54e89f10433C", + "0x6dc88B231Cd04Dd1b1e525161162993F47140006", + "0x935D2fD458fdf41B6F7B62471f593797866a3Ce6", + "0x24e445fe7708Bf4bC2ae8d4df1694C98Af8BDE4F", + "0x49be88f0fcc3a8393a59d3688480d7d253c37d2a", + "0x27Cc4d6bc95b55a3a981BF1F1c7261CDa7bB0931" + ], + "snapshot": 13228907 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/index.ts new file mode 100644 index 00000000..a53d1727 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic/index.ts @@ -0,0 +1,184 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract } from '@ethersproject/contracts'; +import { Provider } from '@ethersproject/providers'; +import { + subgraphRequest + // ipfsGet +} from '../../utils'; +import { + DebtCacheABI, + debtL1, + debtL2, + returnGraphParams, + SNXHoldersResult, + SynthetixStateABI +} from '../synthetix/helper'; + +export const author = 'andytcf'; +export const version = '1.0.0'; + +const MED_PRECISE_UNIT = 1e18; + +// @TODO: check if most-up-to-date version (using https://contracts.synthetix.io/SynthetixState) +const SynthetixStateContractAddress = + '0x4b9Ca5607f1fF8019c1C6A3c2f0CC8de622D5B82'; +// @TODO: check if most-up-to-date version (using http://contracts.synthetix.io/DebtCache) +const DebtCacheContractAddress = '0xe92B4c7428152052B0930c81F4c687a5F1A12292'; + +const defaultGraphs = { + '1': 'https://api.thegraph.com/subgraphs/name/killerbyte/synthetix', + '10': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/optimism-issuance' +}; + +// @TODO: update with the latest ovm snapshot +// const ovmSnapshotJSON = 'QmNwvhq4By1Mownjycg7bWSXqbJWMVyAWRZ1K4mjxuvGXg'; + +const loadLastDebtLedgerEntry = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + SynthetixStateContractAddress, + SynthetixStateABI, + provider + ); + + const lastDebtLedgerEntry = await contract.lastDebtLedgerEntry({ + blockTag: snapshot + }); + + return BigNumber.from(lastDebtLedgerEntry); +}; + +const loadL1TotalDebt = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + DebtCacheContractAddress, + DebtCacheABI, + provider + ); + + const currentDebtObject = await contract.currentDebt({ + blockTag: snapshot + }); + + return Number(currentDebtObject.debt) / MED_PRECISE_UNIT; +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _, + snapshot +) { + const score = {}; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + /* Global Constants */ + + const totalL1Debt = await loadL1TotalDebt(_provider, snapshot); // (high-precision 1e18) + const lastDebtLedgerEntry = await loadLastDebtLedgerEntry( + _provider, + snapshot + ); + + /* EDIT THESE FOR OVM */ + + // @TODO update the currentDebt for the snapshot from (https://contracts.synthetix.io/ovm/DebtCache) + const totalL2Debt = 22617610; + // @TODO update the lastDebtLedgerEntry from (https://contracts.synthetix.io/ovm/SynthetixState) + const lastDebtLedgerEntryL2 = 20222730523217499684984991; + // @TODO update the comparison between OVM:ETH c-ratios at the time of snapshot + const normalisedL2CRatio = 600 / 450; + // @TODO update the L2 block number to use + const L2BlockNumber = 1770186; + + const scaledTotalL2Debt = totalL2Debt * normalisedL2CRatio; + + /* --------------- */ + + /* Using the subgraph, we get the relevant L1 calculations */ + + const l1Results = (await subgraphRequest( + defaultGraphs[1], + returnGraphParams(blockTag, _addresses) + )) as SNXHoldersResult; + + if (l1Results && l1Results.snxholders) { + for (let i = 0; i < l1Results.snxholders.length; i++) { + const holder = l1Results.snxholders[i]; + const weightedVoteL1 = await debtL1( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntry, + true + ); + score[getAddress(holder.id)] = weightedVoteL1; + } + } + + /* Using the subgraph, we get the relevant L2 calculations */ + + const l2Results = (await subgraphRequest( + defaultGraphs[10], + returnGraphParams(L2BlockNumber, _addresses) + )) as SNXHoldersResult; + + // @notice fallback for when subgraph is down + /* + const OVMSnapshot = await ipfsGet('gateway.pinata.cloud', ovmSnapshotJSON); + const array = Object.assign( + {}, + ...OVMSnapshot.data.snxholders.map((key) => ({ + [getAddress(key.id)]: { + initialDebtOwnership: key.initialDebtOwnership, + debtEntryAtIndex: key.debtEntryAtIndex + } + })) + ); + for (let k = 0; k < _addresses.length; k++) { + const address = _addresses[k]; + if (array[getAddress(address)]) { + score[getAddress(address)] += await quadraticWeightedVoteL2( + array[getAddress(address)].initialDebtOwnership, + array[getAddress(address)].debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntryL2 + ); + } else { + continue; + } + } + */ + + if (l2Results && l2Results.snxholders) { + for (let i = 0; i < l2Results.snxholders.length; i++) { + const holder = l2Results.snxholders[i]; + + const weightedVoteL2 = await debtL2( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntryL2, + true + ); + + if (score[getAddress(holder.id)]) { + score[getAddress(holder.id)] += weightedVoteL2; + } else { + score[getAddress(holder.id)] = weightedVoteL2; + } + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/README.md new file mode 100644 index 00000000..43207dbc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/README.md @@ -0,0 +1,15 @@ +# Quadratic Debt Percentage Strategy + +Calculates the quadratic weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["synthetix-quadratic_1"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/examples.json new file mode 100644 index 00000000..f6ef6584 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "synthetix-quadratic_1", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD", + "totalL2Debt": 48646913, + "lastDebtLedgerEntryL2": 9773647546760863848975891, + "L2BlockNumber": 919219 + } + }, + "network": "1", + "addresses": [ + "0xcAc59F91E4536Bc0E79aB816a5cD54e89f10433C", + "0x935D2fD458fdf41B6F7B62471f593797866a3Ce6" + ], + "snapshot": 13764062 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/index.ts new file mode 100644 index 00000000..4806cabc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_1/index.ts @@ -0,0 +1,152 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract } from '@ethersproject/contracts'; +import { Provider } from '@ethersproject/providers'; +import { subgraphRequest } from '../../utils'; +import { + DebtCacheABI, + debtL1, + debtL2, + returnGraphParams, + SNXHoldersResult, + SynthetixStateABI +} from '../synthetix/helper'; + +export const author = 'andytcf'; +export const version = '1.0.0'; + +const MED_PRECISE_UNIT = 1e18; + +// @TODO: check if most-up-to-date version (using https://contracts.synthetix.io/SynthetixState) +const SynthetixStateContractAddress = + '0x4b9Ca5607f1fF8019c1C6A3c2f0CC8de622D5B82'; +// @TODO: check if most-up-to-date version (using http://contracts.synthetix.io/DebtCache) +const DebtCacheContractAddress = '0x9D5551Cd3425Dd4585c3E7Eb7E4B98902222521E'; + +const defaultGraphs = { + '1': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/synthetix', + '10': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/optimism-main' +}; + +const loadLastDebtLedgerEntry = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + SynthetixStateContractAddress, + SynthetixStateABI, + provider + ); + + const lastDebtLedgerEntry = await contract.lastDebtLedgerEntry({ + blockTag: snapshot + }); + + return BigNumber.from(lastDebtLedgerEntry); +}; + +const loadL1TotalDebt = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + DebtCacheContractAddress, + DebtCacheABI, + provider + ); + + const currentDebtObject = await contract.currentDebt({ + blockTag: snapshot + }); + + return Number(currentDebtObject.debt) / MED_PRECISE_UNIT; +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _options, + snapshot +) { + const score = {}; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + /* Global Constants */ + + const totalL1Debt = await loadL1TotalDebt(_provider, snapshot); // (high-precision 1e18) + const lastDebtLedgerEntry = await loadLastDebtLedgerEntry( + _provider, + snapshot + ); + + /* EDIT THESE FOR OVM */ + + // @TODO update the currentDebt for the snapshot from (https://contracts.synthetix.io/ovm/DebtCache) + // const totalL2Debt = 48646913; + const totalL2Debt = _options.totalL2Debt; + // @TODO update the lastDebtLedgerEntry from (https://contracts.synthetix.io/ovm/SynthetixState) + // const lastDebtLedgerEntryL2 = 9773647546760863848975891; + const lastDebtLedgerEntryL2 = _options.lastDebtLedgerEntryL2; + // @TODO update the comparison between OVM:ETH c-ratios at the time of snapshot + const normalisedL2CRatio = 500 / 400; + // @TODO update the L2 block number to use + const L2BlockNumber = _options.L2BlockNumber; + + const scaledTotalL2Debt = totalL2Debt * normalisedL2CRatio; + + /* --------------- */ + + /* Using the subgraph, we get the relevant L1 calculations */ + + const l1Results = (await subgraphRequest( + defaultGraphs[1], + returnGraphParams(blockTag, _addresses) + )) as SNXHoldersResult; + + if (l1Results && l1Results.snxholders) { + for (let i = 0; i < l1Results.snxholders.length; i++) { + const holder = l1Results.snxholders[i]; + const vote = await debtL1( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntry, + true + ); + score[getAddress(holder.id)] = vote; + } + } + + /* Using the subgraph, we get the relevant L2 calculations */ + + const l2Results = (await subgraphRequest( + defaultGraphs[10], + returnGraphParams(L2BlockNumber, _addresses) + )) as SNXHoldersResult; + + if (l2Results && l2Results.snxholders) { + for (let i = 0; i < l2Results.snxholders.length; i++) { + const holder = l2Results.snxholders[i]; + + const vote = await debtL2( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntryL2, + true + ); + + if (score[getAddress(holder.id)]) { + score[getAddress(holder.id)] += vote; + } else { + score[getAddress(holder.id)] = vote; + } + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/README.md new file mode 100644 index 00000000..d80b9dd4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/README.md @@ -0,0 +1,15 @@ +# Quadratic Debt Percentage Strategy + +Calculates the quadratic weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["synthetix-quadratic_2"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/examples.json new file mode 100644 index 00000000..ea890a6c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "synthetix-quadratic_2", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD", + "totalL2Debt": 40201854, + "lastDebtLedgerEntryL2": 10909822163955610660349890, + "L2BlockNumber": 4750287 + } + }, + "network": "1", + "addresses": [ + "0x78b037B39704e88a82DD23CFBE1f57f6AeF8EBC5", + "0x0bc3668d2AaFa53eD5E5134bA13ec74ea195D000", + "0xcAc59F91E4536Bc0E79aB816a5cD54e89f10433C", + "0x6dc88B231Cd04Dd1b1e525161162993F47140006", + "0x935D2fD458fdf41B6F7B62471f593797866a3Ce6", + "0x24e445fe7708Bf4bC2ae8d4df1694C98Af8BDE4F", + "0x49be88f0fcc3a8393a59d3688480d7d253c37d2a", + "0x27Cc4d6bc95b55a3a981BF1F1c7261CDa7bB0931" + ], + "snapshot": 14443440 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/index.ts new file mode 100644 index 00000000..8f030c1a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix-quadratic_2/index.ts @@ -0,0 +1,151 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract } from '@ethersproject/contracts'; +import { Provider } from '@ethersproject/providers'; +import { subgraphRequest } from '../../utils'; +import { + DebtCacheABI, + debtL1, + debtL2, + returnGraphParams, + SNXHoldersResult, + SynthetixStateABI +} from '../synthetix/helper'; + +export const author = 'andytcf'; +export const version = '1.0.0'; + +const MED_PRECISE_UNIT = 1e18; + +// @TODO: check if most-up-to-date version (using https://contracts.synthetix.io/SynthetixState) +const SynthetixStateContractAddress = + '0x4b9Ca5607f1fF8019c1C6A3c2f0CC8de622D5B82'; +// @TODO: check if most-up-to-date version (using http://contracts.synthetix.io/DebtCache) +const DebtCacheContractAddress = '0x1620Aa736939597891C1940CF0d28b82566F9390'; + +const defaultGraphs = { + '1': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/synthetix', + '10': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/optimism-main' +}; + +const loadLastDebtLedgerEntry = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + SynthetixStateContractAddress, + SynthetixStateABI, + provider + ); + + const lastDebtLedgerEntry = await contract.lastDebtLedgerEntry({ + blockTag: snapshot + }); + + return BigNumber.from(lastDebtLedgerEntry); +}; + +const loadL1TotalDebt = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + DebtCacheContractAddress, + DebtCacheABI, + provider + ); + + const currentDebtObject = await contract.currentDebt({ + blockTag: snapshot + }); + + return Number(currentDebtObject.debt) / MED_PRECISE_UNIT; +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _options, + snapshot +) { + const score = {}; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + /* Global Constants */ + + const totalL1Debt = await loadL1TotalDebt(_provider, snapshot); // (high-precision 1e18) + const lastDebtLedgerEntry = await loadLastDebtLedgerEntry( + _provider, + snapshot + ); + + // @TODO update the currentDebt for the snapshot from (https://contracts.synthetix.io/ovm/DebtCache) + // const totalL2Debt = 40201854; + const totalL2Debt = _options.totalL2Debt; + // @TODO update the lastDebtLedgerEntry from (https://contracts.synthetix.io/ovm/SynthetixState) + // const lastDebtLedgerEntryL2 = 10909822163955610660349890; + const lastDebtLedgerEntryL2 = _options.lastDebtLedgerEntryL2; + // @TODO update the comparison between OVM:ETH c-ratios at the time of snapshot + const normalisedL2CRatio = 500 / 400; + // @TODO update the L2 block number to use + // const L2BlockNumber = 4750287; + const L2BlockNumber = _options.L2BlockNumber; + + const scaledTotalL2Debt = totalL2Debt * normalisedL2CRatio; + + /* --------------- */ + + /* Using the subgraph, we get the relevant L1 calculations */ + + const l1Results = (await subgraphRequest( + defaultGraphs[1], + returnGraphParams(blockTag, _addresses) + )) as SNXHoldersResult; + + if (l1Results && l1Results.snxholders) { + for (let i = 0; i < l1Results.snxholders.length; i++) { + const holder = l1Results.snxholders[i]; + const vote = await debtL1( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntry, + true + ); + score[getAddress(holder.id)] = vote; + } + } + + /* Using the subgraph, we get the relevant L2 calculations */ + + const l2Results = (await subgraphRequest( + defaultGraphs[10], + returnGraphParams(L2BlockNumber, _addresses) + )) as SNXHoldersResult; + + if (l2Results && l2Results.snxholders) { + for (let i = 0; i < l2Results.snxholders.length; i++) { + const holder = l2Results.snxholders[i]; + + const vote = await debtL2( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntryL2, + true + ); + + if (score[getAddress(holder.id)]) { + score[getAddress(holder.id)] += vote; + } else { + score[getAddress(holder.id)] = vote; + } + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetix/README.md new file mode 100644 index 00000000..f9b3eef3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix/README.md @@ -0,0 +1,15 @@ +# Quadratic Debt Percentage Strategy + +Calculates the quadratic weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["synthetix"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetix/examples.json new file mode 100644 index 00000000..2e706598 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "synthetix", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD" + } + }, + "network": "1", + "addresses": [ + "0x78b037B39704e88a82DD23CFBE1f57f6AeF8EBC5", + "0x0bc3668d2AaFa53eD5E5134bA13ec74ea195D000", + "0xcAc59F91E4536Bc0E79aB816a5cD54e89f10433C", + "0x6dc88B231Cd04Dd1b1e525161162993F47140006", + "0x935D2fD458fdf41B6F7B62471f593797866a3Ce6", + "0x24e445fe7708Bf4bC2ae8d4df1694C98Af8BDE4F", + "0x49be88f0fcc3a8393a59d3688480d7d253c37d2a", + "0x27Cc4d6bc95b55a3a981BF1F1c7261CDa7bB0931" + ], + "snapshot": 12643795 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix/helper.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix/helper.ts new file mode 100644 index 00000000..936e894e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix/helper.ts @@ -0,0 +1,99 @@ +import { BigNumber } from '@ethersproject/bignumber'; + +const HIGH_PRECISE_UNIT = 1e27; +const MED_PRECISE_UNIT = 1e18; +const SCALING_FACTOR = 1e5; + +export const DebtCacheABI = [ + 'function currentDebt() view returns (uint256 debt, bool anyRateIsInvalid)' +]; + +export const SynthetixStateABI = [ + 'function lastDebtLedgerEntry() view returns (uint256)' +]; + +export type SNXHoldersResult = { + snxholders: { + id: string; + initialDebtOwnership: BigNumber; + debtEntryAtIndex: BigNumber; + }[]; +}; + +export function returnGraphParams( + snapshot: number | string, + addresses: string[] +) { + return { + snxholders: { + __args: { + where: { + id_in: addresses.map((address: string) => address.toLowerCase()) + }, + first: 1000, + block: { + number: snapshot + } + }, + id: true, + initialDebtOwnership: true, + debtEntryAtIndex: true + } + }; +} + +export const debtL1 = async ( + initialDebtOwnership: BigNumber, + debtEntryAtIndex: BigNumber, + totalL1Debt: number, + scaledTotalL2Debt: number, + lastDebtLedgerEntry: BigNumber, + isQuadratic: boolean +) => { + const currentDebtOwnershipPercent = + (Number(lastDebtLedgerEntry) / Number(debtEntryAtIndex)) * + Number(initialDebtOwnership); + + const highPrecisionBalance = + totalL1Debt * + MED_PRECISE_UNIT * + (currentDebtOwnershipPercent / HIGH_PRECISE_UNIT); + + const currentDebtBalance = highPrecisionBalance / MED_PRECISE_UNIT; + + const totalDebtInSystem = totalL1Debt + scaledTotalL2Debt; + + const ownershipPercentOfTotalDebt = currentDebtBalance / totalDebtInSystem; + + const scaledWeighting = ownershipPercentOfTotalDebt * SCALING_FACTOR; + + return isQuadratic ? Math.sqrt(scaledWeighting) : scaledWeighting; +}; + +export const debtL2 = async ( + initialDebtOwnership: BigNumber, + debtEntryAtIndex: BigNumber, + totalL1Debt: number, + scaledTotalL2Debt: number, + lastDebtLedgerEntryL2: number, + isQuadratic: boolean +) => { + const currentDebtOwnershipPercent = + (Number(lastDebtLedgerEntryL2) / Number(debtEntryAtIndex)) * + Number(initialDebtOwnership); + + const highPrecisionBalance = + totalL1Debt * + MED_PRECISE_UNIT * + (currentDebtOwnershipPercent / HIGH_PRECISE_UNIT); + + const currentDebtBalance = highPrecisionBalance / MED_PRECISE_UNIT; + + const totalDebtInSystem = totalL1Debt + scaledTotalL2Debt; + + const ownershipPercentOfTotalDebt = currentDebtBalance / totalDebtInSystem; + + const scaledWeighting = ownershipPercentOfTotalDebt * SCALING_FACTOR; + + return isQuadratic ? Math.sqrt(scaledWeighting) : scaledWeighting; +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix/index.ts new file mode 100644 index 00000000..c05ec10a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix/index.ts @@ -0,0 +1,187 @@ +import { getAddress } from '@ethersproject/address'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Contract } from '@ethersproject/contracts'; +import { Provider } from '@ethersproject/providers'; +import { subgraphRequest, ipfsGet } from '../../utils'; +import { DebtCacheABI, SNXHoldersResult, SynthetixStateABI } from './helper'; + +export const author = 'andytcf'; +export const version = '1.0.0'; + +const SynthetixStateContractAddress = + '0x4b9Ca5607f1fF8019c1C6A3c2f0CC8de622D5B82'; +const DebtCacheContractAddress = '0x9bB05EF2cA7DBAafFC3da1939D1492e6b00F39b8'; + +const defaultGraphs = { + '1': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/synthetix', + '10': 'https://api.thegraph.com/subgraphs/name/synthetixio-team/optimism-general' +}; + +const ovmSnapshotJSON = 'QmNwvhq4By1Mownjycg7bWSXqbJWMVyAWRZ1K4mjxuvGXg'; + +const HIGH_PRECISE_UNIT = 1e27; +const MED_PRECISE_UNIT = 1e18; +const SCALING_FACTOR = 1e5; + +function returnGraphParams(snapshot: number | string, addresses: string[]) { + return { + snxholders: { + __args: { + where: { + id_in: addresses.map((address: string) => address.toLowerCase()) + }, + first: 1000, + block: { + number: snapshot + } + }, + id: true, + initialDebtOwnership: true, + debtEntryAtIndex: true + } + }; +} + +const loadLastDebtLedgerEntry = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + SynthetixStateContractAddress, + SynthetixStateABI, + provider + ); + const lastDebtLedgerEntry = await contract.lastDebtLedgerEntry({ + blockTag: snapshot + }); + + return BigNumber.from(lastDebtLedgerEntry); +}; + +const loadL1TotalDebt = async ( + provider: Provider, + snapshot: number | string +) => { + const contract = new Contract( + DebtCacheContractAddress, + DebtCacheABI, + provider + ); + + const currentDebtObject = await contract.currentDebt({ + blockTag: snapshot + }); + + return Number(currentDebtObject.debt) / MED_PRECISE_UNIT; +}; + +const quadraticWeightedVoteL1 = async ( + initialDebtOwnership: BigNumber, + debtEntryAtIndex: BigNumber, + totalL1Debt: number, + scaledTotalL2Debt: number, + lastDebtLedgerEntry: BigNumber +) => { + const currentDebtOwnershipPercent = + (Number(lastDebtLedgerEntry) / Number(debtEntryAtIndex)) * + Number(initialDebtOwnership); + + const highPrecisionBalance = + totalL1Debt * + MED_PRECISE_UNIT * + (currentDebtOwnershipPercent / HIGH_PRECISE_UNIT); + + const currentDebtBalance = highPrecisionBalance / MED_PRECISE_UNIT; + + const totalDebtInSystem = totalL1Debt + scaledTotalL2Debt; + + const ownershipPercentOfTotalDebt = currentDebtBalance / totalDebtInSystem; + + const scaledWeighting = ownershipPercentOfTotalDebt * SCALING_FACTOR; + + return Math.sqrt(scaledWeighting); +}; + +const quadraticWeightedVoteL2 = async ( + initialDebtOwnership: BigNumber, + totalL1Debt: number, + scaledTotalL2Debt: number, + normalisedL2CRatio: number +) => { + const totalDebtInSystem = totalL1Debt + scaledTotalL2Debt; + + const ownershipPercentBN = Number(initialDebtOwnership) * normalisedL2CRatio; + const ownershipPercent = ownershipPercentBN / HIGH_PRECISE_UNIT; + const ownershipOfDebtDollarValue = ownershipPercent * scaledTotalL2Debt; + const ownershipPercentOfTotalDebt = + ownershipOfDebtDollarValue / totalDebtInSystem; + + const scaledWeighting = ownershipPercentOfTotalDebt * SCALING_FACTOR; + return Math.sqrt(scaledWeighting); +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _, + snapshot +) { + const score = {}; + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const l1Results = (await subgraphRequest( + defaultGraphs[1], + returnGraphParams(blockTag, _addresses) + )) as SNXHoldersResult; + + const normalisedL2CRatio = 1000 / 450; + + const totalL1Debt = await loadL1TotalDebt(_provider, snapshot); // (high-precision 1e18) + const lastDebtLedgerEntry = await loadLastDebtLedgerEntry( + _provider, + snapshot + ); + const totalL2Debt = 4792266; // $4,792,266 (high-precision 1e18) + const scaledTotalL2Debt = totalL2Debt * normalisedL2CRatio; + + if (l1Results && l1Results.snxholders) { + for (let i = 0; i < l1Results.snxholders.length; i++) { + const holder = l1Results.snxholders[i]; + score[getAddress(holder.id)] = await quadraticWeightedVoteL1( + holder.initialDebtOwnership, + holder.debtEntryAtIndex, + totalL1Debt, + scaledTotalL2Debt, + lastDebtLedgerEntry + ); + } + } + + const OVMSnapshot = await ipfsGet('gateway.pinata.cloud', ovmSnapshotJSON); + + const array = Object.assign( + {}, + ...OVMSnapshot.data.snxholders.map((key) => ({ + [getAddress(key.id)]: key.initialDebtOwnership + })) + ); + + for (let k = 0; k < _addresses.length; k++) { + const address = _addresses[k]; + if (array[getAddress(address)]) { + score[getAddress(address)] += await quadraticWeightedVoteL2( + array[getAddress(address)], + totalL1Debt, + scaledTotalL2Debt, + normalisedL2CRatio + ); + } else { + continue; + } + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/README.md b/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/README.md new file mode 100644 index 00000000..d80b9dd4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/README.md @@ -0,0 +1,15 @@ +# Quadratic Debt Percentage Strategy + +Calculates the quadratic weighting of voters, based on their debt percentage in the previous fee period. + +## Examples + +Can be used instead of the erc20-balance-of strategy, the space config will look like this: + +```JSON +{ + "strategies": [ + ["synthetix-quadratic_2"] + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/examples.json b/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/examples.json new file mode 100644 index 00000000..311f6e23 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "synthetix_1", + "params": { + "address": "0x023c66b7e13d30a3c46aa433fd2829763d5817c5", + "symbol": "WD", + "L2BlockNumber": 4497555, + "L1SDS": "0x89FCb32F29e509cc42d0C8b6f058C993013A843F", + "L2SDS": "0x45c55BF488D3Cb8640f12F63CbeDC027E8261E79", + "L1DebtCache": "0x9D5551Cd3425Dd4585c3E7Eb7E4B98902222521E", + "L2DebtCache": "0x01f8C5e421172B67cc14B7f5F369cfb10de0acD4", + "quadratic": false + } + }, + "network": "1", + "addresses": [ + "0x99F4176EE457afedFfCB1839c7aB7A030a5e4A92", + "0x8cA24021E3Ee3B5c241BBfcee0712554D7Dc38a1" + ], + "snapshot": 14396861 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/index.ts b/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/index.ts new file mode 100644 index 00000000..6c2590f2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/synthetix_1/index.ts @@ -0,0 +1,123 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatEther } from '@ethersproject/units'; +import { Contract } from '@ethersproject/contracts'; +import { Provider } from '@ethersproject/providers'; +import { getProvider, Multicaller } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'andytcf'; +export const version = '1.0.0'; + +const SDSABI = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() view returns (uint256)' +]; + +const DebtCacheABI = [ + 'function currentDebt() view returns (uint256 debt, bool anyRateIsInvalid)' +]; + +const calculateSDSValue = async ( + provider: Provider, + blockTag: number | string, + debtCacheAddress: string, + sdsAddress: string +) => { + const DebtCacheContract = new Contract( + debtCacheAddress, + DebtCacheABI, + provider + ); + + const currentDebt = await DebtCacheContract.currentDebt({ + blockTag + }); + + const SDSContract = new Contract(sdsAddress, SDSABI, provider); + + const totalSupply = await SDSContract.totalSupply({ + blockTag + }); + + const value = Number(currentDebt.debt / totalSupply); + + return value; +}; + +export async function strategy( + _space, + _network, + _provider, + _addresses, + _options, + _snapshot +) { + const score: Record = {}; + + const blockTag = typeof _snapshot === 'number' ? _snapshot : 'latest'; + const L2BlockTag = + typeof _options.L2BlockNumber === 'number' + ? _options.L2BlockNumber + : 'latest'; + + const optimismProvider = getProvider('10'); + + const L1SDSValue = await calculateSDSValue( + _provider, + _snapshot, + _options.L1DebtCache, + _options.L1SDS + ); + + const L2SDSValue = await calculateSDSValue( + optimismProvider, + L2BlockTag, + _options.L2DebtCache, + _options.L2SDS + ); + + const callL1SDSBalance = new Multicaller(_network, _provider, SDSABI, { + blockTag + }); + for (const walletAddress of _addresses) { + callL1SDSBalance.call(walletAddress, _options.L1SDS, 'balanceOf', [ + walletAddress + ]); + } + + const L1SDSBalances: Record = + await callL1SDSBalance.execute(); + + Object.entries(L1SDSBalances).forEach(([address, balance]) => { + score[getAddress(address)] = Number(formatEther(balance)) * L1SDSValue; + }); + + const callL2SDSBalance = new Multicaller('10', optimismProvider, SDSABI, { + blockTag: L2BlockTag + }); + + for (const walletAddress of _addresses) { + callL2SDSBalance.call(walletAddress, _options.L2SDS, 'balanceOf', [ + walletAddress + ]); + } + + const L2SDSBalances: Record = + await callL2SDSBalance.execute(); + + Object.entries(L2SDSBalances).forEach(([address, balance]) => { + score[getAddress(address)] += Number(formatEther(balance)) * L2SDSValue; + }); + + /** Quadratic Weighting */ + if (_options.quadratic) { + return Object.fromEntries( + Object.entries(score).map(([address, balance]) => [ + address, + Math.sqrt(balance) + ]) + ); + } else { + return score; + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/README.md b/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/README.md new file mode 100644 index 00000000..f9391397 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/README.md @@ -0,0 +1,13 @@ +# tech-quadratic-ranked-choice + +This strategy was create to allow TECH combine ranked choice voting with quadratic strategy. This strategy will apply squared root to the balance of the voters, making the power of decision spread to the edges. + +Here is an example of parameters: + +```json +{ + "address": "0x799844141C2627bD195c89c3A0c71341d0314c55", + "symbol": "TECH", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/examples.json new file mode 100644 index 00000000..5893f9cc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "tech-quadratic-ranked-choice", + "params": { + "address": "0x799844141C2627bD195c89c3A0c71341d0314c55", + "symbol": "TECH", + "decimals": 18 + } + }, + "network": "100", + "addresses": [ + "0xB22981bA3FE1De2325935c91a3B717168fB86714", + "0xaa79B87DC8B046A5E4f7D03F1562D7fe5BF98737", + "0x66dF5A7715C5D4AfbBFA52435c66aa20733be0d1", + "0x7d547666209755FB833f9B37EebEa38eBF513Abb", + "0x1d921dff757610FbDB0073479e12c0a07d382677", + "0x826976d7C600d45FB8287CA1d7c76FC8eb732030", + "0x839395e20bbB182fa440d08F850E6c7A8f6F0780", + "0xC46c67Bb7E84490D7EbdD0b8ecDaca68Cf3823F4", + "0xF8D1d34956cEa24718cf8687588D6FeDbc6d9AA6" + ], + "snapshot": 18983705 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/index.ts new file mode 100644 index 00000000..c7d704d7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tech-quadratic-ranked-choice/index.ts @@ -0,0 +1,48 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +const ONE = BigNumber.from(1); +const TWO = BigNumber.from(2); + +// For further explanation take a look on @ricmoo explanation here https://github.com/ethers-io/ethers.js/issues/1182 +function sqrt(value: BigNumber) { + let z = value.add(ONE).div(TWO); + let y = value; + while (z.sub(y).isNegative()) { + y = z; + z = value.div(z).add(z).div(TWO); + } + return BigNumber.from(y); +} + +export const author = 'pkretzschmar'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +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) => + multi.call(address, options.address, 'balanceOf', [address]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(sqrt(balance), options.decimals / 2)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/thales/README.md b/Implementations/API/backend/utils/snapshot/strategies/thales/README.md new file mode 100644 index 00000000..28ba495c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/thales/README.md @@ -0,0 +1,15 @@ +# Thales Strategy + +A strategy for Thales governance. +The voting score is based on the THALES staking amount. + +## Examples + +Here is an example of parameters: + +```JSON +{ + "symbol": "THALES", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/thales/examples.json b/Implementations/API/backend/utils/snapshot/strategies/thales/examples.json new file mode 100644 index 00000000..5e0375ba --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/thales/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "thales", + "params": { + "symbol": "THALES", + "decimals": 18, + "blockOptimism": 88627878, + "blockArbitrum": 78979794 + } + }, + "network": "10", + "addresses": [ + "0x9f8e4ee788D9b00A3409584E18034aA7B736C396", + "0x0bc3668d2AaFa53eD5E5134bA13ec74ea195D000", + "0xcAc59F91E4536Bc0E79aB816a5cD54e89f10433C", + "0x6dc88B231Cd04Dd1b1e525161162993F47140006", + "0x935D2fD458fdf41B6F7B62471f593797866a3Ce6", + "0x24e445fe7708Bf4bC2ae8d4df1694C98Af8BDE4F", + "0x49be88f0fcc3a8393a59d3688480d7d253c37d2a", + "0x27Cc4d6bc95b55a3a981BF1F1c7261CDa7bB0931" + ], + "snapshot": 5003555 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/thales/index.ts b/Implementations/API/backend/utils/snapshot/strategies/thales/index.ts new file mode 100644 index 00000000..cbb2bcc1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/thales/index.ts @@ -0,0 +1,90 @@ +import { getAddress } from '@ethersproject/address'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; + +export const author = 'vpoklopic'; +export const version = '1.0.3'; + +const THALES_SUBGRAPH_URL = { + optimism: + 'https://api.thegraph.com/subgraphs/name/thales-markets/thales-token', + arbitrum: + 'https://api.thegraph.com/subgraphs/name/thales-markets/thales-token-arbitrum' +}; + +function returnGraphParams(addresses: string[]) { + return { + stakers: { + __args: { + first: 1000, + orderBy: 'totalStakedAmount', + orderDirection: 'desc', + where: { + totalStakedAmount_gt: 0, + id_in: addresses.map((addr: string) => addr.toLowerCase()) + } + }, + id: true, + timestamp: true, + totalStakedAmount: true + } + }; +} + +export async function strategy( + _space, + _network, + _provider, + addresses, + options +) { + const optimismGraphParams = returnGraphParams(addresses); + if (options.blockOptimism !== undefined) { + // @ts-ignore + optimismGraphParams.stakers.__args.block = { + number: options.blockOptimism + }; + } + + const arbitrumGraphParams = returnGraphParams(addresses); + if (options.blockArbitrum !== undefined) { + // @ts-ignore + arbitrumGraphParams.stakers.__args.block = { + number: options.blockArbitrum + }; + } + + const score = {}; + + const [optimismStakers, arbitrumStakers] = await Promise.all([ + subgraphRequest(THALES_SUBGRAPH_URL.optimism, optimismGraphParams), + subgraphRequest(THALES_SUBGRAPH_URL.arbitrum, arbitrumGraphParams) + ]); + + // We are starting by mapping all Optimism stakers + if (optimismStakers && optimismStakers.stakers) { + optimismStakers.stakers.forEach((staker) => { + score[getAddress(staker.id)] = parseFloat( + formatUnits(staker.totalStakedAmount, options.decimals) + ); + }); + } + + // If the Optimism staker is also staker on Arbitrum, add an amount + // Otherwise, just set Arbitrum staked amount as a score + if (arbitrumStakers && arbitrumStakers.stakers) { + arbitrumStakers.stakers.forEach((staker) => { + const key = getAddress(staker.id); + const stakedAmount = parseFloat( + formatUnits(staker.totalStakedAmount, options.decimals) + ); + if (!!score[key]) { + score[key] += stakedAmount; + } else { + score[key] = stakedAmount; + } + }); + } + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/README.md new file mode 100644 index 00000000..9db8f18d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/README.md @@ -0,0 +1,11 @@ +# The Graph Balance Strategy + +This strategy uses the amount of GRT an address holds to calculate its score. + +## Parameters + +```json +{ + "symbol": "B-GRT" +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/balances.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/balances.ts new file mode 100644 index 00000000..472f0ab0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/balances.ts @@ -0,0 +1,188 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Provider } from '@ethersproject/providers'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; +import { + GRAPH_NETWORK_SUBGRAPH_URL, + GraphAccountScores, + GraphStrategyOptions +} from '../the-graph/graphUtils'; + +const GNS_ADDRESS = '0xadca0dd4729c8ba3acf3e99f3a9f471ef37b6825'; + +export type Curator = { + id: string; + signals: Signal[]; + nameSignals: NameSignal[]; +}; + +export type Signal = { + signal: string; + subgraphDeployment: SubgraphDeployment; +}; + +export type NameSignal = { + signal: string; + nameSignal: string; + subgraph: Subgraph; +}; + +export type SubgraphDeployment = { + id: string; + signalledTokens: string; + signalAmount: string; +}; + +export type Subgraph = { + id: string; + withdrawableTokens: string; + currentSignalledTokens: string; + signalAmount: string; + nameSignalAmount: string; + currentVersion: { + subgraphDeployment: SubgraphDeployment; + }; +}; + +export const ETH_IN_WEI = 1000000000000000000; + +export function getCuratorSignalledGRT(curator: Curator): number { + const { signals, nameSignals } = curator; + let result = 0; + // Get GRT of Curator for the signal they have in each Deployment + for (const signal of signals) { + const deployment = signal.subgraphDeployment; + const signalledTokens = parseFloat(deployment.signalledTokens); + const parsedSignal = parseFloat(signal.signal); + const signalAmount = parseFloat(deployment.signalAmount); + // we calculate the value of their signal based on the share they have + // of the total signal in the subgraph deployment + const deploymentSignalShareCoeficient = signalAmount + ? signalledTokens / signalAmount + : 0; + result += deploymentSignalShareCoeficient * parsedSignal; + } + // Get GRT of Curator for the name signal they have in each Deployment + for (const nameSignal of nameSignals) { + if (nameSignal.subgraph.withdrawableTokens === '0') { + const deployment = nameSignal.subgraph.currentVersion.subgraphDeployment; + const signalledTokens = parseFloat(deployment.signalledTokens); + const signal = parseFloat(nameSignal.signal); + const signalAmount = parseFloat(deployment.signalAmount); + const deploymentSignalShareCoeficient = signalAmount + ? signalledTokens / signalAmount + : 0; + + result += deploymentSignalShareCoeficient * signal; + } else { + // edge case where curators didn't withdraw their signal from a deprecated subgraph + const subgraph = nameSignal.subgraph; + const withdrawableTokens = parseFloat(subgraph.withdrawableTokens); + const _nameSignal = parseFloat(nameSignal.nameSignal); + const nameSignalAmount = parseFloat(subgraph.nameSignalAmount); + const subgraphNameSignalShareCoeficient = nameSignalAmount + ? withdrawableTokens / nameSignalAmount + : 0; + + result += subgraphNameSignalShareCoeficient * _nameSignal; + } + } + // result is in wei, format it in GRT + return result / ETH_IN_WEI; +} + +export async function balanceStrategy( + _space: string, + network: string, + _provider: Provider, + addresses: string[], + _options: GraphStrategyOptions, + snapshot: string | number +): Promise { + const balanceParams = { + graphAccounts: { + __args: { + where: { + id_in: addresses + }, + first: _options.pageSize + }, + id: true, + balance: true + }, + curators: { + __args: { + where: { id_in: addresses, id_not: GNS_ADDRESS }, + first: _options.pageSize + }, + id: true, + signals: { + signal: true, + subgraphDeployment: { + id: true, + signalledTokens: true, + signalAmount: true + } + }, + nameSignals: { + signal: true, + nameSignal: true, + subgraph: { + id: true, + withdrawableTokens: true, + currentSignalledTokens: true, + signalAmount: true, + nameSignalAmount: true, + currentVersion: { + subgraphDeployment: { + signalledTokens: true, + signalAmount: true + } + } + } + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + balanceParams.graphAccounts.__args.block = { number: snapshot }; + // @ts-ignore + balanceParams.curators.__args.block = { number: snapshot }; + } + + const result = await subgraphRequest( + GRAPH_NETWORK_SUBGRAPH_URL[network], + balanceParams + ); + + // No normalization factor for balances. 1 GRT in wallet is the baseline to compare + // Delegators and Indexers to. + const score: GraphAccountScores = {}; + if (result && result.graphAccounts) { + // Must iterate on addresses since the query can return nothing for a beneficiary that has + // only interacted through token lock wallets + addresses.forEach((address) => { + let balanceScore = 0; + const graphAccount = result.graphAccounts.find( + (account) => account.id === address + ); + if (graphAccount) { + balanceScore = parseFloat( + formatUnits(BigNumber.from(graphAccount.balance)) + ); + const curator = result.curators.find( + (_curator) => _curator.id === address + ); + if (curator) { + // For curators we also consider the tokens they have locked in active signal + // as part of their balance + balanceScore += getCuratorSignalledGRT(curator); + } + } + score[address] = balanceScore; + }); + } else { + console.error('Subgraph request failed'); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/examples.json new file mode 100644 index 00000000..acb17b5b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/examples.json @@ -0,0 +1,102 @@ +[ + { + "name": "Graph Tokens Mainnet with Curators", + "strategy": { + "name": "the-graph-balance", + "params": { + "symbol": "B-GRT", + "expectedResults": { + "tokenLockWallets": [ + { + "beneficiary": "0x00139f52e71c80bc71b3fbd2bdb908b56beef1e1", + "id": "0x9c90a2b1c019ef38e52d979915fd5aa55777d217" + } + ], + "scores": { + "0x4fc20ad3bb384764fdc7d588ee3b4dc5eabefce7": 628798.551787525, + "0x4a905ce2d555752f0d906ffdf628b26ad64a741a": 118259.26666409046, + "0xe29b6ad7ecb8bbc798c7144dd0e4fdee11917ca8": 13622.825426152955, + + "0x00139f52e71c80bc71b3fbd2bdb908b56beef1e1": 30487.010146180415, + "0x9c90a2b1c019ef38e52d979915fd5aa55777d217": 93750 + }, + "combinedScores": { + "0x4fc20ad3bb384764fdc7d588ee3b4dc5eabefce7": 628798.551787525, + "0x4a905ce2d555752f0d906ffdf628b26ad64a741a": 118259.26666409046, + "0xe29b6ad7ecb8bbc798c7144dd0e4fdee11917ca8": 13622.825426152955, + + "0x00139f52e71c80bc71b3fbd2bdb908b56beef1e1": 124237.01014618042 + }, + "normalizationFactor": 1, + "explanation": [ + "The first 3 addresses are Curators with no balance and no Token-lock wallets associated to their account.", + "Their score is expected to be the sum of all signal GRT they have in active subgraph deployments.", + "The last address (0x00139f52e71c80bc71b3fbd2bdb908b56beef1e1) is a Curator, has balance and is a beneficiary of ", + "a Tocken-lock wallet. Its score is expected to be a combination of its active signal GRT, its balance and the balance", + "of its TLW (0x9c90a2b1c019ef38e52d979915fd5aa55777d217)." + ] + } + } + }, + "network": "1", + "snapshot": 13507803, + "addresses": [ + "0x4fc20ad3bb384764fdc7d588ee3b4dc5eabefce7", + "0x4a905ce2d555752f0d906ffdf628b26ad64a741a", + "0xe29b6ad7ecb8bbc798c7144dd0e4fdee11917ca8", + "0x00139f52e71c80bc71b3fbd2bdb908b56beef1e1" + ] + }, + { + "name": "Graph Tokens Rinkeby", + "strategy": { + "name": "the-graph-balance", + "params": { + "symbol": "B-GRT", + "expectedResults": { + "tokenLockWallets": [ + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x4236878343d286a2943453e045da46e86d12138d" + }, + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x460c51e3f7609b47c0f9b3a655be804286c04666" + }, + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x4a350a7bc84f8478028bfe662bcc6aa4390aff74" + } + ], + "scores": { + "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd": 10800000, + "0x7f11e5b7fe8c04c1e4ce0dd98ac5c922eccfa4ed": 900000, + "0x140b9b9756ce3de8c8fd296fc9d3e7b3aaa1cb16": 900000, + "0x14b98b26d82421a27608b21baf6bdefc181de546": 690000, + "0xc1240af85ffac1dbf826b7250db2644d62c728c8": 700000, + "0x4236878343d286a2943453e045da46e86d12138d": 800000, + "0x460c51e3f7609b47c0f9b3a655be804286c04666": 900000, + "0x4a350a7bc84f8478028bfe662bcc6aa4390aff74": 1000000 + }, + "combinedScores": { + "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd": 13500000, + "0x7f11e5b7fe8c04c1e4ce0dd98ac5c922eccfa4ed": 900000, + "0x140b9b9756ce3de8c8fd296fc9d3e7b3aaa1cb16": 900000, + "0x14b98b26d82421a27608b21baf6bdefc181de546": 690000, + "0xc1240af85ffac1dbf826b7250db2644d62c728c8": 700000 + }, + "normalizationFactor": 1 + } + } + }, + "network": "4", + "snapshot": 8101550, + "addresses": [ + "0x93606b27cB5e4c780883eC4F6b7Bed5f6572d1dd", + "0x7F11E5B7Fe8C04c1E4Ce0dD98aC5c922ECcfA4ed", + "0x140b9b9756cE3dE8c8fD296FC9D3E7B3AAa1Cb16", + "0x14B98b26D82421a27608B21BaF6BdEfc181DE546", + "0xc1240aF85fFAc1Dbf826b7250db2644D62c728c8" + ] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/index.ts new file mode 100644 index 00000000..e1bbd136 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-balance/index.ts @@ -0,0 +1,24 @@ +import { baseStrategy } from '../the-graph/baseStrategy'; +import { balanceStrategy } from './balances'; + +export const author = 'glmaljkovich'; +export const version = '1.0.1'; + +export async function strategy( + _space, + network, + _provider, + addresses, + _options, + snapshot +) { + return await baseStrategy( + _space, + network, + _provider, + addresses, + { strategyType: 'balance', ..._options }, + snapshot, + balanceStrategy + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/README.md new file mode 100644 index 00000000..e3295a3c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/README.md @@ -0,0 +1,11 @@ +# The Graph Delegation Strategy + +This strategy uses the amount of GRT an address has delegated to calculate its score. + +## Parameters + +```json +{ + "symbol": "D-GRT" +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/delegators.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/delegators.ts new file mode 100644 index 00000000..be3f99a4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/delegators.ts @@ -0,0 +1,119 @@ +import { Provider } from '@ethersproject/providers'; +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest } from '../../utils'; +import { + GRAPH_NETWORK_SUBGRAPH_URL, + bnWEI, + bdMulBn, + GraphAccountScores, + calcNonStakedTokens, + verifyResults, + GraphStrategyOptions +} from '../the-graph/graphUtils'; + +export async function delegatorsStrategy( + _space: string, + network: string, + _provider: Provider, + addresses: string[], + options: GraphStrategyOptions, + snapshot: string | number +): Promise { + const delegatorsParams = { + graphAccounts: { + __args: { + where: { + id_in: addresses + }, + first: options.pageSize + }, + id: true, + delegator: { + stakes: { + shareAmount: true, + lockedTokens: true, + indexer: { + delegationExchangeRate: true + } + } + } + }, + graphNetworks: { + __args: { + first: 1000 + }, + totalSupply: true, + totalDelegatedTokens: true, + totalTokensStaked: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + delegatorsParams.graphAccounts.__args.block = { number: snapshot }; + // @ts-ignore + delegatorsParams.graphNetworks.__args.block = { number: snapshot }; + } + + const result = await subgraphRequest( + GRAPH_NETWORK_SUBGRAPH_URL[network], + delegatorsParams + ); + + const score: GraphAccountScores = {}; + let normalizationFactor = 0; + if (result && result.graphNetworks) { + const nonStakedTokens = calcNonStakedTokens( + result.graphNetworks[0].totalSupply, + result.graphNetworks[0].totalTokensStaked, + result.graphNetworks[0].totalDelegatedTokens + ); + // The normalization factor gives more weight to delegated stake + // over GRT holded + normalizationFactor = + nonStakedTokens / + BigNumber.from(result.graphNetworks[0].totalDelegatedTokens) + .div(bnWEI) + .toNumber(); + } + + if (options.expectedResults && snapshot !== 'latest') { + verifyResults( + normalizationFactor.toString(), + options.expectedResults.normalizationFactor.toString(), + 'Normalization factor' + ); + } + + if (result && result.graphAccounts) { + addresses.forEach((a) => { + let delegationScore = 0; + for (let i = 0; i < result.graphAccounts.length; i++) { + if (result.graphAccounts[i].id == a) { + if (result.graphAccounts[i].delegator != null) { + // Find all stakes from the delegator + // and sum the delegated and locked tokens for each + result.graphAccounts[i].delegator.stakes.forEach((s) => { + const delegatedTokens = bdMulBn( + s.indexer.delegationExchangeRate, + s.shareAmount + ); + const lockedTokens = BigNumber.from(s.lockedTokens); + const oneDelegationScore = delegatedTokens + .add(lockedTokens) + .div(bnWEI) + .toNumber(); + delegationScore = delegationScore + oneDelegationScore; + }); + // Apply normalization factor to the total delegated GRT + delegationScore = delegationScore * normalizationFactor; + } + break; + } + } + score[a] = delegationScore; + }); + } else { + console.error('Subgraph request failed'); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/examples.json new file mode 100644 index 00000000..7b860670 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/examples.json @@ -0,0 +1,54 @@ +[ + { + "name": "Graph Tokens", + "strategy": { + "name": "the-graph-delegation", + "params": { + "symbol": "D-GRT", + "expectedResults": { + "tokenLockWallets": [ + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x4236878343d286a2943453e045da46e86d12138d" + }, + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x460c51e3f7609b47c0f9b3a655be804286c04666" + }, + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x4a350a7bc84f8478028bfe662bcc6aa4390aff74" + } + ], + "scores": { + "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd": 720471750.889876, + "0x7f11e5b7fe8c04c1e4ce0dd98ac5c922eccfa4ed": 0, + "0x140b9b9756ce3de8c8fd296fc9d3e7b3aaa1cb16": 0, + "0x14b98b26d82421a27608b21baf6bdefc181de546": 2233477633.8478913, + "0xc1240af85ffac1dbf826b7250db2644d62c728c8": 2161436975.654308, + "0x4236878343d286a2943453e045da46e86d12138d": 720478991.8847693, + "0x460c51e3f7609b47c0f9b3a655be804286c04666": 720478991.8847693, + "0x4a350a7bc84f8478028bfe662bcc6aa4390aff74": 0 + }, + "combinedScores": { + "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd": 2161429734.659415, + "0x7f11e5b7fe8c04c1e4ce0dd98ac5c922eccfa4ed": 0, + "0x140b9b9756ce3de8c8fd296fc9d3e7b3aaa1cb16": 0, + "0x14b98b26d82421a27608b21baf6bdefc181de546": 2233477633.8478913, + "0xc1240af85ffac1dbf826b7250db2644d62c728c8": 2161436975.654308 + }, + "normalizationFactor": 7240.994893314264 + } + } + }, + "network": "4", + "snapshot": 8101550, + "addresses": [ + "0x93606b27cB5e4c780883eC4F6b7Bed5f6572d1dd", + "0x7F11E5B7Fe8C04c1E4Ce0dD98aC5c922ECcfA4ed", + "0x140b9b9756cE3dE8c8fD296FC9D3E7B3AAa1Cb16", + "0x14B98b26D82421a27608B21BaF6BdEfc181DE546", + "0xc1240aF85fFAc1Dbf826b7250db2644D62c728c8" + ] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/index.ts new file mode 100644 index 00000000..c371a8f4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-delegation/index.ts @@ -0,0 +1,24 @@ +import { baseStrategy } from '../the-graph/baseStrategy'; +import { delegatorsStrategy } from './delegators'; + +export const author = 'glmaljkovich'; +export const version = '1.0.1'; + +export async function strategy( + _space, + network, + _provider, + addresses, + _options, + snapshot +) { + return await baseStrategy( + _space, + network, + _provider, + addresses, + { strategyType: 'delegation', ..._options }, + snapshot, + delegatorsStrategy + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/README.md b/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/README.md new file mode 100644 index 00000000..a84928df --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/README.md @@ -0,0 +1,11 @@ +# The Graph Delegation Strategy + +This strategy uses the amount of GRT an address has staked to calculate its score. + +## Parameters + +```json +{ + "symbol": "I-GRT" +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/examples.json b/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/examples.json new file mode 100644 index 00000000..fa1ad307 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/examples.json @@ -0,0 +1,54 @@ +[ + { + "name": "Graph Tokens", + "strategy": { + "name": "the-graph-indexing", + "params": { + "symbol": "I-GRT", + "expectedResults": { + "tokenLockWallets": [ + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x4236878343d286a2943453e045da46e86d12138d" + }, + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x460c51e3f7609b47c0f9b3a655be804286c04666" + }, + { + "beneficiary": "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd", + "id": "0x4a350a7bc84f8478028bfe662bcc6aa4390aff74" + } + ], + "scores": { + "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd": 28163018.03253715, + "0x7f11e5b7fe8c04c1e4ce0dd98ac5c922eccfa4ed": 28163018.03253715, + "0x140b9b9756ce3de8c8fd296fc9d3e7b3aaa1cb16": 28163018.03253715, + "0x14b98b26d82421a27608b21baf6bdefc181de546": 0, + "0xc1240af85ffac1dbf826b7250db2644d62c728c8": 0, + "0x4236878343d286a2943453e045da46e86d12138d": 28163018.03253715, + "0x460c51e3f7609b47c0f9b3a655be804286c04666": 0, + "0x4a350a7bc84f8478028bfe662bcc6aa4390aff74": 0 + }, + "combinedScores": { + "0x93606b27cb5e4c780883ec4f6b7bed5f6572d1dd": 56326036.0650743, + "0x7f11e5b7fe8c04c1e4ce0dd98ac5c922eccfa4ed": 28163018.03253715, + "0x140b9b9756ce3de8c8fd296fc9d3e7b3aaa1cb16": 28163018.03253715, + "0x14b98b26d82421a27608b21baf6bdefc181de546": 0, + "0xc1240af85ffac1dbf826b7250db2644d62c728c8": 0 + }, + "normalizationFactor": 281.6301803253715 + } + } + }, + "network": "4", + "snapshot": 8101550, + "addresses": [ + "0x93606b27cB5e4c780883eC4F6b7Bed5f6572d1dd", + "0x7F11E5B7Fe8C04c1E4Ce0dD98aC5c922ECcfA4ed", + "0x140b9b9756cE3dE8c8fD296FC9D3E7B3AAa1Cb16", + "0x14B98b26D82421a27608B21BaF6BdEfc181DE546", + "0xc1240aF85fFAc1Dbf826b7250db2644D62c728c8" + ] + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/index.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/index.ts new file mode 100644 index 00000000..60ed714b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/index.ts @@ -0,0 +1,24 @@ +import { baseStrategy } from '../the-graph/baseStrategy'; +import { indexersStrategy } from './indexers'; + +export const author = 'glmaljkovich'; +export const version = '1.0.1'; + +export async function strategy( + _space, + network, + _provider, + addresses, + _options, + snapshot +) { + return await baseStrategy( + _space, + network, + _provider, + addresses, + { strategyType: 'indexing', ..._options }, + snapshot, + indexersStrategy + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/indexers.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/indexers.ts new file mode 100644 index 00000000..eadf0226 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph-indexing/indexers.ts @@ -0,0 +1,100 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Provider } from '@ethersproject/providers'; +import { subgraphRequest } from '../../utils'; +import { + GRAPH_NETWORK_SUBGRAPH_URL, + GraphAccountScores, + calcNonStakedTokens, + bnWEI, + verifyResults, + GraphStrategyOptions +} from '../the-graph/graphUtils'; + +export async function indexersStrategy( + _space: string, + network: string, + _provider: Provider, + addresses: string[], + options: GraphStrategyOptions, + snapshot: string | number +): Promise { + const indexersParams = { + graphAccounts: { + __args: { + where: { + id_in: addresses + }, + first: options.pageSize + }, + id: true, + indexer: { + stakedTokens: true + } + }, + graphNetworks: { + __args: { + first: options.pageSize + }, + totalSupply: true, + totalDelegatedTokens: true, + totalTokensStaked: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + indexersParams.graphAccounts.__args.block = { number: snapshot }; + // @ts-ignore + indexersParams.graphNetworks.__args.block = { number: snapshot }; + } + const result = await subgraphRequest( + GRAPH_NETWORK_SUBGRAPH_URL[network], + indexersParams + ); + const score: GraphAccountScores = {}; + + let normalizationFactor = 0; + if (result && result.graphNetworks) { + const nonStakedTokens = calcNonStakedTokens( + result.graphNetworks[0].totalSupply, + result.graphNetworks[0].totalTokensStaked, + result.graphNetworks[0].totalDelegatedTokens + ); + // The normalization factor gives more weight to staked GRT + // over GRT holded + normalizationFactor = + nonStakedTokens / + BigNumber.from(result.graphNetworks[0].totalTokensStaked) + .div(bnWEI) + .toNumber(); + } + + if (options.expectedResults && snapshot !== 'latest') { + verifyResults( + normalizationFactor.toString(), + options.expectedResults.normalizationFactor.toString(), + 'Normalization factor' + ); + } + + if (result && result.graphAccounts) { + addresses.forEach((a) => { + let indexerScore = 0; + for (let i = 0; i < result.graphAccounts.length; i++) { + if (result.graphAccounts[i].id == a) { + if (result.graphAccounts[i].indexer != null) { + const indexerTokens = BigNumber.from( + result.graphAccounts[i].indexer.stakedTokens + ); + indexerScore = + indexerTokens.div(bnWEI).toNumber() * normalizationFactor; + } + break; + } + } + score[a] = indexerScore; + }); + } else { + console.error('Subgraph request failed'); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph/README.md b/Implementations/API/backend/utils/snapshot/strategies/the-graph/README.md new file mode 100644 index 00000000..19563627 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph/README.md @@ -0,0 +1,7 @@ +# The Graph base strategy logic + +> **Notice** +> +> This is a helper library. It is not meant to be used in a standalone fashion and does not conform with the shape of a strategy function. +> +> It will be used internally by the actual strategies of The Graph. \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph/baseStrategy.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph/baseStrategy.ts new file mode 100644 index 00000000..480cdfef --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph/baseStrategy.ts @@ -0,0 +1,138 @@ +import { Provider } from '@ethersproject/providers'; +import { getAddress } from '@ethersproject/address'; +import { getTokenLockWallets } from './tokenLockWallets'; + +import { + GraphAccountScores, + GraphStrategyOptions, + splitArray, + StrategyFunction, + verifyResults +} from './graphUtils'; + +const DEFAULT_PAGE_SIZE = 1000; +const VALID_STRATEGIES = ['balance', 'indexing', 'delegation']; + +/** + * Fetch scores for a list of addresses and their token-locked wallets + * + * @export + * @param {string} _space snapshot space + * @param {string} network networkId (i.e. ethereum mainnet = '1') + * @param {Provider} _provider + * @param {string[]} addresses + * @param {GraphStrategyOptions} options + * @param {(string | number)} snapshot 'latest' or blockNumber + * @param {StrategyFunction} graphStrategy + * @return {Promise} scores + */ +export async function getScoresPage( + _space: string, + network: string, + _provider: Provider, + addresses: string[], + options: GraphStrategyOptions, + snapshot: string | number, + graphStrategy: StrategyFunction +): Promise { + const tokenLockWallets = await getTokenLockWallets( + _space, + network, + _provider, + addresses, + options, + snapshot + ); + + // Take the token lock wallets object and turn it into an array, pass it into the other strategies + const allAccounts = [...addresses]; + for (const beneficiary in tokenLockWallets) { + tokenLockWallets[beneficiary].forEach((tw) => { + allAccounts.push(tw); + }); + } + // Fetch scores for accounts and TLW + const scores: GraphAccountScores = await graphStrategy( + _space, + network, + _provider, + allAccounts, + options, + snapshot + ); + // Only run tests for specific block + if (options.expectedResults && snapshot !== 'latest') { + verifyResults( + JSON.stringify(scores), + JSON.stringify(options.expectedResults.scores), + 'Scores' + ); + } + + // Combine the Token lock votes into the beneficiaries votes + const combinedScores: GraphAccountScores = {}; + for (const account of addresses) { + let accountScore = scores[account]; + // It was found that this beneficiary has token lock wallets, lets add them + if (tokenLockWallets[account] != null) { + tokenLockWallets[account].forEach((tw) => { + accountScore = accountScore + scores[tw]; + }); + } + combinedScores[account] = accountScore; + } + + return combinedScores; +} + +export async function baseStrategy( + _space: string, + network: string, + _provider: Provider, + _addresses: string[], + options: GraphStrategyOptions, + snapshot: string | number, + graphStrategy: StrategyFunction +) { + const addresses = _addresses.map((address) => address.toLowerCase()); + let combinedScores: GraphAccountScores = {}; + + if (VALID_STRATEGIES.includes(options.strategyType)) { + // Paginate and get combined scores + const pageSize = options.pageSize || DEFAULT_PAGE_SIZE; + const pages = splitArray(addresses, pageSize); + let pageNum = 1; + for (const addressesPage of pages) { + console.info(`Processing page ${pageNum} of ${pages.length}`); + const pageScores = await getScoresPage( + _space, + network, + _provider, + addressesPage, + { ...options, pageSize }, + snapshot, + graphStrategy + ); + combinedScores = { ...combinedScores, ...pageScores }; + pageNum += 1; + } + } else { + console.error('ERROR: Strategy does not exist'); + return combinedScores; + } + // Only run tests for specific block + if (options.expectedResults && snapshot !== 'latest') { + verifyResults( + JSON.stringify(combinedScores), + JSON.stringify(options.expectedResults.combinedScores), + 'Combined scores' + ); + } + + return Object.fromEntries( + Object.entries(combinedScores).map((score) => [ + getAddress(score[0]), + score[1] + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph/graphUtils.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph/graphUtils.ts new file mode 100644 index 00000000..9470d264 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph/graphUtils.ts @@ -0,0 +1,103 @@ +import { parseUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Provider } from '@ethersproject/providers'; + +export const GRAPH_NETWORK_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-mainnet', + '4': 'https://api.thegraph.com/subgraphs/name/graphprotocol/graph-network-testnet' +}; +export const bnWEI = BigNumber.from('1000000000000000000'); + +export interface GraphAccountScores { + [key: string]: number; +} + +export type GraphStrategyOptions = { + symbol: string; + // It should not be provided by the user but injected by the strategies + strategyType: string; + // How many addresses to process per subgraphRequest. Default: 1000 + pageSize?: number; + // Used for pagination. It is set internally by the base strategy and shouldn't be provided by the end user. + skip?: number; + // Only for test purposes + expectedResults?: Record; +}; + +export type StrategyFunction = ( + // Snapshot space + space: string, + // networkId (i.e. ethereum mainnet = '1') + network: string, + provider: Provider, + addresses: string[], + // These are the parameters you can configure in your space settings + // for the strategy. It's up to the strategy developer to define the + // shape of the options and inform them in the README.md of the strategy + // so users know how to configure it + options: GraphStrategyOptions, + // 'latest' or a blockNumber used to ignore votes from newer participants + snapshot: string | number +) => Promise>; // mapping of addresses to scores + +/** + * Pass in a BigDecimal and BigNumber from a subgraph query, and return the multiplication of + * them as a BigNumber + * */ +export function bdMulBn(bd: string, bn: string): BigNumber { + const splitDecimal = bd.split('.'); + let split; + // Truncate the BD so it can be converted to a BN (no decimals) when multiplied by WEI + if (splitDecimal.length > 1) { + split = `${splitDecimal[0]}.${splitDecimal[1].slice(0, 18)}`; + } else { + // Didn't have decimals, even though it was BigDecimal (i.e. "2") + return BigNumber.from(bn).mul(BigNumber.from(bd)); + } + + // Convert it to BN + const bdWithWEI = parseUnits(split, 18); + + // Multiple, then divide by WEI to have it back in BN + return BigNumber.from(bn).mul(bdWithWEI).div(bnWEI); +} + +export function calcNonStakedTokens( + totalSupply: string, + totalTokensStaked: string, + totalDelegatedTokens: string +): number { + return BigNumber.from(totalSupply) + .sub(BigNumber.from(totalTokensStaked)) + .sub(BigNumber.from(totalDelegatedTokens)) + .div(bnWEI) + .toNumber(); +} + +export function verifyResults( + result: string, + expectedResults: string, + type: string +): void { + const diff = `expected:\n ${expectedResults}\ngot:\n ${result}`; + result === expectedResults + ? console.log(`>>> SUCCESS: ${type} match expected results`) + : console.error( + `>>> ERROR: ${type} do not match expected results\n${diff}` + ); +} +/** + * splits an array in even chunks and returns a list of chunks + * + * @export + * @param {string[]} _array + * @param {number} pageSize + * @return {string[][]} chunks + */ +export function splitArray(_array: string[], pageSize: number): string[][] { + const chunks: string[][] = []; + for (let i = 0; i < _array.length; i += pageSize) { + chunks.push(_array.slice(i, i + pageSize)); + } + return chunks; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/the-graph/tokenLockWallets.ts b/Implementations/API/backend/utils/snapshot/strategies/the-graph/tokenLockWallets.ts new file mode 100644 index 00000000..aa523ddb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/the-graph/tokenLockWallets.ts @@ -0,0 +1,66 @@ +import { Provider } from '@ethersproject/providers'; +import { subgraphRequest } from '../../utils'; +import { verifyResults } from './graphUtils'; + +export const TOKEN_DISTRIBUTION_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/graphprotocol/token-distribution', + '4': 'https://api.thegraph.com/subgraphs/name/davekaj/token-distribution-rinkeby' +}; +interface TokenLockWallets { + [key: string]: string[]; +} + +/** + @dev Queries the subgraph to find if an address owns any token lock wallets + @returns An object with the beneficiaries as keys and TLWs as values in an array +*/ +export async function getTokenLockWallets( + _space: string, + network: string, + _provider: Provider, + addresses: string[], + options: Record, + snapshot: string | number +): Promise { + const tokenLockParams = { + tokenLockWallets: { + __args: { + where: { + beneficiary_in: addresses + }, + first: options.pageSize + }, + id: true, + beneficiary: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + tokenLockParams.tokenLockWallets.__args.block = { number: snapshot }; + } + const result = await subgraphRequest( + TOKEN_DISTRIBUTION_SUBGRAPH_URL[network], + tokenLockParams + ); + + const tokenLockWallets: TokenLockWallets = {}; + if (result && result.tokenLockWallets) { + if (options.expectedResults) { + verifyResults( + JSON.stringify(result.tokenLockWallets), + JSON.stringify(options.expectedResults.tokenLockWallets), + 'Token lock wallets' + ); + } + result.tokenLockWallets.forEach((tw) => { + if (tokenLockWallets[tw.beneficiary] == undefined) + tokenLockWallets[tw.beneficiary] = []; + tokenLockWallets[tw.beneficiary] = tokenLockWallets[ + tw.beneficiary + ].concat(tw.id); + }); + } else { + console.error('Subgraph request failed'); + } + return tokenLockWallets || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/thresholds/README.md b/Implementations/API/backend/utils/snapshot/strategies/thresholds/README.md new file mode 100644 index 00000000..cf905f99 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/thresholds/README.md @@ -0,0 +1,28 @@ +# thresholds + +This strategy return the voting power based on the strategy passed with the thresholds. + +| Parameter | Description | +| ------------- | ------------------------------------------ | +| `strategy` | Strategy that you want to apply threshold to | +| `thresholds` | threshold values, Refer to example below | + +Here is an example of parameters: + +```json +{ + "strategy": { + "name": "erc20-balance-of", + "params": { + "address": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", + "decimals": 0 + } + }, + "thresholds": [ + { "threshold": 1, "votes": 1 }, + { "threshold": 4, "votes": 2 }, + { "threshold": 11, "votes": 3 }, + { "threshold": 25, "votes": 4 } + ] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/thresholds/examples.json b/Implementations/API/backend/utils/snapshot/strategies/thresholds/examples.json new file mode 100644 index 00000000..aaa355ef --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/thresholds/examples.json @@ -0,0 +1,32 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "thresholds", + "params": { + "symbol": "BAYC", + "strategy": { + "name": "erc20-balance-of", + "params": { + "address": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", + "decimals": 0 + } + }, + "thresholds": [ + { "threshold": 1, "votes": 1 }, + { "threshold": 4, "votes": 2 }, + { "threshold": 11, "votes": 3 }, + { "threshold": 25, "votes": 4 } + ] + } + }, + "network": "1", + "addresses": [ + "0xed47015bb8080b9399f9d0ddfc427b9cee2caab1", + "0xf56345338cb4cddaf915ebef3bfde63e70fe3053", + "0x7b15e6c439b27a553b65a9904ce571da6691a0fb", + "0x8d2f3a76a76f055d62a931678ab16b042e7badeb" + ], + "snapshot": 12453212 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/thresholds/index.ts b/Implementations/API/backend/utils/snapshot/strategies/thresholds/index.ts new file mode 100644 index 00000000..2c8f91e4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/thresholds/index.ts @@ -0,0 +1,37 @@ +import strategies from '..'; + +export const author = 'snapshot-labs'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const thresholds = options.thresholds || [{ threshold: 1, votes: 1 }]; + if (thresholds.length == 0) thresholds.push({ threshold: 1, votes: 1 }); + + const calculateVotes = (balance) => + thresholds + .sort((a, b) => b.threshold - a.threshold) + .find((t) => t.threshold <= balance)?.votes ?? 0; + + const response = await strategies[options.strategy.name].strategy( + space, + network, + provider, + addresses, + options.strategy.params, + snapshot + ); + + return Object.fromEntries( + Object.keys(response).map((address) => [ + address, + calculateVotes(response[address]) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/README.md b/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/README.md new file mode 100644 index 00000000..85bfa94a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/README.md @@ -0,0 +1,22 @@ +# Ticket validity strategy + +Weight 1 score for any address that holds greater than 0 tokens. + +Parameters: + +| Parameter | Description | Default value | +| :--- | :--- | :--- | +| `min` | Minimum voting power | 0 | +| `address` | Token address to check | | +| `symbol` | Token symbol | optional | + +## Examples + +```JSON +{ + { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI" + } +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/examples.json new file mode 100644 index 00000000..50905a32 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Ticket Validity", + "strategy": { + "name": "ticket-validity", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI" + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/index.ts new file mode 100644 index 00000000..8690bf6d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ticket-validity/index.ts @@ -0,0 +1,28 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'ethedev'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + return Object.fromEntries( + Object.entries(score).map((address: any) => [ + address[0], + address[1] > (options.min || 0) ? 1 : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ticket/README.md b/Implementations/API/backend/utils/snapshot/strategies/ticket/README.md new file mode 100644 index 00000000..6cf92526 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ticket/README.md @@ -0,0 +1,10 @@ +# ticket + +Ticket strategy gives one voting power per one address, you can also pass a `value` parameter to give more voting power to the voter. + +## Params + +| param | type | description | default | +| --- | --- | --- | --- | +| `value` | `number` | The number of votes to give to the voter | 1 | +| `symbol` | `string` | The symbol of the token | optional | diff --git a/Implementations/API/backend/utils/snapshot/strategies/ticket/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ticket/examples.json new file mode 100644 index 00000000..49a82100 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ticket/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ticket", + "params": { + "symbol": "TICKET" + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ticket/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ticket/index.ts new file mode 100644 index 00000000..31c2dfcf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ticket/index.ts @@ -0,0 +1,8 @@ +export const author = 'bonustrack'; +export const version = '0.1.0'; + +export async function strategy(space, network, provider, addresses, options) { + return Object.fromEntries( + addresses.map((address) => [address, options.value || 1]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ticket/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ticket/schema.json new file mode 100644 index 00000000..6cdcffbb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ticket/schema.json @@ -0,0 +1,25 @@ +{ + "$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. DOODLE"], + "maxLength": 16 + }, + "value": { + "type": "number", + "title": "Voting power", + "examples": ["e.g. 1"] + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tokenlon/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tokenlon/examples.json new file mode 100644 index 00000000..a75e607d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tokenlon/examples.json @@ -0,0 +1,34 @@ +[ + { + "name": "Tokenlon", + "strategy": { + "name": "tokenlon", + "params": { + "symbol": "LON", + "uniswap": "0x7924a818013f39cf800f5589ff1f1f0def54f31f", + "sushiswap": "0x55d31f68975e446a40a2d02ffa4b0e1bfb233c2f", + "stakingRewardUniswap2": "0xc348314f74B043Ff79396e14116B6f19122D69f4", + "stakingRewardSushiSwap2": "0x11520d501E10E2E02A2715C4A9d3F8aEb1b72A7A", + "stakingRewardUniswap3": "0x74379CEC6a2c9Fde0537e9D9346222a724A278e4", + "stakingRewardSushiSwap3": "0x539a67B6f9c3caD58f434CC12624b2d520BC03F8", + "stakingRewardUniswap4": "0xb6bC1a713e4B11fa31480d31C825dCFd7e8FaBFD", + "stakingRewardSushiSwap4": "0x9648B119f442a3a096C0d5A1F8A0215B46dbb547", + "xLON": "0xf88506B0F1d30056B9e5580668D5875b9cd30F23", + "token": "0x0000000000095413afC295d19EDeb1Ad7B71c952", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x39422F5065cF7968242747Bc19e812B6Ae98B50F", + "0x3e2764be2a583bab5589223cafabd78c2f9419cf", + "0x77a6ece6eb5a8d54d1b643f03beebad7e6c6ff06", + "0x50a01818f24435f965b09ea8fa80826e4355bed5", + "0xbb66E696A8CE490BBDE612CE7490A948046DB074", + "0x1de118e70cc9416ab658b5e695d3e3de3aba0be5", + "0x57b3c68caf0ebea2d9173bdb013655fd8da31aa4", + "0x69013C49A5818FbB6524D55Ef0eaC292567c3092" + ], + "snapshot": 13409044 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tokenlon/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tokenlon/index.ts new file mode 100644 index 00000000..cfd6cd6d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tokenlon/index.ts @@ -0,0 +1,344 @@ +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'BenjaminLu'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'earned', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const promises: any[] = []; + const responsePromise = multicall( + network, + provider, + abi, + [ + [options.token, 'balanceOf', [options.uniswap]], + [options.uniswap, 'totalSupply'], + [options.token, 'balanceOf', [options.sushiswap]], + [options.sushiswap, 'totalSupply'], + ...addresses.map((address: any) => [ + options.uniswap, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardUniswap2, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardUniswap2, + 'earned', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardUniswap3, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardUniswap3, + 'earned', + [address] + ]), + ...addresses.map((address: any) => [ + options.sushiswap, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardSushiSwap2, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardSushiSwap2, + 'earned', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardSushiSwap3, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardSushiSwap3, + 'earned', + [address] + ]), + ...addresses.map((address: any) => [ + options.token, + 'balanceOf', + [address] + ]), + [options.token, 'balanceOf', [options.xLON]], + [options.xLON, 'totalSupply', []], + ...addresses.map((address: any) => [options.xLON, 'balanceOf', [address]]) + ], + { blockTag } + ); + promises.push(responsePromise); + + if (options.stakingRewardUniswap4 && options.stakingRewardSushiSwap4) { + const responsePhase4Promise = multicall(network, provider, abi, [ + ...addresses.map((address: any) => [ + options.stakingRewardUniswap4, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardUniswap4, + 'earned', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardSushiSwap4, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.stakingRewardSushiSwap4, + 'earned', + [address] + ]) + ]); + promises.push(responsePhase4Promise); + } + + let lpBalancesUniswapStaking4; + let lonEarnedBalancesUniswapStaking4; + let lpBalancesSushiSwapStaking4; + let lonEarnedBalancesSushiSwapStaking4; + const [response, responsePhase4] = await Promise.all(promises); + if (responsePhase4) { + // user's LP tokens in phase 4 uniswap staking contract + lpBalancesUniswapStaking4 = responsePhase4.slice( + addresses.length * 0, + addresses.length * 1 + ); + // user's LON of rewards in phase 4 uniswap staking contract + lonEarnedBalancesUniswapStaking4 = responsePhase4.slice( + addresses.length * 1, + addresses.length * 2 + ); + // user's SLP tokens in phase 4 sushiswap staking contract + lpBalancesSushiSwapStaking4 = responsePhase4.slice( + addresses.length * 2, + addresses.length * 3 + ); + // user's LON of rewards in phase 4 sushiswap staking contract + lonEarnedBalancesSushiSwapStaking4 = responsePhase4.slice( + addresses.length * 3, + addresses.length * 4 + ); + } + + // LON in uniswap ETH-LON pair / LP total supply + const lonPerLPUniswap = parseUnits(response[0][0].toString(), 18).div( + response[1][0] + ); + // LON in sushiswap LON-USDT pair / SLP total supply + const lonPerLPSushiSwap = parseUnits(response[2][0].toString(), 18).div( + response[3][0] + ); + // user's LP tokens + const lpBalancesUniswap = response.slice(4, addresses.length + 4); + // user's LP tokens in phase 2 uniswap staking contract + const lpBalancesUniswapStaking2 = response.slice( + addresses.length * 1 + 4, + addresses.length * 2 + 4 + ); + // user's LON of rewards in phase 2 uniswap staking contract + const lonEarnedBalancesUniswapStaking2 = response.slice( + addresses.length * 2 + 4, + addresses.length * 3 + 4 + ); + // user's LP tokens in phase 3 uniswap staking contract + const lpBalancesUniswapStaking3 = response.slice( + addresses.length * 3 + 4, + addresses.length * 4 + 4 + ); + // user's LON of rewards in phase 3 uniswap staking contract + const lonEarnedBalancesUniswapStaking3 = response.slice( + addresses.length * 4 + 4, + addresses.length * 5 + 4 + ); + // user's SLP tokens + const lpBalancesSushiSwap = response.slice( + addresses.length * 5 + 4, + addresses.length * 6 + 4 + ); + // user's SLP tokens in phase 2 sushiswap staking contract + const lpBalancesSushiSwapStaking2 = response.slice( + addresses.length * 6 + 4, + addresses.length * 7 + 4 + ); + // user's LON of rewards in phase 2 sushiswap staking contract + const lonEarnedBalancesSushiSwapStaking2 = response.slice( + addresses.length * 7 + 4, + addresses.length * 8 + 4 + ); + // user's SLP tokens in phase 3 sushiswap staking contract + const lpBalancesSushiSwapStaking3 = response.slice( + addresses.length * 8 + 4, + addresses.length * 9 + 4 + ); + // user's LON of rewards in phase 3 sushiswap staking contract + const lonEarnedBalancesSushiSwapStaking3 = response.slice( + addresses.length * 9 + 4, + addresses.length * 10 + 4 + ); + // user's LON + const tokenBalances = response.slice( + addresses.length * 10 + 4, + addresses.length * 11 + 4 + ); + // LON staked in xLON contract + const lonBalanceOfxLON = response.slice( + addresses.length * 11 + 4, + addresses.length * 11 + 5 + )[0][0]; + // xLON total supply + const xLONTotalSupply = response.slice( + addresses.length * 11 + 5, + addresses.length * 11 + 6 + )[0][0]; + // user's xLON + const xLONBalanceOfUsers = response.slice( + addresses.length * 11 + 6, + addresses.length * 12 + 6 + ); + + return Object.fromEntries( + Array(addresses.length) + .fill('') + .map((_, i) => { + const xLONBalanceOfUser = xLONBalanceOfUsers[i][0]; + const userLONShares = xLONBalanceOfUser + .mul(lonBalanceOfxLON) + .div(xLONTotalSupply); + const lpBalanceUniswap = lpBalancesUniswap[i][0]; + const lpBalanceUniswapStaking2 = lpBalancesUniswapStaking2[i][0]; + const lpBalanceUniswapStaking3 = lpBalancesUniswapStaking3[i][0]; + let lonLpBalanceUniswap; + lonLpBalanceUniswap = lpBalanceUniswap + .add(lpBalanceUniswapStaking2) + .add(lpBalanceUniswapStaking3); + const lonEarnedBalanceUniswapStaking2 = + lonEarnedBalancesUniswapStaking2[i][0]; + const lonEarnedBalanceUniswapStaking3 = + lonEarnedBalancesUniswapStaking3[i][0]; + let lonEarnedBalanceUniswapStaking = + lonEarnedBalanceUniswapStaking2.add(lonEarnedBalanceUniswapStaking3); + + const lpBalanceSushiSwap = lpBalancesSushiSwap[i][0]; + const lpBalanceSushiSwapStaking2 = lpBalancesSushiSwapStaking2[i][0]; + const lpBalanceSushiSwapStaking3 = lpBalancesSushiSwapStaking3[i][0]; + let lonLpBalanceSushiSwap; + lonLpBalanceSushiSwap = lpBalanceSushiSwap + .add(lpBalanceSushiSwapStaking2) + .add(lpBalanceSushiSwapStaking3); + + const lonEarnedBalanceSushiSwapStaking2 = + lonEarnedBalancesSushiSwapStaking2[i][0]; + const lonEarnedBalanceSushiSwapStaking3 = + lonEarnedBalancesSushiSwapStaking3[i][0]; + let lonEarnedBalanceSushiSwapStaking = + lonEarnedBalanceSushiSwapStaking2.add( + lonEarnedBalanceSushiSwapStaking3 + ); + + if (options.stakingRewardUniswap4 && options.stakingRewardSushiSwap4) { + const lpBalanceUniswapStaking4 = lpBalancesUniswapStaking4[i][0]; + lonLpBalanceUniswap = lonLpBalanceUniswap.add( + lpBalanceUniswapStaking4 + ); + + const lonEarnedBalanceUniswapStaking4 = + lonEarnedBalancesUniswapStaking4[i][0]; + lonEarnedBalanceUniswapStaking = lonEarnedBalanceUniswapStaking.add( + lonEarnedBalanceUniswapStaking4 + ); + + const lpBalanceSushiSwapStaking4 = lpBalancesSushiSwapStaking4[i][0]; + lonLpBalanceSushiSwap = lonLpBalanceSushiSwap.add( + lpBalanceSushiSwapStaking4 + ); + + const lonEarnedBalanceSushiSwapStaking4 = + lonEarnedBalancesSushiSwapStaking4[i][0]; + lonEarnedBalanceSushiSwapStaking = + lonEarnedBalanceSushiSwapStaking.add( + lonEarnedBalanceSushiSwapStaking4 + ); + } + lonLpBalanceUniswap = lonLpBalanceUniswap + .mul(lonPerLPUniswap) + .div(parseUnits('1', 18)); + + lonLpBalanceSushiSwap = lonLpBalanceSushiSwap + .mul(lonPerLPSushiSwap) + .div(parseUnits('1', 18)); + + return [ + addresses[i], + parseFloat( + formatUnits( + tokenBalances[i][0] + .add(userLONShares) + .add(lonLpBalanceUniswap) + .add(lonEarnedBalanceUniswapStaking) + .add(lonLpBalanceSushiSwap) + .add(lonEarnedBalanceSushiSwapStaking), + options.decimals + ) + ) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tomb-finance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tomb-finance/examples.json new file mode 100644 index 00000000..989538c5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tomb-finance/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example tomb-finance query", + "strategy": { + "name": "tomb-finance", + "params": { + "symbol": "TOMB", + "vaultTokens": [ + { + "symbol": "TSHARE in MLNL", + "address": "0x88DBF6b60c939999084e752dA7fa62fE84621193", + "decimals": 18 + } + ] + } + }, + "network": "250", + "addresses": [ + "0x0fA5a3B6f8e26a7C2C67bd205fFcfA9f89B0e8d1", + "0x32439F5A7Dc35590e83AAc0a80762dE27Ab76046" + ], + "snapshot": 12368542 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tomb-finance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tomb-finance/index.ts new file mode 100644 index 00000000..8c933101 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tomb-finance/index.ts @@ -0,0 +1,140 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'thecryptoundertaker'; +export const version = '0.1.0'; + +const MASONRY_ADDRESS = '0x8764DE60236C5843D9faEB1B638fbCE962773B67'; +const TSHARE_TOKEN_ADDRESS = '0x4cdF39285D7Ca8eB3f090fDA0C069ba5F4145B37'; +const TSHARE_LP_TOKEN_ADDRESS = '0x4733bc45eF91cF7CcEcaeeDb794727075fB209F2'; +const CEMETERY_ADDRESS = '0xcc0a87F7e7c693042a9Cc703661F5060c80ACb43'; + +const abi = [ + 'function balanceOf(address) view returns (uint256 amount)', + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt)', + 'function totalSupply() view returns (uint256)', + 'function strategy() view returns (address)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + let multi = new Multicaller(network, provider, abi, { blockTag }); + + options.vaultTokens.forEach((token: any) => { + multi.call(`vaultStrategies.${token.address}`, token.address, 'strategy'); + }); + + const vaultStrategiesResult = await multi.execute(); + + multi = new Multicaller(network, provider, abi, { blockTag }); + + multi.call(`lp.tshareBalance`, TSHARE_TOKEN_ADDRESS, 'balanceOf', [ + TSHARE_LP_TOKEN_ADDRESS + ]); + multi.call(`lp.totalSupply`, TSHARE_LP_TOKEN_ADDRESS, 'totalSupply'); + options.vaultTokens.forEach((token: any) => { + multi.call( + `vaultTokens.totalSupply.${token.address}`, + token.address, + 'totalSupply', + [] + ); + multi.call( + `vaultTokens.lpBalance.${token.address}`, + CEMETERY_ADDRESS, + 'userInfo', + ['1', vaultStrategiesResult.vaultStrategies[token.address]] + ); + }); + + addresses.forEach((address: any) => { + multi.call(`tshare.${address}`, TSHARE_TOKEN_ADDRESS, 'balanceOf', [ + address + ]); + multi.call(`tshareInMasonry.${address}`, MASONRY_ADDRESS, 'balanceOf', [ + address + ]); + multi.call(`lpInCemetery.${address}`, CEMETERY_ADDRESS, 'userInfo', [ + '1', + address + ]); + multi.call(`lp.${address}`, TSHARE_LP_TOKEN_ADDRESS, 'balanceOf', [ + address + ]); + options.vaultTokens.forEach((token: any) => { + multi.call( + `vaultTokens.${address}.${token.address}`, + token.address, + 'balanceOf', + [address] + ); + }); + }); + + const result = await multi.execute(); + + return Object.fromEntries( + addresses.map((address: any) => { + const tshareInWallet = parseFloat( + formatUnits(result.tshare[address], 18) + ); + + const tshareInLpInWallet = parseFloat( + formatUnits( + result.lp[address] + .mul(result.lp.tshareBalance) + .div(result.lp.totalSupply), + 18 + ) + ); + + const tshareInMasonry = parseFloat( + formatUnits(result.tshareInMasonry[address], 18) + ); + + const tshareInCemetery = parseFloat( + formatUnits( + result.lpInCemetery[address].amount + .mul(result.lp.tshareBalance) + .div(result.lp.totalSupply), + 18 + ) + ); + + const tshareInVaults = options.vaultTokens.reduce( + (previous: number, token: any) => + previous + + parseFloat( + formatUnits( + result.vaultTokens[address][token.address] + .mul(result.vaultTokens.lpBalance[token.address].amount) + .div(result.vaultTokens.totalSupply[token.address]) + .mul(result.lp.tshareBalance) + .div(result.lp.totalSupply), + 18 + ) + ), + 0 + ); + + return [ + address, + Math.sqrt( + tshareInWallet + + tshareInLpInWallet + + tshareInMasonry + + tshareInCemetery + + tshareInVaults + ) + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/README.md b/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/README.md new file mode 100644 index 00000000..132af0c8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/README.md @@ -0,0 +1,9 @@ +# Contract Call Strategy + +Fetches [TOMYUM](https://bscscan.com/address/0xa8777a7855cc5e5d4994d890eaad369050a9ff47) balance from the following sources: + +- Wallet +- TOMYUM-BNB LP Farm +- TOMYUM Pool +- TOMYUM Vault +- Pools that were active at the time of the snapshot diff --git a/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/examples.json new file mode 100644 index 00000000..c9bce866 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "tomyumswap", + "params": { + "symbol": "TOMYUM" + } + }, + "network": "56", + "addresses": [ + "0xF677b8EF72C34f63c43f47C30612B1A3Ec1b622F", + "0xd7eAd7DD37EFf97531beA958a58282Fa6D3a31A5" + ], + "snapshot": 7151302 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/index.ts new file mode 100644 index 00000000..3d77ee00 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tomyumswap/index.ts @@ -0,0 +1,132 @@ +import fetch from 'cross-fetch'; +import { subgraphRequest } from '../../utils'; + +export const author = 'tomyumswap'; +export const version = '0.0.1'; + +type VotingResponse = { + verificationHash: string; + block: number; + tomYumBalance: string; + tomYumVaultBalance: string; + tomYumPoolBalance: string; + tomYumBnbLpBalance: string; + poolsBalance: string; + total: string; +}; + +const MINIMUM_VOTING_POWER = 0.01; +const SMART_CHEF_URL = + 'https://api.thegraph.com/subgraphs/name/tomyumswap/smartchef'; +const VOTING_API_URL = 'http://voting-api.tomyumswap.com/api/'; + +/** + * Fetches voting power of one address + */ +// const fetchVotingPower = async ( +// address: string, +// block: number, +// poolAddresses: string[] +// ): Promise => { +// const response = await fetch(`${VOTING_API_URL}power`, { +// method: 'POST', +// headers: { +// 'Content-Type': 'application/json' +// }, +// body: JSON.stringify({ +// block, +// address, +// poolAddresses +// }) +// }); + +// const payload = await response.json(); +// return payload.data; +// }; + +/** + * Fetches voting power of multiple addresses + */ +const fetchVotingPowerMultiple = async ( + addresses: string[], + block: number, + poolAddresses: string[] +): Promise => { + const response = await fetch(`${VOTING_API_URL}powerV2`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + block, + addresses, + poolAddresses + }) + }); + + const payload = await response.json(); + + return payload.data; +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = + typeof snapshot === 'number' ? snapshot : await provider.getBlockNumber(); + + const params = { + smartChefs: { + __args: { + where: { + startBlock_lte: blockTag, + endBlock_gte: blockTag + }, + first: 1000, + orderBy: 'block', + orderDirection: 'desc' + }, + id: true, + startBlock: true, + endBlock: true + } + }; + + const results = await subgraphRequest(SMART_CHEF_URL, params); + + if (!results) { + return; + } + + try { + const poolAddresses = results.smartChefs.map((pool) => pool.id); + const votingPowerResult = await fetchVotingPowerMultiple( + addresses, + blockTag, + poolAddresses + ); + + const calculatedPower = votingPowerResult.reduce( + (accum, response, index) => { + const address = addresses[index]; + const total = parseFloat(response.total); + + return { + ...accum[index], + [address]: + total <= MINIMUM_VOTING_POWER ? MINIMUM_VOTING_POWER : total + }; + }, + {} + ); + + return calculatedPower; + } catch { + return []; + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/total-axion-shares/examples.json b/Implementations/API/backend/utils/snapshot/strategies/total-axion-shares/examples.json new file mode 100644 index 00000000..d0975332 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/total-axion-shares/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Total Axion Shares", + "strategy": { + "name": "total-axion-shares", + "params": { + "symbol": "TAS", + "dataReader": "0x8458fe26CbF3B8295755654C02ABaDFB1d3CBCe6" + } + }, + "network": "137", + "addresses": [ + "0xe8b283b606a212d82036f74f88177375125440f6", + "0x01E5fEafA5133bA6cdcc7b9afF7Aa5A3bE3f772F", + "0xd50938c168cfa25d0C65e7E5B065439aDB0524D6" + ], + "snapshot": 24085428 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/total-axion-shares/index.ts b/Implementations/API/backend/utils/snapshot/strategies/total-axion-shares/index.ts new file mode 100644 index 00000000..44452a6c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/total-axion-shares/index.ts @@ -0,0 +1,60 @@ +import { multicall } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'Axion-Network'; +export const version = '0.3.0'; + +const data_reader_address = '0x8458fe26CbF3B8295755654C02ABaDFB1d3CBCe6'; +const data_reader_abi = [ + { + name: 'getDaoShares', + stateMutability: 'view', + type: 'function', + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + outputs: [ + { + internalType: 'uint256', + name: 'daoShares', + type: 'uint256' + } + ] + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const totalShares = await multicall( + network, + provider, + data_reader_abi, + addresses.map((addr: string) => [ + options.dataReader || data_reader_address, + 'getDaoShares', + [addr] + ]), + { blockTag } + ); + + const shares_by_address = {}; + const _1e18 = BigNumber.from('1000000000000000000'); + + totalShares.forEach((v, i) => { + const sharesBN = BigNumber.from(v.toString()); + shares_by_address[addresses[i]] = sharesBN.div(_1e18).toNumber(); + }); + + return shares_by_address; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tpro-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tpro-staking/examples.json new file mode 100644 index 00000000..230274dc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tpro-staking/examples.json @@ -0,0 +1,32 @@ +[ + { + "name": "tpro-staking", + "strategy": { + "name": "tpro-staking", + "params": { + "address": "0x3540abe4f288b280a0740ad5121aec337c404d15", + "symbol": "TPRO", + "decimals": 18, + "smartContracts": [ + "0x5c9977cA74Be8028a2715229A4ce1e7cABd6eFC6", + "0x764C205a50Ef89A55b2E8FE13B0e06E50391716e", + "0xa3c6dfaDF1ABA174e54406e692b09F61c4bFD689", + "0x674EeEF1Da64b9e398210621A61d1d2a8Bf27387", + "0x1C2AC88532f54A046f842B0651f0C2F6A80E729D" + ], + "contractFactor": [0.03125, 0.0625, 0.125, 0.25, 1], + "powerFactor": 2 + } + }, + "network": "1", + "addresses": [ + "0x9D424a91A4D47ccb858436CE006492D61C8dBA76", + "0x8fc8405d41114D6F8271C44902464Ca1325Ac692", + "0x87Dddb39a02755c8Bfc567e30080b006DaD93Bf6", + "0xB5C8BB51A4cf229ced4cBA3fE352BF0D47088813", + "0xAC02A175594010228AC9dDCa9584b894386f7b27", + "0x8db0c967E6F8c6D8dA1a74147614Fd66c437bF45" + ], + "snapshot": 15402131 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tpro-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tpro-staking/index.ts new file mode 100644 index 00000000..63f53cb0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tpro-staking/index.ts @@ -0,0 +1,80 @@ +import { multicall } from '../../utils'; +import { formatEther } from '@ethersproject/units'; + +export const author = 'tokenomia-pro'; +export const version = '1.0.0'; + +const abi = [ + 'function userInfo(uint256, address) view returns (uint256 amount, uint256 rewardDebt, uint256 pendingRewards, uint256 lockedTimestamp, uint256 lockupTimestamp, uint256 lockupTimerange, uint256 virtAmount)' +]; + +interface StrategyOptions { + address: string; + symbol: string; + decimals: number; + smartContracts: Array; + contractFactor: Array; + powerFactor: number; +} + +interface VotingPower { + [key: string]: number; +} + +export async function strategy( + space: string, + network: string, + provider, + addresses: string[], + options: StrategyOptions, + snapshot: number +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const promises: any[] = []; + + options.smartContracts.forEach((contract) => { + promises.push( + multicall( + network, + provider, + abi, + addresses.map((address: any) => [contract, 'userInfo', [0, address]]), + { blockTag } + ) + ); + }); + + const resolvedPromises = await Promise.all(promises); + const votingPowers: Array = []; + + resolvedPromises.forEach((response, contractIdx) => { + const contractFactor = options.contractFactor[contractIdx]; + const currentTimestamp = Math.floor(Date.now() / 1000); + + for (let i = 0; i < response.length; i++) { + const user = addresses[i]; + const endTimestamp = Number(response[i].lockedTimestamp); + const tokensAmount = Number(formatEther(response[i].amount)); + const remainMonths = + Number(endTimestamp - currentTimestamp) / (60 * 60 * 24 * 30); + + if (tokensAmount <= 0 || remainMonths <= 0) { + continue; + } + + const votePower = Math.floor( + tokensAmount * + Math.pow(remainMonths, options.powerFactor) * + contractFactor + ); + + if (votePower && votingPowers[user]) { + votingPowers[user] += votePower; + } else if (votePower) { + votingPowers[user] = votePower; + } + } + }); + + return votingPowers || []; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-lp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-lp/examples.json new file mode 100644 index 00000000..f986b0a6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-lp/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "tranche-staking-lp", + "params": { + "address": "0x05db908E095662745595Ee42C9C90aaDC5D51c98", + "tokenAddress": "0x0aee8703d34dd9ae107386d3eff22ae75dd616d1", + "lpTokenAddress": "0xa21ECe44Cc5dAc03E3805362bd3f68F22a49Ea1e", + "symbol": "SLICE", + "decimals": 18, + "start": 12240380 + } + }, + "network": "1", + "addresses": [ + "0x625c94ab09883bf455141c54be3a145bdc68a277", + "0x6FD3D5DC46e49BE296c934b97533123C86229999", + "0x6c5c952e85643cc3e229d37a6f2aacd248931d55", + "0x8e3a50fd3564b7027cc2a0d79d606d489ebc58bd" + ], + "snapshot": 12240385 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-lp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-lp/index.ts new file mode 100644 index 00000000..57591ddf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-lp/index.ts @@ -0,0 +1,106 @@ +// Inspired by https://github.com/snapshot-labs/snapshot.js/blob/master/src/strategies/uniswap/index.ts +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { subgraphRequest } from '../../utils'; + +export const author = 'ayush-jibrel'; +export const version = '0.1.0'; + +const UNISWAP_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2' +}; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'user', + type: 'address' + }, + { + internalType: 'address', + name: 'token', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const lpTokenAddress = options.lpTokenAddress.toLowerCase(); + + const tokenAddress = options.tokenAddress.toLowerCase(); + + let rate; + + const params = { + pairs: { + __args: { + where: { + id: lpTokenAddress + } + }, + id: true, + totalSupply: true, + reserve0: true, + reserve1: true, + token0: { + id: true + }, + token1: { + id: true + } + } + }; + + const result = await subgraphRequest(UNISWAP_SUBGRAPH_URL[network], params); + + if (result && result.pairs) { + result.pairs.map((object) => { + rate = + +object.token0.id == tokenAddress + ? +object.reserve0 / +object.totalSupply + : +object.reserve1 / +object.totalSupply; + }, []); + } + + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'balanceOf', + [address, lpTokenAddress] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) * rate + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-slice/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-slice/examples.json new file mode 100644 index 00000000..9f2a96b8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-slice/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Staked SLICE Query", + "strategy": { + "name": "tranche-staking-slice", + "params": { + "address": "0xAB4235a9ACf00A45557E90F7dB127f3b293cA45A", + "symbol": "SLICE", + "decimals": 18, + "start": 12779442 + } + }, + "network": "1", + "addresses": [ + "0xb6f1072B42dB824F8Bcc5d28eF9536Ff9a0C1Ed1", + "0x8de003a17ec9b684f1f8daefa277c3a78a27f242", + "0x099745e5657f3cd139d6d9c234593b805f625c0b" + ], + "snapshot": 12779447 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-slice/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-slice/index.ts new file mode 100644 index 00000000..adda4d19 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking-slice/index.ts @@ -0,0 +1,133 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'ayush-jibrel'; +export const version = '0.1.0'; + +const abi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + }, + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + name: 'stakingDetails', + outputs: [ + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'endTime', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'reward', + type: 'uint256' + }, + { + internalType: 'uint8', + name: 'durationIndex', + type: 'uint8' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'stakeCounter', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const counters = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'stakeCounter', + [address] + ]), + { blockTag } + ); + + const userAddresses: string[] = []; + + const calls = () => { + let callsArr: [any, string, [any, any]][] = []; + addresses.map((address, i) => { + for ( + let j = 1; + j <= parseFloat(formatUnits(counters[i].toString(), 0)); + j++ + ) { + userAddresses.push(address); + callsArr = [ + ...callsArr, + [options.address, 'stakingDetails', [address, j]] + ]; + } + }); + return callsArr; + }; + + const response = await multicall(network, provider, abi, calls(), { + blockTag + }); + + const result = response.reduce((acc, cur, i) => { + if (acc[userAddresses[i]]) { + acc[userAddresses[i]] += parseFloat( + formatUnits(cur.amount.toString(), options.decimals) + ); + } else { + acc[userAddresses[i]] = parseFloat( + formatUnits(cur.amount.toString(), options.decimals) + ); + } + return acc; + }, {}); + + return result; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tranche-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking/examples.json new file mode 100644 index 00000000..a14b3529 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "tranche-staking", + "params": { + "address": "0x5ce49f584257606426498bfd16d770c50bb20254", + "tokenAddress": "0x0AeE8703D34DD9aE107386d3eFF22AE75Dd616D1", + "symbol": "SLICE", + "decimals": 18, + "start": 12113995 + } + }, + "network": "1", + "addresses": [ + "0xd2ddb0e1c223a873c77ee80497e9d82c1002e483", + "0xbfdc3fe26553ac0c6041c1e6e0081f95b8c824b3" + ], + "snapshot": 12114000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tranche-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking/index.ts new file mode 100644 index 00000000..7f3810ef --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tranche-staking/index.ts @@ -0,0 +1,62 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'ayush-jibrel'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'user', + type: 'address' + }, + { + internalType: 'address', + name: 'token', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.address, + 'balanceOf', + [address, options.tokenAddress] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/README.md b/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/README.md new file mode 100644 index 00000000..41e476c6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/README.md @@ -0,0 +1,3 @@ +# tutellus-protocol + +It returns the voting power in the Tutellus Protocol for proposals of Tutellus DAO, this includes holding $TUT, staking $TUT in the Staking contract and staking LP tokens from Sushi Pool against $WBTC in the Farming contract. diff --git a/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/examples.json b/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/examples.json new file mode 100644 index 00000000..4016b938 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "tutellus-protocol", + "params": {} + }, + "network": "137", + "addresses": [ + "0xf3adfDFdB3f9f3151db8595C4e850C11a2b1b70E", + "0x18f43A59D698Ea7a4f5A0c11E7AA50973F5B396B", + "0x81EC959A48756984C2c94DB1BF9D4BB18746Ee2A", + "0x66e2495AEbCa70c141F3936Cb6F6d2e6464a5D99", + "0x0F22A52D6Ee30abEc311fd67C998Ab4B5e00382C", + "0xf79f703A6C80a427E826866cF91BC7c5B249187C" + ], + "snapshot": 32188493 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/index.ts b/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/index.ts new file mode 100644 index 00000000..2b384a5b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/index.ts @@ -0,0 +1,107 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits, parseEther } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'victormer'; +export const version = '0.0.1'; + +const tokenABI = [ + 'function balanceOf(address account) external view returns (uint256)' +]; + +const stakingABI = [ + 'function getUserBalance(address user_) external view returns (uint256)' +]; + +const poolABI = [ + 'function totalSupply() external view returns (uint256)', + 'function getReserves() external view returns (uint112, uint112, uint32)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multiToken = new Multicaller(network, provider, tokenABI, { blockTag }); + const multiStaking = new Multicaller(network, provider, stakingABI, { + blockTag + }); + const multiFarming = new Multicaller(network, provider, stakingABI, { + blockTag + }); + const multiPool = new Multicaller(network, provider, poolABI, { blockTag }); + + addresses.forEach((address) => + multiToken.call( + address, + '0x12a34A6759c871C4C1E8A0A42CFc97e4D7Aaf68d', + 'balanceOf', + [address] + ) + ); + + addresses.forEach((address) => + multiStaking.call( + address, + '0x28Caa843cB577d892A8B6eC3F24Aa682ED22Be68', + 'getUserBalance', + [address] + ) + ); + + addresses.forEach((address) => + multiFarming.call( + address, + '0x57eB1b68F2ae0F77bf54F5EE6133bE80d6381d1B', + 'getUserBalance', + [address] + ) + ); + + multiPool.call( + 'totalSupply', + '0x5d9AC8993B714df01D079d1B5b0b592e579Ca099', + 'totalSupply', + [] + ); + multiPool.call( + 'reserves', + '0x5d9AC8993B714df01D079d1B5b0b592e579Ca099', + 'getReserves', + [] + ); + + const [resultToken, resultStaking, resultFarming, resultPool] = + await Promise.all([ + multiToken.execute(), + multiStaking.execute(), + multiFarming.execute(), + multiPool.execute() + ]); + + const poolRatio = resultPool.reserves[0] + .mul(parseEther('1')) + .div(resultPool.totalSupply); + + const result: Record = {}; + + addresses.forEach((address) => { + result[address] = resultToken[address] + .add(resultStaking[address]) + .add(resultFarming[address].mul(poolRatio).div(parseEther('1'))) + .toString(); + }); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, 18)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/schema.json b/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/schema.json new file mode 100644 index 00000000..8eb7ce14 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/tutellus-protocol/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/typhoon/examples.json b/Implementations/API/backend/utils/snapshot/strategies/typhoon/examples.json new file mode 100644 index 00000000..84c37fcc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/typhoon/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Typhoon", + "strategy": { + "name": "typhoon", + "params": { + "symbol": "TYP", + "token": "0x79256DB1BDB6259315a1a3D7Dd237F69cADFd8FC", + "micLP": "0x73F0D7D96206eE96F4A46eF3587A1BBd4385711e", + "micRewardPool": "0x7D66Dcd9064272051656D961a25170DD50cDe526", + "usdtLP": "0xD1482DC5d5f6C140A69157D945267831d1CF2D6e", + "usdtRewardPool": "0x7dF50B4FCAE42581e40e2D91edD7d4FA1B37bae1" + } + }, + "network": "1", + "addresses": [ + "0x544fca5eef17d75a273955ba6fd16fe3c6e620aa", + "0xcc5b73e4dae936f3bd830f048303c076eae3edf9", + "0x5382859146010ac16e142b6708f8e178e77f66bc" + ], + "snapshot": 11718702 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/typhoon/index.ts b/Implementations/API/backend/utils/snapshot/strategies/typhoon/index.ts new file mode 100644 index 00000000..3a3773b4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/typhoon/index.ts @@ -0,0 +1,96 @@ +import { formatUnits, parseUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'PencilDad'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + multi.call('micLP.phoon', options.token, 'balanceOf', [options.micLP]); + multi.call('micLP.totalSupply', options.micLP, 'totalSupply'); + multi.call('usdtLP.phoon', options.token, 'balanceOf', [options.usdtLP]); + multi.call('usdtLP.totalSupply', options.usdtLP, 'totalSupply'); + addresses.forEach((address) => { + multi.call(`balance.${address}`, options.token, 'balanceOf', [address]); + multi.call(`micLP.${address}.balance`, options.micLP, 'balanceOf', [ + address + ]); + multi.call(`micLP.${address}.staked`, options.micRewardPool, 'balanceOf', [ + address + ]); + multi.call(`usdtLP.${address}.balance`, options.usdtLP, 'balanceOf', [ + address + ]); + multi.call( + `usdtLP.${address}.staked`, + options.usdtRewardPool, + 'balanceOf', + [address] + ); + }); + const result = await multi.execute(); + + const phoonPerMicLP = parseUnits(result.micLP.phoon.toString(), 18).div( + result.micLP.totalSupply + ); + const phoonPerUsdtLP = parseUnits(result.usdtLP.phoon.toString(), 18).div( + result.usdtLP.totalSupply + ); + + return Object.fromEntries( + Array(addresses.length) + .fill('') + .map((_, i) => { + const micPhoon = result.micLP[addresses[i]].balance + .add(result.micLP[addresses[i]].staked) + .mul(phoonPerMicLP) + .div(parseUnits('1', 18)); + const usdtPhoon = result.usdtLP[addresses[i]].balance + .add(result.usdtLP[addresses[i]].staked) + .mul(phoonPerUsdtLP) + .div(parseUnits('1', 18)); + const score = result.balance[addresses[i]].add(micPhoon).add(usdtPhoon); + return [addresses[i], parseFloat(formatUnits(score, 18))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/uma-voting/README.md b/Implementations/API/backend/utils/snapshot/strategies/uma-voting/README.md new file mode 100644 index 00000000..45cd408a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uma-voting/README.md @@ -0,0 +1,46 @@ +# uma-voting strategy + +This strategy returns the UMA balance of the voting wallet for the hot wallet. + +Here is an example of parameters: + +## Example + +```JSON +[ + { + "name": "Example query", + "strategy": { + "name": "uma-voting", + "params": { + "address": "0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828", + "votingFactoryAddress": "0xDE7c02aD2b925587Bd16724810f994a2948c4a38", + "symbol": "UMA", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x6888ff4be3262f3232e1c022de731680a16ddda7", + "0x66b5409f52d23ef87bdbfaa7312f8d790755fe13", + "0xedd049c56b804bcdbf7e704cc312491e3d50485a", + "0x18a31db2ecc0c27ee445266337b144f60810c228", + "0xacd3dc5c6f9cb967b5e1f719001e85039b0c976f", + "0x3d88456277e32d62575267c3564b72387c3f21f7", + "0xdf815836fe1457f7e2c0c3d2348e94190a37c687", + "0x515978b57fc91b6a845dc64574b151c7ed3bc2a7", + "0x8a6800c753ed8ed19e0df473a97a7a94b2cecf54", + "0x1fdef3d3c7984a945008c253caa6cf380fddb16f", + "0x82b6a4ebc3904b7aabd4e85510045eff0d43a6bd", + "0x50e453636f153ded412eca1ecc3e520a206b3496", + "0x336b9de28d67d692c31df93a9b8630b2fa50d88f", + "0x2cf6988d57d0df95d3ff37cdfbfe0520f15c860f", + "0x644a7ff3cdff23c3f10f25807a40fa48aff34885", + "0xfd8355b24ab8815ca2d76d1e495dc29b4330241f", + "0xc00667d8b00f35b3565a5c4458dff1cd718e3527" + ], + "snapshot": 14515308 + } +] + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/uma-voting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/uma-voting/examples.json new file mode 100644 index 00000000..02bdafc9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uma-voting/examples.json @@ -0,0 +1,56 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "uma-voting", + "params": { + "address": "0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828", + "votingFactoryAddress": "0xDE7c02aD2b925587Bd16724810f994a2948c4a38", + "symbol": "UMA", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x9a8f92a830a5cb89a3816e3d267cb7791c16b04d", + "0x691c39fdd4f89db2d885324a2b5cef00fbf8caa3", + "0xfb9e5f5874565a6ef8fcf1debc4975501331a8ca", + "0x075ad09e2b27be785c863d756096d0aebcf13118", + "0x644a7ff3cdff23c3f10f25807a40fa48aff34885", + "0xfd8355b24ab8815ca2d76d1e495dc29b4330241f", + "0x1398fc0793d8b67744bfde4a4ff338c69bba753b", + "0xc3d26aa4954816330c5ffc9bb2d46acbceaff130", + "0x9e3a4ed4eea5c27a5e82a32e90482a4827a4ae80", + "0xdcf99ba641c98e37df8ea31188ad7e7fa5501b09", + "0x3b271e8f1409e8e90c484d3591143bd7e1d2a177", + "0x112eae3093451187c8b945fb601c6df34bdc8105", + "0xfdf7f859807d1dc73873640759b2706822802529", + "0x4b070c428ddbb49524abf2d4be4998c1e3b6f3a5", + "0x8d4f93e9962f3392e4c9d10c08f2f1c757f95866", + "0x49e0cdd718d330e5a27856256236b4e56a76d3fb", + "0xabbcf7b0735e4eb152fd13fa02691cc046fac0bd", + "0xc00667d8b00f35b3565a5c4458dff1cd718e3527", + "0x11efc2638184a0382bf1bb29c4047c6111f8f3c7", + "0x7cac2ab1cf9dc459ca88a85d72ac94de2691f1e5", + "0xb0903a62f5ca7a215aa67232ba691cb801decd5b", + "0xbca0fd352dce62fa1b7c7f9fa8f0c1de7fef09d9", + "0x6888ff4be3262f3232e1c022de731680a16ddda7", + "0x66b5409f52d23ef87bdbfaa7312f8d790755fe13", + "0xedd049c56b804bcdbf7e704cc312491e3d50485a", + "0x18a31db2ecc0c27ee445266337b144f60810c228", + "0xacd3dc5c6f9cb967b5e1f719001e85039b0c976f", + "0x718648c8c531f91b528a7757dd2be813c3940608", + "0x3d88456277e32d62575267c3564b72387c3f21f7", + "0xdf815836fe1457f7e2c0c3d2348e94190a37c687", + "0x515978b57fc91b6a845dc64574b151c7ed3bc2a7", + "0x8a6800c753ed8ed19e0df473a97a7a94b2cecf54", + "0x1fdef3d3c7984a945008c253caa6cf380fddb16f", + "0x82b6a4ebc3904b7aabd4e85510045eff0d43a6bd", + "0x50e453636f153ded412eca1ecc3e520a206b3496", + "0x969397c71eb51f7fdcf6527db4d97871b05d6f63", + "0x336b9de28d67d692c31df93a9b8630b2fa50d88f", + "0x2cf6988d57d0df95d3ff37cdfbfe0520f15c860f" + ], + "snapshot": 14515308 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/uma-voting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/uma-voting/index.ts new file mode 100644 index 00000000..c3bf5828 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uma-voting/index.ts @@ -0,0 +1,64 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'abg4'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function designatedVotingContracts(address) view returns (address)' +]; + +function getArgs(options, address: string) { + const args: Array = options.args || ['%{address}']; + return args.map((arg) => + typeof arg === 'string' ? arg.replace(/%{address}/g, address) : arg + ); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const votingAddress = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.votingFactoryAddress, + 'designatedVotingContracts', + getArgs(options, address) + ]), + { blockTag } + ); + + const response = await multicall( + network, + provider, + abi, + votingAddress.map((address: any) => [ + options.address, + 'balanceOf', + getArgs(options, address) + ]), + { blockTag } + ); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat( + formatUnits( + options?.output ? value[options.output].toString() : value.toString(), + options.decimals + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/umami-voting/README.md b/Implementations/API/backend/utils/snapshot/strategies/umami-voting/README.md new file mode 100644 index 00000000..5d74f9b1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/umami-voting/README.md @@ -0,0 +1,14 @@ +# Contract call strategy + +Allows the tokens locked in marinate contract to be used to calculate voter scores. +Includes mUMAMI, cmUMAMI and Staked cmUMAMI + + +```JSON +{ + "address": "0x2adabd6e8ce3e82f52d9998a7f64a90d294a92a4", + "symbol": "mUMAMI", + "decimals": 9, + "cmUMAMIAddress": "0x1922C36F3bc762Ca300b4a46bB2102F84B1684aB", + "stakedcmUMAMIAddress": "0x6A0F4AFB31e90c378FA2Aaa40371a652578F339B" +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/umami-voting/examples.json b/Implementations/API/backend/utils/snapshot/strategies/umami-voting/examples.json new file mode 100644 index 00000000..e3843e91 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/umami-voting/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "umami-voting", + "params": { + "address": "0x2adabd6e8ce3e82f52d9998a7f64a90d294a92a4", + "symbol": "mUMAMI", + "decimals": 9, + "cmUMAMIAddress": "0x1922C36F3bc762Ca300b4a46bB2102F84B1684aB", + "stakedcmUMAMIAddress": "0x6A0F4AFB31e90c378FA2Aaa40371a652578F339B" + } + }, + "network": "42161", + "addresses": [ + "0xaDbb744a9434AAd006772CF76e79D246c4506D1B", + "0x307923a41f1fe2cf876b2cb103e24438b56aba91", + "0x375cf42640e384d04314cb1b76a36d1d2924460a" + ], + "snapshot": 9024000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/umami-voting/index.ts b/Implementations/API/backend/utils/snapshot/strategies/umami-voting/index.ts new file mode 100644 index 00000000..ee0b190a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/umami-voting/index.ts @@ -0,0 +1,83 @@ +import { formatUnits } from '@ethersproject/units'; +import { call, multicall } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'EncryptedBunny'; +export const version = '0.1.0'; + +/// Voting power For mUMAMI holders +/// Includes mUMAMI in autocompounder and stake farm (cmUMAMI, staked cmUMAMI) +const abi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function stakedBalance(address account) view returns (uint256)', + 'function getDepositTokensForShares(uint256 amount) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Balance of mUMAMI in wallets + const mUmamiBalance = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + // Balance of cmUMAMI in wallets -> mUMAMI in compounder + const cmUmamiBalance = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.cmUMAMIAddress, + 'balanceOf', + [address] + ]), + { blockTag } + ); + + // Balance Staked cmUMAMI -> cmUMAMI in farm + const stakedcmUmamiBalance = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + options.stakedcmUMAMIAddress, + 'stakedBalance', + [address] + ]), + { blockTag } + ); + + // Ratio of mUMAMI per cmUMAMI + const ratio = await call(provider, abi, [ + options.cmUMAMIAddress, + 'getDepositTokensForShares', + ['1000000000000000000'] + ]); + + return Object.fromEntries( + Object.entries(mUmamiBalance).map(([address, balance], index) => [ + address, + balance + + (parseFloat(formatUnits(cmUmamiBalance[index][0], options.decimals)) * + ratio) / + 1000000000000000000 + + (parseFloat( + formatUnits(stakedcmUmamiBalance[index][0], options.decimals) + ) * + ratio) / + 1000000000000000000 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/uni/examples.json b/Implementations/API/backend/utils/snapshot/strategies/uni/examples.json new file mode 100644 index 00000000..46e38dcc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uni/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "uni", + "params": { + "symbol": "UNI", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x2Ab4e42c2190C05b3dccD9B039dAf8743217392a", + "0x00000583Cf2C6aA7488471fbe12ac7f0DfeB1cDa", + "0xEF5AD662a5a42C3023e009035ee235082800B924", + "0x7466ebF3B8aF67511f7163Ab1E31f928b2E60330" + ], + "snapshot": 10911534 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/uni/index.ts b/Implementations/API/backend/utils/snapshot/strategies/uni/index.ts new file mode 100644 index 00000000..55064744 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uni/index.ts @@ -0,0 +1,61 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.1'; + +const UNI_ADDRESS = { + '1': '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' +}; + +const abi = [ + { + constant: true, + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address' + } + ], + name: 'getCurrentVotes', + outputs: [ + { + internalType: 'uint96', + name: '', + type: 'uint96' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const response = await multicall( + network, + provider, + abi, + addresses.map((address: any) => [ + UNI_ADDRESS[network], + 'getCurrentVotes', + [address.toLowerCase()] + ]), + { blockTag } + ); + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/unipilot-vault-pilot-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/unipilot-vault-pilot-balance/examples.json new file mode 100644 index 00000000..b2e072d5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unipilot-vault-pilot-balance/examples.json @@ -0,0 +1,95 @@ +[ + { + "name": "Unipilot vault pilot balance query", + "strategy": { + "name": "unipilot-vault-pilot-balance", + "params": { + "decimals": 18, + "unipilotSubgraphURI": "https://api.thegraph.com/subgraphs/name/unipilotvoirstudio/stats-v2", + "vaultAddress": "0x8fd23f313c147d3c2a0fb11dd6b98515b9cb0603", + "unipilotFarming": "0x52D1Cc3E97B97d3a915f2C91E0aE687c89f6F2E0", + "unipilotVaultMethodABI": [ + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "unipilotFarmingMethodABI": [ + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "userInfo", + "outputs": [ + { + "internalType": "uint256", + "name": "reward", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "altReward", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lpLiquidity", + "type": "uint256" + }, + { + "internalType": "address", + "name": "vault", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + } + }, + "network": "1", + "addresses": [ + "0x899917ef0213b78cd9da7ff930efbd505433de56", + "0x019cdfd2a70dd4b899f330877752edfda5bd34c4" + ], + "snapshot": 15034421 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/unipilot-vault-pilot-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/unipilot-vault-pilot-balance/index.ts new file mode 100644 index 00000000..5d795910 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unipilot-vault-pilot-balance/index.ts @@ -0,0 +1,87 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; + +import { subgraphRequest, call, multicall } from '../../utils'; + +export const author = 'daniyalmanzoor'; +export const version = '0.1.0'; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const params = { + vaults: { + __args: { + where: { + id: options.vaultAddress.toLowerCase() + } + }, + totalLockedToken0: true + } + }; + + const { vaults } = await subgraphRequest(options.unipilotSubgraphURI, params); + + const _totalLP: BigNumber = await call( + provider, + options.unipilotVaultMethodABI, + [options.vaultAddress, 'totalSupply'] + ); + + /** + * Unipilot pilot vault balance + */ + const userVaultLP = await multicall( + network, + provider, + options.unipilotVaultMethodABI, + addresses.map((_address) => [ + options.vaultAddress, + 'balanceOf', + [_address] + ]), + { blockTag } + ); + + /** + * Unipilot Farming pilot balance + */ + const userFarmingVaultLP = await multicall( + network, + provider, + options.unipilotFarmingMethodABI, + addresses.map((_address) => [ + options.unipilotFarming, + 'userInfo', + [options.vaultAddress, _address] + ]), + { blockTag } + ); + + return Object.fromEntries( + addresses.map((_address, i) => { + const _userShare = bn(userVaultLP[i]) + .add(bn(userFarmingVaultLP[i].lpLiquidity)) + .mul(bn(10 ** 18)) + .div(_totalLP); + + const _userBalance = formatUnits( + bn(_userShare).mul(bn(vaults[0].totalLockedToken0)).div(bn(10).pow(18)), + options.decimals + ); + + return [_address, parseFloat(_userBalance)]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/unipool-same-token/examples.json b/Implementations/API/backend/utils/snapshot/strategies/unipool-same-token/examples.json new file mode 100644 index 00000000..5590fa4e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unipool-same-token/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "Unipool rewards contract - staked token = earned token", + "strategy": { + "name": "unipool-same-token", + "params": { + "symbol": "TOKN", + "decimals": 18, + "unipoolAddress": "0xe9Bb6B29B4530C5A76EBd6239925c8f99011d358" + } + }, + "network": "5", + "addresses": ["0xcA5720267Fcb7ccA13007AfA096ae01B0864f765"], + "snapshot": 5145880 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/unipool-same-token/index.ts b/Implementations/API/backend/utils/snapshot/strategies/unipool-same-token/index.ts new file mode 100644 index 00000000..dbd210ee --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unipool-same-token/index.ts @@ -0,0 +1,67 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'dapplion'; +export const version = '0.1.0'; + +// Merged ABIs from below contracts: +// * Unipool contract from @k06a: https://github.com/k06a/Unipool/blob/master/contracts/Unipool.sol +const contractAbi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function earned(address account) view returns (uint256)' +]; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const res = await multicall( + network, + provider, + contractAbi, + [ + ...addresses.map((address: any) => [ + options.unipoolAddress, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.unipoolAddress, + 'earned', + [address] + ]) + ], + { blockTag } + ); + + // Balance of staked tokens in unipool contract per user + const unipoolBalanceOf = res.slice(0, addresses.length).map((num) => { + return bn(num); // decimal: 18 + }); + + // Earned tokens from unipool contract per user + const unipoolEarned = res.slice(addresses.length).map((num) => { + return bn(num); // decimal: options.decimal + }); + + const sumList = unipoolBalanceOf.map((userBalanceOf, i) => { + return userBalanceOf.add(unipoolEarned[i]); + }); + + return Object.fromEntries( + sumList.map((sum, i) => { + const parsedSum = parseFloat(formatUnits(sum, options.decimal)); + return [addresses[i], parsedSum]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/unipool-univ2-lp/examples.json b/Implementations/API/backend/utils/snapshot/strategies/unipool-univ2-lp/examples.json new file mode 100644 index 00000000..23a51e5b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unipool-univ2-lp/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Unipool rewards contract - staked token = Univ2 LP for token", + "strategy": { + "name": "unipool-univ2-lp", + "params": { + "symbol": "TOKN", + "decimals": 18, + "tokenAddress": "0x39A2fad92db0ba608869115eB2E6d26eF16CABC5", + "unipoolAddress": "0x2c46430D4dA467Cf247E49A33ECd27DE1e16F451", + "lpTokenAddress": "0x415bd1074305ac16d3da4f6f1fcac7dd9d9b61a6" + } + }, + "network": "4", + "addresses": [ + "0xc46c67bb7e84490d7ebdd0b8ecdaca68cf3823f4", + "0x839395e20bbb182fa440d08f850e6c7a8f6f0780", + "0x09fac217f9a9f47b50651fb8c7c641dd0d39efc8", + "0xaa989e7f79b1efde9c34d3674f085e489f4c612a", + "0x826976d7c600d45fb8287ca1d7c76fc8eb732030", + "0x33d2b8460128dfa03d187a919211ce5155a008dd" + ], + "snapshot": 8953000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/unipool-univ2-lp/index.ts b/Implementations/API/backend/utils/snapshot/strategies/unipool-univ2-lp/index.ts new file mode 100644 index 00000000..03ffdd48 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unipool-univ2-lp/index.ts @@ -0,0 +1,74 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'dapplion'; +export const version = '0.1.0'; + +const contractAbi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function earned(address account) view returns (uint256)', + 'function totalSupply() public view returns (uint256)' +]; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + let res = await multicall( + network, + provider, + contractAbi, + [ + [options.lpTokenAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.lpTokenAddress]], + ...addresses.map((address: any) => [ + options.unipoolAddress, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.unipoolAddress, + 'earned', + [address] + ]) + ], + { blockTag } + ); + + const lpTokenTotalSupply = bn(res[0]); // decimal: 18 + const totalTokensInPool = bn(res[1]); // decimal: options.decimal + res = res.slice(2); + + // How much tokens user has from staked LP tokens + const usersTokensFromLp = res.slice(0, addresses.length).map((num) => { + const stakedLpTokens = bn(num); // decimal: 18 + // StakedLP x token.balanceOf(LPToken) / LPToken.totalSupply() + return stakedLpTokens.mul(totalTokensInPool).div(lpTokenTotalSupply); // decimal: options.decimal + }); + + // How much rewarded tokens user have in the unipool contract + const usersEarnedTokensList = res.slice(addresses.length).map((num) => { + return bn(num); // decimal: options.decimal + }); + + const sumList = usersTokensFromLp.map((userTokensFromLp, i) => { + return userTokensFromLp.add(usersEarnedTokensList[i]); + }); + + return Object.fromEntries( + sumList.map((sum, i) => { + const parsedSum = parseFloat(formatUnits(sum, options.decimal)); + return [addresses[i], parsedSum]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/unipool-xsushi/examples.json b/Implementations/API/backend/utils/snapshot/strategies/unipool-xsushi/examples.json new file mode 100644 index 00000000..6477bd4a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unipool-xsushi/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Unipool rewards contract (xsushi) - staked token = Univ2 LP for token", + "strategy": { + "name": "unipool-xsushi", + "params": { + "symbol": "xSUSHI", + "decimals": 18, + "tokenAddress": "0x39A2fad92db0ba608869115eB2E6d26eF16CABC5", + "unipoolAddress": "0x2c46430D4dA467Cf247E49A33ECd27DE1e16F451", + "lpTokenAddress": "0x415bd1074305ac16d3da4f6f1fcac7dd9d9b61a6" + } + }, + "network": "4", + "addresses": [ + "0xc46c67bb7e84490d7ebdd0b8ecdaca68cf3823f4", + "0x839395e20bbb182fa440d08f850e6c7a8f6f0780", + "0x09fac217f9a9f47b50651fb8c7c641dd0d39efc8", + "0xaa989e7f79b1efde9c34d3674f085e489f4c612a", + "0x826976d7c600d45fb8287ca1d7c76fc8eb732030", + "0x33d2b8460128dfa03d187a919211ce5155a008dd" + ], + "snapshot": 8953000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/unipool-xsushi/index.ts b/Implementations/API/backend/utils/snapshot/strategies/unipool-xsushi/index.ts new file mode 100644 index 00000000..d3eff54a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unipool-xsushi/index.ts @@ -0,0 +1,74 @@ +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'orakurudata'; +export const version = '0.1.0'; + +const contractAbi = [ + 'function balanceOf(address account) view returns (uint256)', + 'function earned(address account) view returns (uint256)', + 'function totalSupply() public view returns (uint256)' +]; + +function bn(num: any): BigNumber { + return BigNumber.from(num.toString()); +} + +export async function strategy( + _space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + let res = await multicall( + network, + provider, + contractAbi, + [ + [options.lpTokenAddress, 'totalSupply', []], + [options.tokenAddress, 'balanceOf', [options.lpTokenAddress]], + ...addresses.map((address: any) => [ + options.unipoolAddress, + 'balanceOf', + [address] + ]), + ...addresses.map((address: any) => [ + options.unipoolAddress, + 'earned', + [address] + ]) + ], + { blockTag } + ); + + const lpTokenTotalSupply = bn(res[0]); // decimal: 18 + const totalTokensInPool = bn(res[1]); // decimal: options.decimal + res = res.slice(2); + + // How much tokens user has from staked LP tokens + const usersTokensFromLp = res.slice(0, addresses.length).map((num) => { + const stakedLpTokens = bn(num); // decimal: 18 + // StakedLP x token.balanceOf(LPToken) / LPToken.totalSupply() + return stakedLpTokens.mul(totalTokensInPool).div(lpTokenTotalSupply); // decimal: options.decimal + }); + + // How much rewarded tokens user have in the unipool contract + const usersEarnedTokensList = res.slice(addresses.length).map((num) => { + return bn(num); // decimal: options.decimal + }); + + const sumList = usersTokensFromLp.map((userTokensFromLp, i) => { + return userTokensFromLp.mul(2).add(usersEarnedTokensList[i]); + }); + + return Object.fromEntries( + sumList.map((sum, i) => { + const parsedSum = parseFloat(formatUnits(sum, options.decimal)); + return [addresses[i], parsedSum]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/README.md b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/README.md new file mode 100644 index 00000000..a2f8f973 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/README.md @@ -0,0 +1,21 @@ +# uniswap-v3-staking + +This strategy counts the token balance of a staked Uniswap V3 position on the canonical [Uniswap V3 Staker contract](https://github.com/Uniswap/v3-staker). It also accounts for the unclaimed token rewards for an incentive program. + +Here is an example of parameters: + +```json +{ + "symbol": "RBN", + "poolAddress": "0x94981f69f7483af3ae218cbfe65233cc3c60d93a", + "tokenReserve": 0, + "rewardToken": "0x6123B0049F904d730dB3C36a31167D9d4121fA6B", + "startTime": 1633694400, + "endTime": 1638878400, + "refundee": "0xDAEada3d210D2f45874724BeEa03C7d4BBD41674" +} +``` + +The `poolAddress`, `rewardToken`, `startTime`, `endTime` and `refundee` comes from the [IncentiveKey](https://github.com/Uniswap/v3-staker/blob/main/contracts/interfaces/IUniswapV3Staker.sol) for a Staker Incentive program. + +The `tokenReserve` refers to which side of the pair to count the token balance of. It must be either 0 or 1. diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/examples.json new file mode 100644 index 00000000..529571ab --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Uniswap-v3-staking query", + "strategy": { + "name": "uniswap-v3-staking", + "params": { + "symbol": "UNI", + "poolAddress": "0x94981f69f7483af3ae218cbfe65233cc3c60d93a", + "tokenReserve": 0, + "rewardToken": "0x6123B0049F904d730dB3C36a31167D9d4121fA6B", + "startTime": 1633694400, + "endTime": 1638878400, + "refundee": "0xDAEada3d210D2f45874724BeEa03C7d4BBD41674" + } + }, + "network": "1", + "addresses": ["0xfce3a97b5f1d3403f481903da3679039f522089c"], + "snapshot": 13411250 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/helper.ts b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/helper.ts new file mode 100644 index 00000000..e622d7e5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/helper.ts @@ -0,0 +1,136 @@ +import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'; +import { Token } from '@uniswap/sdk-core'; +import { Multicaller } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const getFeeAmount = (fee: string): FeeAmount | undefined => { + const feeAmount: FeeAmount | undefined = Object.values(FeeAmount).includes( + parseFloat(fee) + ) + ? parseFloat(fee) + : undefined; + return feeAmount; +}; + +export const getAllReserves = (positionInfo: any) => { + return positionInfo?.map((info: any) => { + return getReserves(info); + }); +}; + +export const getReserves = ({ + tickLower, + tickUpper, + liquidity, + pool: { tick, sqrtPrice, feeTier }, + token0, + token1 +}: any) => { + const [_baseToken, _quoteToken] = [ + new Token(1, token0.id, Number(token0.decimals), token0.symbol), + new Token(1, token1.id, Number(token1.decimals), token1.symbol) + ]; + + const _fee = getFeeAmount(feeTier) ?? 0; + const pool = new Pool( + _baseToken, + _quoteToken, + _fee, + sqrtPrice, + liquidity, + Number(tick) + ); + + if (pool) { + const position = new Position({ + pool, + liquidity, + tickLower: Number(tickLower.tickIdx), + tickUpper: Number(tickUpper.tickIdx) + }); + return { + token0Reserve: parseFloat(position.amount0.toSignificant(4)), + token1Reserve: parseFloat(position.amount1.toSignificant(4)), + poolTick: tick, + position, + inRange: true + }; + } + return { + token0Reserve: 0, + token1Reserve: 0, + poolTick: 0, + position: undefined, + inRange: false + }; +}; + +const V3_STAKER_ABI = [ + 'function deposits(uint256 tokenId) external view returns ((address owner, uint48 numberOfStakes, int24 tickLower, int24 tickUpper))', + 'function getRewardInfo((address,address,uint256,uint256,address), uint256 tokenId) external view returns (uint256 reward, uint160 secondsInsideX128)' +]; + +// Canonical V3 staker contract across all networks +export const UNISWAP_V3_STAKER = '0x1f98407aaB862CdDeF78Ed252D6f557aA5b0f00d'; + +interface Stake { + owner: string; + reward: BigNumber; +} + +export const getStakeInfo = async ( + blockTag: string | number, + network, + provider, + options, + tokenIDs: number[] +): Promise> => { + const incentiveKey = [ + options.rewardToken, + options.poolAddress, + options.startTime, + options.endTime, + options.refundee + ]; + + // This helps us parallelize everything in one execution + const multi = new Multicaller(network, provider, V3_STAKER_ABI, { blockTag }); + tokenIDs.forEach((tokenID) => { + multi.call(`deposit-${tokenID}`, UNISWAP_V3_STAKER, 'deposits', [tokenID]); + multi.call(`reward-${tokenID}`, UNISWAP_V3_STAKER, 'getRewardInfo', [ + incentiveKey, + tokenID + ]); + }); + const results: Record = await multi.execute(); + + const keys = Object.keys(results); + + const depositResults: Record = Object.fromEntries( + keys + .filter((k) => k.includes('deposit')) + .map((k) => { + const tokenID = k.split('-')[1]; + return [tokenID, results[`deposit-${tokenID}`]]; + }) + ); + + const rewardResults: Record = Object.fromEntries( + keys + .filter((k) => k.includes('reward')) + .map((k) => { + const tokenID = k.split('-')[1]; + return [tokenID, results[`reward-${tokenID}`]]; + }) + ); + + return Object.fromEntries( + Object.entries(depositResults).map(([tokenID, deposit]) => [ + tokenID, + { + owner: deposit.owner.toLowerCase(), + reward: rewardResults[tokenID].reward + } + ]) + ); +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/index.ts new file mode 100644 index 00000000..c20514be --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3-staking/index.ts @@ -0,0 +1,102 @@ +import { getAddress } from '@ethersproject/address'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; +import { getAllReserves, getStakeInfo, UNISWAP_V3_STAKER } from './helper'; + +const UNISWAP_V3_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3' +}; + +export const author = 'ribbon-finance'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const tokenReserve = + options.tokenReserve === 0 ? 'token0Reserve' : 'token1Reserve'; + + const _addresses = addresses.map((address) => address.toLowerCase()); + + // The subgraph query does not paginate past the first 1000 items + const params = { + positions: { + __args: { + first: 1000, + where: { + pool: options.poolAddress.toLowerCase(), + owner: UNISWAP_V3_STAKER.toLowerCase() + } + }, + id: true, + owner: true, + liquidity: true, + tickLower: { + tickIdx: true + }, + tickUpper: { + tickIdx: true + }, + pool: { + tick: true, + sqrtPrice: true, + liquidity: true, + feeTier: true + }, + token0: { + symbol: true, + decimals: true, + id: true + }, + token1: { + symbol: true, + decimals: true, + id: true + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.positions.__args.block = { number: snapshot }; + } + + const rawData = await subgraphRequest( + UNISWAP_V3_SUBGRAPH_URL[network], + params + ); + + const positions = rawData.positions; + const tokenIDs = positions.map((pos) => parseInt(pos.id)); + const stakeInfo = await getStakeInfo( + snapshot, + network, + _provider, + options, + tokenIDs + ); + + const reserves = getAllReserves(positions); + const score = {}; + + reserves?.forEach((position: any, idx) => { + const { owner, reward } = stakeInfo[positions[idx].id]; + const unclaimedReward = parseFloat(formatUnits(reward, 18)); + + if (_addresses.includes(owner)) { + const checksumOwner = getAddress(owner); + if (!score[checksumOwner]) { + score[checksumOwner] = position[tokenReserve] + unclaimedReward; + } else { + score[checksumOwner] += position[tokenReserve] + unclaimedReward; + } + } + }); + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/examples.json b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/examples.json new file mode 100644 index 00000000..23542179 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Uniswap-v3 query", + "strategy": { + "name": "uniswap-v3", + "params": { + "symbol": "UNI", + "poolAddress": "0xfc9f572124d8f469960b94537b493f2676776c03", + "tokenReserve": 0 + } + }, + "network": "1", + "addresses": [ + "0x1a62c9d1746de9fd9d449e80ddbb8a67d2a72a94", + "0x790398726a68a2e81aec3ed0f7cc3758bccc5681" + ], + "snapshot": 12952757 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/helper.ts b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/helper.ts new file mode 100644 index 00000000..2da08dea --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/helper.ts @@ -0,0 +1,76 @@ +import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'; +import { Token } from '@uniswap/sdk-core'; + +export const getFeeAmount = (fee: string): FeeAmount | undefined => { + const feeAmount: FeeAmount | undefined = Object.values(FeeAmount).includes( + parseFloat(fee) + ) + ? parseFloat(fee) + : undefined; + return feeAmount; +}; + +export const getAllReserves = (positionInfo: any) => { + return positionInfo?.map((info: any) => { + return getReserves(info); + }); +}; + +export const getReserves = ({ + tickLower, + tickUpper, + liquidity, + pool: { tick, sqrtPrice, feeTier }, + token0, + token1 +}: any) => { + const [_baseToken, _quoteToken] = [ + new Token(1, token0.id, Number(token0.decimals), token0.symbol), + new Token(1, token1.id, Number(token1.decimals), token1.symbol) + ]; + if ( + parseInt(tick) < parseInt(tickLower.tickIdx) || + parseInt(tick) > parseInt(tickUpper.tickIdx) + ) { + return { + token0Reserve: 0, + token1Reserve: 0, + poolTick: 0, + position: undefined, + inRange: false + }; + } + + const _fee = getFeeAmount(feeTier) ?? 0; + const pool = new Pool( + _baseToken, + _quoteToken, + _fee, + sqrtPrice, + liquidity, + Number(tick) + ); + + if (pool) { + const position = new Position({ + pool, + liquidity, + tickLower: Number(tickLower.tickIdx), + tickUpper: Number(tickUpper.tickIdx) + }); + return { + token0Reserve: parseFloat(position.amount0.toSignificant(4)), + token1Reserve: parseFloat(position.amount1.toSignificant(4)), + poolTick: tick, + position, + inRange: true + }; + } + return { + token0Reserve: 0, + token1Reserve: 0, + poolTick: 0, + position: undefined, + inRange: false + }; +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/index.ts b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/index.ts new file mode 100644 index 00000000..84c1e9a9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap-v3/index.ts @@ -0,0 +1,95 @@ +import { subgraphRequest } from '../../utils'; +import { getAllReserves } from './helper'; + +const UNISWAP_V3_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3' +}; + +export const author = 'anassohail99'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const tokenReserve = + options.tokenReserve === 0 ? 'token0Reserve' : 'token1Reserve'; + + const _addresses = addresses.map((address) => address.toLowerCase()); + + const params = { + positions: { + __args: { + where: { + pool: options.poolAddress.toLowerCase(), + owner_in: _addresses + } + }, + id: true, + owner: true, + liquidity: true, + tickLower: { + tickIdx: true + }, + tickUpper: { + tickIdx: true + }, + pool: { + tick: true, + sqrtPrice: true, + liquidity: true, + feeTier: true + }, + token0: { + symbol: true, + decimals: true, + id: true + }, + token1: { + symbol: true, + decimals: true, + id: true + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.positions.__args.block = { number: snapshot }; + } + + const rawData = await subgraphRequest( + UNISWAP_V3_SUBGRAPH_URL[network], + params + ); + + const usersUniswap = addresses.map(() => ({ + positions: [] + })); + + rawData?.positions?.map((position) => { + usersUniswap[_addresses.indexOf(position?.owner)].positions.push(position); + }); + + const reserves = usersUniswap.map((user) => { + return getAllReserves(user?.positions); + }); + + const score = {}; + + reserves?.forEach((user: any, idx) => { + let tokenReserveAdd = 0; + + user.forEach((position: any) => { + tokenReserveAdd += position[tokenReserve]; + }); + + score[addresses[idx]] = tokenReserveAdd; + }); + + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap/examples.json b/Implementations/API/backend/utils/snapshot/strategies/uniswap/examples.json new file mode 100644 index 00000000..a3b2cdbe --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "uniswap", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI" + } + }, + "network": "1", + "addresses": ["0x79317fc0fb17bc0ce213a2b50f343e4d4c277704"], + "snapshot": 1111000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/uniswap/index.ts b/Implementations/API/backend/utils/snapshot/strategies/uniswap/index.ts new file mode 100644 index 00000000..4ce5d925 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/uniswap/index.ts @@ -0,0 +1,80 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const UNISWAP_SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2' +}; + +export const author = 'vfatouros'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + liquidityPositions: { + __args: { + where: { + liquidityTokenBalance_gt: 0 + } + }, + liquidityTokenBalance: true, + pair: { + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + } + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.users.liquidityPositions.__args.block = { number: snapshot }; + } + const tokenAddress = options.address.toLowerCase(); + const result = await subgraphRequest(UNISWAP_SUBGRAPH_URL[network], params); + const score = {}; + if (result && result.users) { + result.users.forEach((u) => { + u.liquidityPositions + .filter( + (lp) => + lp.pair.token0.id == tokenAddress || + lp.pair.token1.id == tokenAddress + ) + .forEach((lp) => { + const token0perUni = lp.pair.reserve0 / lp.pair.totalSupply; + const token1perUni = lp.pair.reserve1 / lp.pair.totalSupply; + const userScore = + lp.pair.token0.id == tokenAddress + ? token0perUni * lp.liquidityTokenBalance + : token1perUni * lp.liquidityTokenBalance; + + const userAddress = getAddress(u.id); + if (!score[userAddress]) score[userAddress] = 0; + score[userAddress] = score[userAddress] + userScore; + }); + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/README.md b/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/README.md new file mode 100644 index 00000000..4653a77f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/README.md @@ -0,0 +1,32 @@ +# UnstackedToadz and StackedToadz Stakers strategy + +This strategy return the balances of the voters for StackedToadz project using only the staking pool of UnstackedToadz and StackedToadz. + +## Accepted options + +- **staking_stackedtoadz:** StackedToadz staking pool address. + +- **staking_unstackedtoadz:** UnStackedToadz staking pool address. + +## Examples + +```JSON +[ + { + "name": "UnstackedToadz and StackedToadz Stakers", + "strategy": { + "name": "unstackedtoadz-and-stackedtoadz-stakers", + "params": { + "staking_stackedtoadz": "0xBC9d59a9865c094d22fAAE988533F18eA1688722", + "staking_unstackedtoadz": "0x1bbb57def2f6192f0b9b8565f49034bf1fcdb604" + } + }, + "network": "1", + "addresses": [ + "0x6d330e23da437fb66e8419e8f52fcd43fa6b8326" + ], + "snapshot": 13688108 + } +] + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/examples.json b/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/examples.json new file mode 100644 index 00000000..41d81df7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "UnstackedToadz and StackedToadz Stakers", + "strategy": { + "name": "unstackedtoadz-and-stackedtoadz-stakers", + "params": { + "symbol": "STACK", + "decimals": 0, + "tokenAddress": "0x1bbb57def2f6192f0b9b8565f49034bf1fcdb604", + "staking_stackedtoadz": "0xBC9d59a9865c094d22fAAE988533F18eA1688722", + "staking_unstackedtoadz": "0x1bbb57def2f6192f0b9b8565f49034bf1fcdb604" + } + }, + "network": "1", + "addresses": ["0x6d330e23da437fb66e8419e8f52fcd43fa6b8326"], + "snapshot": 13745053 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/index.ts b/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/index.ts new file mode 100644 index 00000000..98625459 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/unstackedtoadz-and-stackedtoadz-stakers/index.ts @@ -0,0 +1,58 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; + +export const author = 'Eh-Marine'; +export const version = '0.1.0'; + +const stakingAbi = [ + 'function depositsOf(address account) external view returns (uint256[] memory)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stacked_stakingPool = new Multicaller(network, provider, stakingAbi, { + blockTag + }); + const unstacked_stakingPool = new Multicaller(network, provider, stakingAbi, { + blockTag + }); + + addresses.forEach((address) => { + stacked_stakingPool.call( + address, + options.staking_stackedtoadz, + 'depositsOf', + [address] + ); + unstacked_stakingPool.call( + address, + options.staking_unstackedtoadz, + 'depositsOf', + [address] + ); + }); + + const [stakingResponse_stacked, stackingResponse_unstacked]: [ + Record, + Record + ] = await Promise.all([ + stacked_stakingPool.execute(), + unstacked_stakingPool.execute() + ]); + + return Object.fromEntries( + addresses.map((address) => { + const stakingCount_stacked = stakingResponse_stacked[address].length; + const stakingCount_unstacked = stackingResponse_unstacked[address].length; + return [address, stakingCount_stacked + stakingCount_unstacked]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/validation/README.md b/Implementations/API/backend/utils/snapshot/strategies/validation/README.md new file mode 100644 index 00000000..6408779f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/validation/README.md @@ -0,0 +1,32 @@ +# validation strategy + +Checks validity of users to vote with validationStrategies passed and if user is not valid it will return 0 as score. If user is valid it will return the voting power of the strategies passed into votingStrategies. + +Parameters: + +| Parameter | Description | Default value | +| ------------- | ------------------------------------------ | ------------- | +| `symbol` | Token symbol | optional | +| `validationStrategies` | List of strategies to check validation (Max 3) | | +| `votingStrategies` | List of strategies to return voting power (Max 3)| | +| `validationThreshold` | Minimum voting power in a strategy | 1 | + +Example to return 1 voting power if user hold any USDC: + +```json +{ + "symbol": "UNI", + "validationStrategies": [{ + "name": "erc20-balance-of", + "params": { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "decimals": 6 + } + }], + "validationThreshold": 1, + "votingStrategies": [{ + "name": "ticket", + "params": {} + }] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/validation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/validation/examples.json new file mode 100644 index 00000000..3c6e34b9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/validation/examples.json @@ -0,0 +1,45 @@ +[ + { + "name": "Validation strategy", + "strategy": { + "name": "validation", + "params": { + "symbol": "UNI", + "validationStrategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "decimals": 18 + } + }, + { + "name": "erc20-balance-of", + "params": { + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "decimals": 6 + } + } + ], + "validationThreshold": 1, + "votingStrategies": [ + { + "name": "ticket", + "params": {} + } + ] + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0xbcca60bb61934080951369a648fb03df4f96263c" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/validation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/validation/index.ts new file mode 100644 index 00000000..2cce2323 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/validation/index.ts @@ -0,0 +1,79 @@ +import { getAddress } from '@ethersproject/address'; +import strategies from '..'; + +export const author = 'snapshot-labs'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const { + validationStrategies = [], + votingStrategies = [], + validationThreshold = 1 + } = options; + // Limit validationStrategies to 3 strategies + if (validationStrategies.length === 0 || votingStrategies.length === 0) { + throw new Error('No validation strategies provided.'); + } + if (validationStrategies.length > 3 || votingStrategies.length > 3) { + throw new Error('Too many strategies provided.'); + } + + const promises: any = []; + for (const strategy of validationStrategies) { + promises.push( + strategies[strategy.name].strategy( + space, + network, + provider, + addresses, + strategy.params, + snapshot + ) + ); + } + + const results = await Promise.all(promises); + let validatedAddresses: string[] = []; + results.forEach((result) => { + for (const address in result) { + if (result[address] >= validationThreshold) { + validatedAddresses.push(getAddress(address)); + } + } + }); + validatedAddresses = [...new Set(validatedAddresses)]; + + const scores = {}; + if (validatedAddresses.length > 0) { + const promises: any = []; + for (const strategy of votingStrategies) { + promises.push( + strategies[strategy.name].strategy( + space, + network, + provider, + validatedAddresses, + strategy.params, + snapshot + ) + ); + } + + const results = await Promise.all(promises); + results.forEach((result) => { + for (const address in result) { + if (!scores[address]) scores[address] = 0; + scores[address] += result[address]; + } + }); + } + + return scores; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/README.md b/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/README.md new file mode 100644 index 00000000..b895c988 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/README.md @@ -0,0 +1,17 @@ +# vault-token-lp-balance + +This strategy computes the amount of a token +in a vault lp pool. The vault must use the function `stakedWantTokens(pid, user)`. + +You must supply the token, vault address, lp address, and pid of the pool +in the vault chef. Here is an example of the params: + +```json +{ + "tokenAddress": "0x0159ED2E06DDCD46a25E74eb8e159Ce666B28687", + "tokenDecimals": 18, + "vaultChefAddress": "0x2914646E782Cc36297c6639734892927B3b6Fe56", + "pid": 8, + "lpAddress": "0xE2E34C07754C4CAb2b6D585C06D418628f8ba553" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/examples.json b/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/examples.json new file mode 100644 index 00000000..dc3be191 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Vault Token in LP balance", + "strategy": { + "name": "vault-token-lp-balance", + "params": { + "tokenAddress": "0x0159ED2E06DDCD46a25E74eb8e159Ce666B28687", + "symbol": "FOX", + "tokenDecimals": 18, + "vaultChefAddress": "0x2914646E782Cc36297c6639734892927B3b6Fe56", + "pid": 8, + "lpAddress": "0xE2E34C07754C4CAb2b6D585C06D418628f8ba553" + } + }, + "network": "1666600000", + "addresses": [ + "0xD20B976584bF506BAf5cC604D1f0A1B8D07138dA", + "0x4ff9B7C1424b9E4375BbbDF3357a318412c02E0c", + "0x57B7713c0E013cfbEC0E4C6c8B264dAf7598ebA9", + "0xBfA58c2516cB64C7f6E19c0B3690158C38E4d0f7" + ], + "snapshot": 18263021 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/index.ts b/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/index.ts new file mode 100644 index 00000000..870b6b3f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vault-token-lp-balance/index.ts @@ -0,0 +1,62 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall, Multicaller } from '../../utils'; + +export const author = 'foxthefarmer'; +export const version = '0.0.1'; + +const vaultAbi = [ + 'function poolInfo(uint256) returns (address want,uint256 allocPoint,uint256 lastRewardBlock,uint256 accAQUAPerShare,address strat)', + 'function stakedWantTokens(uint256 _pid, address _user) returns (uint256)' +]; + +const bep20Abi: any = [ + 'function totalSupply() view returns (uint256)', + 'function balanceOf(address) view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const erc20Multi = new Multicaller(network, provider, bep20Abi, { + blockTag + }); + erc20Multi.call('lpTotalSupply', options.lpAddress, 'totalSupply'); + erc20Multi.call('lpTokenBalance', options.tokenAddress, 'balanceOf', [ + options.lpAddress + ]); + + const erc20Result = await erc20Multi.execute(); + + const tokenPerLp = + parseFloat(erc20Result.lpTokenBalance.toString()) / + parseFloat(erc20Result.lpTotalSupply.toString()); + + const userVaultLpBalanceCalls = multicall( + network, + provider, + vaultAbi, + addresses.map((address: any) => [ + options.vaultChefAddress, + 'stakedWantTokens', + [options.pid, address] + ]), + { blockTag } + ); + + const vaultBalances = await Promise.all([userVaultLpBalanceCalls]); + + return Object.fromEntries( + Object.entries(addresses).map((address: any, index) => [ + address[1], + parseFloat(formatUnits(vaultBalances[0][index].toString(), 18)) * + tokenPerLp + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/README.md b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/README.md new file mode 100644 index 00000000..f5381fc0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/README.md @@ -0,0 +1,13 @@ +# ve-balance-of-at-nft + +This returns the voting power of the voters for his ve NFTs by calling `balanceOfAtNFT` on the contract. + +Here is an example of parameters: + +```json +{ + "address": "0xc8034b3dF18Ea4d607E86D6b6Bf23E2A8Ed70F89", + "symbol": "vedcMST", + "decimals": 18 +} +``` \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/examples.json new file mode 100644 index 00000000..10acdb4c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ve-balance-of-at-nft", + "params": { + "address": "0xc8034b3dF18Ea4d607E86D6b6Bf23E2A8Ed70F89", + "symbol": "vedcMST", + "decimals": 18 + } + }, + "network": "250", + "addresses": [ + "0x378f7533041249cd1806550deaA2f73A856c9889", + "0x83BB8062ea95E7a234c48778590cEA150217627D", + "0x9Fd7f1aE7e600056E34b9515443600F01AD95a28" + ], + "snapshot": 39532840 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/index.ts new file mode 100644 index 00000000..76fb4bee --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/index.ts @@ -0,0 +1,71 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'thlynn'; +export const version = '0.1.0'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)', + 'function balanceOfNFT(uint256 _tokenId) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multiCallBalanceOf = new Multicaller(network, provider, abi, { + blockTag + }); + addresses.forEach((address) => + multiCallBalanceOf.call(address, options.address, 'balanceOf', [address]) + ); + const walletBalanceOf: Record = + await multiCallBalanceOf.execute(); + + const multiCallTokenOfOwner = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletAddress, count] of Object.entries(walletBalanceOf)) { + for (let index = 0; index < count.toNumber(); index++) { + multiCallTokenOfOwner.call( + walletAddress.toString() + '-' + index.toString(), + options.address, + 'tokenOfOwnerByIndex', + [walletAddress, index] + ); + } + } + const walletIDToAddresses: Record = + await multiCallTokenOfOwner.execute(); + + // Third, get voting power for each tokenId + const multiCallBalanceOfNFT = new Multicaller(network, provider, abi, { + blockTag + }); + for (const [walletID, tokenId] of Object.entries(walletIDToAddresses)) { + multiCallBalanceOfNFT.call(walletID, options.address, 'balanceOfNFT', [ + tokenId + ]); + } + const walletVotingPower: Record = + await multiCallBalanceOfNFT.execute(); + + const result = {} as Record; + for (const [walletID, value] of Object.entries(walletVotingPower)) { + const address = walletID.split('-')[0]; + result[address] = + (result[address] || 0) + parseFloat(formatUnits(value, options.decimals)); + } + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [address, balance]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/schema.json new file mode 100644 index 00000000..2113da8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at-nft/schema.json @@ -0,0 +1,33 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/README.md b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/README.md new file mode 100644 index 00000000..1c4d1f33 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/README.md @@ -0,0 +1,13 @@ +# ve-balance-of-at + +This returns the voting power of the voters for a ve-like token by calling `balanceOfAt` on the contract. + +Here is an example of parameters: + +```json +{ + "address": "0xB7d85Ab25b9D478961face285fa3D8AAecAd24a9", + "symbol": "xALPACA", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/examples.json new file mode 100644 index 00000000..b3aec923 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ve-balance-of-at", + "params": { + "address": "0xB7d85Ab25b9D478961face285fa3D8AAecAd24a9", + "symbol": "xALPACA", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0xdf7b69c22b96f2c7e07687971551021058260451", + "0x466b32918a35d8c290793a99dd37b6e14bcf3e92", + "0x4ea21fe653af9b524333512db3605830cc2fe592", + "0x215462dc79523ac795216e1baa27586840fa9382", + "0x5b4e7abcd18351f79211b6c9a6d1c3a3d329867d", + "0x4b7841b4a99eba9b9209c0f63183e32a1f7d87bc", + "0x29fec057b86ef46d240bd271837369f5715335ef", + "0xed7f2016130ca5083261079c7b869cc3ab82181a", + "0xb71d05cf5cdf7a9b15b20b9aab5e91332c271c96", + "0xbc4811f78dcb2ac60c026e871f697d87fc36022b", + "0xd1b38534fe56a0e44a3bc9341d5abea286c78a7c", + "0x64088896525d5ba1079415ecc20818ae76251c6f", + "0x4fcde7f599539b18fe4612e1df2663812bbaa1ff", + "0x14b6b8e79959bb0b332787c4954c2a42d7d4a638", + "0xc96caaac2cd4df2bfaab55f401e1ef5888b02ebb", + "0xf72d9eb746386d3835af331ff791e7900d05b14c", + "0x15c109307b99a10c665e1fa11147502b0001bb1a", + "0x83c4b3a588116a870276e85fb9c7a80a343d13cb" + ], + "snapshot": 14848160 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/index.ts new file mode 100644 index 00000000..bcd08961 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/index.ts @@ -0,0 +1,35 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'spicysquid168'; +export const version = '0.0.1'; + +const abi = [ + 'function balanceOfAt(address _user,uint256 _blockNumber) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = + typeof snapshot === 'number' ? snapshot : await provider.getBlockNumber(); + + const multi = new Multicaller(network, provider, abi); + addresses.forEach((address) => + multi.call(address, options.address, 'balanceOfAt', [address, blockTag]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/schema.json new file mode 100644 index 00000000..e69fcbb5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-balance-of-at/schema.json @@ -0,0 +1,30 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"] + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/README.md new file mode 100644 index 00000000..47aeb7b3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/README.md @@ -0,0 +1,13 @@ +# ve-rbn + +This strategy returns the effective voting power of veRBN lockers + +Here is an example of parameters: + +```json +{ + "address": "0x19854C9A5fFa8116f48f984bDF946fB9CEa9B5f7", + "symbol": "veRBN", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/examples.json new file mode 100644 index 00000000..f31acf5f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/examples.json @@ -0,0 +1,35 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ve-ribbon-voting-power", + "params": { + "address": "0x19854C9A5fFa8116f48f984bDF946fB9CEa9B5f7", + "symbol": "veRBN", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x7b82ddcac8d0b137a09590838057c0a9d5bae3c3", + "0x379bb4d5b82676b98ce29cbe824f808305107d56", + "0x03fb1fd0fec2fb4d979b2e30a4adfaf868f85f0f", + "0x47ed49b4eadce83993b005fd4456cdf66404d58d", + "0xd925e71b56a84f8763895a6939320a2465d2618f", + "0x0fa7744874bfedfc3d67ab57253d840be685fdc2", + "0xcde3725b25d6d9bc78cf0941cc15fd9710c764b9", + "0x02ef8147e2d0997cca48d99f01bad846d16558fa", + "0x81324a39570bba1722a3cbe4a4cd372c24d084d4", + "0x8f688a91695f7d2a1e93e57cedcbf5c5202f617b", + "0x24f8d92794a283b454d1fc32722f51a4f3bc1ae5", + "0x5abd0ffea71da700905c359ce0d0408fa78d120a", + "0x85f15c9b2bfa79805205ed1df3570ad0875a24b2", + "0x704d40d9212e511842fee1b667020a471ebe9ce0", + "0xb4fbd802d9dc5c0208346c311bcb6b9ecff468c6", + "0xb41c77b430e7f36f8b78c5c580828a5c62fdc352", + "0x7b977a4bc02f87f7ac9a2afed8d71f87dbe2ac39", + "0xa7cca50b9a8a2ffb5c0c047913fa1c088cd50bff" + ], + "snapshot": 14357198 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/index.ts new file mode 100644 index 00000000..57c63794 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/index.ts @@ -0,0 +1,34 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller, getBlockNumber } from '../../utils'; + +export const author = 'chuddster'; +export const version = '0.1.0'; + +const abi = [ + 'function getPriorVotes(address account, uint256 block) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = + typeof snapshot === 'number' ? snapshot : await getBlockNumber(provider); + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + multi.call(address, options.address, 'getPriorVotes', [address, blockTag]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/schema.json new file mode 100644 index 00000000..2113da8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon-voting-power/schema.json @@ -0,0 +1,33 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/README.md b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/README.md new file mode 100644 index 00000000..144ca6f2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/README.md @@ -0,0 +1,13 @@ +# ve-rbn + +This strategy returns the RBN balance + locked RBN balance in voting escrow of the voters + +Here is an example of parameters: + +```json +{ + "address": "0x6123b0049f904d730db3c36a31167d9d4121fa6b", + "symbol": "RBN", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/examples.json b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/examples.json new file mode 100644 index 00000000..430e3489 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/examples.json @@ -0,0 +1,29 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "ve-ribbon", + "params": { + "address": "0x6123b0049f904d730db3c36a31167d9d4121fa6b", + "symbol": "RBN", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x6b98259456fa6ca17df0fe5a8cfa829c30484eee", + "0x309c65c98c56c4db478ec67d3d0c91738cbafc36", + "0x57d98609d623b9885721a58b896d40c7871e1573", + "0xdd00270d347ea0fc0fdd82e9385fbef0682c24f8", + "0xcec909e109af27c4220d8c0400ec990126187dce", + "0x7d79ab8c86eef9c6d3001822cca2384d314db999", + "0x7ce68b8796144c4fd1af5d82d79ed2cbaf8b1ea5", + "0xba1d28a156b69f0e07961e1adfd1fba24f249443", + "0x22f9076c7c3436dae40a05cf4ea7d4174b458629", + "0xbe370f522c2d8bfda9476048b1d3afc3a3bf537a", + "0x4559d6b52b6a736684345b42d5e3ee8405991466", + "0xdc212554bf38c0e8e8b2dff80ddba739dfdff5da" + ], + "snapshot": 14321000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/index.ts b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/index.ts new file mode 100644 index 00000000..e2d24e4c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/index.ts @@ -0,0 +1,46 @@ +import { BigNumberish, BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'chuddster'; +export const version = '0.1.1'; + +const VOTING_ESCROW = '0x19854C9A5fFa8116f48f984bDF946fB9CEa9B5f7'; + +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function locked(address account) external view returns (int128, uint256)' +]; + +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) => + multi.call(address, options.address, 'balanceOf', [address]) + ); + const resultUnlocked: Record = await multi.execute(); + addresses.forEach((address) => + multi.call(address, VOTING_ESCROW, 'locked', [address]) + ); + const resultLocked: Record = + await multi.execute(); + + return Object.fromEntries( + Object.entries(resultUnlocked).map(([address, balance]) => [ + address, + parseFloat( + formatUnits( + BigNumber.from(balance).add(BigNumber.from(resultLocked[address][0])), + options.decimals + ) + ) + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/schema.json b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/schema.json new file mode 100644 index 00000000..2113da8b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/ve-ribbon/schema.json @@ -0,0 +1,33 @@ +{ + "$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. UNI"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/vesper/examples.json b/Implementations/API/backend/utils/snapshot/strategies/vesper/examples.json new file mode 100644 index 00000000..27a26659 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vesper/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Vesper", + "strategy": { + "name": "vesper", + "params": { + "token": "0x1b40183efb4dd766f11bda7a7c3ad8982e998421", + "symbol": "VSP", + "votingPower": "0xEbedFD259c9FB1F5c0ab9A9f24E79F8d80E29B23", + "decimals": 18, + "blocksPerPeriod": 40320, + "minBlock": 13545210 + } + }, + "network": "1", + "addresses": [ + "0xb92792552e590339a7dbf1e0d6114fbc7395c86b", + "0xf4087b7ab24bde9c445ddd0bc4df257f81277214", + "0x8b01d375e274213c860ef6ac013dbdd5286cd816", + "0xdbc13e67f678cc00591920cece4dca6322a79ac7" + ], + "snapshot": 13545210 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/vesper/index.ts b/Implementations/API/backend/utils/snapshot/strategies/vesper/index.ts new file mode 100644 index 00000000..85f774af --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vesper/index.ts @@ -0,0 +1,74 @@ +import { formatUnits } from '@ethersproject/units'; +import { getBlockNumber } from '../../utils'; +import { Multicaller } from '../../utils'; + +export const author = 'marcelomorgado'; +export const version = '1.0.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const snapshotBlock = + typeof snapshot === 'number' ? snapshot : await getBlockNumber(provider); + const snapshotBlocks: number[] = []; + + const { blocksPerPeriod, minBlock } = options; + + const periods = 4; + + for (let i = 0; i < periods; i++) { + const blockTag = snapshotBlock - blocksPerPeriod * i; + if (blockTag < minBlock) { + break; + } + snapshotBlocks.push(blockTag); + } + + const scores = await Promise.all([ + ...snapshotBlocks.map((blockTag) => + getScores(provider, addresses, options, blockTag) + ) + ]); + + const averageScore = {}; + addresses.forEach((address: string) => { + const userScore = scores + .map((score) => score[address]) + .reduce((accumulator, score) => (accumulator += score), 0); + averageScore[address] = userScore / snapshotBlocks.length; + }); + + return Object.fromEntries( + Array(addresses.length) + .fill('') + .map((_, i) => { + const score = averageScore[addresses[i]]; + return [addresses[i], score]; + }) + ); +} + +async function getScores(provider, addresses, options, blockTag) { + const { votingPower: votingPowerAddress } = options; + const erc20Abi = ['function balanceOf(address) view returns (uint256)']; + + const multi = new Multicaller('1', provider, erc20Abi, { blockTag }); + addresses.forEach((address: string) => { + multi.call(`token.${address}`, votingPowerAddress, 'balanceOf', [address]); + }); + + const result = await multi.execute(); + + const score = {}; + addresses.forEach((address: string) => { + const balance = result.token[address]; + score[address] = parseFloat(formatUnits(balance, 18)); + }); + + return score; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/vested-deversifi/examples.json b/Implementations/API/backend/utils/snapshot/strategies/vested-deversifi/examples.json new file mode 100644 index 00000000..6fd6233d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vested-deversifi/examples.json @@ -0,0 +1,102 @@ +[ + { + "name": "DeversiFi vested strategy", + "strategy": { + "name": "vested-deversifi", + "params": { + "symbol": "VEST", + "xDVFAddress": "0xdddd0e38d30dd29c683033fa0132f868597763ab", + "decimals": 18, + "vestedContracts": [ + "0x273E9a126EC9609f4d82F27D9e2A29B70225d087", + "0x5a6D5344B786D96fe4237f8FE75e11E4B6f2FBD9", + "0x6cba14acf33de1e581774e7b6b33dd1810a3d956", + "0x59fbb80184ceC833981D0940044bB59a69E5E7cd", + "0xad8A0432b0d78189adae17b535f1E9E110920aa2", + "0x0302f75047aA0935052275E77d15C40BeE7b63B2", + "0xD6aCCa55EC6aDf37b114A5fA10414d290C7c773f", + "0xF1b86628c060603D213D23076DF06abBF86a2b6f", + "0xc2391b7307C783D3c8c93dE55c6eA3f7e94f075C", + "0x9977936A421f9b06C81AFc0cCF1988B92Decb31F", + "0x09De35FBBd04E347368f65997A88121e3aCfA232", + "0x9b1Ab202831417076E4d8a4e1650B8bC4eEcD0c0", + "0xe7Ef4A761F195C5C6e9574d874B6a82d9CaA6423", + "0x8aa593464D906512372DdB2A5719F2ecbec49a56", + "0x216CEf65Bd8eaC33fb08a42032913aaD26c7a1e1", + "0x153Db97D1ADBda7FF6dda87E002544E0F5747Cb2", + "0x174dA672c3b8C45619910FD22b8dF57111ea128b", + "0xcC3eB61737B3161438ab859d1D0DD1A1138C30ab", + "0x839Fd7873702C2cC238dC87338fF3cbf4ee33d66", + "0xef0d1247F9fB0148A85bb342eB8B98b56BA338d3", + "0x773b6601f5bbbEdE75006A728BB22D5efBEA7744", + "0xE05685Cd4b6b3f120EE7539C6c44647CAa413AB8", + "0xF2748C4BC1842B38f466a860D30DE2330203C0e0", + "0x5C6a76A0217D41A5F7c4132b042B0b2CdFd488cF", + "0xc6441cB2d7620c92B636BE864C35Da068F73D157", + "0xEF98E4cad451f55178E3Af2Fca79e67b9d4C587B", + "0x6A9a903C82f662D07497Cd066B134E000B1f06A6", + "0xf4232686a18468Fdd4C60B69033235E6c61e4f22", + "0xE711A0489dEAe50340E9950fA14072ab40a75d95", + "0xa7675EC8b81639caA8a6bd61138D8a45AEd297bB", + "0x93a70c645c70307ca47bad645706c9d9aa429cf2", + "0xFc19ce8f91955458Ad135bedDd201D554DB4Cd09", + "0x6ED13b9BdfEA976D6E9e1D509F4Eb1AbB84C584c", + "0xbe48fb5D4D1cf72f77685b943A291C7293490164", + "0x5adc091c463291a425763C5C225A8dD04A10Ac42", + "0xa8e1B76426aC2B1B93502B30b92445D504d5E9da", + "0x21EA8bbF6C5BadEA97367F7f5dC0773DA2B4621D", + "0x5E0f9bE7BCCBcF500FA1A586a8781A44A35a3E32", + "0x140DCa39e274397aCaf3fD955af31400091A1c49", + "0xfA1807d861cAD31255bF75534e41e7115D498Ed7", + "0x1F3473b2369DbBEad23d4Ba7327013Eb6a1082ED", + "0xF12dE004D643A8e942267A1f0A1933ec4168312E", + "0x9DDa29c60A2739eF92cD4B3A27f3bA4CcD245C16", + "0x9f99E8821813E043A2dEd641A3D130CbBa3E0c66", + "0x4f113098d798E7444255C0D0be7C64A1508Fd0ba", + "0x4e1aDfD70cB9Fbbe78e3a0d7f3D995cdC31AbE02", + "0xCfFFbD937A2B03808a5582B550e9F8F151A463AB", + "0x2F76A1dfaFc7dD90060bc3F04a9C698CCff29f49", + "0x7A06B23E138F2CC2A5ccEc7e0648F589FBC8CEde" + ] + } + }, + "network": "1", + "addresses": [ + "0xC24da173A250e9Ca5c54870639EbE5f88be5102d", + "0x6ae83EAB68b7112BaD5AfD72d6B24546AbFF137D", + "0x2895059cB5a492BEd58D1fB22713006EfaD465eA", + "0x9B5ea8C719e29A5bd0959FaF79C9E5c8206d0499", + "0x57BB6525C489EE43BEe420F90379B047d7c40f31", + "0x4B49530484be6d742F5af6AC7eb00e290d7Bf0e6", + "0xF14c9dbDb31b0a18aF44Fcf97Ed12b0abfE1b92e", + "0xCef1B4Bf8F05F623A2A688b56d9dA679D302EBa7", + "0x1D801181Ed73e8cF3CA738a3fc5E279e17e27181", + "0xe918CD1c2fB9840B1ab682BF7CC939bbbAA9bC8A", + "0x72e7BB93E73b2885a22CA29c34759361399a5C0e", + "0xF5Fb27b912D987B5b6e02A1B1BE0C1F0740E2c6f", + "0x0DC874Fb5260Bd8749e6e98fd95d161b7605774D", + "0x8387ad1Bb69F398B510959e3BE50B6216Ef194BB", + "0xe016ec54349E1Fdc09C86878F25760ED317a7911", + "0x1e550B93a628Dc2bD7f6A592f23867241e562AeE", + "0xF692F721855278611C61A3f8318AcA6377DD0E18", + "0xCd3Df2aaE3fEaCe8BA79e1FA3f57e4478cef1211", + "0x229595552101DE62a8dD2DB99dD3b2239d262709", + "0x9327b80ebB00e4B9215087250CD655F2F07d5e75", + "0x120D214767aca9DEc81457E0CB0e436B866097f3", + "0x160834291e67Aa55F830062cA8a47186b5E319A9", + "0xf0cF9631EE58CC8F3EEC5bd8a78797C1447BB476", + "0x329c54289Ff5D6B7b7daE13592C6B1EDA1543eD4", + "0xb5EDbE540411B60139BCD01A087BdCf52a1dE120", + "0x32254b28F793CC18B3575C86c61fE3D7421cbbef", + "0xBE058618FA6CDcBb77Ec443975368fb4AB48d326", + "0x773bCe9123b1D866b3Be7b4E22255c8f7B76eAB2", + "0x0E93910D2f34A2519a23c488d95D0492516a4300", + "0x80163E9126260E34851FFd82DB2f2897C8751E50", + "0xef8527a364CCEE8EbD2C5Dd17244449e884890f4", + "0xEbCEB5c380E305fC9f061E35ca2B71309b17e6b1", + "0x75726dd8C3766b5B35da749Ea420db4B9be6e5e1", + "0x3d4AD2333629eE478E4f522d60A56Ae1Db5D3Cdb" + ], + "snapshot": 12992220 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/vested-deversifi/index.ts b/Implementations/API/backend/utils/snapshot/strategies/vested-deversifi/index.ts new file mode 100644 index 00000000..7cdc3de6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vested-deversifi/index.ts @@ -0,0 +1,65 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; + +export const author = 'deversifi'; +export const version = '0.1.0'; + +const vestedABI = ['function delegateForVoting() view returns (address)']; + +const xDVFABI = ['function balanceOf(address account) view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const { vestedContracts, xDVFAddress, decimals } = options; + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const lowerCaseAddresses = {}; + addresses.forEach( + (item: any) => (lowerCaseAddresses[item.toLowerCase()] = true) + ); + + let beneficiaries = await multicall( + network, + provider, + vestedABI, + vestedContracts.map((address: any) => [address, 'delegateForVoting', []]), + { blockTag } + ); + + if (beneficiaries.length > 0) { + beneficiaries = beneficiaries + .map((item, index) => ({ + beneficiary: item[0], + vestedContract: vestedContracts[index] + })) + .filter( + (item: any) => lowerCaseAddresses[item.beneficiary.toLowerCase()] + ); + + const balances = await multicall( + network, + provider, + xDVFABI, + beneficiaries.map((item: any) => [ + xDVFAddress, + 'balanceOf', + [item.vestedContract] + ]) + ); + + return Object.fromEntries( + beneficiaries.map((b, i) => [ + b.beneficiary, + parseFloat(formatUnits(balances[i].toString(), decimals)) + ]) + ); + } + + return {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/README.md new file mode 100644 index 00000000..ff14e35d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/README.md @@ -0,0 +1,19 @@ +# Voting shares of VOLT token stakers and holders + +This strategy computes users shares of volt token (erc-20) in an lp pool. + +The strategy calculates user votes using this formula : user's volt balance + staked volt balance + User LP share mapped volt balance + +You must supply the voltwap token address, symbol, decimals, network id, swap subgraph url, staking subgraph url and token address of the lp token. + +```json +{ + "symbol": "VOLT", + "tokenDecimals": 18, + "lpTokenAddress": "0x1071392e4cdf7c01d433b87be92beb1f8fd663a8", + "voltAddress":"0x8df95e66cb0ef38f91d2776da3c921768982fba0", + "network":"82", + "swapSubgraph":"https://graph-meter.voltswap.finance/subgraphs/name/meterio/uniswap-v2-subgraph", + "stakingSubgraph":"https://graph-meter.voltswap.finance/subgraphs/name/meter/geyser-v2" +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/examples.json new file mode 100644 index 00000000..b22e9afc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "Voting shares of VOLT token stakers and holders", + "strategy": { + "name": "volt-voting-power", + "params": { + "symbol": "VOLT", + "tokenDecimals": 18, + "lpTokenAddress": "0x1071392e4cdf7c01d433b87be92beb1f8fd663a8", + "voltAddress": "0x8df95e66cb0ef38f91d2776da3c921768982fba0", + "network": "82", + "swapSubgraph": "https://graph-meter.voltswap.finance/subgraphs/name/meterio/uniswap-v2-subgraph", + "stakingSubgraph": "https://graph-meter.voltswap.finance/subgraphs/name/meter/geyser-v2" + } + }, + + "network": "82", + "addresses": [ + "0xc1a39d256959aa5e97784200f91ce63501dbd990", + "0x8cafd0397e1b09199A1B1239030Cc6b011AE696d" + ], + "snapshot": 12140940 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/index.ts new file mode 100644 index 00000000..f01b22c5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/volt-voting-power/index.ts @@ -0,0 +1,164 @@ +import { formatUnits } from '@ethersproject/units'; + +import { strategy as pagination } from '../pagination'; + +import { subgraphRequest } from '../../utils'; + +export const author = 'philipappiah'; +export const version = '0.1.2'; + +const VOLTSWAP_SUBGRAPH = { + '82': 'https://graph-meter.voltswap.finance/subgraphs/name/meterio/uniswap-v2-subgraph', + '361': + 'https://graph-theta.voltswap.finance/subgraphs/name/theta/uniswap-v2-subgraph' +}; + +const STAKING_SUBGRAPH = { + '82': 'https://graph-meter.voltswap.finance/subgraphs/name/meter/geyser-v2', + '361': 'https://graph-theta.voltswap.finance/subgraphs/name/theta/geyser-v2' +}; + +export async function strategy( + _space, + _network, + _provider, + addresses, + options +) { + const lpTokenAddress = options.lpTokenAddress.toLowerCase(); + const voltAddress = options.voltAddress.toLowerCase(); + const tokenDecimals = options.tokenDecimals; + const network = options.network || _network; + const UNI_GRAPH = options.swapSubgraph || VOLTSWAP_SUBGRAPH[network]; + const STAKE_GRAPH = options.stakingSubgraph || STAKING_SUBGRAPH[network]; + const blockTag = 'latest'; + + const voltDataparams = { + users: { + __args: { + where: { + id_in: addresses.map((address) => address.toLowerCase()) + }, + first: 1000 + }, + id: true, + + vaults: { + id: true, + locks: { + __args: { + where: { + token_in: [voltAddress, lpTokenAddress] + } + }, + id: true, + token: true, + amount: true, + stakeUnits: true + } + } + } + }; + + const subgraphDataParams = { + pairs: { + __args: { + where: { + id: lpTokenAddress.toLowerCase() + }, + first: 1 + }, + id: true, + token0: { + id: true + }, + reserve0: true, + token1: { + id: true + }, + reserve1: true, + totalSupply: true + } + }; + + const poolData = await subgraphRequest(STAKE_GRAPH, voltDataparams); + + const subgraphData = await subgraphRequest(UNI_GRAPH, subgraphDataParams); + + let totalVoltComposition = 0; + let totalStake = 0; + + if (subgraphData && subgraphData.pairs) { + subgraphData.pairs.forEach((lp) => { + const isToken0 = lp.token0.id.toLowerCase() === voltAddress; + if (isToken0) { + totalVoltComposition = lp.reserve0; + } else { + totalVoltComposition = lp.reserve1; + } + totalStake = lp.totalSupply; + }); + } + + let results: any = {}; + + results = await pagination( + _space, + network, + _provider, + addresses, + { + limit: 200, + address: voltAddress, + strategy: { + name: 'erc20-balance-of', + params: { + address: voltAddress, + decimals: tokenDecimals + } + } + }, + blockTag + ); + + return Object.fromEntries( + addresses.map((address) => { + const balance = results?.[address] || 0; + let userLpShare = 0; + let userCurrentStakeInVolt = 0; + let userCurrentStakeInLP = 0; + let stakesOfVoltInLp = 0; + + if (poolData && poolData.users.length) { + const user = poolData.users.find( + (r) => r.id.toLowerCase() === address.toLowerCase() + ); + if (user && user.vaults.length) { + user.vaults.forEach((v) => { + v.locks + .filter((r) => r.token.toLowerCase() === voltAddress) + .forEach((vlock) => { + userCurrentStakeInVolt = + userCurrentStakeInVolt + + Number(formatUnits(vlock.amount, tokenDecimals)); + }); + v.locks + .filter((r) => r.token.toLowerCase() === lpTokenAddress) + .forEach((lplock) => { + userCurrentStakeInLP = + userCurrentStakeInLP + + Number(formatUnits(lplock.amount, tokenDecimals)); + }); + + userLpShare = (userCurrentStakeInLP / totalStake) * 100; + stakesOfVoltInLp = (userLpShare / 100) * totalVoltComposition; + }); + } + + // user address => user's volt balance + staked volt balance + User LP share mapped volt balance + } + + return [address, balance + userCurrentStakeInVolt + stakesOfVoltInLp]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/README.md b/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/README.md new file mode 100644 index 00000000..cbc5b0f7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/README.md @@ -0,0 +1,11 @@ +# Voting power or Share of total supply strategy + +Allows to get Voting power or Share of total supply from the token. + +## Strategy Parameters + +| Param | Type | Description | | | +| ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------- | --- | --- | +| address | string | The Ethereum address of the token to measure voting power or share of total supply from an address at a block. | | | +| powerType | string | Use `votingPower` for Voting Power or `shareOfTotalSupply` for Share of total supply | | | +| | | | | | diff --git a/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/examples.json b/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/examples.json new file mode 100644 index 00000000..186a3612 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Exemple of BaksDAO strategy", + "strategy": { + "name": "vote-power-and-share", + "params": { + "address": "0x300225DBb0a83c4706edDC5f6A34538964B86412", + "powerType": "votingPower", + "symbol": "BDV", + "decimals": 18 + } + }, + "network": "97", + "addresses": [ + "0x96b6d32E9EF3ECdD7245F5F94C72099441e9D8c1", + "0x2150Cb38ee362bceAC3d4A2704A82eeeD02E93EC", + "0xdf2ff5946df21f818d5f67e62d22486ca470798e" + ], + "snapshot": 14188794 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/index.ts b/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/index.ts new file mode 100644 index 00000000..ae4663ad --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vote-power-and-share/index.ts @@ -0,0 +1,70 @@ +import { formatUnits } from '@ethersproject/units'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { multicall } from '../../utils'; + +export const author = 'jinanton'; +export const version = '0.1.0'; + +const abi = ['function totalSupply() public returns (uint256)']; + +enum PowerType { + VotingPower = 'votingPower', + ShareOfTotalSupply = 'shareOfTotalSupply' +} + +interface Options { + address: string; + decimals: number; + symbol: string; + powerType: PowerType; +} + +export async function strategy( + space, + network, + provider, + addresses, + options: Options, + snapshot +) { + const poolShares = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const totalPoolShares = await multicall( + network, + provider, + abi, + [[options.address, 'totalSupply']], + { blockTag } + ); + + if ( + !totalPoolShares || + !Object.keys(poolShares).length || + (options.powerType != PowerType.ShareOfTotalSupply && + options.powerType != PowerType.VotingPower) + ) + return {}; + const totalShares = parseFloat( + formatUnits(totalPoolShares.toString(), options.decimals) + ); + + if (options.powerType == PowerType.ShareOfTotalSupply) { + return Object.fromEntries( + Object.entries(poolShares).map((account) => [ + account[0], + account[1] / totalShares + ]) + ); + } + return Object.fromEntries( + Object.entries(poolShares).map((account) => [account[0], account[1]]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/README.md b/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/README.md new file mode 100644 index 00000000..18924208 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/README.md @@ -0,0 +1,27 @@ +# vsta-pool-staking + +This strategy returns voters underlying VSTA token balance for lping and staking to our reward farms + +## Params + +- `symbol` - (**Optional**, `string`) Symbol of ERC20 token +- `decimals` - (**Required**, `number`) Decimal precision for ERC20 token +- `balancerVaultAddress` - (**Required**, `string`) Address of Balancer Vault to get token balances of LPs +- `poolAddress` - (**Required**, `string`) Address of VSTA-TOKEN lp token +- `poolId` - (**Required**, `string`) Balancer pool ID of VSTA-TOKEN lp +- `farmAddress` - (**Required**, `string`) Vesta farm address +- `vstaAddress` - (**Required**, `string`) Vesta token address + +Here is an example of parameters: + +```json +{ + "balancerVaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "poolAddress": "0xC61ff48f94D801c1ceFaCE0289085197B5ec44F0", + "poolId": "0xc61ff48f94d801c1ceface0289085197b5ec44f000020000000000000000004d", + "farmAddress": "0x65207da01293C692a37f59D1D9b1624F0f21177c", + "vstaAddress": "0xa684cd057951541187f288294a1e1C2646aA2d24", + "symbol": "VSTA", + "decimals": 18 +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/examples.json new file mode 100644 index 00000000..8f74a71b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "VSTA pool staking strategy", + "strategy": { + "name": "vsta-pool-staking", + "params": { + "balancerVaultAddress": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "poolAddress": "0xC61ff48f94D801c1ceFaCE0289085197B5ec44F0", + "poolId": "0xc61ff48f94d801c1ceface0289085197b5ec44f000020000000000000000004d", + "farmAddress": "0x65207da01293C692a37f59D1D9b1624F0f21177c", + "vstaAddress": "0xa684cd057951541187f288294a1e1C2646aA2d24", + "symbol": "VSTA", + "decimals": 18 + } + }, + "network": "42161", + "addresses": [ + "0x88684f6abb5dcd4d3195cc2c714dd79de5a76999", + "0x126824925a67f98dca1eb9d92d78d51c99f3a427", + "0x7a16e350ab5929a40cd9a6f51957d5a45a6e361b" + ], + "snapshot": 44025597 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/index.ts new file mode 100644 index 00000000..c4e3ca9f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/index.ts @@ -0,0 +1,55 @@ +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import { Multicaller } from '../../utils'; + +export const author = 'shinitakunai'; +export const version = '0.1.0'; + +const abi = [ + 'function getPoolTokens(bytes32 poolId) external view returns (address[], uint256[], uint256)', + 'function totalSupply() external view returns (uint256)', + 'function balances(address owner) external view returns (uint256)' +]; + +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 }); + multi.call('poolTokens', options.balancerVaultAddress, 'getPoolTokens', [ + options.poolId + ]); + multi.call('lpTokenTotalSupply', options.poolAddress, 'totalSupply', []); + addresses.forEach((address) => + multi.call(`userBalances.${address}`, options.farmAddress, 'balances', [ + address + ]) + ); + + const { + poolTokens: [tokens, balances], + lpTokenTotalSupply, + userBalances + } = await multi.execute(); + + const vstaIndex = tokens.findIndex( + (address) => address.toLowerCase() === options.vstaAddress.toLowerCase() + ); + + const vstaPerLp = balances[vstaIndex].div(lpTokenTotalSupply); + + const result = Object.fromEntries( + Object.entries(userBalances).map(([address, lpBalance]) => [ + getAddress(address), + parseFloat(formatUnits(vstaPerLp.mul(lpBalance), options.decimals)) + ]) + ); + + return result; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/schema.json b/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/schema.json new file mode 100644 index 00000000..a2d73368 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/vsta-pool-staking/schema.json @@ -0,0 +1,73 @@ +{ + "$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. VSTA"], + "maxLength": 16 + }, + "decimals": { + "type": "number", + "title": "decimals", + "examples": ["e.g. 18"] + }, + "balancerVaultAddress": { + "type": "string", + "title": "balancerVaultAddress", + "examples": ["e.g. 0x2d94AA3e47d9D5024503Ca8491fcE9A2fB4DA198"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "poolAddress": { + "type": "string", + "title": "poolAddress", + "examples": ["e.g. 0x472D0B0DDFE0BC02C27928b8BcbD67E65D07d48a"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "poolId": { + "type": "string", + "title": "poolId", + "examples": [ + "e.g. 0xc61ff48f94d801c1ceface0289085197b5ec44f000020000000000000000004d" + ], + "pattern": "^0x[a-fA-F0-9]{64}$", + "minLength": 66, + "maxLength": 66 + }, + "farmAddress": { + "type": "string", + "title": "farmAddress", + "examples": ["e.g. 0x472D0B0DDFE0BC02C27928b8BcbD67E65D07d48a"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "vstaAddress": { + "type": "string", + "title": "vstaAddress", + "examples": ["e.g. 0x472D0B0DDFE0BC02C27928b8BcbD67E65D07d48a"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": [ + "decimals", + "balancerVaultAddress", + "poolAddress", + "farmAddress", + "vstaAddress" + ], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/README.md b/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/README.md new file mode 100644 index 00000000..b8552f6f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/README.md @@ -0,0 +1,28 @@ +# WAGDIE Balance from Subgraph +### Modified from balance-of-subgraph + +Calculates users balance of users WAGDIE in wallet and staked in the Forsaken Lands. + +``` +characters{ + id + owner {id} + location {id} +} +``` + + +## Example + +The space config will look like this: + +```JSON +{ + // subgraphURL for the request + "subGraphURL": "https://api.thegraph.com/subgraphs/name/wagdie/wagdieworld-mainnet", + // scoreMultiplier can be used to increase users' scores by a certain magnitude + "scoreMultiplier": 1, + // Array of location IDs to limit votes to specific locations. Can be set to ["all"] to include all locations. + "location": ["1", "2"] +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/examples.json b/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/examples.json new file mode 100644 index 00000000..8ba202c7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "wagdie-subgraph", + "params": { + "subGraphURL": "https://api.thegraph.com/subgraphs/name/wagdie/wagdieworld-mainnet", + "scoreMultiplier": 1, + "location": ["all"] + } + }, + "network": "1", + "addresses": [ + "0xca8307d015aa664a186ad67da19ac6fc4b6c0198", + "0x326937d5f4ad932db9d814ee7d852bc77b9d2e0d", + "0x4F0EceDCd73dA0315134741d9D3830B08fE32e95", + "0xa027b41dc6fe7abec1b6ac69335383604766623c" + ], + "snapshot": 16470969 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/index.ts b/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/index.ts new file mode 100644 index 00000000..90de39f6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/wagdie-subgraph/index.ts @@ -0,0 +1,93 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const SUBGRAPH_URL = { + '1': 'https://api.thegraph.com/subgraphs/name/wagdie/wagdieworld-mainnet' +}; + +export const author = 'IcculusHerEmissary'; +export const version = '0.2.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + // initialize scores + const scores = {}; + for (const address of addresses) { + scores[getAddress(address)] = 0; + } + + // If graph doesn't exist return + if (!SUBGRAPH_URL[network]) { + return scores; + } + + const params = { + characters: { + __args: { + where: { + owner_in: addresses.map((address) => address.toLowerCase()), + id_gt: '', + burned: false + }, + orderBy: 'id', + orderDirection: 'asc', + first: 1000 + }, + id: true, + owner: { + id: true + }, + location: { + id: true + }, + searedConcord: { + id: true + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.characters.__args.block = { number: snapshot }; + } + + let hasNext = true; + while (hasNext) { + const result = await subgraphRequest(SUBGRAPH_URL[network], params); + + const characters = result && result.characters ? result.characters : []; + const latest = characters[characters.length - 1]; + console.log(options.location); + + for (const character of characters) { + const userAddress = getAddress(character.owner.id); + const charId = character?.location?.id; + let characterValue = options.scoreMultiplier; + if ( + options.location.includes('all') || + options.location.includes(charId) + ) { + // Staked character 1 bonus point + if (character.location?.id) { + characterValue += 1; + } + // Seared character 4 bonus points + if (character.searedConcord?.id) { + characterValue += 4; + } + scores[userAddress] = (scores[userAddress] ?? 0) + characterValue; + } + } + + hasNext = characters.length === params.characters.__args.first; + params.characters.__args.where.id_gt = latest ? latest.id : ''; + } + + return scores || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-land-ingame/examples.json b/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-land-ingame/examples.json new file mode 100644 index 00000000..254f3fb8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-land-ingame/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Wanakafarm land ingame query", + "strategy": { + "name": "wanakafarm-land-ingame", + "params": { + "address": "0x339C72829AB7DD45C3C52f965E7ABe358dd8761E", + "symbol": "WANA" + } + }, + "network": "56", + "addresses": ["0xf0867d0e0689e63cd4a6a4aefe3d2eaf11bf9d2b"], + "snapshot": 15785276 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-land-ingame/index.ts b/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-land-ingame/index.ts new file mode 100644 index 00000000..1a9a67f5 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-land-ingame/index.ts @@ -0,0 +1,51 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const FLASHSTAKE_SUBGRAPH_URL = { + '1': 'https://score-api.wanakafarm.com/land-ingame/graphql', + '56': 'https://score-api.wanakafarm.com/land-ingame/graphql' +}; + +export const author = 'TranTien139'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + balances: { + __args: {}, + address: true, + point: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.balances.__args.block = snapshot; + } + + if (addresses && addresses?.length > 0) { + // @ts-ignore + params.balances.__args.addresses = addresses; + } + + const result = await subgraphRequest( + FLASHSTAKE_SUBGRAPH_URL[network], + params + ); + + const score = {}; + if (result && result.balances) { + result.balances.map((_data) => { + const address = getAddress(_data.address); + score[address] = Number(_data.point); + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-staking/examples.json new file mode 100644 index 00000000..007d6b4c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-staking/examples.json @@ -0,0 +1,15 @@ +[ + { + "name": "Wanakafarm stake query", + "strategy": { + "name": "wanakafarm-staking", + "params": { + "address": "0x339C72829AB7DD45C3C52f965E7ABe358dd8761E", + "symbol": "WANA" + } + }, + "network": "56", + "addresses": ["0xf0867d0e0689e63cd4a6a4aefe3d2eaf11bf9d2b"], + "snapshot": 15785276 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-staking/index.ts new file mode 100644 index 00000000..39ee5855 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/wanakafarm-staking/index.ts @@ -0,0 +1,51 @@ +import { getAddress } from '@ethersproject/address'; +import { subgraphRequest } from '../../utils'; + +const FLASHSTAKE_SUBGRAPH_URL = { + '1': 'https://score-api.wanakafarm.tech/graphql', + '56': 'https://score-api.wanakafarm.tech/graphql' +}; + +export const author = 'TranTien139'; +export const version = '0.1.0'; + +export async function strategy( + _space, + network, + _provider, + addresses, + options, + snapshot +) { + const params = { + balances: { + __args: {}, + address: true, + point: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.balances.__args.block = snapshot; + } + + if (addresses && addresses?.length > 0) { + // @ts-ignore + params.balances.__args.addresses = addresses; + } + + const result = await subgraphRequest( + FLASHSTAKE_SUBGRAPH_URL[network], + params + ); + + const score = {}; + if (result && result.balances) { + result.balances.map((_data) => { + const address = getAddress(_data.address); + score[address] = Number(_data.point); + }); + } + return score || {}; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/README.md b/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/README.md new file mode 100644 index 00000000..a0c75720 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/README.md @@ -0,0 +1,3 @@ +# Whitelist Weighted Strategy + +This strategy returns weighted votes for addresses matching a static whitelist. diff --git a/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/examples.json new file mode 100644 index 00000000..7b0dc756 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Whitelist weighted address strategy", + "strategy": { + "name": "whitelist-weighted", + "params": { + "symbol": "ABC", + "addresses": { + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11": 5, + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7": 2 + } + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/index.ts new file mode 100644 index 00000000..3f41e16c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/whitelist-weighted/index.ts @@ -0,0 +1,14 @@ +export const author = 'vsergeev'; +export const version = '0.1.0'; + +export async function strategy(space, network, provider, addresses, options) { + const whitelist = Object.fromEntries( + Object.entries(options?.addresses).map(([addr, weight]) => [ + addr.toLowerCase(), + weight + ]) + ); + return Object.fromEntries( + addresses.map((address) => [address, whitelist[address.toLowerCase()] || 0]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/whitelist/examples.json b/Implementations/API/backend/utils/snapshot/strategies/whitelist/examples.json new file mode 100644 index 00000000..fc4004be --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/whitelist/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "whitelist", + "params": { + "symbol": "POINT", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7" + ] + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad" + ], + "snapshot": 11437846 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/whitelist/index.ts b/Implementations/API/backend/utils/snapshot/strategies/whitelist/index.ts new file mode 100644 index 00000000..35967da2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/whitelist/index.ts @@ -0,0 +1,12 @@ +export const author = 'bonustrack'; +export const version = '0.1.0'; + +export async function strategy(space, network, provider, addresses, options) { + const whitelist = options?.addresses.map((address) => address.toLowerCase()); + return Object.fromEntries( + addresses.map((address) => [ + address, + whitelist.includes(address.toLowerCase()) ? 1 : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/winr-staking/README.md b/Implementations/API/backend/utils/snapshot/strategies/winr-staking/README.md new file mode 100644 index 00000000..2cfa7175 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/winr-staking/README.md @@ -0,0 +1,20 @@ +# Winr Governance Staking Strategy + +## Overview + +This strategy is used for Winr governance staking. It allows users that staked WINR and vWINR to vote on proposals by delegating their voting power to a delegatee. + +## Parameters + +- `address`: Address of the staking contract + +## Example + +```json +{ + "name": "winr-staking", + "params": { + "address": "0xddAEcf4B02A3e45b96FC2d7339c997E072b0d034" + } +} +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/winr-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/winr-staking/examples.json new file mode 100644 index 00000000..1c138f4a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/winr-staking/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "WINR Staking", + "strategy": { + "name": "winr-staking", + "params": { + "address": "0xddAEcf4B02A3e45b96FC2d7339c997E072b0d034" + } + }, + "network": "42161", + "addresses": [ + "0x954B6dE2E9C58B0ca7B21e9A048fD0A6CEa6f92C", + "0x40Db8365d1252bcb06598927698238a99D39228E", + "0xd2f5b5fECc86b02F1A77823fb96a7052C62571D1", + "0xc0d4499A3E452bEd3CE794c2360916452bFbf6A5", + "0x9dC2aEa18955DD9F898d7ef130e4e166B9800354", + "0x3EC3486f491f16B7A325F15380090eD1A9Dc79D6", + "0x52caB1b95fb1049c76ED0f72c27B37D1B63F4691", + "0x6035507146b6B5B96e3Fe5A093c83714f27898ED" + ], + "snapshot": 95804680 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/winr-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/winr-staking/index.ts new file mode 100644 index 00000000..70f4c57c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/winr-staking/index.ts @@ -0,0 +1,88 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; + +export const author = 'flushjb'; +export const version = '0.1.0'; + +const abi = [ + 'function getActiveIndexes(address staker) external view returns (uint256[])', + 'function vestingStakedAmount(address _account, uint256[] _indexes) external view returns (uint256 _totalStaked)', + 'function getDividendStake(address _account, bool _isVested) external view returns (uint256 amount, uint256 profitDebt, uint256 weight, uint128 depositTime)' +]; + +// 'function getActiveIndexes(address staker) external view returns (uint256[])', + +type VotingPowers = Record; + +export async function strategy( + space, + network, + provider, + addresses: string[], + options, + snapshot +): Promise { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multicaller = new Multicaller(network, provider, abi, { blockTag }); + + // Get each user's active indexes and dividend stakes + addresses.map(async (address) => { + multicaller.call( + `activeIndexes.${address}`, + options.address, + 'getActiveIndexes', + [address] + ); + multicaller.call( + `dividendStakes.${address}`, + options.address, + 'getDividendStake', + [address, true] + ); + multicaller.call( + `dividendStakesVested.${address}`, + options.address, + 'getDividendStake', + [address, false] + ); + }); + + const result: { + activeIndexes: Record; + dividendStakes: Record; + dividendStakesVested: Record; + } = await multicaller.execute(); + + const multicaller2 = new Multicaller(network, provider, abi, { blockTag }); + + // Get each user's staked amount from each active index + addresses.map(async (address) => { + const activeIndexes = result.activeIndexes[address]; + multicaller2.call( + `stakedAmount.${address}`, + options.address, + 'vestingStakedAmount', + [address, activeIndexes] + ); + }); + + const result2: { + stakedAmount: Record; + } = await multicaller2.execute(); + + // Calculate the total staked amount for each user and map it to the address + return Object.fromEntries( + Object.entries(result2.stakedAmount).map(([address, stakedAmount]) => { + const dividendStakes = result.dividendStakes[address].amount; + const dividendStakesVested = result.dividendStakesVested[address].amount; + const totalStaked = + parseFloat(formatUnits(stakedAmount, 18)) + + parseFloat(formatUnits(dividendStakes, 18)) + + parseFloat(formatUnits(dividendStakesVested, 18)); + + return [address, totalStaked]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/winr-staking/schema.json b/Implementations/API/backend/utils/snapshot/strategies/winr-staking/schema.json new file mode 100644 index 00000000..dab2305a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/winr-staking/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "address": { + "type": "string", + "title": "WINR Staking Contract address", + "examples": ["e.g. 0xddAEcf4B02A3e45b96FC2d7339c997E072b0d034"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": ["address"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/with-delegation/README.md b/Implementations/API/backend/utils/snapshot/strategies/with-delegation/README.md new file mode 100644 index 00000000..a63464cc --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/with-delegation/README.md @@ -0,0 +1,45 @@ +# with-delegation + +If you want to delegate your voting power to another wallet address, +Unlike `delegation` strategy, delegator can't take back their voting power from the delegatee. and also delegate's voting power is added up. so no need to have an additional strategy + +```TEXT +Total VP = delegated VP + own VP (if not delegated to anyone) +``` + +The sub strategies defined in params are used to delegate vote from one address to another. + +> Important Note: Don't pass strategies that need override + +| Param Name | Description | +| ----------- | ----------- | +| strategies | list of sub strategies to calculate voting power based on delegation | +| delegationSpace (optional) | Get delegations of a particular space (by default it take delegations of current space) | +| delegationNetwork (optional) | Get delegations of a particular network (by default it take delegations of current network) | + +Here is an example of parameters: + +```json +{ + "symbol": "YFI (delegated)", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0xBa37B002AbaFDd8E89a1995dA52740bbC013D992", + "symbol": "YFI", + "decimals": 18 + } + }, + { + "name": "yearn-vault", + "params": { + "address": "0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1", + "symbol": "YFI (yYFI)", + "decimals": 18 + } + } + ] +} + +``` diff --git a/Implementations/API/backend/utils/snapshot/strategies/with-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/with-delegation/examples.json new file mode 100644 index 00000000..c9af9fe7 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/with-delegation/examples.json @@ -0,0 +1,28 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "with-delegation", + "params": { + "symbol": "POH (delegated)", + "delegationSpace": "poh.eth", + "strategies": [ + { + "name": "erc20-balance-of", + "params": { + "address": "0x1dAD862095d40d43c2109370121cf087632874dB", + "decimals": 0 + } + } + ] + } + }, + "network": "1", + "addresses": [ + "0x3c13f2B56AF614aC6381265EcB3B619bA26CC641", + "0x048fee7c3279a24af0790b6b002ded42be021d2b", + "0x139a9032a46c3afe3456eb5f0a35183b5f189cae" + ], + "snapshot": 15705816 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/with-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/with-delegation/index.ts new file mode 100644 index 00000000..71ae7d6d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/with-delegation/index.ts @@ -0,0 +1,76 @@ +import { getAddress } from '@ethersproject/address'; +import { getDelegationsData } from '../../utils/delegation'; +import { getScoresDirect, getSnapshots } from '../../utils'; + +export const author = 'snapshot-labs'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + addresses = addresses.map(getAddress); + const delegationSpace = options.delegationSpace || space; + const delegationNetwork = options.delegationNetwork || network; + let delegationSnapshot = snapshot; + if (delegationNetwork !== network) { + const snapshots = await getSnapshots(network, snapshot, provider, [ + delegationNetwork + ]); + delegationSnapshot = snapshots[delegationNetwork]; + } + + const delegationsData = await getDelegationsData( + delegationSpace, + delegationNetwork, + addresses, + delegationSnapshot + ); + const delegations = delegationsData.delegations; + + // Get scores for all addresses and delegators + if (Object.keys(delegations).length === 0) return {}; + const allAddresses = Object.values(delegations).reduce( + (a: string[], b: string[]) => a.concat(b), + [] + ); + allAddresses.push(...addresses); + const scores = ( + await getScoresDirect( + space, + options.strategies, + network, + provider, + allAddresses, + snapshot + ) + ).filter((score) => Object.keys(score).length !== 0); + + const finalScore = Object.fromEntries( + addresses.map((address) => { + const addressScore = delegations[address] + ? delegations[address].reduce( + (a, b) => a + scores.reduce((x, y) => (y[b] ? x + y[b] : x), 0), + 0 + ) + : 0; + return [address, addressScore]; + }) + ); + + // Add own scores if not delegated to anyone + addresses.forEach((address) => { + if (!delegationsData.allDelegators.includes(address)) { + finalScore[address] += scores.reduce( + (a, b) => a + (b[address] ? b[address] : 0), + 0 + ); + } + }); + + return finalScore; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/work/examples.json b/Implementations/API/backend/utils/snapshot/strategies/work/examples.json new file mode 100644 index 00000000..eefbc51f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/work/examples.json @@ -0,0 +1,39 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "work", + "params": { + "whitelist": "0x473e11Aa2B90Ee6E3c46D8B860D968A4Bfc7569D", + "stake": "0x402456616f2678D823414a973e057b63542aC5a8", + "symbol": "WORK", + "decimals": 18 + } + }, + "network": "42", + "addresses": [ + "0x737CA57695354079e50eDc878056f7D9d1B556ee", + "0x5ab0Be21C3C777a1899eE1c024560eD5CeE2365D", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad" + ], + "snapshot": 23232581 + }, + { + "name": "Example query xDAI", + "strategy": { + "name": "work", + "params": { + "whitelist": "0xc61a0A7db8E34A24d3f4AA1feF6907Ecf9d4e850", + "stake": "0x3DCb8295f2c74C4Be77D8Ea329eB6240C47709B3", + "symbol": "WORK", + "decimals": 18 + } + }, + "network": "100", + "addresses": [ + "0x737CA57695354079e50eDc878056f7D9d1B556ee", + "0x624123ec4A9f48Be7AA8a307a74381E4ea7530D4" + ], + "snapshot": 14352264 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/work/index.ts b/Implementations/API/backend/utils/snapshot/strategies/work/index.ts new file mode 100644 index 00000000..8dabbaed --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/work/index.ts @@ -0,0 +1,52 @@ +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +const abi = [ + { + inputs: [{ internalType: 'address', name: '_address', type: 'address' }], + name: 'isWhitelisted', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'stakes', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => { + multi.call(`${address}.isWhitelisted`, options.whitelist, 'isWhitelisted', [ + address + ]); + multi.call(`${address}.stake`, options.stake, 'stakes', [address]); + }); + const result = await multi.execute(); + return Object.fromEntries( + addresses.map((address) => { + const stake = parseFloat( + formatUnits(result[address].stake.toString(), options.decimals) + ); + return [ + address, + result[address].isWhitelisted ? Math.sqrt(stake) + 1 : 0 + ]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xcover/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xcover/examples.json new file mode 100644 index 00000000..fde7a0e1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xcover/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "xCOVER token", + "strategy": { + "name": "xcover", + "params": { + "_comment_": "Any standard xToken with `balanceOf` and `getShareValue` can use this strategy.", + "tokenAddress": "0xa921392015eB37c5977c4Fd77E14DD568c59D5F8", + "symbol": "xCOVER", + "decimals": 18 + } + }, + "network": "1", + "addresses": ["0x406a0c87a6bb25748252cb112a7a837e21aacd98"], + "snapshot": 12067186 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xcover/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xcover/index.ts new file mode 100644 index 00000000..51e5ea2e --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xcover/index.ts @@ -0,0 +1,56 @@ +import { multicall } from '../../utils'; + +export const author = 'crypto_pumpkin'; +export const version = '0.1.0'; + +/** + * Any standard xToken with `balanceOf` and `getShareValue` can use this strategy. + */ +const abi = [ + { + constant: true, + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'getShareValue', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + params, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const balanceCallParams = addresses.map((addr) => [ + params.tokenAddress, + 'balanceOf', + [addr] + ]); + const res = await multicall( + network, + provider, + abi, + [[params.tokenAddress, 'getShareValue'], ...balanceCallParams], + { blockTag } + ); + const shareValue = res[0] / 1e18; + const balances = res.slice(1); + + return Object.fromEntries( + balances.map((balance, i) => [addresses[i], (balance * shareValue) / 1e18]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/examples.json new file mode 100644 index 00000000..e9a6842f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "STAKE token holders (from EasyStaking)", + "strategy": { + "name": "xdai-easy-staking", + "params": { + "address": "0x0Ae055097C6d159879521C384F1D2123D1f195e6", + "symbol": "STAKE", + "decimals": 18, + "start": 9877420 + } + }, + "network": "1", + "addresses": [ + "0x24d19f100ba142543a863fc2294b188e35ab55b9", + "0x57e54ecf82df98b3fadc1cd50d6835d5dd870916", + "0x9bc4a93883c522d3c79c81c2999aab52e2268d03", + "0x3cfe51b61e25750ab1426b0072e5d0cc5c30aafa", + "0x953533d6d085c503f6ec78a66cb2f454d090faed", + "0x2390e5658cdc602b7655307882371c867be5013c", + "0xac01ec664c1062234a73574d44523c906a3a6369" + ], + "snapshot": 11477385 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/index.ts new file mode 100644 index 00000000..78745e2d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/index.ts @@ -0,0 +1,148 @@ +import { formatUnits } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { subgraphRequest, call } from '../../utils'; +import { calculateEmission } from './utils'; + +export const author = 'maxaleks'; +export const version = '0.1.0'; + +const EASY_STAKING_SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/maxaleks/easy-staking'; + +const ercABI = [ + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +async function getEasyStakingDeposits(addresses, snapshot) { + const params = { + deposits: { + __args: { + where: { + user_in: addresses.map((address) => address.toLowerCase()), + amount_gt: 0 + }, + first: 1000, + skip: 0 + }, + user: true, + amount: true, + timestamp: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.deposits.__args.block = { number: snapshot }; + } + let page = 0; + let deposits = []; + while (true) { + params.deposits.__args.skip = page * 1000; + const data = await subgraphRequest(EASY_STAKING_SUBGRAPH_URL, params); + deposits = deposits.concat(data.deposits); + page++; + if (data.deposits.length < 1000) break; + } + return deposits.map((deposit: any) => ({ + ...deposit, + amount: BigNumber.from(deposit.amount), + timestamp: BigNumber.from(deposit.timestamp) + })); +} + +async function getEasyStakingParams(snapshot) { + const params = { + commonData: { + __args: { + id: 'common' + }, + sigmoidParamA: true, + sigmoidParamB: true, + sigmoidParamC: true, + totalSupplyFactor: true, + totalStaked: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params.commonData.__args.block = { number: snapshot }; + } + const { commonData } = await subgraphRequest( + EASY_STAKING_SUBGRAPH_URL, + params + ); + return { + sigmoidParameters: { + a: BigNumber.from(commonData.sigmoidParamA), + b: BigNumber.from(commonData.sigmoidParamB), + c: BigNumber.from(commonData.sigmoidParamC) + }, + totalSupplyFactor: BigNumber.from(commonData.totalSupplyFactor), + totalStaked: BigNumber.from(commonData.totalStaked) + }; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const [ + easyStakingDeposits, + { sigmoidParameters, totalSupplyFactor, totalStaked }, + block, + totalSupply + ] = await Promise.all([ + getEasyStakingDeposits(addresses, snapshot), + getEasyStakingParams(snapshot), + provider.getBlock(snapshot), + call(provider, ercABI, [options.address, 'totalSupply', []]) + ]); + const result = {}; + addresses.forEach((address) => { + result[address] = 0; + }); + if (!easyStakingDeposits || easyStakingDeposits.length === 0) { + return result; + } + return Object.fromEntries( + Object.entries(result).map(([address, balance]: any) => { + let totalBalance = balance; + const userDeposits = easyStakingDeposits.filter( + (deposit) => deposit.user.toLowerCase() === address.toLowerCase() + ); + userDeposits.forEach((deposit) => { + const timePassed = BigNumber.from(block.timestamp).sub( + deposit.timestamp + ); + const emission = calculateEmission( + deposit.amount, + timePassed, + sigmoidParameters, + totalSupplyFactor, + totalSupply, + totalStaked + ); + totalBalance += parseFloat( + formatUnits(deposit.amount.add(emission).toString(), options.decimals) + ); + }); + return [address, totalBalance]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/utils.ts b/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/utils.ts new file mode 100644 index 00000000..5df62150 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-easy-staking/utils.ts @@ -0,0 +1,58 @@ +import { BigNumber } from '@ethersproject/bignumber'; + +const ZERO = BigNumber.from(0); +const ONE = BigNumber.from(1); +const TWO = BigNumber.from(2); +const THREE = BigNumber.from(3); + +export const squareRoot = (y) => { + let z = ZERO; + if (y.gt(THREE)) { + z = y; + let x = y.div(TWO).add(ONE); + while (x.lt(z)) { + z = x; + x = y.div(x).add(x).div(TWO); + } + } else if (!y.isZero()) { + z = ONE; + } + return z; +}; + +const YEAR = BigNumber.from(31536000); // year in seconds +const ONE_ETHER = BigNumber.from('1000000000000000000'); +const MAX_EMISSION_RATE = BigNumber.from('150000000000000000'); // 15% + +export const calculateEmission = ( + deposit, + timePassed, + sigmoidParams, + totalSupplyFactor, + totalSupply, + totalStaked +) => { + const d = timePassed.sub(sigmoidParams.b); + let personalEmissionRate = ZERO; + if (d.gt(ZERO)) { + personalEmissionRate = sigmoidParams.a + .mul(d) + .div(squareRoot(d.pow(TWO).add(sigmoidParams.c))); + } + const targetTotalStaked = totalSupply.mul(totalSupplyFactor).div(ONE_ETHER); + let generalEmissionRate = MAX_EMISSION_RATE.div(TWO); + if (totalStaked.lt(targetTotalStaked)) { + generalEmissionRate = generalEmissionRate + .mul(totalStaked) + .div(targetTotalStaked); + } + if (personalEmissionRate.isZero()) { + generalEmissionRate = ZERO; + } + const emissionRate = personalEmissionRate.add(generalEmissionRate); + const emission = deposit + .mul(emissionRate) + .mul(timePassed) + .div(YEAR.mul(ONE_ETHER)); + return emission; +}; diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-posdao-staking/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xdai-posdao-staking/examples.json new file mode 100644 index 00000000..01a26479 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-posdao-staking/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "Stakers on xDai (from POSDAO staking)", + "strategy": { + "name": "xdai-posdao-staking", + "params": { + "symbol": "XDAI" + } + }, + "network": "1", + "addresses": [ + "0x24d19f100ba142543a863fc2294b188e35ab55b9", + "0x29cf39de6d963d092c177a60ce67879eea9910bb", + "0x71FBE58b64f14854cF5A3645a67a72d10A09CaE4", + "0x7ACEe696E2165e33c578d8956cbCf575e5d631d1", + "0x2DdB8A7541e6cAA50F74e7FACFF9Fe9da00e0A6c", + "0x481c034c6d9441db23Ea48De68BCAe812C5d39bA", + "0x7D32e95f9894f679833edbf91f67211289e8f713", + "0x5d2F076261F4f6c0209452c17f6e966C153ED3a7" + ], + "snapshot": 12140940 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-posdao-staking/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xdai-posdao-staking/index.ts new file mode 100644 index 00000000..d79188d0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-posdao-staking/index.ts @@ -0,0 +1,22 @@ +import { strategy as xdaiStakersAndHoldersStrategy } from '../xdai-stakers-and-holders'; + +export const author = 'maxaleks'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + return xdaiStakersAndHoldersStrategy( + space, + network, + provider, + addresses, + { ...options, userType: 'stakers' }, + snapshot + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-delegation/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-delegation/examples.json new file mode 100644 index 00000000..b5a7e018 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-delegation/examples.json @@ -0,0 +1,24 @@ +[ + { + "name": "STAKE token holders", + "strategy": { + "name": "xdai-stake-delegation", + "params": { + "address": "0x0Ae055097C6d159879521C384F1D2123D1f195e6", + "symbol": "STAKE", + "decimals": 18, + "start": 9877420 + } + }, + "network": "1", + "addresses": [ + "0x24d19f100ba142543a863fc2294b188e35ab55b9", + "0x57e54ecf82df98b3fadc1cd50d6835d5dd870916", + "0x9bc4a93883c522d3c79c81c2999aab52e2268d03", + "0x3cfe51b61e25750ab1426b0072e5d0cc5c30aafa", + "0x953533d6d085c503f6ec78a66cb2f454d090faed", + "0x80B3a71723FD38CD9589Ca32FA40EDC781f1A9C2" + ], + "snapshot": 11724607 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-delegation/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-delegation/index.ts new file mode 100644 index 00000000..be294890 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-delegation/index.ts @@ -0,0 +1,77 @@ +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; +import { strategy as xdaiEasyStakingStrategy } from '../xdai-easy-staking'; +import { strategy as xdaiPOSDAOStakingStrategy } from '../xdai-posdao-staking'; +import { strategy as xdaiStakeHoldersStrategy } from '../xdai-stake-holders'; +import { getDelegations } from '../../utils/delegation'; + +export const author = 'maxaleks'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const delegations = await getDelegations(space, network, addresses, snapshot); + if (Object.keys(delegations).length === 0) return {}; + + const delegationsArray = Object.values(delegations).reduce( + (a: string[], b: string[]) => a.concat(b) + ); + const erc20Balances = await erc20BalanceOfStrategy( + space, + network, + provider, + delegationsArray, + options, + snapshot + ); + const easyStakingBalances = await xdaiEasyStakingStrategy( + space, + network, + provider, + delegationsArray, + options, + snapshot + ); + const posdaoStakingBalances = await xdaiPOSDAOStakingStrategy( + space, + network, + provider, + delegationsArray, + options, + snapshot + ); + const erc20BalancesOnXdai = await xdaiStakeHoldersStrategy( + space, + network, + provider, + delegationsArray, + options, + snapshot + ); + // console.debug('Delegators ERC20 balances', erc20Balances); + // console.debug('Delegators EasyStaking balances', easyStakingBalances); + // console.debug('Delegators POSDAO Staking balances', posdaoStakingBalances); + // console.debug('Delegators ERC20 balances on xDai', erc20BalancesOnXdai); + + return Object.fromEntries( + addresses.map((address) => { + const addressScore = delegations[address] + ? delegations[address].reduce( + (a, b) => + a + + erc20Balances[b] + + easyStakingBalances[b] + + posdaoStakingBalances[b] + + erc20BalancesOnXdai[b], + 0 + ) + : 0; + return [address, addressScore]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-holders/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-holders/examples.json new file mode 100644 index 00000000..4fe5bad8 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-holders/examples.json @@ -0,0 +1,23 @@ +[ + { + "name": "STAKE token holders on xDai", + "strategy": { + "name": "xdai-stake-holders", + "params": { + "symbol": "XDAI" + } + }, + "network": "1", + "addresses": [ + "0x24d19f100ba142543a863fc2294b188e35ab55b9", + "0x29cf39de6d963d092c177a60ce67879eea9910bb", + "0x71FBE58b64f14854cF5A3645a67a72d10A09CaE4", + "0x7ACEe696E2165e33c578d8956cbCf575e5d631d1", + "0x2DdB8A7541e6cAA50F74e7FACFF9Fe9da00e0A6c", + "0x481c034c6d9441db23Ea48De68BCAe812C5d39bA", + "0x7D32e95f9894f679833edbf91f67211289e8f713", + "0x5d2F076261F4f6c0209452c17f6e966C153ED3a7" + ], + "snapshot": 12140940 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-holders/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-holders/index.ts new file mode 100644 index 00000000..e6e5a4bd --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-stake-holders/index.ts @@ -0,0 +1,22 @@ +import { strategy as xdaiStakersAndHoldersStrategy } from '../xdai-stakers-and-holders'; + +export const author = 'maxaleks'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + return xdaiStakersAndHoldersStrategy( + space, + network, + provider, + addresses, + { ...options, userType: 'holders' }, + snapshot + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-stakers-and-holders/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xdai-stakers-and-holders/examples.json new file mode 100644 index 00000000..af06a811 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-stakers-and-holders/examples.json @@ -0,0 +1,16 @@ +[ + { + "name": "STAKE token stakers and holders on xDai", + "strategy": { + "name": "xdai-stakers-and-holders" + }, + "network": "1", + "addresses": [ + "0x24d19f100ba142543a863fc2294b188e35ab55b9", + "0x29cf39de6d963d092c177a60ce67879eea9910bb", + "0x71FBE58b64f14854cF5A3645a67a72d10A09CaE4", + "0x7ACEe696E2165e33c578d8956cbCf575e5d631d1" + ], + "snapshot": 12140940 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xdai-stakers-and-holders/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xdai-stakers-and-holders/index.ts new file mode 100644 index 00000000..03a60a82 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xdai-stakers-and-holders/index.ts @@ -0,0 +1,82 @@ +import fetch from 'cross-fetch'; +import { formatUnits } from '@ethersproject/units'; +import { subgraphRequest } from '../../utils'; + +export const author = 'maxaleks'; +export const version = '0.1.0'; + +const SUBGRAPH_URL = + 'https://api.thegraph.com/subgraphs/name/maxaleks/xdai-stakers'; + +async function getUsers(addresses, snapshot, userType) { + const params = { + [userType]: { + __args: { + where: { + address_in: addresses.map((address) => address.toLowerCase()), + balance_gt: 0 + }, + first: 1000, + skip: 0 + }, + address: true, + balance: true + } + }; + if (snapshot !== 'latest') { + // @ts-ignore + params[userType].__args.block = { number: snapshot }; + } + let page = 0; + let users = []; + while (true) { + params[userType].__args.skip = page * 1000; + const data = await subgraphRequest(SUBGRAPH_URL, params); + users = users.concat(data[userType]); + page++; + if (data[userType].length < 1000) break; + } + return users; +} + +const getXdaiBlockNumber = async (timestamp: number): Promise => + fetch( + `https://blockscout.com/xdai/mainnet/api?module=block&action=getblocknobytime×tamp=${timestamp}&closest=before` + ) + .then((r) => r.json()) + .then((r) => Number(r.result.blockNumber)); + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + let xdaiSnapshot: any = 'latest'; + if (snapshot !== 'latest') { + const { timestamp } = await provider.getBlock(snapshot); + xdaiSnapshot = await getXdaiBlockNumber(timestamp); + } + const users = await getUsers(addresses, xdaiSnapshot, options.userType); + const result = {}; + addresses.forEach((address) => { + result[address] = 0; + }); + if (!users || users.length === 0) { + return result; + } + return Object.fromEntries( + Object.entries(result).map(([address]: any) => { + const user: any = users.find( + (item: any) => item.address.toLowerCase() === address.toLowerCase() + ); + let balance = 0; + if (user) { + balance = parseFloat(formatUnits(user.balance, options.decimals)); + } + return [address, balance]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/README.md b/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/README.md new file mode 100644 index 00000000..1975565f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/README.md @@ -0,0 +1,3 @@ +# xkawa-farm + +Returns voting power based on xKawa token held and stacked in the xKawa Single-side farm, as well as associated pending xKawa reward \ No newline at end of file diff --git a/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/examples.json new file mode 100644 index 00000000..bf8e8684 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "xKAWA farm balance", + "strategy": { + "name": "xkawa-farm", + "params": { + "farm": "0xC68844Cd3BA9d3Ad88F2cC278213F64b8C0bCddf", + "token": "0xdC386452F9FFDa7F0d2940e5c60Dc0F9469b1097", + "LPxKawa": "0x876692166cAC59506Eac1DB7f01B120F5Da98A50", + "symbol": "xKAWA", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x41a1b36fdef3df142d5116e52d6e72c02b904fda", + "0x7766ef005ec1b38a8472831e2f0631b12c811a5f" + ], + "snapshot": 13677274 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/index.ts new file mode 100644 index 00000000..d580f2bf --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xkawa-farm/index.ts @@ -0,0 +1,53 @@ +import { BigNumberish, BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'drgorillamd'; +export const version = '1.0.0'; + +const abi = [ + 'function userInfo(address,uint256) external view returns(uint256 amount,uint256,uint256,uint256)', + 'function balanceOf(address) external view returns(uint256)', + 'function getReserves() external view returns(uint112,uint112 reserve1,uint32)', + 'function totalSupply() external view returns(uint256)' +]; + +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) => { + multi.call(address + '-staked', options.farm, 'userInfo', [address, '1']); // .amount: uint + multi.call(address + '-balance', options.token, 'balanceOf', [address]); // uint + multi.call(address + '-LPstaked', options.farm, 'userInfo', [address, '3']); // .amount: uint + }); + + // xKawa is token1 + multi.call('reserves', options.LPxKawa, 'getReserves', []); // .reserve1: uint + multi.call('totalSupplyLP', options.LPxKawa, 'totalSupply', []); // uint + + const result: Record = await multi.execute(); + + return Object.fromEntries( + addresses.map((adr) => { + let bal = result[adr + '-staked']['amount']; + bal = bal.add(result[adr + '-balance']); + + bal = bal.add( + BigNumber.from(result[adr + '-LPstaked']['amount']) + .mul(result['reserves']['reserve1']) + .div(result['totalSupplyLP']) + ); + + return [adr, parseFloat(formatUnits(bal, options.decimals))]; + }) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xrc20-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xrc20-balance-of/examples.json new file mode 100644 index 00000000..aa74be29 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xrc20-balance-of/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "XRC20 query", + "strategy": { + "name": "xrc20-balance-of", + "params": { + "address": "0x0043c2cc478d4c01e662ffc75bd3d712420ca5af", + "symbol": "USDT", + "decimals": 6 + } + }, + "network": "4690", + "addresses": [ + "0x0ddfc506136fb7c050cc2e9511eccd81b15e7426", + "0x1904BFcb93EdC9BF961Eead2e5c0de81dCc1D37D", + "0x87Eea07540789af85B64947aEA21A3f00400B597" + ], + "snapshot": 8947000 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xrc20-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xrc20-balance-of/index.ts new file mode 100644 index 00000000..f78d9880 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xrc20-balance-of/index.ts @@ -0,0 +1,58 @@ +import fetch from 'cross-fetch'; +import { strategy as erc20BalanceStrategy } from '../erc20-balance-of'; + +interface ApiReturn { + balance: string[]; +} + +export const author = 'iotexproject'; +export const version = '0.0.2'; + +const testNetUrl = 'https://analyser-api.testnet.iotex.io'; +const mainNetUrl = 'https://analyser-api.iotex.io'; + +function getUrl(network) { + return network == 4689 ? mainNetUrl : testNetUrl; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + if (blockTag == 'latest') + return erc20BalanceStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const apiUrl = getUrl(network); + const response = await fetch( + `${apiUrl}/api.AccountService.Erc20TokenBalanceByHeight`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + address: addresses, + height: snapshot, + contract_address: options.address + }) + } + ); + + const ret: ApiReturn = await response.json(); + return Object.fromEntries( + ret.balance.map((v, i) => [addresses[i], parseFloat(v)]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xrook-balance-of-underlying-weighted/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xrook-balance-of-underlying-weighted/examples.json new file mode 100644 index 00000000..fb4e85a6 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xrook-balance-of-underlying-weighted/examples.json @@ -0,0 +1,37 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "xrook-balance-of-underlying-weighted", + "params": { + "symbol": "xROOK", + "address": "0x8aC32F0a635a0896a8428A9c31fBf1AB06ecf489", + "decimals": 18, + "weight": 1.2, + "underlyingTokenAddress": "0xfA5047c9c78B8877af97BDcb85Db743fD7313d4a", + "liquidityPoolAddress": "0x4F868C1aa37fCf307ab38D215382e88FCA6275E2" + } + }, + "network": "1", + "addresses": [ + "0x759a159D78342340EbACffB027c05910c093f430", + "0x2D21170928854F7Da6dC4b1C89dcb95BBc948338", + "0x722f531740937fc21A2FaC7648670C7f2872A1c7", + "0xA1ebb64d1f19D36A31221fFAd19747EF573Cd5A4", + "0x698EE32575dc58DDC0dd2e8B80d2EaB8Af1E94e1", + "0xBE6DcFDf4F1bf2ec0d3dEbD894a02a0765af6D69", + "0xFe4C4A27BeD5E9113480C84A177068F7421A1Eb7", + "0x0054D2575820cf60F5B6D350fb8d3d352BB3B6FD", + "0x4A9A3815060C3Bd08fb4d44C9e74513874771b0C", + "0x99BcEa6bB0403927fB3c038163478D5b42082Fd9", + "0x04b92e3b5De16Dc7e307ea4CEBc5d7dabaE1894F", + "0xe41eeB9ab53Ab208fD57A5E10dFC2FeE464Ca216", + "0x51F13C84b49B64ba6B1615e7D91b11066908BF3C", + "0x2ee8D80de1c389f1254e94bc44D2d1Bc391eD402", + "0x7453019b806919563EaC33870Fe5f8e5154fdF38", + "0x0B72513E73BB60375a4329FABF9BA16f861C0f12", + "0x9a82d59f46913913808bFE905F6157F967AAa28E" + ], + "snapshot": 15039737 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xrook-balance-of-underlying-weighted/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xrook-balance-of-underlying-weighted/index.ts new file mode 100644 index 00000000..ccd61330 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xrook-balance-of-underlying-weighted/index.ts @@ -0,0 +1,58 @@ +import { call } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'TudorSante'; +export const version = '1.0.0'; + +const erc20ABI = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function totalSupply() external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const score = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const xROOKTotalSupply = await call( + provider, + erc20ABI, + [options.address, 'totalSupply', []], + { blockTag } + ).then((res) => parseFloat(formatUnits(res, options.decimals))); + + const liquidityPoolBalance = await call( + provider, + erc20ABI, + [ + options.underlyingTokenAddress, + 'balanceOf', + [options.liquidityPoolAddress] + ], + { blockTag } + ).then((res) => parseFloat(formatUnits(res, options.decimals))); + + const underlyingValue = liquidityPoolBalance / xROOKTotalSupply; + + return Object.fromEntries( + Object.entries(score).map((res: any) => [ + res[0], + res[1] * options.weight * underlyingValue + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/xseen/examples.json b/Implementations/API/backend/utils/snapshot/strategies/xseen/examples.json new file mode 100644 index 00000000..a4e6840f --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xseen/examples.json @@ -0,0 +1,17 @@ +[ + { + "name": "xSEEN token", + "strategy": { + "name": "xseen", + "params": { + "_comment_": "The seen.haus governance token, where xSEEN represents a value of SEEN tokens including rewards.", + "tokenAddress": "0x38747baf050d3c22315a761585868dba16abfd89", + "symbol": "xSEEN", + "decimals": 18 + } + }, + "network": "1", + "addresses": ["0x8a83716acd66d9e1fb18c9b79540b72e04f80ac0"], + "snapshot": 12328235 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/xseen/index.ts b/Implementations/API/backend/utils/snapshot/strategies/xseen/index.ts new file mode 100644 index 00000000..3ebc45ac --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/xseen/index.ts @@ -0,0 +1,92 @@ +import { multicall } from '../../utils'; + +export const author = 'JayWelsh'; +export const version = '0.1.0'; + +/** + * xSEEN token ABI + */ +const xseenAbi = [ + { + constant: true, + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +]; + +/** + * SEEN token ABI + */ +const seenAbi = [ + { + constant: true, + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + params, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const stakingContractSeenBalanceCallParams = [ + '0xCa3FE04C7Ee111F0bbb02C328c699226aCf9Fd33', + 'balanceOf', + [params.tokenAddress] + ]; + + const seenRes = await multicall( + network, + provider, + seenAbi, + [stakingContractSeenBalanceCallParams], + { blockTag } + ); + + const seenBalanceInStakingContract = seenRes[0]; + + const xseenBalanceCallParams = addresses.map((addr) => [ + params.tokenAddress, + 'balanceOf', + [addr] + ]); + + const xseenRes = await multicall( + network, + provider, + xseenAbi, + [[params.tokenAddress, 'totalSupply'], ...xseenBalanceCallParams], + { blockTag } + ); + + const totalSupply = xseenRes[0]; + const balances = xseenRes.slice(1); + + return Object.fromEntries( + balances.map((balance, i) => [ + addresses[i], + (balance * (seenBalanceInStakingContract / totalSupply)) / 1e18 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/yearn-vault/examples.json b/Implementations/API/backend/utils/snapshot/strategies/yearn-vault/examples.json new file mode 100644 index 00000000..27bae477 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/yearn-vault/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Stakers on xDai (from POSDAO staking)", + "strategy": { + "name": "yearn-vault", + "params": { + "symbol": "YFI (yYFI)", + "address": "0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x8C612B03b3358C1E535706695c635C360034B968", + "0x24d19f100ba142543a863fc2294b188e35ab55b9", + "0x29cf39de6d963d092c177a60ce67879eea9910bb" + ], + "snapshot": 11884857 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/yearn-vault/index.ts b/Implementations/API/backend/utils/snapshot/strategies/yearn-vault/index.ts new file mode 100644 index 00000000..37e4521b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/yearn-vault/index.ts @@ -0,0 +1,60 @@ +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +const abi = [ + { + constant: true, + inputs: [], + name: 'getPricePerFullShare', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + payable: false, + stateMutability: 'view', + type: 'function' + } +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + // eslint-disable-next-line prefer-const + let [score, [pricePerFullShare]] = await Promise.all([ + erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ), + multicall( + network, + provider, + abi, + [[options.address, 'getPricePerFullShare', []]], + { blockTag } + ) + ]); + pricePerFullShare = parseFloat(formatUnits(pricePerFullShare.toString(), 18)); + return Object.fromEntries( + Object.entries(score).map((address: any) => [ + address[0], + address[1] * pricePerFullShare + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/zorro/README.md b/Implementations/API/backend/utils/snapshot/strategies/zorro/README.md new file mode 100644 index 00000000..e333b4df --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zorro/README.md @@ -0,0 +1,33 @@ +# Zorro Proof of Personhood + +This strategy distributes voting power democratically using the Zorro Proof of Personhood protocol (https://zorro.xyz). + +- The voting power of an address is `0` if they have not been proven as a unique person within your Snapshot space. If they are proven unique, then their voting power is given by the `power` parameter (which defaults to `1`). + +- No matter how many Ethereum addresses someone creates, Zorro aims to make it so only one of them can receive a non-zero voting power from this strategy for a given `purposeIdentifier` (which defaults to your space's ENS name). + +## Example options parameters + +#### Minimal example + +```json +{ + "symbol": "VOTES" +} +``` + +In the above example, `purposeIdentifier` defaults to your space's ens name (e.g. `yourdao.eth`), and `power` defaults to `1`. + +#### options + +```json +{ + "symbol": "VOTES", + "purposeIdentifier": "YourDaoName", + "power": 1000 +} +``` + +## Customization + +If you want a proof-of-personhood strategy customized for your purposes, just email ted@suzman.net and I'll be happy to help. diff --git a/Implementations/API/backend/utils/snapshot/strategies/zorro/examples.json b/Implementations/API/backend/utils/snapshot/strategies/zorro/examples.json new file mode 100644 index 00000000..0b7d654c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zorro/examples.json @@ -0,0 +1,31 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "zorro", + "params": { + "purposeIdentifier": "roboteddy.eth", + "symbol": "VOTES", + "power": 1 + } + }, + "network": "1", + "addresses": [ + "0xa478c2975ab1ea89e8196811f51a7b7ade33eb11", + "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "0x1E1A51E25f2816335cA436D65e9Af7694BE232ad", + "0x1F717Ce8ff07597ee7c408b5623dF40AaAf1787C", + "0x1c7a9275F2BD5a260A9c31069F77d53473b8ae2e", + "0x1d5E65a087eBc3d03a294412E46CE5D6882969f4", + "0x1f254336E5c46639A851b9CfC165697150a6c327", + "0x2ec3F80BeDA63Ede96BA20375032CDD3aAfb3030", + "0x4AcBcA6BE2f8D2540bBF4CA77E45dA0A4a095Fa2", + "0x4F3D348a6D09837Ae7961B1E0cEe2cc118cec777", + "0x6D7f23A509E212Ba7773EC1b2505d1A134f54fbe", + "0x07a1f6fc89223c5ebD4e4ddaE89Ac97629856A0f", + "0x33fA82CC7AB518Aae0f2Caee754E78d08A0cB190", + "0x8d07D225a769b7Af3A923481E1FdF49180e6A265" + ], + "snapshot": 13891032 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/zorro/index.ts b/Implementations/API/backend/utils/snapshot/strategies/zorro/index.ts new file mode 100644 index 00000000..35465e7b --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zorro/index.ts @@ -0,0 +1,36 @@ +import fetch from 'cross-fetch'; + +export const author = 'zorro-project'; +export const version = '0.1.0'; + +const API_URL = 'http://api.zorro.xyz'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const response = await fetch(API_URL + '/getVerifiedExternalAddresses', { + method: 'POST', + headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + purposeIdentifier: options.purposeIdentifier || space, + externalAddresses: addresses, + snapshot + }) + }); + const { verifiedExternalAddresses } = await response.json(); + const lookup = Object.fromEntries( + verifiedExternalAddresses.map((addr) => [addr.toLowerCase(), true]) + ); + const power = options.power || 1; + return Object.fromEntries( + addresses.map((address) => [ + address, + lookup[address.toLowerCase()] ? power : 0 + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/zorro/schema.json b/Implementations/API/backend/utils/snapshot/strategies/zorro/schema.json new file mode 100644 index 00000000..a887206d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zorro/schema.json @@ -0,0 +1,30 @@ +{ + "$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. VOTES"], + "maxLength": 16 + }, + "purposeIdentifier": { + "type": "string", + "title": "Purpose identifier (defaults to snapshot space ens)", + "examples": ["e.g. YourDaoName"] + }, + "power": { + "type": "number", + "title": "Voting power per verified person (defaults to 1)", + "examples": ["e.g. 1000"] + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/README.md b/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/README.md new file mode 100644 index 00000000..7ee83e9d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/README.md @@ -0,0 +1,5 @@ +# Contract call strategy + +Allows to get Voting power from ZRX governer contract + erc20 balance + +## Strategy Parameters diff --git a/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/examples.json b/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/examples.json new file mode 100644 index 00000000..3bf70312 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/examples.json @@ -0,0 +1,22 @@ +[ + { + "name": "ZRX Voting Power strategy", + "strategy": { + "name": "zrx-voting-power", + "params": { + "address": "0xe41d2489571d322189246dafa5ebde1f4699f498", + "symbol": "ZRX", + "governerContract": "0x0bB1810061C2f5b2088054eE184E6C79e1591101", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x6bf18019d3b286c7f9fd675de7ef1d89fd985104", + "0xba4f44e774158408e2dc6c5cb65bc995f0a89180", + "0x4990cE223209FCEc4ec4c1ff6E0E81eebD8Cca08", + "0x5265bde27f57e738be6c1f6ab3544e82cdc92a8f" + ], + "snapshot": 12855152 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/index.ts b/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/index.ts new file mode 100644 index 00000000..24d0bafb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zrx-voting-power/index.ts @@ -0,0 +1,78 @@ +import fetch from 'cross-fetch'; +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { multicall } from '../../utils'; +import { strategy as erc20BalanceOfStrategy } from '../erc20-balance-of'; + +export const author = 'benlyaunzon'; +export const version = '0.1.0'; + +const ZRX_STAKING_POOLS = { + '1': 'https://api.0x.org/staking/pools', + '42': 'https://staging.api.0x.org/staking/pools' +}; + +const abi = [ + 'function getVotingPower(address account, bytes32[] operatedPoolIds) view returns (uint256 votingPower)' +]; + +const encodePoolId = (poolId: number) => + `0x${poolId.toString(16).padStart(64, '0')}`; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // Early return 0 voting power if governanceContract not correctly set + if (!options.governerContract) { + return Object.fromEntries(addresses.map((address) => [address, '0'])); + } + + const erc20Balances = await erc20BalanceOfStrategy( + space, + network, + provider, + addresses, + options, + snapshot + ); + + const zrxStakingPoolsRes = await fetch(ZRX_STAKING_POOLS[network]); + const { stakingPools } = await zrxStakingPoolsRes.json(); + const response: BigNumber[] = await multicall( + network, + provider, + abi, + addresses.map((address: any) => { + const addressOperatedPools = stakingPools.filter( + (p) => p.operatorAddress.toLowerCase() === address.toLowerCase() + ); + + const pools = addressOperatedPools + ? addressOperatedPools.map((pool) => + encodePoolId(parseInt(pool.poolId, 10)) + ) + : []; + return [ + options.governerContract, + 'getVotingPower', + [address.toLowerCase(), pools] + ]; + }), + { blockTag } + ); + + return Object.fromEntries( + response.map((value, i) => [ + addresses[i], + parseFloat(formatUnits(value.toString(), options.decimals)) + + erc20Balances[addresses[i]] + ]) + ); +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/README.md b/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/README.md new file mode 100644 index 00000000..950a5f6c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/README.md @@ -0,0 +1,3 @@ +# zunami-pool-gauge-aggregated-balance-of + +Collect aggregated balance from curve pool/gauges diff --git a/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/examples.json b/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/examples.json new file mode 100644 index 00000000..e401bfe9 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "zunami-pool-gauge-aggregated-balance-of", + "params": { + "address": "0x0AD930970b60d24bd30F612D287f188A7626B147", + "fraxStakingAddress": "0xb8ebc210BCF78be8Ef3F09Dd0d8e85Fa5e252e86", + "decimals": 18, + "lpPriceDecimals": 18, + "curvePoolAddress": "0x68934F60758243eafAf4D2cFeD27BF8010bede3a", + "blackListAddresses": [ + "0xBdCA4F610e7101Cc172E2135ba025737B99AbD30", + "0xbc61f6973cE564eFFB16Cd79B5BC3916eaD592E2", + "0x989AEb4d175e16225E39E87d0D97A3360524AD80" + ] + } + }, + "network": "1", + "addresses": [ + "0xF9605D8c4c987d7Cb32D0d11FbCb8EeeB1B22D5d", + "0x1c012C98676eAFB6C1D4886a543c4fFf50a43FE0", + "0x5180db0237291A6449DdA9ed33aD90a38787621c", + "0x924402163f01cCA1b161A3188d3f32601843Ae4A", + "0xa6dC407C39bd07F6D6C3780C2F5a53e690387F4a", + "0x287CDD0A59d69E9F101E90BDBCC892607DF08CF9", + "0x2d34816C3c83554CE97144c623C381b303Aba732", + "0x6De72e5568CfbB9d0fb91Fb384D2E52d7e5C1E94", + + "0xBdCA4F610e7101Cc172E2135ba025737B99AbD30", + "0xbc61f6973cE564eFFB16Cd79B5BC3916eaD592E2", + "0x989AEb4d175e16225E39E87d0D97A3360524AD80" + ], + "snapshot": 17330743 + } +] diff --git a/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/index.ts b/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/index.ts new file mode 100644 index 00000000..2409c175 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/index.ts @@ -0,0 +1,154 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { call, Multicaller } from '../../utils'; + +export const author = 'fextr'; +export const version = '1.0.1'; + +const zunamiSnapshotHelperAbi = [ + 'function aggregatedBalanceOf(address _account) external view returns (uint256)' +]; +const fraxStakingAbi = [ + 'function lockedLiquidityOf(address account) external view returns (uint256)' +]; +const curveAbi = ['function get_virtual_price() view returns (uint256)']; + +interface StrategyOptions { + address: string; + decimals: number; + lpPriceDecimals: number; + curvePoolAddress: string; + fraxStakingAddress: string; + blackListAddresses: string[]; +} + +export async function strategy( + space, + network, + provider, + addresses: string[], + options: StrategyOptions, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const lpPrice = parseFloat( + formatUnits( + await call( + provider, + curveAbi, + [options.curvePoolAddress, 'get_virtual_price'], + { blockTag } + ), + options.lpPriceDecimals + ) + ); + + const blackListAddressesArr = Array.from(options.blackListAddresses).map( + (address) => address.toLowerCase() + ); + const filteredAddresses = addresses.filter( + (address) => !blackListAddressesArr.includes(address.toLowerCase()) + ); + + const aggregatedResult: Record = + await getAggregatedBalance( + network, + provider, + blockTag, + filteredAddresses, + options.address + ); + + const fraxStakingResult: Record = + await getFraxStakingBalance( + network, + provider, + blockTag, + filteredAddresses, + options.fraxStakingAddress + ); + + const result: [string, BigNumberish][] = mergeResults( + aggregatedResult, + fraxStakingResult + ); + + return Object.fromEntries( + result.map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, options.decimals)) * lpPrice + ]) + ); +} + +async function getFraxStakingBalance( + network, + provider, + blockTag, + filteredAddresses: string[], + fraxStakingAddress: string +): Promise> { + const fraxStakingMulti = new Multicaller(network, provider, fraxStakingAbi, { + blockTag + }); + filteredAddresses.forEach((address) => + fraxStakingMulti.call(address, fraxStakingAddress, 'lockedLiquidityOf', [ + address + ]) + ); + const fraxStakingResult: Record = + await fraxStakingMulti.execute(); + + return fraxStakingResult; +} + +async function getAggregatedBalance( + network, + provider, + blockTag, + filteredAddresses: string[], + contractAddress: string +): Promise> { + const multi = new Multicaller(network, provider, zunamiSnapshotHelperAbi, { + blockTag + }); + filteredAddresses.forEach((address) => + multi.call(address, contractAddress, 'aggregatedBalanceOf', [address]) + ); + const aggregatedResult: Record = await multi.execute(); + + return aggregatedResult; +} + +function mergeResults( + aggregatedResult: Record, + fraxStakingResult: Record +): [string, BigNumberish][] { + const fraxStakingEntries = Object.entries(fraxStakingResult); + const fraxStakingResultMap: Map = new Map( + fraxStakingEntries + ); + const aggregatedPlusStaking: [string, BigNumberish][] = Object.entries( + aggregatedResult + ).map(([address, balance]) => { + const fraxStakingBalance = fraxStakingResultMap.get(address); + if (fraxStakingBalance) { + return [ + address, + BigNumber.from(balance).add(BigNumber.from(fraxStakingBalance)) + ]; + } + return [address, balance]; + }); + const aggregatedPluStakingMap: Map = new Map( + aggregatedPlusStaking + ); + fraxStakingEntries.map(([address, balance]) => { + if (!aggregatedPluStakingMap.has(address)) { + aggregatedPlusStaking.push([address, balance]); + } + }); + + return aggregatedPlusStaking; +} diff --git a/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/schema.json b/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/schema.json new file mode 100644 index 00000000..d6eb6240 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/strategies/zunami-pool-gauge-aggregated-balance-of/schema.json @@ -0,0 +1,70 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "zunami-pool-gauge-aggregated-balance-of", + "type": "object", + "properties": { + "address": { + "type": "string", + "title": "Snapshot helper address", + "examples": ["e.g. 0x0AD930970b60d24bd30F612D287f188A7626B147"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "fraxStakingAddress": { + "type": "string", + "title": "Zunami convex frax staking address", + "examples": ["e.g. 0xb8ebc210BCF78be8Ef3F09Dd0d8e85Fa5e252e86"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"], + "minimum": 1 + }, + "curvePoolAddress": { + "type": "string", + "title": "Curve pool address", + "examples": ["e.g. 0x68934F60758243eafAf4D2cFeD27BF8010bede3a"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "lpPriceDecimals": { + "type": "number", + "title": "Curve lp price decimals", + "examples": ["e.g. 18"], + "minimum": 1 + }, + "blackListAddresses": { + "type": "array", + "title": "Black listed addresses", + "examples": [ + "0xBdCA4F610e7101Cc172E2135ba025737B99AbD30", + "0xbc61f6973cE564eFFB16Cd79B5BC3916eaD592E2" + ], + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + } + }, + "required": [ + "address", + "decimals", + "curvePoolAddress", + "lpPriceDecimals", + "blackListAddresses" + ], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/typings.d.ts b/Implementations/API/backend/utils/snapshot/typings.d.ts new file mode 100644 index 00000000..68218507 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/typings.d.ts @@ -0,0 +1,4 @@ +declare module '*.md' { + const content: string; + export = content; +} diff --git a/Implementations/API/backend/utils/snapshot/utils.ts b/Implementations/API/backend/utils/snapshot/utils.ts new file mode 100644 index 00000000..2764b555 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/utils.ts @@ -0,0 +1,97 @@ +import fetch from 'cross-fetch'; +import _strategies from './strategies'; +import snapshot from '@snapshot-labs/snapshot.js'; +import { getDelegations } from './utils/delegation'; +import { getVp, getDelegations as getCoreDelegations } from './utils/vp'; + +async function callStrategy(space, network, addresses, strategy, snapshot) { + if ( + (snapshot !== 'latest' && strategy.params?.start > snapshot) || + (strategy.params?.end && + (snapshot === 'latest' || snapshot > strategy.params?.end)) + ) + return {}; + const score: any = await _strategies[strategy.name].strategy( + space, + network, + getProvider(network), + addresses, + strategy.params, + snapshot + ); + const addressesLc = addresses.map((address) => address.toLowerCase()); + return Object.fromEntries( + Object.entries(score).filter( + ([address, vp]: any[]) => + vp > 0 && addressesLc.includes(address.toLowerCase()) + ) + ); +} + +export async function getScoresDirect( + space: string, + strategies: any[], + network: string, + provider, + addresses: string[], + snapshot: number | string = 'latest' +) { + try { + const networks = strategies.map((s) => s.network || network); + const snapshots = await getSnapshots(network, snapshot, provider, networks); + // @ts-ignore + if (addresses.length === 0) return strategies.map(() => ({})); + return await Promise.all( + strategies.map((strategy) => + callStrategy( + space, + strategy.network || network, + addresses, + strategy, + snapshots[strategy.network || network] + ) + ) + ); + } catch (e) { + return Promise.reject(e); + } +} + +export function customFetch(url, options, timeout = 20000): Promise { + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => + setTimeout(() => reject(new Error('API request timeout')), timeout) + ) + ]); +} + +export const { + multicall, + Multicaller, + subgraphRequest, + ipfsGet, + call, + getDelegatesBySpace, + getBlockNumber, + getProvider, + getSnapshots, + SNAPSHOT_SUBGRAPH_URL +} = snapshot.utils; + +export default { + getScoresDirect, + multicall, + Multicaller, + subgraphRequest, + ipfsGet, + call, + getDelegatesBySpace, + getBlockNumber, + getProvider, + getDelegations, + getSnapshots, + SNAPSHOT_SUBGRAPH_URL, + getVp, + getCoreDelegations +}; diff --git a/Implementations/API/backend/utils/snapshot/utils/delegation.ts b/Implementations/API/backend/utils/snapshot/utils/delegation.ts new file mode 100644 index 00000000..f38a02e4 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/utils/delegation.ts @@ -0,0 +1,95 @@ +import { getAddress } from '@ethersproject/address'; +import { getDelegatesBySpace } from '../utils'; + +const DELEGATION_DATA_CACHE = {}; + +// delegations with overrides +export async function getDelegations(space, network, addresses, snapshot) { + const addressesLc = addresses.map((addresses) => addresses.toLowerCase()); + const delegatesBySpace = await getDelegatesBySpace(network, space, snapshot); + + const delegations = delegatesBySpace.filter( + (delegation: any) => + addressesLc.includes(delegation.delegate) && + !addressesLc.includes(delegation.delegator) + ); + if (!delegations) return {}; + + const delegationsReverse = {}; + delegations.forEach( + (delegation: any) => + (delegationsReverse[delegation.delegator] = delegation.delegate) + ); + delegations + .filter((delegation: any) => delegation.space !== '') + .forEach( + (delegation: any) => + (delegationsReverse[delegation.delegator] = delegation.delegate) + ); + + return Object.fromEntries( + addresses.map((address) => [ + address, + Object.entries(delegationsReverse) + .filter(([, delegate]) => address.toLowerCase() === delegate) + .map(([delegator]) => getAddress(delegator)) + ]) + ); +} + +function getDelegationReverseData(delegation) { + return { + delegate: delegation.delegate, + delegateAddress: getAddress(delegation.delegate), + delegator: delegation.delegator, + delegatorAddress: getAddress(delegation.delegator) + }; +} + +export async function getDelegationsData(space, network, addresses, snapshot) { + const cacheKey = `${space}-${network}-${snapshot}`; + let delegationsReverse = DELEGATION_DATA_CACHE[cacheKey]; + + if (!delegationsReverse) { + delegationsReverse = {}; + + const delegatesBySpace = await getDelegatesBySpace( + network, + space, + snapshot + ); + + delegatesBySpace.forEach( + (delegation: any) => + (delegationsReverse[delegation.delegator] = + getDelegationReverseData(delegation)) + ); + delegatesBySpace + .filter((delegation: any) => delegation.space !== '') + .forEach( + (delegation: any) => + (delegationsReverse[delegation.delegator] = + getDelegationReverseData(delegation)) + ); + + if (space === 'stgdao.eth' && snapshot !== 'latest') { + // TODO: implement LRU so memory doesn't explode + // we only cache stgdao for now + console.log(`[with-delegation] Caching ${cacheKey}`); + DELEGATION_DATA_CACHE[cacheKey] = delegationsReverse; + } + } + return { + delegations: Object.fromEntries( + addresses.map((address) => [ + address, + Object.values(delegationsReverse) + .filter((data) => address.toLowerCase() === (data as any).delegate) + .map((data) => (data as any).delegatorAddress) + ]) + ), + allDelegators: Object.values(delegationsReverse).map( + (data) => (data as any).delegatorAddress + ) + }; +} diff --git a/Implementations/API/backend/utils/snapshot/utils/vp.ts b/Implementations/API/backend/utils/snapshot/utils/vp.ts new file mode 100644 index 00000000..6bd74f2a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/utils/vp.ts @@ -0,0 +1,220 @@ +import { formatBytes32String } from '@ethersproject/strings'; +import { getAddress } from '@ethersproject/address'; +import subgraphs from '@snapshot-labs/snapshot.js/src/delegationSubgraphs.json'; +import { + getProvider, + getSnapshots, + Multicaller, + subgraphRequest +} from '../utils'; +import _strategies from '../strategies'; + +const DELEGATION_CONTRACT = '0x469788fE6E9E9681C6ebF3bF78e7Fd26Fc015446'; +const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000'; +const EMPTY_SPACE = formatBytes32String(''); +const abi = ['function delegation(address, bytes32) view returns (address)']; + +interface Delegation { + in: string[]; + out: string | null; +} + +export async function getVp( + address: string, + network: string, + strategies: any[], + snapshot: number | 'latest', + space: string, + delegation?: boolean +) { + const networks = [...new Set(strategies.map((s) => s.network || network))]; + const snapshots = await getSnapshots( + network, + snapshot, + getProvider(network), + networks + ); + + const delegations = {}; + if (delegation) { + const ds = await Promise.all( + networks.map((n) => getDelegations(address, n, snapshots[n], space)) + ); + ds.forEach((d, i) => (delegations[networks[i]] = d)); + } + + const p = strategies.map((strategy) => { + const n = strategy.network || network; + let addresses = [address]; + + if (delegation) { + addresses = delegations[n].in; + if (!delegations[n].out) addresses.push(address); + addresses = [...new Set(addresses)]; + if (addresses.length === 0) return {}; + } + + addresses = addresses.map(getAddress); + return _strategies[strategy.name].strategy( + space, + n, + getProvider(n), + addresses, + strategy.params, + snapshots[n] + ); + }); + const scores = await Promise.all(p); + + const vpByStrategy = scores.map((score, i) => { + const n = strategies[i].network || network; + let addresses = [address]; + + if (delegation) { + addresses = delegations[n].in; + if (!delegations[n].out) addresses.push(address); + addresses = [...new Set(addresses)]; + } + + addresses = addresses.map(getAddress); + return addresses.reduce((a, b) => a + (score[b] || 0), 0); + }); + const vp = vpByStrategy.reduce((a, b) => a + b, 0); + let vpState = 'final'; + if (snapshot === 'latest') vpState = 'pending'; + + return { + vp, + vp_by_strategy: vpByStrategy, + vp_state: vpState + }; +} + +export async function getDelegationsOut( + addresses: string[], + network: string, + snapshot: number | 'latest', + space: string +) { + if (!subgraphs[network]) + return Object.fromEntries(addresses.map((address) => [address, null])); + + const id = formatBytes32String(space); + const options = { blockTag: snapshot }; + const multi = new Multicaller(network, getProvider(network), abi, options); + addresses.forEach((account) => { + multi.call(`${account}.base`, DELEGATION_CONTRACT, 'delegation', [ + account, + EMPTY_SPACE + ]); + multi.call(`${account}.space`, DELEGATION_CONTRACT, 'delegation', [ + account, + id + ]); + }); + const delegations = await multi.execute(); + + return Object.fromEntries( + Object.entries(delegations).map(([address, delegation]: any) => { + if (delegation.space !== EMPTY_ADDRESS) + return [address, delegation.space]; + if (delegation.base !== EMPTY_ADDRESS) return [address, delegation.base]; + return [address, null]; + }) + ); +} + +export async function getDelegationOut( + address: string, + network: string, + snapshot: number | 'latest', + space: string +): Promise { + const usersDelegationOut = await getDelegationsOut( + [address], + network, + snapshot, + space + ); + return usersDelegationOut[address] || null; +} + +export async function getDelegationsIn( + address: string, + network: string, + snapshot: number | 'latest', + space: string +): Promise { + if (!subgraphs[network]) return []; + + const max = 1000; + let result = []; + let page = 0; + + const query = { + delegations: { + __args: { + first: max, + skip: 0, + block: { number: snapshot }, + where: { + space_in: ['', space], + delegate: address + } + }, + delegator: true, + space: true + } + }; + // @ts-ignore + if (snapshot === 'latest') delete query.delegations.__args.block; + while (true) { + query.delegations.__args.skip = page * max; + const pageResult = await subgraphRequest(subgraphs[network], query); + const pageDelegations = pageResult.delegations || []; + result = result.concat(pageDelegations); + page++; + if (pageDelegations.length < max) break; + } + + const delegations: string[] = []; + let baseDelegations: string[] = []; + result.forEach((delegation: any) => { + const delegator = getAddress(delegation.delegator); + if (delegation.space === space) delegations.push(delegator); + if ([null, ''].includes(delegation.space)) baseDelegations.push(delegator); + }); + + baseDelegations = baseDelegations.filter( + (delegator) => !delegations.includes(delegator) + ); + if (baseDelegations.length > 0) { + const delegationsOut = await getDelegationsOut( + baseDelegations, + network, + snapshot, + space + ); + Object.entries(delegationsOut).map(([delegator, out]: any) => { + if (out === address) delegations.push(delegator); + }); + } + + return [...new Set(delegations)]; +} + +export async function getDelegations( + address: string, + network: string, + snapshot: number | 'latest', + space: string +): Promise { + const [delegationOut, delegationsIn] = await Promise.all([ + getDelegationOut(address, network, snapshot, space), + getDelegationsIn(address, network, snapshot, space) + ]); + return { + in: delegationsIn, + out: delegationOut + }; +} diff --git a/Implementations/API/backend/utils/snapshot/validations/arbitrum/README.md b/Implementations/API/backend/utils/snapshot/validations/arbitrum/README.md new file mode 100644 index 00000000..21afa8f3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/arbitrum/README.md @@ -0,0 +1,19 @@ +# Arbitrum DAO Percentage of Votable Supply + +- [Overview](#overview) +- [Options](#options) + +## Overview + +This validation module check the number of basis points (bps, i.e. 0.01%) of vote a delegate control, which is used to enforce the 0.01% snapshot proposer requirement specified in [The Constitution of the Arbitrum DAO](https://docs.arbitrum.foundation/dao-constitution). + +Arbitrum use an EXCLUDE_ADDRESS (0x00000000000000000000000000000000000A4B86) to mark non-votable tokens, tokens delegated to the EXCLUDE_ADDRESS will be excluded from votable supply. + +You should use this validation module with erc20-votes strategy configured with the same token address and decimals. + +## Options + +- **minBps:** The minimum basis points required to pass validation, e.g. 1 +- **address:** The address of the ERC-20 token contract, e.g. "0x912CE59144191C1204E64559FE8253a0e49E6548" +- **decimals:** The decimal of the token, e.g. 18 +- **excludeaddr** The special delegation address for non-votable token, e.g. "0x00000000000000000000000000000000000A4B86" diff --git a/Implementations/API/backend/utils/snapshot/validations/arbitrum/examples.json b/Implementations/API/backend/utils/snapshot/validations/arbitrum/examples.json new file mode 100644 index 00000000..6c7c603a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/arbitrum/examples.json @@ -0,0 +1,26 @@ +[ + { + "name": "Example of Arbitrum DAO Validation", + "author": "0x0eb5b03c0303f2f47cd81d7be4275af8ed347576", + "space": "arbitrumfoundation.eth", + "network": "42161", + "snapshot": "latest", + "params": { + "minBps": 1, + "address": "0x912CE59144191C1204E64559FE8253a0e49E6548", + "decimals": 18, + "excludeaddr": "0x00000000000000000000000000000000000A4B86", + "strategies": [ + { + "name": "erc20-votes", + "params": { + "symbol": "ARB", + "address": "0x912CE59144191C1204E64559FE8253a0e49E6548", + "decimals": 18 + }, + "network": "42161" + } + ] + } + } +] diff --git a/Implementations/API/backend/utils/snapshot/validations/arbitrum/index.ts b/Implementations/API/backend/utils/snapshot/validations/arbitrum/index.ts new file mode 100644 index 00000000..009b51a2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/arbitrum/index.ts @@ -0,0 +1,61 @@ +import Validation from '../validation'; +import { getProvider, getScoresDirect } from '../../utils'; +import { multicall } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; + +const abi = [ + 'function getVotes(address account) view returns (uint256)', + 'function totalSupply() view returns (uint256)' +]; + +export default class extends Validation { + public id = 'arbitrum'; + public github = 'gzeoneth'; + public version = '0.1.0'; + public title = 'Arbitrum DAO Percentage of Votable Supply'; + public description = + 'Use with erc20-votes to validate by percentage of votable supply.'; + public proposalValidationOnly = true; + + async validate(): Promise { + if (this.params.strategies?.length > 8) + throw new Error(`Max number of strategies exceeded`); + const minBps = this.params.minBps; + const decimals = this.params.decimals; + const excludeaddr = + this.params.excludeaddr ?? '0x00000000000000000000000000000000000A4B86'; + + if (minBps) { + const scores = await getScoresDirect( + this.space, + this.params.strategies, + this.network, + getProvider(this.network), + [this.author], + this.snapshot || 'latest' + ); + const totalScore: any = scores + .map((score: any) => + Object.values(score).reduce((a, b: any) => a + b, 0) + ) + .reduce((a, b: any) => a + b, 0); + const [[totalSupply], [excludedSupply]] = await multicall( + this.network, + getProvider(this.network), + abi, + [ + [this.params.address, 'totalSupply', []], + [this.params.address, 'getVotes', [excludeaddr]] + ], + { blockTag: this.snapshot || 'latest' } + ); + const votableSupply = parseFloat( + formatUnits(totalSupply.sub(excludedSupply).toString(), decimals) + ); + const bpsOfVotable = (totalScore * 10000) / votableSupply; + if (bpsOfVotable < minBps) return false; + } + + return true; + } +} diff --git a/Implementations/API/backend/utils/snapshot/validations/basic/examples.json b/Implementations/API/backend/utils/snapshot/validations/basic/examples.json new file mode 100644 index 00000000..82b132d3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/basic/examples.json @@ -0,0 +1,18 @@ +[ + { + "name": "Example of a basic validation", + "author": "0xeF8305E140ac520225DAf050e2f71d5fBcC543e7", + "space": "fabien.eth", + "network": "1", + "snapshot": "latest", + "params": { + "minScore": 1, + "strategies": [ + { + "name": "ticket", + "params": {} + } + ] + } + } +] diff --git a/Implementations/API/backend/utils/snapshot/validations/basic/index.ts b/Implementations/API/backend/utils/snapshot/validations/basic/index.ts new file mode 100644 index 00000000..3dee0b85 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/basic/index.ts @@ -0,0 +1,35 @@ +import Validation from '../validation'; +import { getProvider, getScoresDirect } from '../../utils'; + +export default class extends Validation { + public id = 'basic'; + public github = 'bonustrack'; + public version = '0.2.0'; + public title = 'Basic'; + public description = 'Use any strategy to determine if a user can vote.'; + + async validate(): Promise { + if (this.params.strategies?.length > 8) + throw new Error(`Max number of strategies exceeded`); + const minScore = this.params.minScore; + + if (minScore) { + const scores = await getScoresDirect( + this.space, + this.params.strategies, + this.network, + getProvider(this.network), + [this.author], + this.snapshot || 'latest' + ); + const totalScore: any = scores + .map((score: any) => + Object.values(score).reduce((a, b: any) => a + b, 0) + ) + .reduce((a, b: any) => a + b, 0); + if (totalScore < minScore) return false; + } + + return true; + } +} diff --git a/Implementations/API/backend/utils/snapshot/validations/basic/schema.json b/Implementations/API/backend/utils/snapshot/validations/basic/schema.json new file mode 100644 index 00000000..9a2822c1 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/basic/schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Validation", + "definitions": { + "Validation": { + "title": "Basic validation", + "type": "object", + "properties": { + "minScore": { + "title": "Minimum score", + "type": "number", + "minimum": 1 + }, + "strategies": { + "title": "Strategies", + "type": "array", + "items": { + "title": "Strategy", + "type": "object", + "default": { + "name": "ticket", + "network": "1", + "params": { + "symbol": "DAI" + } + }, + "properties": { + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "params": { + "type": "object" + } + }, + "required": ["name", "params"] + } + } + }, + "required": ["minScore"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/validations/index.ts b/Implementations/API/backend/utils/snapshot/validations/index.ts new file mode 100644 index 00000000..ee47e256 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/index.ts @@ -0,0 +1,67 @@ +import { readFileSync } from 'fs'; +import path from 'path'; +import basic from './basic'; +import passportGated from './passport-gated'; +import passportWeighted from './passport-weighted'; +import arbitrum from './arbitrum'; + +const validationClasses = { + basic, + 'passport-gated': passportGated, + 'passport-weighted': passportWeighted, + arbitrum: arbitrum +}; + +const validations = {}; +Object.keys(validationClasses).forEach(function (validationName) { + let examples = null; + let schema = null; + let about = ''; + + try { + examples = JSON.parse( + readFileSync( + path.join(__dirname, validationName, 'examples.json'), + 'utf8' + ) + ); + } catch (error) { + examples = null; + } + + try { + schema = JSON.parse( + readFileSync(path.join(__dirname, validationName, 'schema.json'), 'utf8') + ); + } catch (error) { + schema = null; + } + + try { + about = readFileSync( + path.join(__dirname, validationName, 'README.md'), + 'utf8' + ); + } catch (error) { + about = ''; + } + + const validationClass = validationClasses[validationName]; + const validationInstance = new validationClass(); + + validations[validationName] = { + validation: validationClass, + examples, + schema, + about, + id: validationInstance.id, + github: validationInstance.github, + version: validationInstance.version, + title: validationInstance.title, + description: validationInstance.description, + proposalValidationOnly: validationInstance.proposalValidationOnly, + votingValidationOnly: validationInstance.votingValidationOnly + }; +}); + +export default validations; diff --git a/Implementations/API/backend/utils/snapshot/validations/passport-gated/README.md b/Implementations/API/backend/utils/snapshot/validations/passport-gated/README.md new file mode 100644 index 00000000..7cd6d0a0 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/passport-gated/README.md @@ -0,0 +1,29 @@ +# Gitcoin Passport Gated Validation + +This repository provides a passport-gated validation strategy for Snapshot. The implementation integrates with the Gitcoin API to validate whether a user is authorized to vote on a proposal. + +## Prerequisites + +Before using this code, ensure that you have the following information stored in a `.env` file at the project root: + +- `PASSPORT_API_KEY=` + +## Overview + +This implementation uses the Gitcoin Passport API to check whether a user has a valid passport by looking for their stamps. + +## Code Explanation + +The main function in this codebase checks stamps for a user and returns a boolean value indicating whether the user has a valid passport. + +## Modifications + +The original code utilized the Passport SDK to check if the user has a valid passport and stamps. + +[Coming Soon] However, with the introduction of the Passport API, we can now simplify the process by checking for a score. + +## Last Modified + +This code was last modified on June 1, 2023. + +Feel free to customize and extend this implementation to suit your specific needs. diff --git a/Implementations/API/backend/utils/snapshot/validations/passport-gated/examples.json b/Implementations/API/backend/utils/snapshot/validations/passport-gated/examples.json new file mode 100644 index 00000000..ba7428ae --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/passport-gated/examples.json @@ -0,0 +1,13 @@ +[ + { + "name": "Example of a passport gated validation", + "author": "0x24F15402C6Bb870554489b2fd2049A85d75B982f", + "space": "fabien.eth", + "network": "1", + "snapshot": "latest", + "params": { + "stamps": ["Ens", "Github", "POAP", "SnapshotVotesProvider"], + "operator": "OR" + } + } +] diff --git a/Implementations/API/backend/utils/snapshot/validations/passport-gated/index.ts b/Implementations/API/backend/utils/snapshot/validations/passport-gated/index.ts new file mode 100644 index 00000000..9fa100b3 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/passport-gated/index.ts @@ -0,0 +1,72 @@ +import fetch from 'cross-fetch'; +import Validation from '../validation'; +import snapshot from '@snapshot-labs/snapshot.js'; + +const API_KEY = + process.env.PASSPORT_API_KEY || '0cErnp4F.nRDEUU4Z8y5YyxcU32swrggDFNfWtXtI'; + +const headers = API_KEY + ? { + 'Content-Type': 'application/json', + 'X-API-Key': API_KEY + } + : undefined; + +const GET_PASSPORT_STAMPS_URI = `https://api.scorer.gitcoin.co/registry/stamps/`; + +function hasValidIssuanceAndExpiration(credential, proposalTs) { + const issuanceDate = Number( + new Date(credential.issuanceDate).getTime() / 1000 + ).toFixed(0); + const expirationDate = Number( + new Date(credential.expirationDate).getTime() / 1000 + ).toFixed(0); + if (issuanceDate <= proposalTs && expirationDate >= proposalTs) { + return true; + } + return false; +} + +export default class extends Validation { + public id = 'passport-gated'; + public github = 'snapshot-labs'; + public version = '0.1.0'; + public title = 'Gitcoin Passport Gated'; + public description = + 'Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.'; + + async validate(currentAddress = this.author): Promise { + const requiredStamps = this.params.stamps || []; + const operator = this.params.operator; + if (!operator) throw new Error('Operator is required'); + + const stampsResponse = await fetch( + GET_PASSPORT_STAMPS_URI + currentAddress, + { headers } + ); + + const stampsData = await stampsResponse.json(); + + if (!stampsData?.items) { + console.log('[passport] Unknown error', stampsData); + throw new Error('Unknown error'); + } + if (stampsData.items.length === 0) return false; + + const provider = snapshot.utils.getProvider(this.network); + const proposalTs = (await provider.getBlock(this.snapshot)).timestamp; + // check expiration for all stamps + const validStamps = stampsData.items + .filter((stamp) => + hasValidIssuanceAndExpiration(stamp.credential, proposalTs) + ) + .map((stamp) => stamp.credential.credentialSubject.provider); + + if (operator === 'AND') { + return requiredStamps.every((stamp) => validStamps.includes(stamp)); + } else if (operator === 'OR') { + return requiredStamps.some((stamp) => validStamps.includes(stamp)); + } + return false; + } +} diff --git a/Implementations/API/backend/utils/snapshot/validations/passport-gated/schema.json b/Implementations/API/backend/utils/snapshot/validations/passport-gated/schema.json new file mode 100644 index 00000000..76ada86d --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/passport-gated/schema.json @@ -0,0 +1,271 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Validation", + "definitions": { + "Validation": { + "title": "Gitcoin passport gated", + "type": "object", + "properties": { + "stamps": { + "type": "array", + "title": "Stamps", + "uniqueItems": true, + "minItems": 1, + "maxItems": 32, + "items": { + "type": "string", + "anyOf": [ + { + "const": "Google", + "title": "Google account" + }, + { + "const": "Ens", + "title": "ENS owner" + }, + { + "const": "Poh", + "title": "Proof of humanity" + }, + { + "const": "Twitter", + "title": "Twitter account" + }, + { + "const": "TwitterTweetGT10", + "title": "Twitter followers > 10" + }, + { + "const": "TwitterFollowerGT100", + "title": "Twitter followers > 100" + }, + { + "const": "TwitterFollowerGT500", + "title": "Twitter followers > 500" + }, + { + "const": "TwitterFollowerGTE1000", + "title": "Twitter followers > 1000" + }, + { + "const": "TwitterFollowerGT5000", + "title": "Twitter followers > 5000" + }, + { + "const": "POAP", + "title": "POAP owner" + }, + { + "const": "Facebook", + "title": "Facebook account" + }, + { + "const": "FacebookFriends", + "title": "Facebook friends > 100" + }, + { + "const": "FacebookProfilePicture", + "title": "Facebook profile picture" + }, + { + "const": "Brightid", + "title": "Brightid" + }, + { + "const": "Github", + "title": "Github account" + }, + { + "const": "FiveOrMoreGithubRepos", + "title": "Github repos at least 5" + }, + { + "const": "ForkedGithubRepoProvider", + "title": "Github repo forked" + }, + { + "const": "StarredGithubRepoProvider", + "title": "Github repo starred" + }, + { + "const": "TenOrMoreGithubFollowers", + "title": "Github followers at least 10" + }, + { + "const": "FiftyOrMoreGithubFollowers", + "title": "Github followers at least 50" + }, + { + "const": "GitcoinContributorStatistics#numGrantsContributeToGte#1", + "title": "Grants contributed to at least 1" + }, + + { + "const": "GitcoinContributorStatistics#numGrantsContributeToGte#10", + "title": "Grants contributed to at least 10" + }, + { + "const": "GitcoinContributorStatistics#numGrantsContributeToGte#25", + "title": "Grants contributed to at least 25" + }, + { + "const": "GitcoinContributorStatistics#numGrantsContributeToGte#100", + "title": "Grants contributed to at least 100" + }, + { + "const": "GitcoinContributorStatistics#totalContributionAmountGte#10", + "title": "Grant contributions at least $10" + }, + { + "const": "GitcoinContributorStatistics#totalContributionAmountGte#100", + "title": "Grant contributions at least $100" + }, + { + "const": "GitcoinContributorStatistics#totalContributionAmountGte#1000", + "title": "Grant contributions at least $1000" + }, + { + "const": "GitcoinGranteeStatistics#numOwnedGrants#1", + "title": "Grant owner of at least 1 Grant" + }, + { + "const": "GitcoinGranteeStatistics#numGrantContributors#10", + "title": "Grant contributors at least 10" + }, + { + "const": "GitcoinGranteeStatistics#numGrantContributors#25", + "title": "Grant contributors at least 25" + }, + + { + "const": "GitcoinGranteeStatistics#numGrantContributors#100", + "title": "Grant contributors at least 100" + }, + { + "const": "GitcoinGranteeStatistics#totalContributionAmount#100", + "title": "Grant have received at least $100" + }, + { + "const": "GitcoinGranteeStatistics#totalContributionAmount#1000", + "title": "Grant have received at least $1000" + }, + { + "const": "GitcoinGranteeStatistics#totalContributionAmount#10000", + "title": "Grant have received at least $10000" + }, + { + "const": "GitcoinGranteeStatistics#numGrantsInEcoAndCauseRound#1", + "title": "Grant owner at least 1 Eco & Cause" + }, + { + "const": "Linkedin", + "title": "Linkedin account" + }, + { + "const": "Discord", + "title": "Discord account" + }, + { + "const": "SnapshotVotesProvider", + "title": "Snapshot voter" + }, + { + "const": "SnapshotProposalsProvider", + "title": "Snapshot proposer" + }, + { + "const": "ethPossessionsGte#1", + "title": "ETH at least 1" + }, + { + "const": "ethPossessionsGte#10", + "title": "ETH at least 10" + }, + { + "const": "ethPossessionsGte#32", + "title": "ETH at least 32" + }, + { + "const": "FirstEthTxnProvider", + "title": "First ETH more than 30 days ago" + }, + { + "const": "EthGTEOneTxnProvider", + "title": "ETH tx at least 1" + }, + { + "const": "EthGasProvider", + "title": "ETH gas at least 0.5" + }, + { + "const": "gtcPossessionsGte#10", + "title": "Gitcoin GTC at least 10" + }, + { + "const": "gtcPossessionsGte#100", + "title": "Gitcoin GTC at least 100" + }, + { + "const": "SelfStakingBronze", + "title": "GTC staked at least 1" + }, + { + "const": "SelfStakingSilver", + "title": "GTC staked at least 10" + }, + { + "const": "SelfStakingGold", + "title": "GTC staked at least 100" + }, + { + "const": "CommunityStakingBronze", + "title": "Comm GTC staked at least 1" + }, + { + "const": "CommunityStakingSilver", + "title": "Comm GTC staked at least 10" + }, + { + "const": "CommunityStakingGold", + "title": "Comm GTC staked at least 100" + }, + { + "const": "NFT", + "title": "NFT holder" + }, + { + "const": "ZkSync", + "title": "ZkSync account" + }, + { + "const": "Lens", + "title": "Lens account" + }, + { + "const": "GnosisSafe", + "title": "GnosisSafe singer/owner" + } + ] + } + }, + "operator": { + "type": "string", + "title": "Approval operator", + "description": "Control how many or which stamps are required to vote.", + "anyOf": [ + { + "const": "AND", + "title": "Require all stamps" + }, + { + "const": "OR", + "title": "Require at least one stamp" + } + ] + } + }, + "required": ["stamps"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/validations/passport-weighted/README.md b/Implementations/API/backend/utils/snapshot/validations/passport-weighted/README.md new file mode 100644 index 00000000..48a20f34 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/passport-weighted/README.md @@ -0,0 +1,31 @@ +# Gitcoin passport weighted validation + +This repository provides a passport-weighted validation strategy for Snapshot. The implementation integrates with the Gitcoin API to validate whether a user is authorized to vote on a proposal. + +## Prerequisites + +Before using this code, ensure that you have the following information stored in a `.env` file at the project root: + +- `NEXT_PUBLIC_GC_API_KEY=` +- `NEXT_PUBLIC_GC_SCORER_ID=` + +## Overview + +This implementation uses the Gitcoin Passport API to check whether a user has a passport score thats above the minScore threshold value. + +## Code Explanation + +The main function in this codebase returns a threshold score based on the user's passport. + +## Modifications + +The original code utilized the Passport SDK to check if the user meets the passport score threshold. However, with the introduction of the Passport API, we can now simplify the process by checking directly for the score. + +## Last Modified + +This code was last modified on May 11, 2023. + +Feel free to customize and extend this implementation to suit your specific needs. + + + diff --git a/Implementations/API/backend/utils/snapshot/validations/passport-weighted/examples.json b/Implementations/API/backend/utils/snapshot/validations/passport-weighted/examples.json new file mode 100644 index 00000000..5e6c883c --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/passport-weighted/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example of a passport-weighted validation", + "author": "0x24F15402C6Bb870554489b2fd2049A85d75B982f", + "space": "fabien.eth", + "network": "1", + "snapshot": "latest", + "params": { + "minScore": 10, + "stamps": [ + { "id": "Ens", "weight": 2 }, + { "id": "Twitter", "weight": 0.5 }, + { "id": "Github", "weight": 0.5 }, + { "id": "POAP", "weight": 2 }, + { "id": "SnapshotVotesProvider", "weight": 2 } + ], + "min_weight": 1 + } + } +] diff --git a/Implementations/API/backend/utils/snapshot/validations/passport-weighted/index.ts b/Implementations/API/backend/utils/snapshot/validations/passport-weighted/index.ts new file mode 100644 index 00000000..d72299e2 --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/passport-weighted/index.ts @@ -0,0 +1,52 @@ +import fetch from 'cross-fetch'; +import Validation from '../validation'; +import dotenv from 'dotenv'; +dotenv.config({ path: '.env' }); + +const API_KEY = process.env.NEXT_PUBLIC_GC_API_KEY; +const SCORER_ID = process.env.NEXT_PUBLIC_GC_SCORER_ID; + +const headers = API_KEY + ? { + 'Content-Type': 'application/json', + 'X-API-Key': API_KEY + } + : undefined; + +export default class extends Validation { + public id = 'passport-weighted'; + public github = 'snapshot-labs'; + public version = '0.1.0'; + public title = 'Gitcoin Passport Weighted'; + public description = + 'Protect your proposals from spam and vote manipulation by requiring users to have a Gitcoin Passport.'; + + async validate(currentAddress = this.author): Promise { + const GET_PASSPORT_SCORE_URI = `https://api.scorer.gitcoin.co/registry/score/${SCORER_ID}/${currentAddress}`; + + const THRESHOLD_NUMBER = this.params.minScore || 1; + + try { + const response = await fetch(GET_PASSPORT_SCORE_URI, { + headers + }); + const passportData = await response.json(); + if (passportData.score) { + const roundedScore = Math.round(passportData.score * 100) / 100; + if (roundedScore >= THRESHOLD_NUMBER) return true; + console.log( + `Your passport score (${roundedScore}) is lower than the threshold score (${THRESHOLD_NUMBER}).` + ); + return false; + } else { + console.log( + 'You do not have a valid Gitcoin Passport. Create one by visiting https://passport.gitcoin.co/ ' + ); + return false; + } + } catch (err) { + console.log('error: ', err); + throw err; + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/validations/passport-weighted/schema.json b/Implementations/API/backend/utils/snapshot/validations/passport-weighted/schema.json new file mode 100644 index 00000000..0d741f6a --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/passport-weighted/schema.json @@ -0,0 +1,278 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Validation", + "definitions": { + "Validation": { + "title": "Gitcoin passport weighted", + "type": "object", + "properties": { + "minScore": { + "type": "number", + "title": "Minimum score", + "minimum": 10 + }, + "stamps": { + "type": "array", + "title": "Stamps", + "uniqueItems": true, + "minItems": 1, + "maxItems": 32, + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "Id", + "anyOf": [ + { + "const": "Google", + "title": "Google account" + }, + { + "const": "Ens", + "title": "ENS owner" + }, + { + "const": "Poh", + "title": "Proof of humanity" + }, + { + "const": "Twitter", + "title": "Twitter account" + }, + { + "const": "TwitterTweetGT10", + "title": "Twitter followers > 10" + }, + { + "const": "TwitterFollowerGT100", + "title": "Twitter followers > 100" + }, + { + "const": "TwitterFollowerGT500", + "title": "Twitter followers > 500" + }, + { + "const": "TwitterFollowerGTE1000", + "title": "Twitter followers > 1000" + }, + { + "const": "TwitterFollowerGT5000", + "title": "Twitter followers > 5000" + }, + { + "const": "POAP", + "title": "POAP owner" + }, + { + "const": "Facebook", + "title": "Facebook account" + }, + { + "const": "FacebookFriends", + "title": "Facebook friends > 100" + }, + { + "const": "FacebookProfilePicture", + "title": "Facebook profile picture" + }, + { + "const": "Brightid", + "title": "Brightid" + }, + { + "const": "Github", + "title": "Github account" + }, + { + "const": "FiveOrMoreGithubRepos", + "title": "Github repos > 4" + }, + { + "const": "ForkedGithubRepoProvider", + "title": "Github repo forked" + }, + { + "const": "StarredGithubRepoProvider", + "title": "Github repo starred" + }, + { + "const": "TenOrMoreGithubFollowers", + "title": "Github followers > 9" + }, + { + "const": "FiftyOrMoreGithubFollowers", + "title": "Github followers > 49" + }, + { + "const": "GitcoinContributorStatistics#numGrantsContributeToGte#1", + "title": "Grants contributed > 1" + }, + + { + "const": "GitcoinContributorStatistics#numGrantsContributeToGte#10", + "title": "Grants contributed > 10" + }, + { + "const": "GitcoinContributorStatistics#numGrantsContributeToGte#25", + "title": "Grants contributed > 25" + }, + { + "const": "GitcoinContributorStatistics#numGrantsContributeToGte#100", + "title": "Grants contributed > 100" + }, + { + "const": "GitcoinContributorStatistics#totalContributionAmountGte#10", + "title": "Grant contributions > $10" + }, + { + "const": "GitcoinContributorStatistics#totalContributionAmountGte#100", + "title": "Grant contributions > $100" + }, + { + "const": "GitcoinContributorStatistics#totalContributionAmountGte#1000", + "title": "Grant contributions > $1000" + }, + { + "const": "GitcoinGranteeStatistics#numOwnedGrants#1", + "title": "Grant owner > 1" + }, + { + "const": "GitcoinGranteeStatistics#numGrantContributors#10", + "title": "Grant contributors > 10" + }, + { + "const": "GitcoinGranteeStatistics#numGrantContributors#25", + "title": "Grant contributors > 25" + }, + + { + "const": "GitcoinGranteeStatistics#numGrantContributors#100", + "title": "Grant contributors > 100" + }, + { + "const": "GitcoinGranteeStatistics#totalContributionAmount#100", + "title": "Grant contributions > $100" + }, + { + "const": "GitcoinGranteeStatistics#totalContributionAmount#1000", + "title": "Grant contributions > $1000" + }, + { + "const": "GitcoinGranteeStatistics#totalContributionAmount#10000", + "title": "Grant contributions > $10000" + }, + { + "const": "GitcoinGranteeStatistics#numGrantsInEcoAndCauseRound#1", + "title": "Grant owner Eco & Cause" + }, + { + "const": "Linkedin", + "title": "Linkedin account" + }, + { + "const": "Discord", + "title": "Discord account" + }, + { + "const": "SnapshotVotesProvider", + "title": "Snapshot voter" + }, + { + "const": "SnapshotProposalsProvider", + "title": "Snapshot proposer" + }, + { + "const": "ethPossessionsGte#1", + "title": "ETH > 1" + }, + { + "const": "ethPossessionsGte#10", + "title": "ETH > 10" + }, + { + "const": "ethPossessionsGte#32", + "title": "ETH > 32" + }, + { + "const": "FirstEthTxnProvider", + "title": "First ETH > 30 days" + }, + { + "const": "EthGTEOneTxnProvider", + "title": "ETH tx > 1" + }, + { + "const": "EthGasProvider", + "title": "ETH gas > 0.5" + }, + { + "const": "gtcPossessionsGte#10", + "title": "Gitcoin GTC > 10" + }, + { + "const": "gtcPossessionsGte#100", + "title": "Gitcoin GTC > 100" + }, + { + "const": "SelfStakingBronze", + "title": "GTC staked > 1" + }, + { + "const": "SelfStakingSilver", + "title": "GTC staked > 10" + }, + { + "const": "SelfStakingGold", + "title": "GTC staked > 100" + }, + { + "const": "CommunityStakingBronze", + "title": "Comm GTC staked > 1" + }, + { + "const": "CommunityStakingSilver", + "title": "Comm GTC staked > 10" + }, + { + "const": "CommunityStakingGold", + "title": "Comm GTC staked > 100" + }, + { + "const": "NFT", + "title": "NFT holder" + }, + { + "const": "ZkSync", + "title": "ZkSync account" + }, + { + "const": "Lens", + "title": "Lens account" + }, + { + "const": "GnosisSafe", + "title": "GnosisSafe singer/owner" + } + ] + }, + "weight": { + "type": "number", + "title": "Weight" + } + }, + "default": { "id": "Ens", "weight": "1" }, + "required": ["id", "weight"], + "additionalProperties": false + } + }, + "min_weight": { + "type": "number", + "title": "Min. weight" + } + }, + "required": ["stamps", "min_weight"], + "additionalProperties": false + } + } +} diff --git a/Implementations/API/backend/utils/snapshot/validations/validation.ts b/Implementations/API/backend/utils/snapshot/validations/validation.ts new file mode 100644 index 00000000..48913bcb --- /dev/null +++ b/Implementations/API/backend/utils/snapshot/validations/validation.ts @@ -0,0 +1,31 @@ +export default class Validation { + public id = ''; + public github = ''; + public version = ''; + public title = ''; + public description = ''; + + public author: string; + public space: string; + public network: string; + public snapshot: number | 'latest'; + public params: any; + + constructor( + author: string, + space: string, + network: string, + snapshot: number | 'latest', + params: any + ) { + this.author = author; + this.space = space; + this.network = network; + this.snapshot = snapshot; + this.params = params; + } + + async validate(): Promise { + return true; + } +} diff --git a/Implementations/API/package-lock.json b/Implementations/API/package-lock.json index fafa7da5..d5c198d6 100644 --- a/Implementations/API/package-lock.json +++ b/Implementations/API/package-lock.json @@ -1,32 +1,5776 @@ { "name": "rest-api", "version": "0.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "rest-api", + "version": "0.0.0", + "workspaces": [ + "backend" + ], + "dependencies": { + "@ethersproject/shims": "^5.7.0", + "@snapshot-labs/snapshot.js": "^0.4.98", + "aws-lambda": "^1.0.7", + "ethers": "^5.7.2", + "node-fetch": "^3.2.6" + }, + "devDependencies": { + "@serverless-stack/cli": "^1.2.8", + "@serverless-stack/resources": "^1.2.8", + "@tsconfig/node16": "^1.0.2", + "@types/aws-lambda": "^8.10.97", + "typescript": "^4.7.2", + "vitest": "^0.12.9" + } + }, + "backend": { + "version": "0.0.0", + "dependencies": { + "aws-sdk": "^2.1143.0" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.97" + } + }, + "node_modules/@aws-cdk/aws-apigatewayv2-alpha": { + "version": "2.24.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-alpha/-/aws-apigatewayv2-alpha-2.24.0-alpha.0.tgz", + "integrity": "sha512-0Ajo4ENUh8RNorG66p1AfLkBAtZJkcxGsgyfz9PbvBAYl0OjpB7ALjEXS2+lrdMMJ8Ewhuhdqut7XfQYhyXtkA==", + "dev": true, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.24.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/aws-apigatewayv2-authorizers-alpha": { + "version": "2.24.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-authorizers-alpha/-/aws-apigatewayv2-authorizers-alpha-2.24.0-alpha.0.tgz", + "integrity": "sha512-h4SQiquBB6MQ1v6MrnlVaK68OLp6zn3NAMnORZh20qd44EgQhRmqClCLFvXRCt1o/kN/CferGFepPXy64qLyOQ==", + "dev": true, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "2.24.0-alpha.0", + "aws-cdk-lib": "^2.24.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/aws-apigatewayv2-integrations-alpha": { + "version": "2.24.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-integrations-alpha/-/aws-apigatewayv2-integrations-alpha-2.24.0-alpha.0.tgz", + "integrity": "sha512-hDP1VQpYKmdgFXwron6weZlo4NV1qTQ6TJ5cRZl+JfOnWhsE6zcTiNog+U45ZQNLkQt7Is6JzdmB7+4Fpc0aLw==", + "dev": true, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "2.24.0-alpha.0", + "aws-cdk-lib": "^2.24.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/aws-appsync-alpha": { + "version": "2.24.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-appsync-alpha/-/aws-appsync-alpha-2.24.0-alpha.0.tgz", + "integrity": "sha512-kqv6mRXySBcQtAvHlumunJ7bWll38u8SP6ZSOPHS84uzu9BGhasIi3cFDdNNxvSqnN2eOJHoGf2LQJE4cPKxrw==", + "dev": true, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.24.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@ensdomains/eth-ens-namehash": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@ensdomains/eth-ens-namehash/-/eth-ens-namehash-2.0.15.tgz", + "integrity": "sha512-JRDFP6+Hczb1E0/HhIg0PONgBYasfGfDheujmfxaZaAv/NAH4jE6Kf48WbqfRZdxt4IZI3jl3Ri7sZ1nP09lgw==" + }, + "node_modules/@ethersproject/abi": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", + "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", + "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", + "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", + "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/rlp": "^5.7.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", + "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz", + "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/properties": "^5.7.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", + "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", + "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", + "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0" + } + }, + "node_modules/@ethersproject/contracts": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz", + "integrity": "sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/transactions": "^5.7.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", + "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz", + "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/basex": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/pbkdf2": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wordlists": "^5.7.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz", + "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hdnode": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/pbkdf2": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", + "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", + "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ] + }, + "node_modules/@ethersproject/networks": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", + "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz", + "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/sha2": "^5.7.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", + "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", + "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/base64": "^5.7.0", + "@ethersproject/basex": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/networks": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0", + "bech32": "1.1.4", + "ws": "7.4.6" + } + }, + "node_modules/@ethersproject/providers/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@ethersproject/random": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz", + "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", + "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz", + "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/shims": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/shims/-/shims-5.7.0.tgz", + "integrity": "sha512-WeDptc6oAprov5CCN2LJ/6/+dC9gTonnkdAtLepm/7P5Z+3PRxS5NpfVWmOMs1yE4Vitl2cU8bOPWC0GvGSbVg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ] + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", + "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "bn.js": "^5.2.1", + "elliptic": "6.5.4", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", + "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/sha2": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", + "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", + "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0" + } + }, + "node_modules/@ethersproject/units": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz", + "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/logger": "^5.7.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", + "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/hdnode": "^5.7.0", + "@ethersproject/json-wallets": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wordlists": "^5.7.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", + "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/base64": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz", + "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/logger": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/strings": "^5.7.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rest-api/backend": { + "resolved": "backend", + "link": true + }, + "node_modules/@serverless-stack/aws-lambda-ric": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@serverless-stack/aws-lambda-ric/-/aws-lambda-ric-2.0.13.tgz", + "integrity": "sha512-Aj4X2wMW6O5/PQoKoBdQGC3LwQyGTgW1XZtF0rs07WE9s6Q+46zWaVgURQjoNmTNQKpHSGJYo6B+ycp9u7/CSA==", + "dev": true, + "dependencies": { + "node-addon-api": "3.2.1", + "node-gyp": "8.1.0" + }, + "bin": { + "aws-lambda-ric": "bin/index.js" + } + }, + "node_modules/@serverless-stack/cli": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@serverless-stack/cli/-/cli-1.2.8.tgz", + "integrity": "sha512-bTqyN2TCyHphv/Zj0s2H+ClZwKDmcy6YnUaS15SuniJ1hk7Er/zFbUmmP+SC+3/RpsfyF3CZeU8y8LRHeNSsWQ==", + "dev": true, + "dependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "2.24.0-alpha.0", + "@serverless-stack/core": "1.2.8", + "@serverless-stack/resources": "1.2.8", + "aws-cdk": "2.24.0", + "aws-cdk-lib": "2.24.0", + "aws-sdk": "^2.1110.0", + "body-parser": "^1.19.0", + "chalk": "^4.1.0", + "chokidar": "^3.4.3", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "esbuild": "^0.14.11", + "esbuild-runner": "^2.2.1", + "express": "^4.17.1", + "fs-extra": "^9.0.1", + "remeda": "^0.0.32", + "source-map-support": "^0.5.19", + "ws": "^8.6.0", + "yargs": "^15.4.1" + }, + "bin": { + "sst": "bin/scripts.mjs" + } + }, + "node_modules/@serverless-stack/core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@serverless-stack/core/-/core-1.2.8.tgz", + "integrity": "sha512-sOsEZi6RL/gGTZqTvXaFqN8AzmSedzCxcXipODNiLA1LTFI8y2Kh/MWGjqOedN8HtuUN2fgvJ/qtgtV/9MbuVQ==", + "dev": true, + "dependencies": { + "@serverless-stack/aws-lambda-ric": "^2.0.13", + "@trpc/server": "^9.16.0", + "acorn": "^8.7.1", + "acorn-walk": "^8.2.0", + "async-retry": "^1.3.3", + "aws-cdk": "2.24.0", + "aws-cdk-lib": "2.24.0", + "aws-sdk": "^2.1110.0", + "chalk": "^4.1.0", + "chokidar": "^3.5.2", + "ci-info": "^3.3.0", + "conf": "^10.1.1", + "constructs": "^10.0.29", + "cross-spawn": "^7.0.3", + "dendriform-immer-patch-optimiser": "^2.1.0", + "dotenv": "^10.0.0", + "dotenv-expand": "^5.1.0", + "esbuild": "^0.14.11", + "escodegen": "^2.0.0", + "express": "^4.17.1", + "fs-extra": "^9.0.1", + "immer": "^9.0.7", + "js-yaml": "^4.1.0", + "log4js": "^6.3.0", + "picomatch": "^2.3.0", + "remeda": "^0.0.32", + "typescript": "^4.6.2", + "uuid": "^8.3.2", + "ws": "^8.6.0", + "xstate": "4.26.1", + "zod": "^3.11.6" + }, + "peerDependencies": { + "graphql": "^16.5.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@serverless-stack/resources": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@serverless-stack/resources/-/resources-1.2.8.tgz", + "integrity": "sha512-FMUkTA5jfvzRGY+NivDKLVshQ6xQ6z+B37kNnhz3yMjKak/Xth64mrD2iCraFeLr+gGaCNnNx/EE0QsIhzuVqQ==", + "dev": true, + "dependencies": { + "@aws-cdk/aws-apigatewayv2-alpha": "2.24.0-alpha.0", + "@aws-cdk/aws-apigatewayv2-authorizers-alpha": "2.24.0-alpha.0", + "@aws-cdk/aws-apigatewayv2-integrations-alpha": "2.24.0-alpha.0", + "@aws-cdk/aws-appsync-alpha": "2.24.0-alpha.0", + "@serverless-stack/core": "1.2.8", + "archiver": "^5.3.0", + "aws-cdk-lib": "2.24.0", + "chalk": "^4.1.0", + "constructs": "^10.0.29", + "cross-spawn": "^7.0.3", + "fs-extra": "^9.0.1", + "glob": "^7.1.7", + "zip-local": "^0.3.4", + "zod": "^3.14.3" + }, + "peerDependencies": { + "@graphql-tools/merge": "^8.2.12", + "graphql": "^16.5.0" + }, + "peerDependenciesMeta": { + "@graphql-tools/merge": { + "optional": true + }, + "graphql": { + "optional": true + } + } + }, + "node_modules/@snapshot-labs/snapshot.js": { + "version": "0.4.98", + "resolved": "https://registry.npmjs.org/@snapshot-labs/snapshot.js/-/snapshot.js-0.4.98.tgz", + "integrity": "sha512-4slGeX1TW+R3dsTZfn/GrfgfuRMVW7d9HUsX5bqBMH3RPV57PtMCrKagvZZiaHQcNh7jww5I+pW69Q05n6lOrg==", + "dependencies": { + "@ensdomains/eth-ens-namehash": "^2.0.15", + "@ethersproject/abi": "^5.6.4", + "@ethersproject/address": "^5.6.1", + "@ethersproject/bytes": "^5.6.1", + "@ethersproject/contracts": "^5.6.2", + "@ethersproject/hash": "^5.6.1", + "@ethersproject/providers": "^5.6.8", + "@ethersproject/units": "^5.7.0", + "@ethersproject/wallet": "^5.6.2", + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "cross-fetch": "^3.1.6", + "json-to-graphql-query": "^2.2.4", + "lodash.set": "^4.3.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@trpc/server": { + "version": "9.23.6", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-9.23.6.tgz", + "integrity": "sha512-xdoHjv0Km6kr7fBpu91rN3wURWGazUA7+dRquWywWLUDH3EiERg6n0Lkz/YgxH7n+liu6UkRs1Npjt6oo01+xA==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.97", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.97.tgz", + "integrity": "sha512-BZk3qO4R2KN8Ts3eR6CW1n8LI46UOgv1KoDZjo8J9vOQvDeX/rsrv1H0BpEAMcSqZ1mLwTEyAMtlua5tlSn0kw==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", + "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.0.tgz", + "integrity": "sha512-tNEZYz5G/zYunxFm7sfhAxkXEuLj3K6BKwv6ZURlsF6yiUQ65z0Q2wZW9L5cPUl9ocofGvXOdFYbFHp0+6MOig==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "dev": true + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/async-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "dev": true, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/aws-cdk": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.24.0.tgz", + "integrity": "sha512-XqAyD2WIaKnglUPlePt6c7WMICemo6Rl9RPnKqI0IKnnzTORuXHyVw9i6F9S0jNV31iDw2uxZPDUfcZ83owq/Q==", + "dev": true, + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.24.0.tgz", + "integrity": "sha512-icYISk6uddpBDe2csqIpZ0PPo8ryg8Polyxyh9ri6rmRIElOJIkBmTpJc7ORcP9vv9LjFIH6BRV7OltUxpcWUg==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "yaml" + ], + "dev": true, + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^9.1.0", + "ignore": "^5.2.0", + "jsonschema": "^1.4.0", + "minimatch": "^3.1.2", + "punycode": "^2.1.1", + "semver": "^7.3.7", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/at-least-node": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "dev": true, + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "9.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.10", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.3.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/aws-lambda": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/aws-lambda/-/aws-lambda-1.0.7.tgz", + "integrity": "sha512-9GNFMRrEMG5y3Jvv+V4azWvc+qNWdWLTjDdhf/zgMlz8haaaLWv0xeAIWxz9PuWUBawsVxy0zZotjCdR3Xq+2w==", + "dependencies": { + "aws-sdk": "^2.814.0", + "commander": "^3.0.2", + "js-yaml": "^3.14.1", + "watchpack": "^2.0.0-beta.10" + }, + "bin": { + "lambda": "bin/lambda" + } + }, + "node_modules/aws-lambda/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aws-lambda/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/aws-sdk": { + "version": "2.1143.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1143.0.tgz", + "integrity": "sha512-WSFpJomqDbsO9buVBL9O3R5/qM2uGzqSEBCyo2W9IiE81mOZ/LrX0fWa9vHhYwCVBYQ2HhS+C0rwJxhRvYEhjQ==", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.1.tgz", + "integrity": "sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==", + "dev": true + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" + }, + "node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/conf": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.1.2.tgz", + "integrity": "sha512-o9Fv1Mv+6A0JpoayQ8JleNp3hhkbOJP/Re/Q+QqxMPHPkABVsRjQGWZn9A5GcqLiTNC6d89p2PB5ZhHVDSMwyg==", + "dev": true, + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "node_modules/constructs": { + "version": "10.1.18", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.18.tgz", + "integrity": "sha512-kEQmVqCnB/fqmAX+Eww3OiOL1r1c7Bu1GaKA5sTBjHreIvhsfZZe+u0j5bU+DpxPvZqsf/U8EqBV8V/Ai3XhAA==", + "dev": true, + "engines": { + "node": ">= 14.17.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "dependencies": { + "node-fetch": "^2.6.11" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/date-format": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.10.tgz", + "integrity": "sha512-RuMIHocrVjF84bUSTcd1uokIsLsOsk1Awb7TexNOI3f48ukCu39mjslWquDTA08VaDMF2umr3MB9ow5EyJTWyA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "node_modules/dendriform-immer-patch-optimiser": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/dendriform-immer-patch-optimiser/-/dendriform-immer-patch-optimiser-2.1.2.tgz", + "integrity": "sha512-IGoxH1AsYMjwGnuRqCrCzJwWESdgRh9334hDxayRWj1Loa2QhyTiu5PcQ6i+b5YRHnkrFMrCIX5zpvnQTxBFuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "immer": "9" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "dev": true, + "dependencies": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" + }, + "engines": { + "node": ">= 4.2.1" + } + }, + "node_modules/detect-port-alt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/detect-port-alt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.39.tgz", + "integrity": "sha512-2kKujuzvRWYtwvNjYDY444LQIA3TyJhJIX3Yo4+qkFlDDtGlSicWgeHVJqMUP/2sSfH10PGwfsj+O2ro1m10xQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.39", + "esbuild-android-arm64": "0.14.39", + "esbuild-darwin-64": "0.14.39", + "esbuild-darwin-arm64": "0.14.39", + "esbuild-freebsd-64": "0.14.39", + "esbuild-freebsd-arm64": "0.14.39", + "esbuild-linux-32": "0.14.39", + "esbuild-linux-64": "0.14.39", + "esbuild-linux-arm": "0.14.39", + "esbuild-linux-arm64": "0.14.39", + "esbuild-linux-mips64le": "0.14.39", + "esbuild-linux-ppc64le": "0.14.39", + "esbuild-linux-riscv64": "0.14.39", + "esbuild-linux-s390x": "0.14.39", + "esbuild-netbsd-64": "0.14.39", + "esbuild-openbsd-64": "0.14.39", + "esbuild-sunos-64": "0.14.39", + "esbuild-windows-32": "0.14.39", + "esbuild-windows-64": "0.14.39", + "esbuild-windows-arm64": "0.14.39" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.39.tgz", + "integrity": "sha512-EJOu04p9WgZk0UoKTqLId9VnIsotmI/Z98EXrKURGb3LPNunkeffqQIkjS2cAvidh+OK5uVrXaIP229zK6GvhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.39.tgz", + "integrity": "sha512-+twajJqO7n3MrCz9e+2lVOnFplRsaGRwsq1KL/uOy7xK7QdRSprRQcObGDeDZUZsacD5gUkk6OiHiYp6RzU3CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.39.tgz", + "integrity": "sha512-ImT6eUw3kcGcHoUxEcdBpi6LfTRWaV6+qf32iYYAfwOeV+XaQ/Xp5XQIBiijLeo+LpGci9M0FVec09nUw41a5g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.39.tgz", + "integrity": "sha512-/fcQ5UhE05OiT+bW5v7/up1bDsnvaRZPJxXwzXsMRrr7rZqPa85vayrD723oWMT64dhrgWeA3FIneF8yER0XTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.39.tgz", + "integrity": "sha512-oMNH8lJI4wtgN5oxuFP7BQ22vgB/e3Tl5Woehcd6i2r6F3TszpCnNl8wo2d/KvyQ4zvLvCWAlRciumhQg88+kQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.39.tgz", + "integrity": "sha512-1GHK7kwk57ukY2yI4ILWKJXaxfr+8HcM/r/JKCGCPziIVlL+Wi7RbJ2OzMcTKZ1HpvEqCTBT/J6cO4ZEwW4Ypg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.39.tgz", + "integrity": "sha512-g97Sbb6g4zfRLIxHgW2pc393DjnkTRMeq3N1rmjDUABxpx8SjocK4jLen+/mq55G46eE2TA0MkJ4R3SpKMu7dg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.39.tgz", + "integrity": "sha512-4tcgFDYWdI+UbNMGlua9u1Zhu0N5R6u9tl5WOM8aVnNX143JZoBZLpCuUr5lCKhnD0SCO+5gUyMfupGrHtfggQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.39.tgz", + "integrity": "sha512-t0Hn1kWVx5UpCzAJkKRfHeYOLyFnXwYynIkK54/h3tbMweGI7dj400D1k0Vvtj2u1P+JTRT9tx3AjtLEMmfVBQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.39.tgz", + "integrity": "sha512-23pc8MlD2D6Px1mV8GMglZlKgwgNKAO8gsgsLLcXWSs9lQsCYkIlMo/2Ycfo5JrDIbLdwgP8D2vpfH2KcBqrDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.39.tgz", + "integrity": "sha512-epwlYgVdbmkuRr5n4es3B+yDI0I2e/nxhKejT9H0OLxFAlMkeQZxSpxATpDc9m8NqRci6Kwyb/SfmD1koG2Zuw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.39.tgz", + "integrity": "sha512-W/5ezaq+rQiQBThIjLMNjsuhPHg+ApVAdTz2LvcuesZFMsJoQAW2hutoyg47XxpWi7aEjJGrkS26qCJKhRn3QQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.39.tgz", + "integrity": "sha512-IS48xeokcCTKeQIOke2O0t9t14HPvwnZcy+5baG13Z1wxs9ZrC5ig5ypEQQh4QMKxURD5TpCLHw2W42CLuVZaA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.39.tgz", + "integrity": "sha512-zEfunpqR8sMomqXhNTFEKDs+ik7HC01m3M60MsEjZOqaywHu5e5682fMsqOlZbesEAAaO9aAtRBsU7CHnSZWyA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.39.tgz", + "integrity": "sha512-Uo2suJBSIlrZCe4E0k75VDIFJWfZy+bOV6ih3T4MVMRJh1lHJ2UyGoaX4bOxomYN3t+IakHPyEoln1+qJ1qYaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.39.tgz", + "integrity": "sha512-secQU+EpgUPpYjJe3OecoeGKVvRMLeKUxSMGHnK+aK5uQM3n1FPXNJzyz1LHFOo0WOyw+uoCxBYdM4O10oaCAA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-runner": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/esbuild-runner/-/esbuild-runner-2.2.1.tgz", + "integrity": "sha512-VP0VfJJZiZ3cKzdOH59ZceDxx/GzBKra7tiGM8MfFMLv6CR1/cpsvtQ3IsJI3pz7HyeYxtbPyecj3fHwR+3XcQ==", + "dev": true, + "dependencies": { + "source-map-support": "0.5.19", + "tslib": "2.3.1" + }, + "bin": { + "esr": "bin/esr.js" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/esbuild-runner/node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.39.tgz", + "integrity": "sha512-qHq0t5gePEDm2nqZLb+35p/qkaXVS7oIe32R0ECh2HOdiXXkj/1uQI9IRogGqKkK+QjDG+DhwiUw7QoHur/Rwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.39.tgz", + "integrity": "sha512-XPjwp2OgtEX0JnOlTgT6E5txbRp6Uw54Isorm3CwOtloJazeIWXuiwK0ONJBVb/CGbiCpS7iP2UahGgd2p1x+Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.39.tgz", + "integrity": "sha512-E2wm+5FwCcLpKsBHRw28bSYQw0Ikxb7zIMxw3OPAkiaQhLVr3dnVO8DofmbWhhf6b97bWzg37iSZ45ZDpLw7Ow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.39", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.39.tgz", + "integrity": "sha512-sBZQz5D+Gd0EQ09tZRnz/PpVdLwvp/ufMtJ1iDFYddDaPpZXKqPyaxfYBLs3ueiaksQ26GGa7sci0OqFzNs7KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "node_modules/escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/fetch-blob": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz", + "integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/immer": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.14.tgz", + "integrity": "sha512-ubBeqQutOSLIFCUBN03jGeOS6a3DoYlSYwYJTa+gSKEZKU5redJIqkIdZ3JVv/4RZpfcXdAWH5zCNLWPRv2WDw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", + "dev": true + }, + "node_modules/json-to-graphql-query": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/json-to-graphql-query/-/json-to-graphql-query-2.2.5.tgz", + "integrity": "sha512-5Nom9inkIMrtY992LMBBG1Zaekrc10JaRhyZgprwHBVMDtRgllTvzl0oBbg13wJsVZoSoFNNMaeIVQs0P04vsA==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jszip": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.6.1.tgz", + "integrity": "sha1-uI86ey5noqBIFSmCx6N1bZxIKPA=", + "dev": true, + "dependencies": { + "pako": "~1.0.2" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/local-pkg": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.1.tgz", + "integrity": "sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", + "dev": true + }, + "node_modules/log4js": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.5.1.tgz", + "integrity": "sha512-z1hRRe5DDPzsP73PgN/GYmeSbIAl/g9kX3GLjABCpcU1ojns8S4cyjpJ21jU1P7z1wWkm69PjyMcEGqYYdDqaA==", + "dev": true, + "dependencies": { + "date-format": "^4.0.10", + "debug": "^4.3.4", + "flatted": "^3.2.5", + "rfdc": "^1.3.0", + "streamroller": "^3.1.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz", + "integrity": "sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.0.5", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^5.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.6.tgz", + "integrity": "sha512-LAy/HZnLADOVkVPubaxHDft29booGglPFDr2Hw0J1AercRh01UiVFm++KMDnJeH9sHgNB4hsXPii7Sgym/sTbw==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.1.0.tgz", + "integrity": "sha512-o2elh1qt7YUp3lkMwY3/l4KF3j/A3fI/Qt4NH+CQQgPJdqGE9y7qnP84cjIWN27Q0jJkrSAhCVDg+wBVNBYdBg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^8.0.14", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.0", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.1.tgz", + "integrity": "sha512-Y/jF6vnvEtOPGiKD1+q+X0CiUYRQtEHp89MLLUJ7TUivtH8Ugn2+3A7Rynqk7BRsAoqeOQWnFnjpDrKSxDgIGA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.1.tgz", + "integrity": "sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/remeda": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-0.0.32.tgz", + "integrity": "sha512-FEdl8ONpqY7AvvMHG5WYdomc0mGf2khHPUDu6QvNkOq4Wjkw5BvzWM4QyksAQ/US1sFIIRG8TVBn6iJx6HbRrA==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.74.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.74.1.tgz", + "integrity": "sha512-K2zW7kV8Voua5eGkbnBtWYfMIhYhT9Pel2uhBk2WO5eMee161nPze/XRfvEQPFYz7KgrCCnmh2Wy0AMFLGGmMA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==" + }, + "node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/send/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", + "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "dev": true, + "dependencies": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "4", + "socks": "^2.3.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamroller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.1.tgz", + "integrity": "sha512-iPhtd9unZ6zKdWgMeYGfSBuqCngyJy1B/GPi/lTpwGpa3bajuX30GjUVd0/Tn/Xhg0mr4DOSENozz9Y06qyonQ==", + "dev": true, + "dependencies": { + "date-format": "^4.0.10", + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tinypool": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.1.3.tgz", + "integrity": "sha512-2IfcQh7CP46XGWGGbdyO4pjcKqsmVqFAPcXfPxcPXmOWt9cYkTP9HcDmGgsfijYoAEc4z9qcpM/BaBz46Y9/CQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-0.3.2.tgz", + "integrity": "sha512-2+40EP4D3sFYy42UkgkFFB+kiX2Tg3URG/lVvAZFfLxgGpnWl5qQJuBw1gaLttq8UOS+2p3C0WrhJnQigLTT2Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", + "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "2.9.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.9.tgz", + "integrity": "sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.12.9.tgz", + "integrity": "sha512-1NtyUANS72Qw5PwYvoztk067NX4fSiis2xQxhByOWS33eL2er/yupHyLxlBCOkF2ANe0dLFRvT1GVb+nczL5aw==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.1", + "@types/chai-subset": "^1.3.3", + "chai": "^4.3.6", + "debug": "^4.3.4", + "local-pkg": "^0.4.1", + "tinypool": "^0.1.3", + "tinyspy": "^0.3.2", + "vite": "^2.9.8" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vitest/ui": "*", + "c8": "*", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@vitest/ui": { + "optional": true + }, + "c8": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", + "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/ws": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", + "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "node_modules/xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xstate": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.26.1.tgz", + "integrity": "sha512-JLofAEnN26l/1vbODgsDa+Phqa61PwDlxWu8+2pK+YbXf+y9pQSDLRvcYH2H1kkeUBA5fGp+xFL/zfE8jNMw4g==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-local": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/zip-local/-/zip-local-0.3.5.tgz", + "integrity": "sha512-GRV3D5TJY+/PqyeRm5CYBs7xVrKTKzljBoEXvocZu0HJ7tPEcgpSOYa2zFIsCZWgKWMuc4U3yMFgFkERGFIB9w==", + "dev": true, + "dependencies": { + "async": "^1.4.2", + "graceful-fs": "^4.1.3", + "jszip": "^2.6.1", + "q": "^1.4.1" + } + }, + "node_modules/zip-local/node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true + }, + "node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/zod": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.17.3.tgz", + "integrity": "sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + }, "dependencies": { "@aws-cdk/aws-apigatewayv2-alpha": { "version": "2.24.0-alpha.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-alpha/-/aws-apigatewayv2-alpha-2.24.0-alpha.0.tgz", "integrity": "sha512-0Ajo4ENUh8RNorG66p1AfLkBAtZJkcxGsgyfz9PbvBAYl0OjpB7ALjEXS2+lrdMMJ8Ewhuhdqut7XfQYhyXtkA==", - "dev": true + "dev": true, + "requires": {} }, "@aws-cdk/aws-apigatewayv2-authorizers-alpha": { "version": "2.24.0-alpha.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-authorizers-alpha/-/aws-apigatewayv2-authorizers-alpha-2.24.0-alpha.0.tgz", "integrity": "sha512-h4SQiquBB6MQ1v6MrnlVaK68OLp6zn3NAMnORZh20qd44EgQhRmqClCLFvXRCt1o/kN/CferGFepPXy64qLyOQ==", - "dev": true + "dev": true, + "requires": {} }, "@aws-cdk/aws-apigatewayv2-integrations-alpha": { "version": "2.24.0-alpha.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-apigatewayv2-integrations-alpha/-/aws-apigatewayv2-integrations-alpha-2.24.0-alpha.0.tgz", "integrity": "sha512-hDP1VQpYKmdgFXwron6weZlo4NV1qTQ6TJ5cRZl+JfOnWhsE6zcTiNog+U45ZQNLkQt7Is6JzdmB7+4Fpc0aLw==", - "dev": true + "dev": true, + "requires": {} }, "@aws-cdk/aws-appsync-alpha": { "version": "2.24.0-alpha.0", "resolved": "https://registry.npmjs.org/@aws-cdk/aws-appsync-alpha/-/aws-appsync-alpha-2.24.0-alpha.0.tgz", "integrity": "sha512-kqv6mRXySBcQtAvHlumunJ7bWll38u8SP6ZSOPHS84uzu9BGhasIi3cFDdNNxvSqnN2eOJHoGf2LQJE4cPKxrw==", - "dev": true + "dev": true, + "requires": {} + }, + "@ensdomains/eth-ens-namehash": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@ensdomains/eth-ens-namehash/-/eth-ens-namehash-2.0.15.tgz", + "integrity": "sha512-JRDFP6+Hczb1E0/HhIg0PONgBYasfGfDheujmfxaZaAv/NAH4jE6Kf48WbqfRZdxt4IZI3jl3Ri7sZ1nP09lgw==" }, "@ethersproject/abi": { "version": "5.7.0", @@ -266,7 +6010,8 @@ "ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "requires": {} } } }, @@ -437,6 +6182,13 @@ "rimraf": "^3.0.2" } }, + "@rest-api/backend": { + "version": "file:backend", + "requires": { + "@types/aws-lambda": "^8.10.97", + "aws-sdk": "^2.1143.0" + } + }, "@serverless-stack/aws-lambda-ric": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@serverless-stack/aws-lambda-ric/-/aws-lambda-ric-2.0.13.tgz", @@ -535,6 +6287,27 @@ "zod": "^3.14.3" } }, + "@snapshot-labs/snapshot.js": { + "version": "0.4.98", + "resolved": "https://registry.npmjs.org/@snapshot-labs/snapshot.js/-/snapshot.js-0.4.98.tgz", + "integrity": "sha512-4slGeX1TW+R3dsTZfn/GrfgfuRMVW7d9HUsX5bqBMH3RPV57PtMCrKagvZZiaHQcNh7jww5I+pW69Q05n6lOrg==", + "requires": { + "@ensdomains/eth-ens-namehash": "^2.0.15", + "@ethersproject/abi": "^5.6.4", + "@ethersproject/address": "^5.6.1", + "@ethersproject/bytes": "^5.6.1", + "@ethersproject/contracts": "^5.6.2", + "@ethersproject/hash": "^5.6.1", + "@ethersproject/providers": "^5.6.8", + "@ethersproject/units": "^5.7.0", + "@ethersproject/wallet": "^5.6.2", + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "cross-fetch": "^3.1.6", + "json-to-graphql-query": "^2.2.4", + "lodash.set": "^4.3.2" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -647,7 +6420,6 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -659,7 +6431,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, "requires": { "ajv": "^8.0.0" } @@ -1472,6 +7243,24 @@ } } }, + "cross-fetch": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "requires": { + "node-fetch": "^2.6.11" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1543,7 +7332,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/dendriform-immer-patch-optimiser/-/dendriform-immer-patch-optimiser-2.1.2.tgz", "integrity": "sha512-IGoxH1AsYMjwGnuRqCrCzJwWESdgRh9334hDxayRWj1Loa2QhyTiu5PcQ6i+b5YRHnkrFMrCIX5zpvnQTxBFuQ==", - "dev": true + "dev": true, + "requires": {} }, "depd": { "version": "1.1.2", @@ -1648,7 +7438,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "requires": { "iconv-lite": "^0.6.2" @@ -2020,8 +7809,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-levenshtein": { "version": "2.0.6", @@ -2339,7 +8127,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -2494,8 +8281,7 @@ "json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-schema-typed": { "version": "7.0.3", @@ -2503,6 +8289,11 @@ "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", "dev": true }, + "json-to-graphql-query": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/json-to-graphql-query/-/json-to-graphql-query-2.2.5.tgz", + "integrity": "sha512-5Nom9inkIMrtY992LMBBG1Zaekrc10JaRhyZgprwHBVMDtRgllTvzl0oBbg13wJsVZoSoFNNMaeIVQs0P04vsA==" + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2581,6 +8372,11 @@ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", "dev": true }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==" + }, "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -3182,8 +8978,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "require-main-filename": { "version": "2.0.0", @@ -3242,7 +9037,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "sax": { "version": "1.2.1", @@ -3464,6 +9259,15 @@ } } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -3475,15 +9279,6 @@ "strip-ansi": "^3.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -3575,6 +9370,11 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -3646,7 +9446,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" }, @@ -3654,8 +9453,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" } } }, @@ -3735,6 +9533,20 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3820,7 +9632,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", - "dev": true + "dev": true, + "requires": {} }, "xml2js": { "version": "0.4.19", diff --git a/Implementations/API/package.json b/Implementations/API/package.json index 89d14016..4ec26de7 100644 --- a/Implementations/API/package.json +++ b/Implementations/API/package.json @@ -24,6 +24,7 @@ ], "dependencies": { "@ethersproject/shims": "^5.7.0", + "@snapshot-labs/snapshot.js": "^0.4.98", "aws-lambda": "^1.0.7", "ethers": "^5.7.2", "node-fetch": "^3.2.6" diff --git a/Implementations/API/stacks/MyStack.ts b/Implementations/API/stacks/MyStack.ts index bbbcdb4a..b6300915 100644 --- a/Implementations/API/stacks/MyStack.ts +++ b/Implementations/API/stacks/MyStack.ts @@ -19,6 +19,7 @@ export function MyStack({ stack }: StackContext) { 'GET /daodao/members/{network}/{id}': 'functions/daodao/getMembers.handler', 'GET /daodao/proposals/{network}/{id}': 'functions/daodao/getProposals.handler', 'GET /snapshot/members/{id}': 'functions/snapshot/getMembers.handler', + 'GET /snapshot/delegations/{id}': 'functions/snapshot/getDelegations.handler', 'GET /snapshot/proposals/{id}': 'functions/snapshot/getProposals.handler', 'GET /nouns/members/{network}/{id}': 'functions/nouns/getMembers.handler', },