diff --git a/src/strategies/index.ts b/src/strategies/index.ts index a9d8d78a3..3b7fc6671 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -418,6 +418,7 @@ import * as babywealthyclub from './babywealthyclub'; import * as battleflyVGFLYAndStakedGFLY from './battlefly-vgfly-and-staked-gfly'; import * as nexonArmyNFT from './nexon-army-nft'; import * as stakedotlinkVesting from './stakedotlink-vesting'; +import * as pspInSePSP2Balance from './psp-in-sepsp2-balance'; const strategies = { 'forta-shares': fortaShares, @@ -841,7 +842,8 @@ const strategies = { babywealthyclub, 'battlefly-vgfly-and-staked-gfly': battleflyVGFLYAndStakedGFLY, 'nexon-army-nft': nexonArmyNFT, - 'stakedotlink-vesting': stakedotlinkVesting + 'stakedotlink-vesting': stakedotlinkVesting, + 'psp-in-sepsp2-balance': pspInSePSP2Balance }; Object.keys(strategies).forEach(function (strategyName) { diff --git a/src/strategies/psp-in-sepsp2-balance/README.md b/src/strategies/psp-in-sepsp2-balance/README.md new file mode 100644 index 000000000..68d1671ed --- /dev/null +++ b/src/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/src/strategies/psp-in-sepsp2-balance/examples.json b/src/strategies/psp-in-sepsp2-balance/examples.json new file mode 100644 index 000000000..9ccc11cfa --- /dev/null +++ b/src/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 + } +] \ No newline at end of file diff --git a/src/strategies/psp-in-sepsp2-balance/index.ts b/src/strategies/psp-in-sepsp2-balance/index.ts new file mode 100644 index 000000000..08dc87455 --- /dev/null +++ b/src/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/src/strategies/psp-in-sepsp2-balance/schema.json b/src/strategies/psp-in-sepsp2-balance/schema.json new file mode 100644 index 000000000..cbe115d84 --- /dev/null +++ b/src/strategies/psp-in-sepsp2-balance/schema.json @@ -0,0 +1,110 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "symbol": { + "type": "string", + "title": "Symbol", + "examples": [ + "PSP" + ] + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": [ + "18" + ] + }, + "address": { + "type": "string", + "title": "Address", + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42, + "examples": [ + "0xcafe001067cdef266afb7eb5a286dcfd277f3de5" + ] + }, + "sePSP2": { + "type": "object", + "title": "sePSP2", + "properties": { + "address": { + "type": "string", + "title": "Address", + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42, + "examples": [ + "0x593F39A4Ba26A9c8ed2128ac95D109E8e403C485" + ] + }, + "decimals": { + "type": "number", + "title": "Decimals", + "examples": [ + "18" + ] + } + } + }, + "balancer": { + "type": "object", + "title": "Balancer", + "properties": { + "poolId": { + "type": "string", + "title": "Balancer_PoolId", + "examples": [ + "0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a" + ], + "pattern": "^0x[a-fA-F0-9]{64}$", + "minLength": 66, + "maxLength": 66 + }, + "BalancerHelpers": { + "type": "string", + "title": "BalancerHelpers_address", + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42, + "examples": [ + "0x5aDDCCa35b7A0D07C74063c48700C8590E87864E" + ] + }, + "Vault": { + "type": "string", + "title": "Vault_address", + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42, + "examples": [ + "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + ] + } + } + }, + "multiplier": { + "type": "number", + "title": "Multiplier", + "examples": [ + "2.5" + ] + } + }, + "required": [ + "decimals", + "address", + "sePSP2", + "balancer", + "multiplier" + ], + "additionalProperties": false + } + } +} \ No newline at end of file