diff --git a/src/strategies/index.ts b/src/strategies/index.ts index e46ecf2e3..100a05eb8 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -469,6 +469,7 @@ 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 = { 'delegatexyz-erc721-balance-of': delegatexyzErc721BalanceOf, @@ -948,7 +949,8 @@ const strategies = { 'nayms-staking': naymsStaking, 'morpho-delegation': morphoDelegation, 'lizcoin-strategy-2024': lizcoinStrategy2024, - realt + realt, + 'superfluid-vesting': superfluidVesting }; Object.keys(strategies).forEach(function (strategyName) { diff --git a/src/strategies/superfluid-vesting/README.md b/src/strategies/superfluid-vesting/README.md new file mode 100644 index 000000000..cd178443d --- /dev/null +++ b/src/strategies/superfluid-vesting/README.md @@ -0,0 +1,28 @@ +# Superfluid Vesting + +Superfluid Vesting is done in the typical Superfluid way, not requiring prior capital lockup. +The Vesting Scheduler contract allows for the creation of Vesting Schedules which are then automatically executed. + +Vesting Schedules are created by the _vesting sender_. +This sender, by creating a schedule, expresses the intent to provide the promised funds, as specified in the schedule. +In order for this intent to become executable, the sender also needs to +- grant the necessary ACL permissions to the VestingScheduler contract +- have enough funds available when needed + +Note: In order to create a vesting schedule with hard guarantees of successful execution, a vesting sender needs to be a contract which is pre-funded and has no means to withdraw funds. + +## Voting + +In order to map vesting schedules to voting power, we need to monitor vesting schedules. +We need to restrict the schedules taken into consideration to those originating from a known and trusted vesting sender. Otherwise anybody could trivially cheat and gain more voting power by creating schedules for themselves. + +With a trusted vesting sender defined, we can enumerate the vesting schedules created by it (where it is the _sender_) via subgraph query. +The total vesting amount of a vesting schedule is to be calculated as `cliffAmount + (endDate - startDate) * flowRate)`. +We need to subtract from this amount the already vested amount in order to not double-count it. This is assuming that another strategy accounts for the voting power of the already vested portion. + +## Dev + +Run test with +``` +yarn test --strategy=superfluid-vesting +``` diff --git a/src/strategies/superfluid-vesting/examples.json b/src/strategies/superfluid-vesting/examples.json new file mode 100644 index 000000000..b46b537bd --- /dev/null +++ b/src/strategies/superfluid-vesting/examples.json @@ -0,0 +1,21 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "superfluid-vesting", + "params": { + "superTokenAddress": "0xe58267cd7299c29a1b77F4E66Cd12Dd24a2Cd2FD", + "vestingSenderAddress": "0xd7086bf0754383c065d81b62fc2e874373660caa" + } + }, + "network": "8453", + "addresses": [ + "0xf8a025B42B07db05638FE596cce339707ec3cC71", + "0x389E3d1c46595aF7335F8C6D3e403ce2E8a9cf8A", + "0x264Ff25e609363cf738e238CBc7B680300509BED", + "0x5782BD439d3019F61bFac53f6358C30c3566737C", + "0x4ee5D45eB79aEa04C02961a2e543bbAf5cec81B3" + ], + "snapshot": 24392394 + } +] diff --git a/src/strategies/superfluid-vesting/index.ts b/src/strategies/superfluid-vesting/index.ts new file mode 100644 index 000000000..1e4a2a404 --- /dev/null +++ b/src/strategies/superfluid-vesting/index.ts @@ -0,0 +1,96 @@ +import { subgraphRequest } from '../../utils'; +import { getAddress } from '@ethersproject/address'; + +export const author = 'd10r'; +export const version = '0.1.0'; + +const SUBGRAPH_URL_MAP = { + '10': + 'https://subgrapher.snapshot.org/subgraph/arbitrum/6YMD95vYriDkmTJewC2vYubqVZrc6vdk3Sp3mR3YCQUw', + '8453': + 'https://subgrapher.snapshot.org/subgraph/arbitrum/4Zp6n8jcsJMBNa3GY9RZwoK4SLjoagwXGq6GhUQNMgSM', + '11155420': + 'https://subgrapher.snapshot.org/subgraph/arbitrum/5UctBWuaQgr2HVSG6XtAKt5shyjg9snGvD6F424FcjMN' +}; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + + const subgraphUrl = options.subgraphUrl || SUBGRAPH_URL_MAP[network]; + if (!subgraphUrl) { + throw new Error('Subgraph URL not specified'); + } + + const query = { + vestingSchedules: { + __args: { + where: { + superToken: options.superTokenAddress, + sender: options.vestingSenderAddress, + endExecutedAt: null, + failedAt: null, + deletedAt: null + }, + orderBy: 'cliffAndFlowDate', + orderDirection: 'asc' + }, + cliffAmount: true, + cliffAndFlowDate: true, + endDate: true, + flowRate: true, + cliffAndFlowExecutedAt: true, + receiver: true, + remainderAmount: true + } + }; + + if (snapshot !== 'latest') { + // @ts-ignore + query.vestingSchedules.__args.block = { number: snapshot }; + } + + // Get block timestamp - needed for calculating the remaining amount + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + const block = await provider.getBlock(blockTag); + const timestamp = block.timestamp; + + const subgraphResult = await subgraphRequest(subgraphUrl, query); + + const processedMap = subgraphResult.vestingSchedules.map((schedule) => { + const endDate = BigInt(schedule.endDate); + const cliffAndFlowDate = BigInt(schedule.cliffAndFlowDate); + const flowRate = BigInt(schedule.flowRate); + const cliffAmount = BigInt(schedule.cliffAmount); + const remainderAmount = BigInt(schedule.remainderAmount); + + // the initial vesting amount + const fullAmount = cliffAmount + flowRate * (endDate - cliffAndFlowDate) + remainderAmount; + + // the remaining amount which hasn't yet vested + let remainingAmount = fullAmount; + if (schedule.cliffAndFlowExecutedAt !== null) { + remainingAmount -= cliffAmount + flowRate * (BigInt(timestamp) - cliffAndFlowDate); + } + + return { + schedule: schedule, + fullAmount: fullAmount, + remainingAmount: remainingAmount + } + }); + + // create a map of the remaining amounts + return Object.fromEntries( + processedMap.map(item => [ + getAddress(item.schedule.receiver), + // in theory remainingAmount could become negative, thus we cap at 0 + Math.max(Number(item.remainingAmount) / 1e18, 0) + ]) + ); +} diff --git a/src/strategies/superfluid-vesting/schema.json b/src/strategies/superfluid-vesting/schema.json new file mode 100644 index 000000000..b761a2c15 --- /dev/null +++ b/src/strategies/superfluid-vesting/schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "superTokenAddress": { + "type": "string", + "title": "Super Token contract address", + "examples": ["e.g. 0x6C210F071c7246C452CAC7F8BaA6dA53907BbaE1"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "vestingSenderAddress": { + "type": "string", + "title": "Vesting sender address", + "examples": ["e.g. 0x6C210F071c7246C452CAC7F8BaA6dA53907BbaE1"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "subgraphUrl": { + "type": "string", + "title": "Subgraph URL (optional - if not already known to the strategy)", + "examples": ["e.g. https://subgrapher.snapshot.org/subgraph/arbitrum/4Zp6n8jcsJMBNa3GY9RZwoK4SLjoagwXGq6GhUQNMgSM"] + } + }, + "required": ["superTokenAddress", "vestingSenderAddress"], + "additionalProperties": false + } + } +}