diff --git a/package.json b/package.json index 5e9f8cb80..e0d169ff2 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@ethersproject/strings": "^5.6.1", "@ethersproject/units": "^5.6.1", "@ethersproject/wallet": "^5.6.2", - "@snapshot-labs/snapshot.js": "^0.12.36", + "@snapshot-labs/snapshot.js": "^0.12.40", "@spruceid/didkit-wasm-node": "^0.2.1", "@uniswap/sdk-core": "^3.0.1", "@uniswap/v3-sdk": "^3.9.0", diff --git a/src/strategies/botto-dao-base/index.ts b/src/strategies/botto-dao-base/index.ts index 969dd1235..b3481f9d4 100644 --- a/src/strategies/botto-dao-base/index.ts +++ b/src/strategies/botto-dao-base/index.ts @@ -5,9 +5,7 @@ import { Multicaller } from '../../utils'; export const author = 'agustinjch'; export const version = '1.0.0'; -const abi = [ - 'function userStakes(address) external view returns(uint256)' -]; +const abi = ['function userStakes(address) external view returns(uint256)']; export async function strategy( space, diff --git a/src/strategies/index.ts b/src/strategies/index.ts index f69161c38..100a05eb8 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -383,6 +383,7 @@ import * as sdVoteBoostTWAVPV2 from './sd-vote-boost-twavp-v2'; import * as sdVoteBoostTWAVPV3 from './sd-vote-boost-twavp-v3'; import * as sdVoteBoostTWAVPV4 from './sd-vote-boost-twavp-v4'; import * as sdGaugeLessVoteBoost from './sd-gauge-less-vote-boost'; +import * as sdGaugeLessVoteBoostCrosschain from './sd-gauge-less-vote-boost-crosschain'; import * as sdVoteBalanceOfTwavpPool from './sdvote-balanceof-twavp-pool'; import * as sdVoteBoostTWAVPVsdToken from './sd-vote-boost-twavp-vsdtoken'; import * as sdVoteBoostTWAVPVCrossChain from './sd-vote-boost-twavp-vsdcrv-crosschain'; @@ -466,6 +467,8 @@ import * as sacraSubgraph from './sacra-subgraph'; import * as fountainhead from './fountainhead'; import * as naymsStaking from './nayms-staking'; import * as morphoDelegation from './morpho-delegation'; +import * as lizcoinStrategy2024 from './lizcoin-strategy-2024'; +import * as realt from './realt'; import * as superfluidVesting from './superfluid-vesting'; const strategies = { @@ -862,6 +865,7 @@ const strategies = { 'sd-vote-boost-twavp-v3': sdVoteBoostTWAVPV3, 'sd-vote-boost-twavp-v4': sdVoteBoostTWAVPV4, 'sd-gauge-less-vote-boost': sdGaugeLessVoteBoost, + 'sd-gauge-less-vote-boost-crosschain': sdGaugeLessVoteBoostCrosschain, 'sdvote-balanceof-twavp-pool': sdVoteBalanceOfTwavpPool, 'sd-vote-boost-twavp-vsdtoken': sdVoteBoostTWAVPVsdToken, 'sd-vote-boost-twavp-vsdcrv-crosschain': sdVoteBoostTWAVPVCrossChain, @@ -944,6 +948,8 @@ const strategies = { fountainhead, 'nayms-staking': naymsStaking, 'morpho-delegation': morphoDelegation, + 'lizcoin-strategy-2024': lizcoinStrategy2024, + realt, 'superfluid-vesting': superfluidVesting }; diff --git a/src/strategies/lizcoin-strategy-2024/README.md b/src/strategies/lizcoin-strategy-2024/README.md new file mode 100644 index 000000000..f569ca54f --- /dev/null +++ b/src/strategies/lizcoin-strategy-2024/README.md @@ -0,0 +1,36 @@ +# Lizcoin Voting Strategy 2024 +lizcoin-strategy-2024 + +A voting strategy for Lizard Labs' Lizcoin ERC20 Token (LIZ). + +The strategy is based on the quadratic voting formula. The strategy counts only staked and vested tokens – details +follow bellow. + +## Eligible $LIZ forms: +* Staked $LIZ +* Staked LP tokens (converted back to their value in $LIZ) +* vLIZ pre-tokens (investors) in a wallet +* veLIZ pre-tokens (team) in a wallet +* ANY position in the pre-token vesting contract, whether it came from vLIZ, veLIZ, or was set manually for e.g. KOLs + +## Ineligible $LIZ forms: +* Regular $LIZ or LP tokens in a wallet +* cLIZ tokens in a wallet +* Staking or loot box rewards that are unclaimed + +Here is an example of parameters: + +```json +{ + "lizcoinAddress": "0xAF4144cd943ed5362Fed2BaE6573184659CBe6FF", + "cLIZAddress": "0x0F9dc0c0A46733c8b9a6C2E4850913Ed31d31205", + "vLIZAddress": "0xe20C4edb8440CaDD4001c144B4F38576d1AA3820", + "veLIZAddress": "0xC817C0B518e8Fc98034ad867d679d4f8A284BFBE", + "cLIZConversionRate": 0.01, + "vLIZConversionRate": 0.01, + "veLIZConversionRate": 0.01, + "uniswapV2PoolAddress": "0xD47B93360EAADBA2678c30F64209a42b9800cEE4", + "stakingContractAddress": "0xEf2E841AA9F49cc0E54697A4Afa6361eA24d682F", + "vestingContractAddress": "0x895ecdCAC6431272946Ec615eD368d2f42fC2b44" +} +``` diff --git a/src/strategies/lizcoin-strategy-2024/examples.json b/src/strategies/lizcoin-strategy-2024/examples.json new file mode 100644 index 000000000..8ea76d147 --- /dev/null +++ b/src/strategies/lizcoin-strategy-2024/examples.json @@ -0,0 +1,44 @@ +[ + { + "name": "Example query for Lizcoin Voting Strategy 2024", + "strategy": { + "name": "lizcoin-strategy-2024", + "params": { + "lizcoinAddress": "0xAF4144cd943ed5362Fed2BaE6573184659CBe6FF", + "cLIZAddress": "0x0F9dc0c0A46733c8b9a6C2E4850913Ed31d31205", + "vLIZAddress": "0xe20C4edb8440CaDD4001c144B4F38576d1AA3820", + "veLIZAddress": "0xC817C0B518e8Fc98034ad867d679d4f8A284BFBE", + "cLIZConversionRate": 0.01, + "vLIZConversionRate": 0.01, + "veLIZConversionRate": 0.01, + "uniswapV2PoolAddress": "0xD47B93360EAADBA2678c30F64209a42b9800cEE4", + "stakingContractAddress": "0xEf2E841AA9F49cc0E54697A4Afa6361eA24d682F", + "vestingContractAddress": "0x895ecdCAC6431272946Ec615eD368d2f42fC2b44" + } + }, + "network": "1", + "addresses": [ + "0xC5015Af75881b4aBed54043B46561ca829EC2468", + "0x11619E71E2B53AF33e1CFEAaab51d4E570aFC299", + "0x922dfB7092BB3A14A1335c843C1760f88A406E99", + "0xAe8513960374736fAe8CBA47e1A2Ae1Cd842e439", + "0xc9b95B403dC5C9E2A8cB6A703A78016A537DBAE3", + "0xAB12253171A0d73df64B115cD43Fe0A32Feb9dAA", + "0x7C175168d76849Aba1EAD344AD30AC94f120a077", + "0xC1305f9Df23799596a12E6C7cCD8D2663B875D61", + "0x601071a8fB7890bAda13E24c6Bf7838e90778A78", + "0xc6184b7e9C896f5035F7A670BBeA5A445fDA6E1C", + "0xf102978E0C95207AD6Ee2d39d18f1e7497d89EAD", + "0xf91D65180d0e5eD3C5a56B9211b6B4b26E24C2d5", + "0xf02b4c61Bb87817690034667186EF9836F4F0898", + "0xdD020966b963FFC7623498dF5B8Ed14b11AF7B2A", + "0x6C6a234D1806F7d0baC872eD49933A6852424467", + "0x3cC31eFcEEFAdC2A86716A3BeE91D1917418E29a", + "0x8f60501dE5b9b01F9EAf1214dbE1924aA97F7fd0", + "0x9B8e8dD9151260c21CB6D7cc59067cd8DF306D58", + "0x17ea92D6FfbAA1c7F6B117c1E9D0c88ABdc8b84C", + "0x38C0039247A31F3939baE65e953612125cB88268" + ], + "snapshot": 21428809 + } +] diff --git a/src/strategies/lizcoin-strategy-2024/index.ts b/src/strategies/lizcoin-strategy-2024/index.ts new file mode 100644 index 000000000..6ae3bf1a5 --- /dev/null +++ b/src/strategies/lizcoin-strategy-2024/index.ts @@ -0,0 +1,161 @@ +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'ethlizards'; +export const version = '0.1.0'; + +const abi = [ + 'function decimals() external view returns (uint8)', + 'function totalSupply() external view returns (uint256)', + 'function balanceOf(address account) external view returns (uint256)', + 'function sumDepositAmounts(address depositToken, address rewardToken, address accountAddress) external view returns (uint96)', + // TODO: introduce sumVestedAmounts(address holder) function + 'function getVestingSchedulesArray(address holder) external view returns (tuple(uint32 startDate, uint32 duration, uint96 amountVested, uint96 amountClaimed)[])' +]; + +interface Options { + lizcoinAddress: string; + cLIZAddress: string; + vLIZAddress: string; + veLIZAddress: string; + cLIZConversionRate: number; + vLIZConversionRate: number; + veLIZConversionRate: number; + uniswapV2PoolAddress: string; + stakingContractAddress: string; + vestingContractAddress: string; +} + +interface VestingSchedule { + startDate: number; + duration: number; + amountVested: BigNumber; + amountClaimed: BigNumber; +} + +export async function strategy( + space: string, + network: string, + provider: StaticJsonRpcProvider, + addresses: string[], + options: Options, + snapshot: number | 'latest' +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + const multi = new Multicaller(network, provider, abi, { blockTag }); + + // Fetch decimals for LIZ, vLIZ, veLIZ, and LP + multi.call('lizcoinDecimals', options.lizcoinAddress, 'decimals'); + multi.call('vLIZDecimals', options.vLIZAddress, 'decimals'); + multi.call('veLIZDecimals', options.veLIZAddress, 'decimals'); + multi.call('lpDecimals', options.uniswapV2PoolAddress, 'decimals'); + // Fetch LP/LIZ conversion rate + multi.call('lpSupply', options.uniswapV2PoolAddress, 'totalSupply'); + multi.call('lpLizcoinBalance', options.lizcoinAddress, 'balanceOf', [ + options.uniswapV2PoolAddress + ]); + + // Fetch balances for LIZ, vLIZ, and veLIZ + addresses.forEach((address: string) => { + // Staked $LIZ + multi.call( + `stakedLIZ_${address}`, + options.stakingContractAddress, + 'sumDepositAmounts', + [options.lizcoinAddress, options.lizcoinAddress, address] + ); + // Staked LP tokens + multi.call( + `stakedLP_${address}`, + options.stakingContractAddress, + 'sumDepositAmounts', + [options.uniswapV2PoolAddress, options.lizcoinAddress, address] + ); + // vLIZ pre-tokens (investors) in a wallet + multi.call(`vLIZ_${address}`, options.vLIZAddress, 'balanceOf', [address]); + // veLIZ pre-tokens (team) in a wallet + multi.call(`veLIZ_${address}`, options.veLIZAddress, 'balanceOf', [ + address + ]); + // ANY position in the pre-token vesting contract, whether it came from vLIZ, veLIZ, or was set manually for e.g. KOLs + multi.call( + `vestingSchedules_${address}`, + options.vestingContractAddress, + 'getVestingSchedulesArray', + [address] + ); + }); + + const result: Record = await multi.execute(); + + // decimals + const lizcoinDecimals = result['lizcoinDecimals']; + const vLIZDecimals = result['vLIZDecimals']; + const veLIZDecimals = result['veLIZDecimals']; + const lpDecimals = result['lpDecimals']; + // LP/LIZ conversion rate + const lpSupply = parseFloat(formatUnits(result['lpSupply'], lpDecimals)); + const lpLizcoinBalance = parseFloat( + formatUnits(result['lpLizcoinBalance'], lpDecimals) + ); + const lpLizcoinConvRate = lpSupply / lpLizcoinBalance / 2; + + // derive the voting power as an address:number mapping + const votingPower = Object.fromEntries( + addresses.map(function (address) { + // Staked $LIZ + const stakedLizcoinBalance = parseFloat( + formatUnits(result[`stakedLIZ_${address}`], lizcoinDecimals) + ); + + // Staked LP tokens (converted back to their value in $LIZ) + const stakedLpBalance = + parseFloat( + formatUnits(result[`stakedLP_${address}`], lizcoinDecimals) + ) / lpLizcoinConvRate; + + // vLIZ pre-tokens (investors) in a wallet (converted to their value in $LIZ) + const vLIZBalance = + parseFloat(formatUnits(result[`vLIZ_${address}`], vLIZDecimals)) / + options.vLIZConversionRate; + + // veLIZ pre-tokens (team) in a wallet (converted to their value in $LIZ) + const veLIZBalance = + parseFloat(formatUnits(result[`veLIZ_${address}`], veLIZDecimals)) / + options.veLIZConversionRate; + + // ANY position in the pre-token vesting contract, whether it came from vLIZ, veLIZ, or was set manually for e.g. KOLs + const schedules: VestingSchedule[] = result[ + `vestingSchedules_${address}` + ] as unknown as VestingSchedule[]; + const vestedAmountsSum = schedules.reduce( + (accumulator, currentValue) => + BigNumber.from(accumulator).add( + BigNumber.from(currentValue.amountVested).sub( + BigNumber.from(currentValue.amountClaimed) + ) + ), + BigNumber.from(0) + ); + const vestedLizcoinBalance = parseFloat( + formatUnits(vestedAmountsSum, lizcoinDecimals) + ); + + // sum all the balances (already converted to LIZ equivalents) + const totalBalance = + stakedLizcoinBalance + + stakedLpBalance + + vLIZBalance + + veLIZBalance + + vestedLizcoinBalance; + + // apply the quadratic voting formula and return + return [address, Math.sqrt(totalBalance)]; + }) + ); + // console.log(votingPower); + return votingPower; +} diff --git a/src/strategies/lizcoin-strategy-2024/schema.json b/src/strategies/lizcoin-strategy-2024/schema.json new file mode 100644 index 000000000..a267ab166 --- /dev/null +++ b/src/strategies/lizcoin-strategy-2024/schema.json @@ -0,0 +1,109 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Lizcoin Voting Strategy 2024", + "type": "object", + "properties": { + "lizcoinAddress": { + "type": "string", + "title": "Lizcoin ERC20 Token (LIZ) Address", + "default": "0xAF4144cd943ed5362Fed2BaE6573184659CBe6FF", + "examples": ["e.g. 0xAF4144cd943ed5362Fed2BaE6573184659CBe6FF"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "cLIZAddress": { + "type": "string", + "title": "cLIZ Convertable Coin Address", + "default": "0x0F9dc0c0A46733c8b9a6C2E4850913Ed31d31205", + "examples": ["e.g. 0x0F9dc0c0A46733c8b9a6C2E4850913Ed31d31205"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "vLIZAddress": { + "type": "string", + "title": "vLIZ Convertable Coin Address", + "default": "0xe20C4edb8440CaDD4001c144B4F38576d1AA3820", + "examples": ["e.g. 0xe20C4edb8440CaDD4001c144B4F38576d1AA3820"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "veLIZAddress": { + "type": "string", + "title": "veLIZ Convertable Coin Address", + "default": "0xC817C0B518e8Fc98034ad867d679d4f8A284BFBE", + "examples": ["e.g. 0xC817C0B518e8Fc98034ad867d679d4f8A284BFBE"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "cLIZConversionRate": { + "type": "number", + "title": "cLIZ to LIZ Conversion Rate", + "default": 0.01, + "examples": [0.01], + "minimum": 0.000000001, + "maximum": 1000000000 + }, + "vLIZConversionRate": { + "type": "number", + "title": "vLIZ to LIZ Conversion Rate", + "default": 0.01, + "examples": [0.01], + "minimum": 0.000000001, + "maximum": 1000000000 + }, + "veLIZConversionRate": { + "type": "number", + "title": "veLIZ to LIZ Conversion Rate", + "default": 0.01, + "examples": [0.01], + "minimum": 0.000000001, + "maximum": 1000000000 + }, + "uniswapV2PoolAddress": { + "type": "string", + "title": "Voting Eligible Uniswap V2 LP Address", + "examples": ["e.g. 0xD47B93360EAADBA2678c30F64209a42b9800cEE4"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "stakingContractAddress": { + "type": "string", + "title": "Voting Eligible Staking Contract Address", + "examples": ["e.g. 0xEf2E841AA9F49cc0E54697A4Afa6361eA24d682F"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "vestingContractAddress": { + "type": "string", + "title": "Voting Eligible Vesting Contract Address", + "examples": ["e.g. 0x895ecdCAC6431272946Ec615eD368d2f42fC2b44"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + } + }, + "required": [ + "lizcoinAddress", + "cLIZAddress", + "vLIZAddress", + "veLIZAddress", + "cLIZConversionRate", + "vLIZConversionRate", + "veLIZConversionRate", + "uniswapV2PoolAddress", + "stakingContractAddress", + "vestingContractAddress" + ], + "additionalProperties": false + } + } +} diff --git a/src/strategies/realt/README.md b/src/strategies/realt/README.md new file mode 100644 index 000000000..bc38be178 --- /dev/null +++ b/src/strategies/realt/README.md @@ -0,0 +1,5 @@ +# reALT + +## Description + +The reALT strategy calculates the total balance of a user's holdings in the reALT token and their shares in the reALT Strategy on the Ethereum mainnet. diff --git a/src/strategies/realt/examples.json b/src/strategies/realt/examples.json new file mode 100644 index 000000000..161df7e3e --- /dev/null +++ b/src/strategies/realt/examples.json @@ -0,0 +1,20 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "realt", + "params": { + "address": "0xf96798f49936efb1a56f99ceae924b6b8359affb", + "symbol": "reALT", + "decimals": 18 + } + }, + "network": "1", + "addresses": [ + "0x15de7B6a83ee7735EA00Dc4a0506059cDA4Bef49", + "0x16Ce1B15ed1278921d7Cae34Bf60a81227CFC295", + "0x16f665dA6D806760aFC317ee29Ef2feF2Ff4976E" + ], + "snapshot": 21488157 + } +] diff --git a/src/strategies/realt/index.ts b/src/strategies/realt/index.ts new file mode 100644 index 000000000..48f8ebde2 --- /dev/null +++ b/src/strategies/realt/index.ts @@ -0,0 +1,62 @@ +import { BigNumberish, BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; +import { Multicaller } from '../../utils'; + +export const author = 'altlayer'; +export const version = '0.1.0'; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const reAltStrategy = '0x6075546538c3eFbD607ea6aFC24149fCcFb2edF4'; // reALT Strategy Mainnet + + const balanceOfMulticaller = new Multicaller( + network, + provider, + ['function balanceOf(address account) external view returns (uint256)'], + { blockTag } + ); + addresses.forEach((address) => { + if (address !== reAltStrategy) { + balanceOfMulticaller.call(address, options.address, 'balanceOf', [ + address + ]); + } + }); + + const sharesMulticaller = new Multicaller( + network, + provider, + ['function shares(address user) external view returns (uint256)'], + { blockTag } + ); + + addresses.forEach((address) => + sharesMulticaller.call(address, reAltStrategy, 'shares', [address]) + ); + + const [balanceOfResults, sharesResults]: [ + Record, + Record + ] = await Promise.all([ + balanceOfMulticaller.execute(), + sharesMulticaller.execute() + ]); + + return Object.fromEntries( + addresses.map((address) => { + const balanceOf = balanceOfResults[address] || BigNumber.from(0); + const shares = sharesResults[address] || BigNumber.from(0); + const totalBalance = BigNumber.from(balanceOf).add( + BigNumber.from(shares) + ); + return [address, parseFloat(formatUnits(totalBalance, options.decimals))]; + }) + ); +} diff --git a/src/strategies/realt/schema.json b/src/strategies/realt/schema.json new file mode 100644 index 000000000..e5beefb92 --- /dev/null +++ b/src/strategies/realt/schema.json @@ -0,0 +1,34 @@ +{ + "$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. reALT"], + "maxLength": 16 + }, + "address": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0xf96798f49936efb1a56f99ceae924b6b8359affb"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": ["e.g. 18"], + "minimum": 0 + } + }, + "required": ["address", "decimals"], + "additionalProperties": false + } + } +} diff --git a/src/strategies/sd-gauge-less-vote-boost-crosschain/README.md b/src/strategies/sd-gauge-less-vote-boost-crosschain/README.md new file mode 100644 index 000000000..f9949f814 --- /dev/null +++ b/src/strategies/sd-gauge-less-vote-boost-crosschain/README.md @@ -0,0 +1,24 @@ +# sd-gauge-less-vote-boost-crosschain + +This strategy is used by Stake DAO to vote with sdToken using Time Weighted Averaged Voting Power (TWAVP) system and adapted for veSDT boost delegation with possibility to whiteliste address to by pass TWAVP. + +``` +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)_ + +Here is an example of parameters: + +```json +{ + "sdTokenGauge": "0xE2496134149e6CD3f3A577C2B08A6f54fC23e6e4", + "symbol": "sdToken", + "decimals": 18, + "twavpDaysInterval": 10, + "twavpNumberOfBlocks": 2, + "whiteListedAddress": ["0x1c0D72a330F2768dAF718DEf8A19BAb019EEAd09", "0x931DaBf6721E47E6f5aeb19F6f5d48646144f484"], + "blocksPerDay": 28798 +} +``` \ No newline at end of file diff --git a/src/strategies/sd-gauge-less-vote-boost-crosschain/examples.json b/src/strategies/sd-gauge-less-vote-boost-crosschain/examples.json new file mode 100644 index 000000000..18628c86c --- /dev/null +++ b/src/strategies/sd-gauge-less-vote-boost-crosschain/examples.json @@ -0,0 +1,33 @@ +[ + { + "name": "Stake DAO vote boost using working balance", + "strategy": { + "name": "sd-gauge-less-vote-boost-crosschain", + "params": { + "sdTokenGauge": "0xE2496134149e6CD3f3A577C2B08A6f54fC23e6e4", + "sdToken": "0x6a1c1447F97B27dA23dC52802F5f1435b5aC821A", + "veToken": "0x5692DB8177a81A6c6afc8084C2976C9933EC1bAB", + "liquidLocker": "0x1E6F87A9ddF744aF31157d8DaA1e3025648d042d", + "symbol": "sdToken-voting-power", + "decimals": 18, + "twavpDaysInterval": 10, + "twavpNumberOfBlocks": 2, + "whiteListedAddress": ["0x1c0D72a330F2768dAF718DEf8A19BAb019EEAd09"], + "blocksPerDay": 28798, + "pools": ["0xb8204D31379A9B317CD61C833406C972F58ecCbC"], + "botAddress": "0xb4542526AfeE2FdA1D584213D1521272a398B42a" + } + }, + "network": "56", + "addresses": [ + "0xb4542526AfeE2FdA1D584213D1521272a398B42a", + "0x931DaBf6721E47E6f5aeb19F6f5d48646144f484", + "0x3FbE6216dB591372D3Bb8428ac87B162F7B8601f", + "0x803585733d4554e712dcaCc2AcEA3a23b96de1e0", + "0xD4f9FE0039Da59e6DDb21bbb6E84e0C9e83D73eD", + "0x87e21De231FA9bf7cF5E30E8FF5388E781e9080C", + "0xb24B5d309a1A64135770A954496b3c408c558806" + ], + "snapshot": 44392800 + } +] diff --git a/src/strategies/sd-gauge-less-vote-boost-crosschain/index.ts b/src/strategies/sd-gauge-less-vote-boost-crosschain/index.ts new file mode 100644 index 000000000..18e25e03c --- /dev/null +++ b/src/strategies/sd-gauge-less-vote-boost-crosschain/index.ts @@ -0,0 +1,281 @@ +import { getAddress } from '@ethersproject/address'; +import { getProvider, multicall, subgraphRequest } from '../../utils'; +import { BigNumber } from '@ethersproject/bignumber'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'pierremarsotlyon1'; +export const version = '0.0.1'; + +const VE_SDT = '0x0C30476f66034E11782938DF8e4384970B6c9e8a'; +const VE_PROXY_BOOST_SDT = '0xD67bdBefF01Fc492f1864E61756E5FBB3f173506'; +const TOKENLESS_PRODUCTION = 40; +const MIN_BOOST = 0.4; + +// Used ABI +const abi = [ + 'function balanceOf(address account) external view returns (uint256)', + 'function working_supply() external view returns (uint256)', + 'function totalSupply() external view returns (uint256)', + 'function working_balances(address account) external view returns (uint256)', + 'function balances(uint256 i) external view returns (uint256)', + 'function adjusted_balance_of(address user) external view returns (uint256)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + // Maximum of 2 multicall! + if (options.twavpNumberOfBlocks > 2) { + throw new Error('maximum of 2 calls'); + } + + // Maximum of 20 whitelisted address + if (options.whiteListedAddress.length > 20) { + throw new Error('maximum of 20 whitelisted address'); + } + + // Addresses in tlc + addresses = addresses.map((addr) => addr.toLowerCase()); + + // --- Create block number list for twavp + // Obtain last block number + // Create block tag + let blockTag = 0; + if (typeof snapshot === 'number') { + blockTag = snapshot; + } else { + blockTag = await provider.getBlockNumber(); + } + + // Mainnet data + const mainnetProvider = getProvider('1'); + + // Get corresponding block number on mainnet + let mainnetBlockTag = await getIDChainBlock(blockTag, provider, '1'); + + if (mainnetBlockTag === 'latest') { + mainnetBlockTag = await mainnetProvider.getBlockNumber(); + } + + // Create block lists + const blockListMainnet = getPreviousBlocks( + mainnetBlockTag, + options.twavpNumberOfBlocks, + options.twavpDaysInterval, + 7200 + ); + + const blockList = getPreviousBlocks( + blockTag, + options.twavpNumberOfBlocks, + options.twavpDaysInterval, + options.blocksPerDay + ); + + // Queries + const ajustedBalancesMainnet = addresses.map((address: any) => [ + VE_PROXY_BOOST_SDT, + 'adjusted_balance_of', + [address] + ]); + + const sdTknGaugeBalanceCurrentChain = addresses.map((address: any) => [ + options.sdTokenGauge, + 'balanceOf', + [address] + ]); + + const responsesMainnet: any[] = []; + const responsesCurrentChain: any[] = []; + + let veSDTTotalSupply = 0; + let sdTokenGaugeTotalSupply = 0; + let sdTokenTotalSupply = 0; + let sumPoolsBalance = 0; + let lockerVotingPower = 0; + + for (let i = 0; i < options.twavpNumberOfBlocks; i++) { + const isEnd = i === options.twavpNumberOfBlocks - 1; + + // Mainnet + let calls: any[] = ajustedBalancesMainnet; + if (isEnd) { + // Fetch veSDT total supply + calls.push([VE_SDT, 'totalSupply']); + } + + let callResp: any[] = await multicall('1', mainnetProvider, abi, calls, { + blockTag: blockListMainnet[i] + }); + + if (isEnd) { + veSDTTotalSupply = parseFloat(formatUnits(callResp.pop()[0], 18)); + } + + responsesMainnet.push(callResp); + + // Destination chain + calls = sdTknGaugeBalanceCurrentChain; + if (isEnd) { + calls.push([options.sdTokenGauge, 'totalSupply']); + calls.push([options.sdToken, 'totalSupply']); + calls.push([options.veToken, 'balanceOf', [options.liquidLocker]]); + + // Fetch pools balance + if (options.pools && Array.isArray(options.pools)) { + for (const pool of options.pools) { + calls.push([options.sdToken, 'balanceOf', [pool]]); + } + } + } + + callResp = await multicall(network, provider, abi, calls, { + blockTag: blockList[i] + }); + + if (isEnd) { + if (options.pools && Array.isArray(options.pools)) { + const poolsReverse = [...options.pools].reverse(); + for (let i = 0; i < poolsReverse.length; i++) { + sumPoolsBalance += parseFloat(formatUnits(callResp.pop()[0], 18)); + } + } + + lockerVotingPower = parseFloat(formatUnits(callResp.pop()[0], 18)); + + sdTokenTotalSupply = parseFloat(formatUnits(callResp.pop()[0], 18)); + + sdTokenGaugeTotalSupply = parseFloat(formatUnits(callResp.pop()[0], 18)); + } + + responsesCurrentChain.push(callResp); + } + + const liquidityVoteFee = + (MIN_BOOST * sumPoolsBalance * lockerVotingPower) / sdTokenTotalSupply; + + const totalUserVotes = lockerVotingPower - liquidityVoteFee; + + return Object.fromEntries( + Array(addresses.length) + .fill('x') + .map((_, i) => { + // Init array of working balances for user + const userWorkingBalances: number[] = []; + + for (let j = 0; j < options.twavpNumberOfBlocks; j++) { + const voting_balance = parseFloat( + formatUnits(BigNumber.from(responsesMainnet[j].shift()[0]), 18) + ); + const l = parseFloat( + formatUnits(BigNumber.from(responsesCurrentChain[j].shift()[0]), 18) + ); + + let lim = (l * TOKENLESS_PRODUCTION) / 100; + if (veSDTTotalSupply > 0) { + lim += + (((sdTokenGaugeTotalSupply * voting_balance) / veSDTTotalSupply) * + (100 - TOKENLESS_PRODUCTION)) / + 100; + } + + userWorkingBalances.push(Math.min(l, lim)); + } + + let userVote = 0; + if ( + options.botAddress && + addresses[i].toLowerCase() === options.botAddress.toLowerCase() + ) { + userVote = liquidityVoteFee * totalUserVotes; + } else { + // Get average working balance. + const averageWorkingBalance = average( + userWorkingBalances, + addresses[i], + options.whiteListedAddress + ); + + userVote = averageWorkingBalance * totalUserVotes; + } + + // Return address and voting power + userVote /= 10_000; + return [getAddress(addresses[i]), Number(userVote)]; + }) + ); +} + +function getPreviousBlocks( + currentBlockNumber: number, + numberOfBlocks: number, + daysInterval: number, + blocksPerDay: number +): number[] { + // Calculate total blocks interval + const totalBlocksInterval = blocksPerDay * daysInterval; + // Calculate block interval + const blockInterval = + totalBlocksInterval / + (numberOfBlocks > 1 ? numberOfBlocks - 1 : numberOfBlocks); + + // 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: number[], + address: string, + whiteListedAddress: string[] +): number { + // If no numbers, return 0 to avoid division by 0. + if (numbers.length === 0) return 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 = 0; + // Loop through all elements and add them to sum + for (let i = 0; i < numbers.length; i++) { + sum += numbers[i]; + } + + // Return sum divided by array length to get mean + return sum / numbers.length; +} + +async function getIDChainBlock(snapshot, provider, chainId) { + const ts = (await provider.getBlock(snapshot)).timestamp; + const query = { + blocks: { + __args: { + where: { + ts: ts, + network_in: [chainId] + } + }, + number: true + } + }; + const url = 'https://blockfinder.snapshot.org'; + const data = await subgraphRequest(url, query); + return data.blocks[0].number; +} diff --git a/src/utils.ts b/src/utils.ts index 4061818f6..ca35b5cd6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -83,9 +83,10 @@ export function customFetch( } throw error; }), - new Promise(() => + new Promise((_, reject) => setTimeout(() => { controller.abort(); + reject(new Error('API request timeout')); }, timeout) ) ]); diff --git a/yarn.lock b/yarn.lock index a9c64e67f..e7dac6cf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1239,10 +1239,10 @@ dependencies: "@sinonjs/commons" "^2.0.0" -"@snapshot-labs/snapshot.js@^0.12.36": - version "0.12.36" - resolved "https://registry.yarnpkg.com/@snapshot-labs/snapshot.js/-/snapshot.js-0.12.36.tgz#89ff38be1b2ab2b239ab5caf0b20c5c4e65eed4e" - integrity sha512-cCwX8mLattshjMLzb731DWDQ8/D0uHINBjTNyFhdyxwW/7B1Dr4wAIQHR+iXwJDA3aiqDJPIP6llD1HRRllvBQ== +"@snapshot-labs/snapshot.js@^0.12.40": + version "0.12.40" + resolved "https://registry.yarnpkg.com/@snapshot-labs/snapshot.js/-/snapshot.js-0.12.40.tgz#ab07a35f057de380cf0c5f6f661370001dce2a50" + integrity sha512-pZb3xERF6w8n6p8tm9pD5OIwJMPklrFA8UzOKZQvJFH+nn3CMJdlbp1rp496oVzd6tArrHrQ6sLI0Nbo59nBCw== dependencies: "@ensdomains/eth-ens-namehash" "^2.0.15" "@ethersproject/abi" "^5.6.4"