diff --git a/src/strategies/forte-staking/README.md b/src/strategies/forte-staking/README.md new file mode 100644 index 000000000..50923f6ad --- /dev/null +++ b/src/strategies/forte-staking/README.md @@ -0,0 +1,80 @@ +# forte-staking + +## Description + +This strategy works with staking contracts with the optionality of adding an external-contract call to be used as a multiplier factor. The voting power is calculated based on the amount of tokens staked, the amount of days staked, an internal multiplier factor, and an optional external multiplier factor. The staking contract must comply with a specific interface shown below as it takes into account different stakes deposited at different times by the same account. + +```solidity +struct StakeBatch { + uint256 amount; // total amount of tokens staked in the batch + uint256 timestamp; // timestamp when the batch was created +} + +interface IStakedBatches { + /** + * @dev get the current stake batches of the user + * @param _user the address of the user to check + * @return the list of all the staked batches of the user + */ + function getStakedBatches(address _user) external view returns (StakeBatch[] memory); +} +``` + +The external-multiplier function can have any name as long as it takes only an address as argument, and the return value is an unsigned integer of any size. + +```solidity +function (address _user) external view returns (uint8) +``` + +Notice that the return type can be of any size. For example, a `uint256` would be also valid. + +## Calculation + +The equation of the voting power is: + +$votingPower(snapshot) = \sum_{i=1}^n amountStaked(i) * (daysStaked(i) + daysOffset) * \frac{multiplierNumerator}{ multiplierDenominator} * [externalMultiplier]$ + +where _n_ is the number of batches of staked tokens by the user. + +Notice that the multiplier is a function of a range due to the ceiling parameter: + +$externalMultiplier = + \begin{cases} + externalMultiplier > externalMultiplierCeiling & \quad externalMultiplierCeiling\\ + externalMultiplier ≤ externalMultiplierCeiling & \quad externalMultiplier + \end{cases} +$ + +## Examples + +### Using KYC as an external multiplier + +In this example, the external multiplier is the KYC level of the user in an external contract. The KYC level itself can be any positive number. However, any value greater than 2 will be treated as 2 due to the ceiling parameter: + +```json +{ + "stakingAddress": "0x249E662fe228Eff1e7dCE7cF3E78dFD481C7Ba3E", + "externalMultiplierAddress": "0x86f53212865b6fddb99633dc002a7f7aacaaa8db", + "symbol": "FORTE", + "externalMultiplierABI": "function getAccessLevel(address _account) external view returns (uint8)", + "externalMultiplierFunction": "getAccessLevel", + "multiplierNumerator": 4, + "multiplierDenominator": 1461, + "daysOffset": -1, + "externalMultiplierCeiling": 2 +} +``` + +### No external multiplier + +This is the same example as the one above but without the external multiplier which makes it simpler. Just remove the external multiplier parameters: + +```json +{ + "stakingAddress": "0x249E662fe228Eff1e7dCE7cF3E78dFD481C7Ba3E", + "symbol": "FORTE", + "multiplierNumerator": 4, + "multiplierDenominator": 1461, + "daysOffset": -1 +} +``` diff --git a/src/strategies/forte-staking/examples.json b/src/strategies/forte-staking/examples.json new file mode 100644 index 000000000..7c3c1dfe3 --- /dev/null +++ b/src/strategies/forte-staking/examples.json @@ -0,0 +1,29 @@ +[ + { + "name": "Example query", + "strategy": { + "name": "forte-staking", + "params": { + "stakingAddress": "0x249E662fe228Eff1e7dCE7cF3E78dFD481C7Ba3E", + "externalMultiplierAddress": "0x86f53212865b6fddb99633dc002a7f7aacaaa8db", + "symbol": "FORTE", + "externalMultiplierABI": "function getAccessLevel(address _account) external view returns (uint8)", + "externalMultiplierFunction": "getAccessLevel", + "multiplierNumerator": 4, + "multiplierDenominator": 1461, + "daysOffset": -1, + "externalMultiplierCeiling": 2 + } + }, + "network": "11155111", + "addresses": [ + "0xe5405Dabe7d24F044941cEE4eA73a194659D7bD4", + "0x9Fb190F6Da7cdDd939bB4f661c134051ecf6A8B5", + "0x12363F899a1b2039a2A5a1A717be896AFA446382", + "0xE3D777106b622474a2974839061D652a7Af3C989", + "0x9B42F52E69C9bA94991A7D7e7aF73b2340a43216", + "0xd99675E7583e73E8DDC6bdbcbff5321983232763" + ], + "snapshot": 8541444 + } +] diff --git a/src/strategies/forte-staking/index.ts b/src/strategies/forte-staking/index.ts new file mode 100644 index 000000000..a8c130bd2 --- /dev/null +++ b/src/strategies/forte-staking/index.ts @@ -0,0 +1,112 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { Multicaller } from '../../utils'; +import { formatUnits } from '@ethersproject/units'; + +export const author = 'oscarsernarosero'; +export const version = '0.1.0'; + +type batch = [stakeAmount: BigNumber, timeStamp: BigNumber]; +const stakeAmount = 0; // index of the stake amount in the batch tuple +const timeStamp = 1; // index of the timestamp in the batch tuple +const abiStaking = [ + 'function getStakedBatches(address _user) external view returns ((uint256, uint256)[] memory)' +]; + +export async function strategy( + space, + network, + provider, + addresses, + options, + snapshot +): Promise> { + const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; + + // getting the staked batches + const stakeCall = new Multicaller(network, provider, abiStaking, { + blockTag + }); + addresses.forEach((address) => + stakeCall.call(address, options.stakingAddress, 'getStakedBatches', [ + address + ]) + ); + const stakeResult: Record = await stakeCall.execute(); + + // getting external multiplier + + const externalMultiplierCall = new Multicaller( + network, + provider, + [options.externalMultiplierABI], + { + blockTag + } + ); + addresses.forEach((address) => + externalMultiplierCall.call( + address, + options.externalMultiplierAddress, + options.externalMultiplierFunction, + [address] + ) + ); + const externalMultiplierResult: Record = + options.externalMultiplierAddress + ? await externalMultiplierCall.execute() + : []; + + // return voting power + return Object.fromEntries( + Object.entries(stakeResult).map(([address, rawBatches]) => [ + address, + calculateVotingPower( + rawBatches, + options.multiplierNumerator, + options.multiplierDenominator, + options.daysOffset, + externalMultiplierResult[address], + options.externalMultiplierCeiling + ) + ]) + ); +} + +function calculateVotingPower( + rawBatches: batch[], + numerator: number, + denominator: number, + offset: number, + externalMultiplier = 1, + externalMultiplierCeiling = 1 +): number { + const rawVotingPower: bigint = rawBatches.reduce( + (votingPower: bigint, batch: batch): bigint => { + const today: number = +new Date(); + const stakeDate: number = +new Date(Number(batch[timeStamp]._hex) * 1000); // * 1000 to convert to ms + const daysStaked: number = Math.floor( + (today - stakeDate) / (60 * 60 * 24 * 1000) // (...) / (1 day in ms) + ); + const stake = BigInt(batch[stakeAmount]._hex); + return ( + votingPower + + (stake * + BigInt( + numerator * (daysStaked > -offset ? daysStaked + offset : 0) + )) / + BigInt(denominator) + ); + }, + 0n + ); + return parseFloat( + formatUnits( + rawVotingPower * + BigInt( + externalMultiplier > externalMultiplierCeiling + ? externalMultiplierCeiling + : externalMultiplier + ) + ) + ); +} diff --git a/src/strategies/forte-staking/schema.json b/src/strategies/forte-staking/schema.json new file mode 100644 index 000000000..4704b48f0 --- /dev/null +++ b/src/strategies/forte-staking/schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/Strategy", + "definitions": { + "Strategy": { + "title": "Strategy", + "type": "object", + "properties": { + "symbol": { + "type": "string", + "title": "Symbol", + "examples": ["e.g. FOR"], + "maxLength": 16 + }, + "stakingAddress": { + "type": "string", + "title": "Contract address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "externalMultiplierAddress": { + "type": "string", + "title": "External multiplier address", + "examples": ["e.g. 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"], + "pattern": "^0x[a-fA-F0-9]{40}$", + "minLength": 42, + "maxLength": 42 + }, + "externalMultiplierABI": { + "type": "string", + "title": "External multiplier ABI", + "examples": [ + "e.g. function getAccessLevel(address _account) external view returns (uint8)" + ] + }, + "externalMultiplierFunction": { + "type": "string", + "title": "External multiplier ABI", + "examples": ["e.g. getAccessLevel"] + }, + "multiplierNumerator": { + "type": "number", + "title": "numerator", + "examples": ["e.g. 4"] + }, + "multiplierDenominator": { + "type": "number", + "title": "denominator", + "examples": ["e.g. 1461"] + }, + "daysOffset": { + "type": "number", + "title": "offset", + "examples": ["e.g. -1"] + }, + "externalMultiplierCeiling": { + "type": "number", + "title": "numerator", + "examples": ["e.g. 2"] + } + }, + "required": [ + "stakingAddress", + "multiplierNumerator", + "multiplierDenominator", + "daysOffset" + ], + "additionalProperties": false + } + } +} diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 93df6a796..1d322fbe3 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -485,6 +485,7 @@ import * as shroomyVotingPower from './shroomy-voting-power'; import * as pufferGetPastVotes from './puffer-getpastvotes'; import * as prlInSpRL2Balance from './prl-in-sprl2-balance'; import * as edenOnlineOverride from './eden-online-override'; +import * as forteStaking from './forte-staking'; const strategies = { 'shroomy-voting-power': shroomyVotingPower, @@ -981,7 +982,8 @@ const strategies = { 'dappcomposer-getvotingunits': dappcomposerGetVotingUnits, 'puffer-getpastvotes': pufferGetPastVotes, 'prl-in-sprl2-balance': prlInSpRL2Balance, - 'eden-online-override': edenOnlineOverride + 'eden-online-override': edenOnlineOverride, + 'forte-staking': forteStaking }; Object.keys(strategies).forEach(function (strategyName) {