diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..4f1006822 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + allow: + # Allow updates for snapshot.js only + - dependency-name: "@snapshot-labs/snapshot.js" diff --git a/package.json b/package.json index e8c1e883e..4157d65b5 100755 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@ethersproject/strings": "^5.6.1", "@ethersproject/units": "^5.6.1", "@ethersproject/wallet": "^5.6.2", - "@snapshot-labs/snapshot.js": "^0.4.109", + "@snapshot-labs/snapshot.js": "^0.5.8", "@spruceid/didkit-wasm-node": "^0.2.1", "@uniswap/sdk-core": "^3.0.1", "@uniswap/v3-sdk": "^3.9.0", diff --git a/src/strategies/aave-governance-power/README.md b/src/strategies/aave-governance-power/README.md index f091e23ba..6ab02a140 100644 --- a/src/strategies/aave-governance-power/README.md +++ b/src/strategies/aave-governance-power/README.md @@ -1,11 +1,10 @@ -# Contract call strategy +# aave-governance-power strategy Allows to get Voting power or Proposition power from an Aave GovernanceStrategy contract. -## Strategy Parameters +## Params -| 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 | | | -| | | | | | +| 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/src/strategies/aave-governance-power/examples.json b/src/strategies/aave-governance-power/examples.json index 76b6e2b5f..e297a3380 100644 --- a/src/strategies/aave-governance-power/examples.json +++ b/src/strategies/aave-governance-power/examples.json @@ -4,14 +4,14 @@ "strategy": { "name": "aave-governance-power", "params": { - "governanceStrategy": "0xb7e383ef9b1e9189fc0f71fb30af8aa14377429e", + "symbol": "AAVE+stkAAVE", + "decimals": 18, "powerType": "vote", - "symbol": "Voting Power", - "decimals": 18 + "governanceStrategy": "0xb7e383ef9b1e9189fc0f71fb30af8aa14377429e" } }, "network": "1", - "addresses": ["0x5BC928BF0DAb1e4A2ddd9e347b0F22e88026D76c"], - "snapshot": 12657715 + "addresses": ["0x329c54289Ff5D6B7b7daE13592C6B1EDA1543eD4", "0x57ab7ee15cE5ECacB1aB84EE42D5A9d0d8112922", "0x0ab97008cad303a8C90ea630c282760284c19e93"], + "snapshot": 18054498 } ] diff --git a/src/strategies/aave-governance-power/index.ts b/src/strategies/aave-governance-power/index.ts index 0e0542f97..a7157abf3 100644 --- a/src/strategies/aave-governance-power/index.ts +++ b/src/strategies/aave-governance-power/index.ts @@ -10,26 +10,8 @@ export const version = '0.1.0'; */ 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' - } + 'function getPropositionPowerAt(address user, uint256 blockNumber) view returns (uint256)', + 'function getVotingPowerAt(address user, uint256 blockNumber) view returns (uint256)' ]; const powerTypesToMethod = { diff --git a/src/strategies/aave-governance-power/schema.json b/src/strategies/aave-governance-power/schema.json new file mode 100644 index 000000000..25cedec86 --- /dev/null +++ b/src/strategies/aave-governance-power/schema.json @@ -0,0 +1,40 @@ +{ + "$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 + }, + "governanceStrategy": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "powerType": { + "type": "string", + "title": "Power type", + "enum": ["vote", "proposition"] + }, + "decimals": { + "type": "integer", + "title": "Decimals", + "examples": ["e.g. 18"], + "minimum": 0, + "maximum": 18 + } + }, + "required": ["governanceStrategy", "powerType", "decimals"], + "additionalProperties": false + } + } +} diff --git a/src/strategies/deposit-in-sablier-stream/examples.json b/src/strategies/deposit-in-sablier-stream/examples.json deleted file mode 100644 index e0eea6f0d..000000000 --- a/src/strategies/deposit-in-sablier-stream/examples.json +++ /dev/null @@ -1,35 +0,0 @@ -[ - { - "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/src/strategies/erc20-balance-of-top-holders/README.md b/src/strategies/erc20-balance-of-top-holders/README.md new file mode 100644 index 000000000..211310382 --- /dev/null +++ b/src/strategies/erc20-balance-of-top-holders/README.md @@ -0,0 +1,17 @@ +# erc20-balance-of-top-holders + +Strategy, that accept votes only from top N token holders + +Subgraph should be compatible with [OpenZeppelin ERC20 Subgraph](https://github.com/OpenZeppelin/openzeppelin-subgraphs) + +Here is an example of parameters: + +```json +{ + "address": "0x8494Aee22e0DB34daA1e8D6829d85710357be9F7", + "symbol": "HANDZ", + "decimals": 18, + "subgraphUrl": "https://api.thegraph.com/subgraphs/name/kostyamospan/handz-token", + "topHolders": 5 +} +``` diff --git a/src/strategies/erc20-balance-of-top-holders/examples.json b/src/strategies/erc20-balance-of-top-holders/examples.json new file mode 100644 index 000000000..fda35a613 --- /dev/null +++ b/src/strategies/erc20-balance-of-top-holders/examples.json @@ -0,0 +1,30 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "erc20-balance-of-top-holders", + "params": { + "address": "0x8494Aee22e0DB34daA1e8D6829d85710357be9F7", + "symbol": "HANDZ", + "decimals": 18, + "subgraphUrl": "https://api.thegraph.com/subgraphs/name/kostyamospan/handz-token", + "topHolders": 5 + } + }, + "network": "1", + "addresses": [ + "0xD87dec8eE5d941234d85a9d2636D077fE03B0660", + "0x54B9ca09248C49e9ed0968bfD0AA0bd13E85992A", + "0x254c8A6225CE32903D2657D0945701ACD6e42188", + "0xF59Dd6525529F77cF90Ec9A8205d52874af7a425", + "0x0a0A1669A7E3f16b3010dD78F53D3E22Dcf4b739", + "0x5b941eA7387190Ba6a0bF4B766c1F3eB0Ab30A25", + "0xc0D57b31F33Ce4f6627d353Ac51461fB7cDC1519", + "0xF5De2507726A312aD6f806A9972Ed438344022b2", + "0x33b5752C03495014A130f8f4A235f6280C58f266", + "0x6323b71f37a07642F8055E9847B58Ffc9FA44243", + "0xf5F730146D177Cf7Dd28AA39F81359DaE51D4379" + ], + "snapshot": 18020926 + } +] diff --git a/src/strategies/erc20-balance-of-top-holders/index.ts b/src/strategies/erc20-balance-of-top-holders/index.ts new file mode 100644 index 000000000..353f5e1e1 --- /dev/null +++ b/src/strategies/erc20-balance-of-top-holders/index.ts @@ -0,0 +1,73 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'RedDuck-Software'; +export const version = '0.0.1'; + +async function getTopHoldersBalance( + url, + options, + snapshot +): Promise> { + const topHoldersAmount = +options.topHolders || 0; + + const query = { + erc20Balances: { + __args: { + where: { + account_not: null, + contract: options.address + }, + orderBy: 'valueExact', + orderDirection: 'desc', + first: topHoldersAmount + }, + value: true, + contract: true, + account: { + id: true + } + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + query.erc20Balances.__args.block = { number: snapshot }; + } + + const topHolders: Record = {}; + + const result = await subgraphRequest(url, query); + + if (result && result.erc20Balances) { + result.erc20Balances.forEach((tokenBalance) => { + const address = getAddress(tokenBalance.account.id); + const balance = parseFloat(tokenBalance.value); + topHolders[address] = balance; + }); + } + + return topHolders; +} + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const topHoldersBalancesScores = await getTopHoldersBalance( + options.subgraphUrl, + options, + snapshot + ); + + return Object.fromEntries( + addresses.map((address) => [ + address, + topHoldersBalancesScores[address] ? topHoldersBalancesScores[address] : 0 + ]) + ); +} diff --git a/src/strategies/friend-tech/README.md b/src/strategies/friend-tech/README.md new file mode 100644 index 000000000..34377b453 --- /dev/null +++ b/src/strategies/friend-tech/README.md @@ -0,0 +1,12 @@ +# friend-tech + +This strategy allocates voting power according to the number of keys (or shares) a voter possesses on friend.tech for a particular user. + +Here is an example of parameters: + +```json +{ + "address": "0xe12a2f60b400e6c6971d5602df454e5da63edd78", + "symbol": "KEYS" +} +``` diff --git a/src/strategies/friend-tech/examples.json b/src/strategies/friend-tech/examples.json new file mode 100644 index 000000000..38a2fcfbd --- /dev/null +++ b/src/strategies/friend-tech/examples.json @@ -0,0 +1,19 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "friend-tech", + "params": { + "address": "0xe12A2f60B400E6c6971D5602DF454E5dA63eDD78", + "symbol": "KEYS" + } + }, + "network": "8453", + "addresses": [ + "0x81a9a7979f5EB27588b5AB9448398ad321Dba90C", + "0x3B7576DF0Ef2d6c1656245aE15Ad52DCf34FD04a", + "0x7C2FDC7de9F536560E47105257a57C8C8dF79372" + ], + "snapshot": 2933820 + } +] diff --git a/src/strategies/friend-tech/index.ts b/src/strategies/friend-tech/index.ts new file mode 100644 index 000000000..cf5ebc6b3 --- /dev/null +++ b/src/strategies/friend-tech/index.ts @@ -0,0 +1,33 @@ +import { BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'bonustrack'; +export const version = '0.1.0'; + +const abi = ['function sharesBalance(address,address) view returns (uint256)']; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const contract = '0xcf205808ed36593aa40a44f10c7f7c2f67d4a4d4'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + addresses.forEach((address) => + multi.call(address, contract, 'sharesBalance', [options.address, address]) + ); + const result: Record = await multi.execute(); + + return Object.fromEntries( + Object.entries(result).map(([address, balance]) => [ + address, + parseFloat(formatUnits(balance, 0)) + ]) + ); +} diff --git a/src/strategies/friend-tech/schema.json b/src/strategies/friend-tech/schema.json new file mode 100644 index 000000000..8213636da --- /dev/null +++ b/src/strategies/friend-tech/schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "address": { + "type": "string", + "title": "User address", + "examples": ["e.g. 0xe12a2f60b400e6c6971d5602df454e5da63edd78"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "symbol": { + "type": "string", + "title": "Symbol", + "examples": ["e.g. KEYS"], + "maxLength": 16 + } + }, + "required": ["address"], + "additionalProperties": false + } + } +} diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 629582442..c9068b0c2 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -31,6 +31,7 @@ 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 erc20BalanceOfTopHolders from './erc20-balance-of-top-holders'; import * as erc20BalanceOfWeighted from './erc20-balance-of-weighted'; import * as ethalendBalanceOf from './ethalend-balance-of'; import * as prepoVesting from './prepo-vesting'; @@ -57,6 +58,7 @@ import * as uniswap from './uniswap'; import * as faralandStaking from './faraland-staking'; import * as flashstake from './flashstake'; import * as pancake from './pancake'; +import * as pancakeProfile from './pancake-profile'; import * as synthetix from './synthetix'; import * as aelinCouncil from './aelin-council'; import * as synthetixQuadratic from './synthetix-quadratic'; @@ -192,6 +194,7 @@ 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 taraxaDelegation from './taraxa-delegation'; import * as poap from './poap'; import * as poapWithWeight from './poap-with-weight'; import * as poapWithWeightV2 from './poap-with-weight-v2'; @@ -398,7 +401,6 @@ 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 nation3CoopPassportWithDelegations from './nation3-passport-coop-with-delegations'; @@ -452,10 +454,13 @@ import * as hatsProtocolSingleVotePerOrg from './hats-protocol-single-vote-per-o import * as karmaDiscordRoles from './karma-discord-roles'; import * as seedifyHoldStakingFarming from './seedify-cumulative-voting-power-hodl-staking-farming'; import * as stakedMoreKudasai from './staked-morekudasai'; +import * as sablierV1Deposit from './sablier-v1-deposit'; import * as sablierV2 from './sablier-v2'; import * as gelatoStaking from './gelato-staking'; import * as erc4626AssetsOf from './erc4626-assets-of'; import * as sdVoteBoostTWAVPV2 from './sd-vote-boost-twavp-v2'; +import * as friendTech from './friend-tech'; +import * as moonbase from './moonbase'; const strategies = { 'cap-voting-power': capVotingPower, @@ -506,6 +511,7 @@ const strategies = { 'erc20-balance-of-delegation': erc20BalanceOfDelegation, 'erc20-balance-of-with-delegation': erc20BalanceOfWithDelegation, 'erc20-balance-of-quadratic-delegation': erc20BalanceOfQuadraticDelegation, + 'erc20-balance-of-top-holders': erc20BalanceOfTopHolders, 'erc20-balance-of-weighted': erc20BalanceOfWeighted, 'minto-balance-of-all': mintoBalanceAll, 'erc20-balance-of-indexed': erc20BalanceOfIndexed, @@ -545,6 +551,7 @@ const strategies = { 'faraland-staking': faralandStaking, flashstake, pancake, + 'pancake-profile': pancakeProfile, synthetix, 'aelin-council': aelinCouncil, 'synthetix-quadratic': synthetixQuadratic, @@ -662,6 +669,7 @@ const strategies = { 'unipool-same-token': unipoolSameToken, 'unipool-univ2-lp': unipoolUniv2Lp, 'unipool-xsushi': unipoolXSushi, + 'taraxa-delegation': taraxaDelegation, poap: poap, 'poap-with-weight': poapWithWeight, 'poap-with-weight-v2': poapWithWeightV2, @@ -865,7 +873,6 @@ const strategies = { '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, 'nation3-passport-coop-with-delegations': nation3CoopPassportWithDelegations, @@ -917,10 +924,13 @@ const strategies = { 'seedify-cumulative-voting-power-hodl-staking-farming': seedifyHoldStakingFarming, 'staked-morekudasai': stakedMoreKudasai, + 'sablier-v1-deposit': sablierV1Deposit, 'sablier-v2': sablierV2, 'gelato-staking': gelatoStaking, 'erc4626-assets-of': erc4626AssetsOf, - 'sd-vote-boost-twavp-v2': sdVoteBoostTWAVPV2 + 'friend-tech': friendTech, + 'sd-vote-boost-twavp-v2': sdVoteBoostTWAVPV2, + 'moonbase': moonbase }; Object.keys(strategies).forEach(function (strategyName) { diff --git a/src/strategies/karma-discord-roles/schema.json b/src/strategies/karma-discord-roles/schema.json index 74e0b97fc..ada8692b0 100644 --- a/src/strategies/karma-discord-roles/schema.json +++ b/src/strategies/karma-discord-roles/schema.json @@ -14,7 +14,8 @@ "roles": { "type": "array", "title": "Discord Roles", - "examples": ["e.g. Assembly, Trader, Moderator"] + "examples": ["e.g. Assembly, Trader, Moderator"], + "items": { "type": "string" } } }, "required": ["name", "roles"], diff --git a/src/strategies/moonbase/README.md b/src/strategies/moonbase/README.md new file mode 100644 index 000000000..aa7bc5d1b --- /dev/null +++ b/src/strategies/moonbase/README.md @@ -0,0 +1,37 @@ +# Moonbase + +This is the strategy, it returns the balances of the voters for MBG token balances +in Moonbase project(pools, farms, vaults, token). + +Here is an example of parameters: + +```json +[ + { + "name": "Example query Moonbase", + "strategy": { + "name": "moonbase", + "params": { + "address": "0xc97c478Fc35d75b51549C39974053a679A5C67E1", + "masterChef": "0x830304d6C669d33738c7E4c1F2310CC1E530Df63", + "moonbaseLPs": [ + { + "address": "0x230C64C42886A1F6b91eD8C11B59a2D45865d38F", + "pid": 7 + } + ], + "symbol": "MBG", + "decimals": 18 + } + }, + "network": "84531", + "addresses": [ + "0xe32C26Be24232ba92cd89d116985F81f94Dd26a8", + "0x7DC90A11489C384dc72234120B0f84C3932d94Ce", + "0xf704872349a62ceBb40F841B635de268b2F7B9Fb" + ], + "snapshot": 9182354 + } +] +``` +Note: A maximum of 1,000,000,000 moonbaseLPs are allowed in the strategy to avoid memory issues. \ No newline at end of file diff --git a/src/strategies/moonbase/examples.json b/src/strategies/moonbase/examples.json new file mode 100644 index 000000000..1668c37e4 --- /dev/null +++ b/src/strategies/moonbase/examples.json @@ -0,0 +1,27 @@ +[ + { + "name": "Example query Moonbase", + "strategy": { + "name": "moonbase", + "params": { + "address": "0xc97c478Fc35d75b51549C39974053a679A5C67E1", + "masterChef": "0x830304d6C669d33738c7E4c1F2310CC1E530Df63", + "moonbaseLPs": [ + { + "address": "0x230C64C42886A1F6b91eD8C11B59a2D45865d38F", + "pid": 7 + } + ], + "symbol": "MBG", + "decimals": 18 + } + }, + "network": "84531", + "addresses": [ + "0xe32C26Be24232ba92cd89d116985F81f94Dd26a8", + "0x7DC90A11489C384dc72234120B0f84C3932d94Ce", + "0xf704872349a62ceBb40F841B635de268b2F7B9Fb" + ], + "snapshot": 9182354 + } + ] \ No newline at end of file diff --git a/src/strategies/moonbase/index.ts b/src/strategies/moonbase/index.ts new file mode 100644 index 000000000..a48a17357 --- /dev/null +++ b/src/strategies/moonbase/index.ts @@ -0,0 +1,89 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; +import examplesFile from './examples.json'; + +export const author = 'MoonbaseMarkets'; +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 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); + } +}; + +const MAX_MOONBASE_LPS = 1000000000; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + + if (options.moonbaseLPs.length > MAX_MOONBASE_LPS) { + throw new Error(`Too many moonbaseLPs. Maximum allowed is ${MAX_MOONBASE_LPS}.`); + } + + 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]); + }); + options.moonbaseLPs.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 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]); + options.moonbaseLPs.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)) + ]) + ); +} \ No newline at end of file diff --git a/src/strategies/pancake-profile/README.md b/src/strategies/pancake-profile/README.md new file mode 100644 index 000000000..18a0ab900 --- /dev/null +++ b/src/strategies/pancake-profile/README.md @@ -0,0 +1,13 @@ +# pancake-profile + +## Description + +This strategy calculates the voting power of users who have locked their NFTs from a specific collection within the Pancake Profile contract. + +```json +{ + "address": "0x0a8901b0E25DEb55A87524f0cC164E9644020EBA", + "symbol": "PCS", + "decimals": 18 +} +``` diff --git a/src/strategies/pancake-profile/examples.json b/src/strategies/pancake-profile/examples.json new file mode 100644 index 000000000..d979ad404 --- /dev/null +++ b/src/strategies/pancake-profile/examples.json @@ -0,0 +1,37 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "pancake-profile", + "params": { + "address": "0x0a8901b0E25DEb55A87524f0cC164E9644020EBA", + "symbol": "PCS", + "decimals": 18 + } + }, + "network": "56", + "addresses": [ + "0x89eBA09BbFf0CD6f750bCDB423A3cD1f09d876fD", + "0x56d0b5ed3d525332f00c9bc938f93598ab16aaa7", + "0x49e4dbff86a2e5da27c540c9a9e8d2c3726e278f", + "0x4757ce43dc5429b8f1a132dc29ef970e55ae722b", + "0xB4E597e34E3eC254e9e4795ECF1A31b9Fa1e40F4", + "0xd7539FCdC0aB79a7B688b04387cb128E75cb77Dc", + "0x6E33e22f7aC5A4b58A93C7f6D8Da8b46c50A3E20", + "0xC9dA7343583fA8Bb380A6F04A208C612F86C7701", + "0x5315A1C257FD6266F9608f31AC9b6501C98c5750", + "0x2AC89522CB415AC333E64F52a1a5693218cEBD58", + "0xd90c6f6D37716b1Cc4dd2B116be42e8683550F45", + "0x69ABF813a683391C0ec888351912E14590B56e88", + "0x5bFE87274C671b4Cf6A1AF554916819F6141EaA1", + "0x85924aA0B2cb5a0BbeC583Dd090bF7CEdBa5D2Ea", + "0x9149B2b87159c4CC9e2f10C2711357720Da4DA08", + "0xa0710d3b4BA0f848f7edf9CC827aF70A183EAd26", + "0xAE1220f6bFEb414Ed0A95fbb5A6Ecc303b10aa46", + "0x776b913480d4326430F52F58b16DdF67eEB08DEb", + "0xebe986802F7858E1919451C6Ff893e294F31CE54", + "0x2d7cAA8462023af022A5004dA7b781b8ccF81Da7" + ], + "snapshot": 31132896 + } +] diff --git a/src/strategies/pancake-profile/index.ts b/src/strategies/pancake-profile/index.ts new file mode 100644 index 000000000..3d2840a46 --- /dev/null +++ b/src/strategies/pancake-profile/index.ts @@ -0,0 +1,67 @@ +import { getAddress } from '@ethersproject/address'; +import { multicall } from '../../utils'; + +export const author = 'skyrocktech'; +export const version = '0.0.2'; + +const hasRegisteredAbi = [ + 'function hasRegistered(address _userAddress) view returns (bool)' +]; + +const getUserProfileAbi = [ + 'function getUserProfile(address _userAddress) view returns (uint256, uint256, uint256, address, uint256, bool)' +]; + +const pancakeProfileAddress = '0xdf4dbf6536201370f95e06a0f8a7a70fe40e388a'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +) { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const { address: checkAddress } = options; + + const users = Object.fromEntries( + addresses.map((address: any) => [getAddress(address), 0]) + ); + + const hasRegistered = await multicall( + network, + provider, + hasRegisteredAbi, + addresses.map((address: any) => [ + pancakeProfileAddress, + 'hasRegistered', + [address] + ]), + { blockTag } + ); + + const usersWithProfile = addresses.filter( + (address: any, i: number) => hasRegistered[i][0] + ); + + if (usersWithProfile.length) { + const profiles = await multicall( + network, + provider, + getUserProfileAbi, + usersWithProfile.map((user: any) => [ + pancakeProfileAddress, + 'getUserProfile', + [user] + ]), + { blockTag } + ); + + usersWithProfile.forEach((user: any, i: number) => { + users[user] = profiles[i][3] === checkAddress && profiles[i][5] ? 1 : 0; + }); + } + + return users; +} diff --git a/src/strategies/pancake-profile/schema.json b/src/strategies/pancake-profile/schema.json new file mode 100644 index 000000000..2d6e68b84 --- /dev/null +++ b/src/strategies/pancake-profile/schema.json @@ -0,0 +1,33 @@ +{ + "$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. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "symbol": { + "type": "string", + "title": "Symbol", + "examples": ["e.g. UNI"], + "maxLength": 16 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"] + } + }, + "required": ["address"], + "additionalProperties": false + } + } +} diff --git a/src/strategies/razor-network-voting/examples.json b/src/strategies/razor-network-voting/examples.json index e3adcd773..3d3454a50 100644 --- a/src/strategies/razor-network-voting/examples.json +++ b/src/strategies/razor-network-voting/examples.json @@ -7,16 +7,18 @@ "symbol": "RAZOR" } }, - "network": "80001", - "snapshot": 665995, + "network": "278611351", + "snapshot": 434071, "addresses": [ - "0xE79772bC4798fE17dABD3f033af63cc9f887B50F", - "0x6ee26af75a47e15ab040af73cbdc077740f4825c", - "0xfbf427e3c5456b8f3b2e0b98145fa13c16747369", - "0xe0add3053b836e1e1006540b4de959f2b4472a24", - "0xfBf3140A02C1E8fe08a39b86b97c50175B698111", - "0x26A98513240401567733253C6CdD16F006fcd9b1", - "0xA74b60E368c7C870EeB581dc899cFA8158058bb8" + "0xf5ad93418e727607bfea3adf5c056e056d0236a7", + "0x7ee4e7a1403db07c6908ef29c00f20270a28fd2d", + "0x5faf079b1CD8e3Cd526FDbbf3d4e4179ddE476AC", + "0x24566839d381e2f5a5d8f5bf880354f0851cbb76", + "0x7af34d14524104ec65b882b7e31b022eebc88936", + "0x66851befbec4f6acf4bfd2371dab3dfe45f2b920", + "0x35ed54562ddaddd7dd699cdf92128c1d9c1a1529", + "0x13E5f89515B0C781B7118b5dAEEde7Da4BCf9d7b", + "0x13E5f89515B0C781B7118b5dAEEde7Da4BCf9d7b" ] } ] diff --git a/src/strategies/razor-network-voting/index.ts b/src/strategies/razor-network-voting/index.ts index f83d3671a..ea8e101a9 100644 --- a/src/strategies/razor-network-voting/index.ts +++ b/src/strategies/razor-network-voting/index.ts @@ -4,6 +4,7 @@ import { BigNumber } from '@ethersproject/bignumber'; export const author = 'razor-network'; export const version = '0.1.0'; +const PAGE_SIZE = 1000; const RAZOR_NETWORK_SUBGRAPH_URL = 'https://graph-indexer.razorscan.io/subgraphs/name/razor/razor'; @@ -25,6 +26,68 @@ function wei_to_ether(amount: number) { return amount / 10 ** 18; } +export async function getAllData(snapshot) { + let skip = 0; + let allDelegators = []; + let allStakers = []; + + while (true) { + const params = { + delegators: { + __args: { + first: PAGE_SIZE, + skip + }, + staker: { + totalSupply: true, + stake: true, + staker: true + }, + delegatorAddress: true, + sAmount: true + }, + stakers: { + __args: { + first: PAGE_SIZE, + skip + }, + stake: true, + totalSupply: true, + staker: true, + sAmount: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + params.delegators.__args.block = { number: snapshot }; + // @ts-ignore + params.stakers.__args.block = { number: snapshot }; + } + + const response = await subgraphRequest(RAZOR_NETWORK_SUBGRAPH_URL, params); + + if (response.delegators && response.delegators.length) { + allDelegators = allDelegators.concat(response.delegators); + } + + if (response.stakers && response.stakers.length) { + allStakers = allStakers.concat(response.stakers); + } + + if ( + response.stakers.length === PAGE_SIZE || + response.delegators.length === PAGE_SIZE + ) { + skip += PAGE_SIZE; + } else { + break; + } + } + + return { stakers: allStakers, delegators: allDelegators }; +} + export async function strategy( space: any, network: any, @@ -34,43 +97,10 @@ export async function strategy( //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); + // subgraph request : it fetches all the details of the stakers and delegators. + const result = await getAllData(snapshot); if (result.delegators || result.stakers) { result.delegators.forEach( async (delegator: { diff --git a/src/strategies/deposit-in-sablier-stream/README.md b/src/strategies/sablier-v1-deposit/README.md similarity index 55% rename from src/strategies/deposit-in-sablier-stream/README.md rename to src/strategies/sablier-v1-deposit/README.md index 9c60aac49..d8e03ed6a 100644 --- a/src/strategies/deposit-in-sablier-stream/README.md +++ b/src/strategies/sablier-v1-deposit/README.md @@ -1,10 +1,13 @@ # 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; +In Sablier V1, a stream creator locks up an amount of ERC-20 tokens in a contract that progressively allocates the funds to the designated +recipient. The tokens are released by the second, and the recipient can withdraw them at any time. + +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 +```json { "sender": "0xC9F2D9adfa6C24ce0D5a999F2BA3c6b06E36F75E", "token": "0x7f8F6E42C169B294A384F5667c303fd8Eedb3CF3" diff --git a/src/strategies/sablier-v1-deposit/examples.json b/src/strategies/sablier-v1-deposit/examples.json new file mode 100644 index 000000000..c52b59ad3 --- /dev/null +++ b/src/strategies/sablier-v1-deposit/examples.json @@ -0,0 +1,36 @@ +[ + { + "name": "Example query 0", + "strategy": { + "name": "sablier-v1-deposit", + "params": { + "sender": "0xcCe2CbDcD0eee72984c58A84678F0D49a95257ae", + "token": "0x97cb342Cf2F6EcF48c1285Fb8668f5a4237BF862" + } + }, + "network": "5", + "addresses": [ + "0x06255FA39EBD18796eCCCc17DB8153Ef58DBA0a8", + "0xf976aF93B0A5A9F55A7f285a3B5355B8575Eb5bc", + "0x5bCAc9fC8827231839c5861b719e0cAE57da3CfB" + ], + "snapshot": 9515850 + }, + { + "name": "Example query 1", + "strategy": { + "name": "sablier-v1-deposit", + "params": { + "sender": "0xcCe2CbDcD0eee72984c58A84678F0D49a95257ae", + "token": "0x97cb342Cf2F6EcF48c1285Fb8668f5a4237BF862" + } + }, + "network": "5", + "addresses": [ + "0x06255FA39EBD18796eCCCc17DB8153Ef58DBA0a8", + "0xf976aF93B0A5A9F55A7f285a3B5355B8575Eb5bc", + "0x5bCAc9fC8827231839c5861b719e0cAE57da3CfB" + ], + "snapshot": 9515900 + } +] diff --git a/src/strategies/deposit-in-sablier-stream/index.ts b/src/strategies/sablier-v1-deposit/index.ts similarity index 55% rename from src/strategies/deposit-in-sablier-stream/index.ts rename to src/strategies/sablier-v1-deposit/index.ts index afad5ab87..e83febe47 100644 --- a/src/strategies/deposit-in-sablier-stream/index.ts +++ b/src/strategies/sablier-v1-deposit/index.ts @@ -3,19 +3,21 @@ 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 + '1': 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier', // mainnet + '3': 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-ropsten', // ropsten + '4': 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-rinkeby', // rinkeby + '5': 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-goerli', // goerli + '10': 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-optimism', // optimism + '42': 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-kovan', // kovan + '56': 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-bsc', // bsc + '137': 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-matic', // polygon + '42161': + 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-arbitrum', // arbitrum + '43114': + 'https://api.thegraph.com/subgraphs/name/sablier-labs/sablier-avalanche' // avalanche }; -export const author = 'dan13ram'; +export const author = 'sablier-labs'; export const version = '0.1.0'; export async function strategy( @@ -48,10 +50,7 @@ export async function strategy( // @ts-ignore params.streams.__args.block = { number: snapshot }; } - const result = await subgraphRequest( - options.subGraphURL ? options.subGraphURL : SUBGRAPH_URL[network], - params - ); + const result = await subgraphRequest(SUBGRAPH_URL[network], params); const score = Object.fromEntries( addresses.map((address) => [getAddress(address), 0]) ); diff --git a/src/strategies/deposit-in-sablier-stream/schema.json b/src/strategies/sablier-v1-deposit/schema.json similarity index 82% rename from src/strategies/deposit-in-sablier-stream/schema.json rename to src/strategies/sablier-v1-deposit/schema.json index 583a6148d..2e364e335 100644 --- a/src/strategies/deposit-in-sablier-stream/schema.json +++ b/src/strategies/sablier-v1-deposit/schema.json @@ -21,11 +21,6 @@ "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"], diff --git a/src/strategies/sablier-v2/README.md b/src/strategies/sablier-v2/README.md index addf64417..fd1a2ffd8 100644 --- a/src/strategies/sablier-v2/README.md +++ b/src/strategies/sablier-v2/README.md @@ -26,14 +26,14 @@ Based on the chosen strategy, the values filled in the `Addresses` field will re #### Primary policies -| Policy ⭐️ | Methodology | -| :--------------------- | :------------------------------------------------------------------------ | -| withdrawable-recipient | Tokens available/withdrawable by the stream's recipient. | -| reserved-recipient | Tokens available/withdrawable aggregated with unstreamed tokens (future). | +| Policy | Methodology | +| :--------------------- | :---------------------------------------------------------------- | +| withdrawable-recipient | Tokens that are available for the stream's recipient to withdraw. | +| reserved-recipient | Tokens available for withdraw aggregated with unstreamed tokens. | #### Secondary policies -These computation methods are here to aid with special use cases. We still recommend using the primary policies to avoid most caveats. +These policies are designed to address specific edge cases. We strongly recommend using the primary policies. | Policy | Methodology | | :------------------- | :------------------------------------------------------------------------------------ | @@ -74,8 +74,11 @@ Snapshot: Day 15 (midway) with a streamed amount of TKN 500 For the best results, we recommend using the primary policies. -1. The first option is to use the `withdrawable-recipient` policy alongside `erc20-balance-of`. Doing so will aggregate tokens streamed but not withdrawn yet, as well as tokens in the user's wallet. -2. The second best option is using `reserved-recipient` with `erc20-balance-of`. It will aggregate: tokens streamed but not withdrawn yet, unstreamed funds (accessible in the future) and finally, tokens in the user's wallet. +1. The best option is to combine the `withdrawable-recipient` policy with `erc20-balance-of`. Doing so will aggregate + tokens streamed but not withdrawn yet, as well as tokens in the user's wallet. +2. The second best option is to combine `reserved-recipient` with `erc20-balance-of`. They will aggregate (i) tokens + streamed but not withdrawn yet, (ii) unstreamed funds (which will become available in the future), and (iii) the + tokens in the user's wallet. ### Details and Caveats @@ -83,13 +86,18 @@ For the best results, we recommend using the primary policies. The withdrawable amount counts tokens that have been streamed but not withdrawn yet by the recipient. -This is provided using the [`withdrawableAmountOf`](https://docs.sablier.com/contracts/v2/reference/core/abstracts/abstract.SablierV2Lockup#withdrawableamountof) contract method. +This is provided using the +[`withdrawableAmountOf`](/contracts/v2/reference/core/abstracts/abstract.SablierV2Lockup#withdrawableamountof) contract +method. Voting power: realized (present). #### `reserved-recipient` ⭐️ -The reserved amount combines tokens that have been streamed but not withdrawn yet (similar to `withdrawable-recipient`) with tokens that haven't been streamed (still locked yet accessible in the future). It can be computed as `reserved = withdrawable + unstreamed === deposited - withdrawn`. Canceled streams will only count the final withdrawable amount, if any. +The reserved amount combines tokens that have been streamed but not withdrawn yet (similar to `withdrawable-recipient`) +with tokens that haven't been streamed (which will become available in the future). Can be computed as +`reserved = withdrawable + unstreamed === deposited - withdrawn`. Canceled streams will only count the final +withdrawable amount, if any. Voting power: realized (present) + expected (future). diff --git a/src/strategies/sd-vote-boost-twavp-v2/examples.json b/src/strategies/sd-vote-boost-twavp-v2/examples.json index dea07fe5a..27e792d10 100644 --- a/src/strategies/sd-vote-boost-twavp-v2/examples.json +++ b/src/strategies/sd-vote-boost-twavp-v2/examples.json @@ -11,9 +11,7 @@ "decimals": 18, "sampleSize": 10, "sampleStep": 5, - "whiteListedAddress": [ - "0x1c0D72a330F2768dAF718DEf8A19BAb019EEAd09" - ] + "whiteListedAddress": ["0x1c0D72a330F2768dAF718DEf8A19BAb019EEAd09"] } }, "network": "1", @@ -25,4 +23,4 @@ ], "snapshot": 17835000 } -] \ No newline at end of file +] diff --git a/src/strategies/sd-vote-boost-twavp-v2/index.ts b/src/strategies/sd-vote-boost-twavp-v2/index.ts index d432cfb4d..085144226 100644 --- a/src/strategies/sd-vote-boost-twavp-v2/index.ts +++ b/src/strategies/sd-vote-boost-twavp-v2/index.ts @@ -6,134 +6,155 @@ export const version = '0.0.1'; // Used ABI 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)' + '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 + space, + network, + provider, + addresses, + options, + snapshot ): Promise> { - // Maximum of 5 multicall! - if (options.sampleStep > 5) { - throw new Error('maximum of 5 call'); - } - - // Maximum of 20 whitelisted address - if (options.whiteListedAddress.length > 20) { - throw new Error('maximum of 20 whitelisted address'); - } - - // --- Create block number list for twavp - // Obtain last block number - let lastBlock = await provider.getBlockNumber(); - // Create block tag - let blockTag = typeof snapshot === 'number' ? snapshot : lastBlock; - // Create block list - let blockList = getPreviousBlocks(blockTag, options.sampleStep, options.sampleSize); - - // Query working balance of users - const workingBalanceQuery = addresses.map((address: any) => [ - options.sdTokenGauge, - 'working_balances', - [address] - ]); - - // Execute multicall `sampleStep` times - let response: number[] = []; - for (let i = 0; i < options.sampleStep; i++) { - // Use good block number - blockTag = blockList[i]; - - // Add mutlicall response to array - response.push( - await multicall( - network, - provider, - abi, - [ - [options.sdTokenGauge, 'working_supply'], - [options.veToken, 'balanceOf', [options.liquidLocker]], - ...workingBalanceQuery - ], - { blockTag } - ) - ) - }; - - // Get working supply - const workingSupply = response[response.length - 1][0][0]; // Last response, latest block - // Get voting power of liquid locker - const votingPowerLiquidLocker = response[response.length - 1][1][0]; // Last response, latest block - - return Object.fromEntries( - Array(addresses.length) - .fill('x') - .map((_, i) => { - // Init array of working balances for user - let userWorkingBalances: BigNumber[] = []; - - for (let j = 0; j < options.sampleStep; j++) { - // Add working balance to array. - userWorkingBalances.push(response[j][i + 2][0]); - } - - // Get average working balance. - let averageWorkingBalance = average(userWorkingBalances, addresses[i], options.whiteListedAddress); - - // Calculate voting power. - let votingPower = - workingSupply != 0 - ? (averageWorkingBalance).mul(votingPowerLiquidLocker).div(workingSupply.mul(BigNumber.from(10).pow(options.decimals))) - : 0; - - // Return address and voting power - return [addresses[i], Number(votingPower)]; - }) + // Maximum of 5 multicall! + if (options.sampleStep > 5) { + throw new Error('maximum of 5 call'); + } + + // Maximum of 20 whitelisted address + if (options.whiteListedAddress.length > 20) { + throw new Error('maximum of 20 whitelisted address'); + } + + // --- Create block number list for twavp + // Obtain last block number + const lastBlock = await provider.getBlockNumber(); + // Create block tag + let blockTag = typeof snapshot === 'number' ? snapshot : lastBlock; + // Create block list + const blockList = getPreviousBlocks( + blockTag, + options.sampleStep, + options.sampleSize + ); + + // Query working balance of users + const workingBalanceQuery = addresses.map((address: any) => [ + options.sdTokenGauge, + 'working_balances', + [address] + ]); + + // Execute multicall `sampleStep` times + const response: number[] = []; + for (let i = 0; i < options.sampleStep; i++) { + // Use good block number + blockTag = blockList[i]; + + // Add mutlicall response to array + response.push( + await multicall( + network, + provider, + abi, + [ + [options.sdTokenGauge, 'working_supply'], + [options.veToken, 'balanceOf', [options.liquidLocker]], + ...workingBalanceQuery + ], + { blockTag } + ) ); + } + + // Get working supply + const workingSupply = response[response.length - 1][0][0]; // Last response, latest block + // Get voting power of liquid locker + const votingPowerLiquidLocker = response[response.length - 1][1][0]; // Last response, latest block + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + // Init array of working balances for user + const userWorkingBalances: BigNumber[] = []; + + for (let j = 0; j < options.sampleStep; j++) { + // Add working balance to array. + userWorkingBalances.push(response[j][i + 2][0]); + } + + // Get average working balance. + const averageWorkingBalance = average( + userWorkingBalances, + addresses[i], + options.whiteListedAddress + ); + + // Calculate voting power. + const votingPower = + workingSupply != 0 + ? averageWorkingBalance + .mul(votingPowerLiquidLocker) + .div( + workingSupply.mul(BigNumber.from(10).pow(options.decimals)) + ) + : 0; + + // Return address and voting power + return [addresses[i], Number(votingPower)]; + }) + ); } -function getPreviousBlocks(currentBlockNumber: number, numberOfBlocks: number, daysInterval: number): number[] { - // Estimate number of blocks per day - const blocksPerDay = 86400 / 12; - // Calculate total blocks interval - const totalBlocksInterval = blocksPerDay * daysInterval; - // Calculate block interval - const blockInterval = totalBlocksInterval / (numberOfBlocks - 1); - - // Init array of block numbers - const blockNumbers: number[] = []; - - for (let i = 0; i < numberOfBlocks; i++) { - // Calculate block number - let blockNumber = currentBlockNumber - totalBlocksInterval + (blockInterval * i); - // Add block number to array - blockNumbers.push(Math.round(blockNumber)); - } - - // Return array of block numbers - return blockNumbers; +function getPreviousBlocks( + currentBlockNumber: number, + numberOfBlocks: number, + daysInterval: number +): number[] { + // Estimate number of blocks per day + const blocksPerDay = 86400 / 12; + // Calculate total blocks interval + const totalBlocksInterval = blocksPerDay * daysInterval; + // Calculate block interval + const blockInterval = totalBlocksInterval / (numberOfBlocks - 1); + + // Init array of block numbers + const blockNumbers: number[] = []; + + for (let i = 0; i < numberOfBlocks; i++) { + // Calculate block number + const blockNumber = + currentBlockNumber - totalBlocksInterval + blockInterval * i; + // Add block number to array + blockNumbers.push(Math.round(blockNumber)); + } + + // Return array of block numbers + return blockNumbers; } -function average(numbers: BigNumber[], address: string, whiteListedAddress: string[]): BigNumber { - // If no numbers, return 0 to avoid division by 0. - if (numbers.length === 0) return BigNumber.from(0); - - // If address is whitelisted, return most recent working balance. i.e. no twavp applied. - if (whiteListedAddress.includes(address)) return numbers[numbers.length - 1]; - - // Init sum - let sum = BigNumber.from(0); - // Loop through all elements and add them to sum - for (let i = 0; i < numbers.length; i++) { - sum = sum.add(numbers[i]); - } - - // Return sum divided by array length to get mean - return sum.div(numbers.length); -} \ No newline at end of file +function average( + numbers: BigNumber[], + address: string, + whiteListedAddress: string[] +): BigNumber { + // If no numbers, return 0 to avoid division by 0. + if (numbers.length === 0) return BigNumber.from(0); + + // If address is whitelisted, return most recent working balance. i.e. no twavp applied. + if (whiteListedAddress.includes(address)) return numbers[numbers.length - 1]; + + // Init sum + let sum = BigNumber.from(0); + // Loop through all elements and add them to sum + for (let i = 0; i < numbers.length; i++) { + sum = sum.add(numbers[i]); + } + + // Return sum divided by array length to get mean + return sum.div(numbers.length); +} diff --git a/src/strategies/taraxa-delegation/README.md b/src/strategies/taraxa-delegation/README.md new file mode 100644 index 000000000..c3f25122d --- /dev/null +++ b/src/strategies/taraxa-delegation/README.md @@ -0,0 +1,15 @@ +# Simple Taraxa Delegation Strategy + +Calculates the stakes of voters, based on their DPOS stakes in the previous specified snapshot. + +## Examples + +Used as the base vote strategy for Taraxa Governance, the space config will look like this: + +```JSON +{ + "strategies": [ + ["taraxa-delegation"] + ] +} +``` diff --git a/src/strategies/taraxa-delegation/examples.json b/src/strategies/taraxa-delegation/examples.json new file mode 100644 index 000000000..aea6a3538 --- /dev/null +++ b/src/strategies/taraxa-delegation/examples.json @@ -0,0 +1,25 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "taraxa-delegation", + "params": { + "address": "0x00000000000000000000000000000000000000fe", + "symbol": "TARA", + "decimals": 18 + } + }, + "network": "841", + "addresses": [ + "0xc6a808A6EC3103548f0b38d32DCb6a705B734c89", + "0xf3F6dBBE59AA219eAcE04955Bd6c3045ab4fF615", + "0x0dc0d841f962759da25547c686fa440cf6c28c61", + "0x4a179a005dcbe770c6970ee390a43d2284f67527", + "0x931d0b36e648906a27a67bc6057579984765e198", + "0xfc43217e71ec0a1cc480f3d210cd07cbde7374ec", + "0x21db400dcb1ef3bc3aee4f3d028ec1939b7fadd6", + "0x1daaa59a8d4a5c08080cadb65204d2d275838a99" + ], + "snapshot": 3112430 + } +] diff --git a/src/strategies/taraxa-delegation/index.ts b/src/strategies/taraxa-delegation/index.ts new file mode 100644 index 000000000..bc0385277 --- /dev/null +++ b/src/strategies/taraxa-delegation/index.ts @@ -0,0 +1,57 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import networks from '@snapshot-labs/snapshot.js/src/networks.json'; +import { Multicaller } from '../../utils'; + +export const author = 'Taraxa-project'; +export const version = '0.1.0'; + +const MIN_SCORE_AMOUNT = BigNumber.from(10).mul(BigNumber.from(10).pow(18)); + +const abi = [ + 'function getTotalDelegation(address delegator) external view returns (uint256 total_delegation)', + 'function getEthBalance(address account) public view returns (uint256 balance)' +]; + +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: string) => { + multi.call(address, options.address, 'getTotalDelegation', [address]); + }); + const resultDelegations: Record = await multi.execute(); + + addresses.forEach((address: string) => { + multi.call(address, networks[network].multicall, 'getEthBalance', [ + address + ]); + }); + const resultBalances: Record = await multi.execute(); + + const scores = {}; + + for (const address of addresses) { + const score = BigNumber.from(resultBalances[address] || 0).add( + BigNumber.from(resultDelegations[address] || 0) + ); + + if (score.lt(MIN_SCORE_AMOUNT)) { + scores[getAddress(address)] = 0; + } else { + scores[getAddress(address)] = parseFloat( + formatUnits(score, options.decimals) + ); + } + } + + return scores; +} diff --git a/src/strategies/taraxa-delegation/schema.json b/src/strategies/taraxa-delegation/schema.json new file mode 100644 index 000000000..6a71ae4f5 --- /dev/null +++ b/src/strategies/taraxa-delegation/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. TARA"], + "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/src/utils.ts b/src/utils.ts index 2764b5552..ed4d1372c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,6 +11,11 @@ async function callStrategy(space, network, addresses, strategy, snapshot) { (snapshot === 'latest' || snapshot > strategy.params?.end)) ) return {}; + + if (!_strategies.hasOwnProperty(strategy.name)) { + throw new Error(`Invalid strategy: ${strategy.name}`); + } + const score: any = await _strategies[strategy.name].strategy( space, network, diff --git a/src/validations/index.ts b/src/validations/index.ts index ee47e2564..ec757114c 100644 --- a/src/validations/index.ts +++ b/src/validations/index.ts @@ -4,12 +4,14 @@ import basic from './basic'; import passportGated from './passport-gated'; import passportWeighted from './passport-weighted'; import arbitrum from './arbitrum'; +import karmaEasAttestation from './karma-eas-attestation'; const validationClasses = { basic, 'passport-gated': passportGated, 'passport-weighted': passportWeighted, - arbitrum: arbitrum + arbitrum: arbitrum, + 'karma-eas-attestation': karmaEasAttestation }; const validations = {}; diff --git a/src/validations/karma-eas-attestation/README.md b/src/validations/karma-eas-attestation/README.md new file mode 100644 index 000000000..02749373e --- /dev/null +++ b/src/validations/karma-eas-attestation/README.md @@ -0,0 +1,8 @@ +# eas-attestation +Karma's solution to validate proposal creators using EAS Attest.sh. +> This should only be used to validate proposal creation. + +Parameters: +`schemaId`: EAS's schema UID given when the schema is created. Example: [`0xc0f979976278d9e1d4fa97b7270c0cc07835aa5f27dd897a871b3332ec6cff22`](https://sepolia.easscan.org/schema/view/0xc0f979976278d9e1d4fa97b7270c0cc07835aa5f27dd897a871b3332ec6cff22) +`address`: The proposer's address +`network`: The chain ID. Currently supports only Mainnet (1) diff --git a/src/validations/karma-eas-attestation/examples.json b/src/validations/karma-eas-attestation/examples.json new file mode 100644 index 000000000..bb5a38647 --- /dev/null +++ b/src/validations/karma-eas-attestation/examples.json @@ -0,0 +1,12 @@ +[ + { + "name": "Example of a Karma's EAS Attestation", + "author": "0xd7d1DB401EA825b0325141Cd5e6cd7C2d01825f2", + "space": "0xmury.eth", + "network": "11155111", + "snapshot": "latest", + "params": { + "schemaId": "0xc0f979976278d9e1d4fa97b7270c0cc07835aa5f27dd897a871b3332ec6cff22" + } + } +] diff --git a/src/validations/karma-eas-attestation/index.ts b/src/validations/karma-eas-attestation/index.ts new file mode 100644 index 000000000..adb7ba365 --- /dev/null +++ b/src/validations/karma-eas-attestation/index.ts @@ -0,0 +1,82 @@ +import Validation from '../validation'; +import fetch from 'cross-fetch'; + +interface Attestation { + attester: string; + data: string; + recipient: string; + revoked: boolean; +} +interface AttestationSchema { + schema: { + attestations: Attestation[]; + }; +} +interface SubgraphResponse { + data: AttestationSchema; +} + +const EASNetworks = { + 1: 'https://easscan.org/graphql', + 10: 'https://optimism.easscan.org/graphql', + 42161: 'https://arbitrum.easscan.org/graphql', + 11155111: 'https://sepolia.easscan.org/graphql' +}; + +const easScanQuery = ` +query Attestations($schemaId: String!) { + schema(where: {id: $schemaId}) { + attestations { + attester + data + recipient + revoked + } + } +} +`; + +/** + * Query EAS subgraph to determine if user is attested + * @param schemaId Schema UID + * @param address Targer user address + * @param network Network ID (1 = mainnet, 11155111 = sepolia) + * @returns + */ +async function isAttested(schemaId: string, address: string, network = 1) { + const easUrl = EASNetworks[network]; + if (!easUrl) throw new Error(`EAS network ${network} not supported`); + + const response: SubgraphResponse = await fetch(easUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: easScanQuery, + variables: { schemaId } + }) + }).then((res) => res.json()); + const index = response.data.schema.attestations.findIndex( + (a) => a.recipient.toLowerCase() === address.toLowerCase() && !a.revoked + ); + + return index >= 0; +} + +export default class extends Validation { + public id = 'karma-eas-attestation'; + public github = 'karmahq'; + public version = '0.1.0'; + public title = 'Karma EAS Attestation'; + public description = + 'Use EAS attest.sh to determine if user can create a proposal.'; + public proposalValidationOnly = true; + async validate(): Promise { + const schemaId = this.params.schemaId; + if (!schemaId) throw new Error(`Attestation schema not provided`); + + if (!Number.isSafeInteger(+this.network)) + throw new Error(`Network ID ${this.network} not supported`); + + return isAttested(schemaId, this.author, +this.network); + } +} diff --git a/src/validations/karma-eas-attestation/schema.json b/src/validations/karma-eas-attestation/schema.json new file mode 100644 index 000000000..987929438 --- /dev/null +++ b/src/validations/karma-eas-attestation/schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Validation", + "definitions": { + "Validation": { + "title": "Eas attestation", + "type": "object", + "properties": { + "schemaId": { + "type": "string", + "title": "Schema Id", + "examples": [ + "e.g. 0xc0f979976278d9e1d4fa97b7270c0cc07835aa5f27dd897a871b3332ec6cff22" + ] + } + }, + "required": [], + "additionalProperties": false + } + } +} diff --git a/yarn.lock b/yarn.lock index a6a5abe8a..c4850455c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1363,10 +1363,10 @@ dependencies: "@sinonjs/commons" "^2.0.0" -"@snapshot-labs/snapshot.js@^0.4.109": - version "0.4.109" - resolved "https://registry.yarnpkg.com/@snapshot-labs/snapshot.js/-/snapshot.js-0.4.109.tgz#2946dd6a87b03c44a1c2e5ba187add5e0ea2a34a" - integrity sha512-urV42YwiGWpxnrVMSg9QI4iJdCyYNfyOVZ4r6xYrFT+5Wz3YKUWmAgVm5C1v0HAFPDchGxzwcISHhnX3oMky/Q== +"@snapshot-labs/snapshot.js@^0.5.8": + version "0.5.8" + resolved "https://registry.yarnpkg.com/@snapshot-labs/snapshot.js/-/snapshot.js-0.5.8.tgz#16bf19b27e487e937be8472e53f0ed71f25e47af" + integrity sha512-bFe4VBUOD2LbKKesD52G7hqBCrmx7BS/4n8JfnzR+GZddL4UWmjRhfnxx/6YLeJvaHOWSPs7LHkr16fpJXD7pQ== dependencies: "@ensdomains/eth-ens-namehash" "^2.0.15" "@ethersproject/abi" "^5.6.4"