diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e638e9b2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*.{ts,json,js}] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.yml] +indent_size = 2 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..66e95efb --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,27 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js', 'hardhat.config.ts'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'indent': ['error', 2], + 'semi': [2, 'always'], + 'quotes': [2, 'single', 'avoid-escape'], + '@typescript-eslint/no-empty-function': 0, + }, +}; diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 00000000..0b9ee47c --- /dev/null +++ b/.solhint.json @@ -0,0 +1,23 @@ +{ + "extends": "solhint:recommended", + "plugins": [], + "rules": { + "avoid-suicide": "error", + "avoid-sha3": "warn", + "comprehensive-interface": "warn", + "func-visibility": [ + "warn", + { + "ignoreConstructors": true + } + ], + "ordering": "warn", + "mark-callable-contracts": "off", + "max-line-length": [ + "error", + 120 + ], + "compiler-version": "off", + "not-rely-on-time": "off" + } +} \ No newline at end of file diff --git a/test-e2e/register-validator-gas.ts b/test-e2e/register-validator-gas.ts new file mode 100644 index 00000000..67370b89 --- /dev/null +++ b/test-e2e/register-validator-gas.ts @@ -0,0 +1,225 @@ +// Declare imports +import * as helpers from '../test/helpers/contract-helpers'; + +// Declare globals +let ssvNetworkContract: any, minDepositAmount: any; +type gasStruct = { + Operators: number, + New_Pod: number, + Second_Val_With_Deposit: number, + Third_Val_No_Deposit: number, +} +const gasTable: gasStruct[] = []; +let firstPodEverGas = 0; + +describe('Register Validator Gas Tests', () => { + beforeEach(async () => { + // Initialize contract + ssvNetworkContract = (await helpers.initializeContract()).contract; + + // Register operators + await helpers.registerOperators(0, 14, helpers.CONFIG.minimalOperatorFee); + + // Define a minimum deposit amount + minDepositAmount = (helpers.CONFIG.minimalBlocksBeforeLiquidation + 2) * helpers.CONFIG.minimalOperatorFee * 13; + + // Register first validator ever + await helpers.DB.ssvToken.approve(helpers.DB.ssvNetwork.contract.address, minDepositAmount); + const tx = await ssvNetworkContract.registerValidator( + helpers.DataGenerator.publicKey(5), + [1, 2, 3, 4], + helpers.DataGenerator.shares(4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0, + active: true + } + ); + + // Save gas fee for first validator ever + const receipt = await tx.wait(); + firstPodEverGas = +receipt.gasUsed; + + // Approve SSV + await helpers.DB.ssvToken.connect(helpers.DB.owners[1]).approve(ssvNetworkContract.address, minDepositAmount * 500); + }); + + it('4 Operators Gas Usage', async () => { + // New Pod + const tx1 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(1), + [1, 2, 3, 4], + helpers.DataGenerator.shares(4), + minDepositAmount, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0, + active: true + } + ); + const receipt1 = await tx1.wait(); + const cluster1 = (receipt1.events[2].args[4]); + + // Second Validator with a deposit + const tx2 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(2), + [1, 2, 3, 4], + helpers.DataGenerator.shares(4), + minDepositAmount, + cluster1 + ); + const receipt2 = await tx2.wait(); + const cluster2 = (receipt2.events[2].args[4]); + + // Third Validator without a deposit + const tx3 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(3), + [1, 2, 3, 4], + helpers.DataGenerator.shares(4), + 0, + cluster2 + ); + const receipt3 = await tx3.wait(); + + // Save the gas costs + gasTable.push({ Operators: 4, New_Pod: +receipt1.gasUsed, Second_Val_With_Deposit: +receipt2.gasUsed, Third_Val_No_Deposit: +receipt3.gasUsed }); + }); + + it('7 Operators Gas Usage', async () => { + // New Pod + const tx1 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(1), + [1, 2, 3, 4, 5, 6, 7], + helpers.DataGenerator.shares(7), + minDepositAmount * 2, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0, + active: true + } + ); + const receipt1 = await tx1.wait(); + const cluster1 = (receipt1.events[2].args[4]); + + // Second Validator with a deposit + const tx2 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(2), + [1, 2, 3, 4, 5, 6, 7], + helpers.DataGenerator.shares(7), + minDepositAmount * 2, + cluster1 + ); + const receipt2 = await tx2.wait(); + const cluster2 = (receipt2.events[2].args[4]); + + // Third Validator without a deposit + const tx3 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(3), + [1, 2, 3, 4, 5, 6, 7], + helpers.DataGenerator.shares(7), + 0, + cluster2 + ); + const receipt3 = await tx3.wait(); + + // Save the gas costs + gasTable.push({ Operators: 7, New_Pod: +receipt1.gasUsed, Second_Val_With_Deposit: +receipt2.gasUsed, Third_Val_No_Deposit: +receipt3.gasUsed }); + }); + + it('10 Operators Gas Usage', async () => { + // New Pod + const tx1 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(1), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + helpers.DataGenerator.shares(10), + minDepositAmount * 3, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0, + active: true + } + ); + const receipt1 = await tx1.wait(); + const cluster1 = (receipt1.events[2].args[4]); + + // Second Validator with a deposit + const tx2 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(2), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + helpers.DataGenerator.shares(10), + minDepositAmount * 3, + cluster1 + ); + const receipt2 = await tx2.wait(); + const cluster2 = (receipt2.events[2].args[4]); + + // Third Validator without a deposit + const tx3 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(3), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + helpers.DataGenerator.shares(10), + 0, + cluster2 + ); + const receipt3 = await tx3.wait(); + + // Save the gas costs + gasTable.push({ Operators: 10, New_Pod: +receipt1.gasUsed, Second_Val_With_Deposit: +receipt2.gasUsed, Third_Val_No_Deposit: +receipt3.gasUsed }); + }); + + it('13 Operators Gas Usage', async () => { + // New Pod + const tx1 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(1), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], + helpers.DataGenerator.shares(13), + minDepositAmount * 4, + { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0, + active: true + } + ); + const receipt1 = await tx1.wait(); + const cluster1 = (receipt1.events[2].args[4]); + + // Second Validator with a deposit + const tx2 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(2), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], + helpers.DataGenerator.shares(13), + minDepositAmount * 4, + cluster1 + ); + const receipt2 = await tx2.wait(); + const cluster2 = (receipt2.events[2].args[4]); + + // Third Validator without a deposit + const tx3 = await ssvNetworkContract.connect(helpers.DB.owners[1]).registerValidator( + helpers.DataGenerator.publicKey(3), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], + helpers.DataGenerator.shares(13), + 0, + cluster2 + ); + const receipt3 = await tx3.wait(); + + // Save the gas costs + gasTable.push({ Operators: 13, New_Pod: +receipt1.gasUsed, Second_Val_With_Deposit: +receipt2.gasUsed, Third_Val_No_Deposit: +receipt3.gasUsed }); + + // Log the table + console.log(`First validator ever: ${firstPodEverGas}`); + console.table(gasTable); + }); +}); diff --git a/test-e2e/stress.ts b/test-e2e/stress.ts new file mode 100644 index 00000000..4fc9cf36 --- /dev/null +++ b/test-e2e/stress.ts @@ -0,0 +1,146 @@ +// Define imports +import * as helpers from '../test/helpers/contract-helpers'; +import { trackGas, GasGroup } from '../test/helpers/gas-usage'; +import { progressTime } from '../test/helpers/utils'; + +// Define global variables +let ssvNetworkContract: any; +let validatorData: any = []; + +describe('Stress Tests', () => { + beforeEach(async () => { + // Initialize contract + ssvNetworkContract = (await helpers.initializeContract()).contract; + + validatorData = []; + + for (let i = 0; i < 7; i++) { + await helpers.DB.ssvToken.connect(helpers.DB.owners[i]).approve(helpers.DB.ssvNetwork.contract.address, '9000000000000000000000'); + } + // Register operators + await helpers.registerOperators(0, 250, helpers.CONFIG.minimalOperatorFee); + await helpers.registerOperators(1, 250, helpers.CONFIG.minimalOperatorFee); + await helpers.registerOperators(2, 250, helpers.CONFIG.minimalOperatorFee); + await helpers.registerOperators(3, 250, helpers.CONFIG.minimalOperatorFee); + + for (let i = 1000; i < 2001; i++) { + // Define random values + const randomOwner = Math.floor(Math.random() * 6); + const randomOperatorPoint = 1 + Math.floor(Math.random() * 995); + const validatorPublicKey = `0x${'0'.repeat(92)}${i}`; + + // Define minimum deposit amount + const minDepositAmount = (helpers.CONFIG.minimalBlocksBeforeLiquidation * 500) * helpers.CONFIG.minimalOperatorFee * 4; + + // Define empty cluster data to send + let clusterData = { + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + balance: 0, + active: true + }; + + // Loop through all created validators to see if the cluster you chose is created or not + for (let j = validatorData.length - 1; j >= 0; j--) { + if (validatorData[j].operatorPoint === randomOperatorPoint && validatorData[j].owner === randomOwner) { + clusterData = validatorData[j].cluster; + break; + } + } + + // Register a validator + const tx = await ssvNetworkContract.connect(helpers.DB.owners[randomOwner]).registerValidator( + validatorPublicKey, + [randomOperatorPoint, randomOperatorPoint + 1, randomOperatorPoint + 2, randomOperatorPoint + 3], + helpers.DataGenerator.shares(0), + minDepositAmount, + clusterData + ); + + const receipt = await tx.wait(); + + // Get the cluster event + const args = (receipt.events[3].args[4]); + + // Break the cluster event to a struct + const cluster = { + validatorCount: args.validatorCount, + networkFeeIndex: args.networkFeeIndex, + index: args.index, + balance: args.balance, + active: args.active + }; + + // Push the validator to an array + validatorData.push({ publicKey: validatorPublicKey, operatorPoint: randomOperatorPoint, owner: randomOwner, cluster: cluster }); + } + }); + + it('Update 1000 operators', async () => { + for (let i = 0; i < 1000; i++) { + let owner = 0; + if (i > 249 && i < 500) owner = 1; + else if (i > 499 && i < 750) owner = 2; + else if (i > 749) owner = 3; + await ssvNetworkContract.connect(helpers.DB.owners[owner]).declareOperatorFee(i + 1, 110000000); + await progressTime(helpers.CONFIG.declareOperatorFeePeriod); + await ssvNetworkContract.connect(helpers.DB.owners[owner]).executeOperatorFee(i + 1); + } + }); + + it('Remove 1000 operators', async () => { + for (let i = 0; i < 250; i++) await trackGas(await ssvNetworkContract.connect(helpers.DB.owners[0]).removeOperator(i + 1), [GasGroup.REMOVE_OPERATOR]); + for (let i = 250; i < 500; i++) await trackGas(await ssvNetworkContract.connect(helpers.DB.owners[1]).removeOperator(i + 1), [GasGroup.REMOVE_OPERATOR]); + for (let i = 500; i < 750; i++) await trackGas(await ssvNetworkContract.connect(helpers.DB.owners[2]).removeOperator(i + 1), [GasGroup.REMOVE_OPERATOR]); + for (let i = 750; i < 1000; i++) await trackGas(await ssvNetworkContract.connect(helpers.DB.owners[3]).removeOperator(i + 1), [GasGroup.REMOVE_OPERATOR]); + }); + + it('Remove 1000 validators', async () => { + const newValidatorData: any = []; + + for (let i = 0; i < validatorData.length - 1; i++) { + + let clusterData: any; + + // Loop and get latest cluster + for (let j = validatorData.length - 1; j >= 0; j--) { + if (validatorData[j].operatorPoint === validatorData[i].operatorPoint && validatorData[j].owner === validatorData[i].owner) { + clusterData = validatorData[j].cluster; + break; + } + } + + // Loop through new emits to get even more up to date cluster if neeeded + for (let j = newValidatorData.length - 1; j >= 0; j--) { + if (newValidatorData[j].operatorPoint === validatorData[i].operatorPoint && newValidatorData[j].owner === validatorData[i].owner) { + clusterData = newValidatorData[j].cluster; + break; + } + } + + // Remove a validator + const { eventsByName } = await trackGas(await ssvNetworkContract.connect(helpers.DB.owners[validatorData[i].owner]).removeValidator( + validatorData[i].publicKey, + [validatorData[i].operatorPoint, validatorData[i].operatorPoint + 1, validatorData[i].operatorPoint + 2, validatorData[i].operatorPoint + 3], + clusterData + ), [GasGroup.REMOVE_VALIDATOR]); + + // Get removal event + const args = (eventsByName.ValidatorRemoved[0].args).cluster; + + // Form a cluster struct + const cluster = { + validatorCount: args.validatorCount, + networkFeeIndex: args.networkFeeIndex, + index: args.index, + balance: args.balance, + active: args.active + }; + + // Save new validator data + newValidatorData.push({ publicKey: validatorData[i].validatorPublicKey, operatorPoint: validatorData[i].operatorPoint, owner: validatorData[i].owner, cluster: cluster }); + } + }); + +}); diff --git a/test/dao/operational.ts b/test/dao/operational.ts new file mode 100644 index 00000000..b8b28785 --- /dev/null +++ b/test/dao/operational.ts @@ -0,0 +1,77 @@ +// Declare imports +import * as helpers from '../helpers/contract-helpers'; +import { trackGas } from '../helpers/gas-usage'; +import { progressBlocks } from '../helpers/utils'; + +import { expect } from 'chai'; + +let ssvNetworkContract: any, ssvNetworkViews: any, firstCluster: any; + +// Declare globals +describe('DAO operational Tests', () => { + beforeEach(async () => { + // Initialize contract + const metadata = await helpers.initializeContract(); + ssvNetworkContract = metadata.contract; + ssvNetworkViews = metadata.ssvViews; + }); + + it('Starting the transfer process does not change owner', async () => { + await ssvNetworkContract.transferOwnership(helpers.DB.owners[4].address); + + expect(await ssvNetworkContract.owner()).equals(helpers.DB.owners[0].address); + }); + + it('Ownership is transferred in a 2-step process', async () => { + await ssvNetworkContract.transferOwnership(helpers.DB.owners[4].address); + await ssvNetworkContract.connect(helpers.DB.owners[4]).acceptOwnership(); + + expect(await ssvNetworkContract.owner()).equals(helpers.DB.owners[4].address); + }); + + it('Get the network validators count (add/remove validaotor)', async () => { + await helpers.registerOperators(0, 4, helpers.CONFIG.minimalOperatorFee); + + const deposit = (helpers.CONFIG.minimalBlocksBeforeLiquidation + 2) * helpers.CONFIG.minimalOperatorFee * 13; + + const cluster = await helpers.registerValidators( + 4, + deposit.toString(), + [1], + [1, 2, 3, 4], + helpers.getClusterForValidator(0, 0, 0, 0, true), + ); + + firstCluster = cluster.args; + + expect(await ssvNetworkViews.getNetworkValidatorsCount()).to.equal(1); + + await ssvNetworkContract + .connect(helpers.DB.owners[4]) + .removeValidator(helpers.DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster); + expect(await ssvNetworkViews.getNetworkValidatorsCount()).to.equal(0); + }); + + it('Get the network validators count (add/remove validaotor)', async () => { + await helpers.registerOperators(0, 4, helpers.CONFIG.minimalOperatorFee); + + const deposit = (helpers.CONFIG.minimalBlocksBeforeLiquidation + 2) * helpers.CONFIG.minimalOperatorFee * 13; + firstCluster = await helpers.registerValidators( + 4, + deposit.toString(), + [1], + [1, 2, 3, 4], + helpers.getClusterForValidator(0, 0, 0, 0, true), + ); + + expect(await ssvNetworkViews.getNetworkValidatorsCount()).to.equal(1); + + await progressBlocks(helpers.CONFIG.minimalBlocksBeforeLiquidation); + + await ssvNetworkContract + .connect(helpers.DB.owners[4]) + .liquidate(firstCluster.args.owner, firstCluster.args.operatorIds, firstCluster.args.cluster); + + expect(await ssvNetworkViews.getNetworkValidatorsCount()).to.equal(0); + }); +}); diff --git a/test/deployment/version.ts b/test/deployment/version.ts new file mode 100644 index 00000000..8611a20f --- /dev/null +++ b/test/deployment/version.ts @@ -0,0 +1,26 @@ +// Declare imports +import * as helpers from '../helpers/contract-helpers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +// Declare globals +let ssvNetworkContract: any, ssvNetworkViews: any; + +describe('Version upgrade tests', () => { + beforeEach(async () => { + const metadata = await helpers.initializeContract(); + ssvNetworkContract = metadata.contract; + ssvNetworkViews = metadata.ssvViews; + }); + + it('Upgrade contract version number', async () => { + expect(await ssvNetworkViews.getVersion()).to.equal(helpers.CONFIG.initialVersion); + const ssvViews = await ethers.getContractFactory('SSVViewsT'); + const viewsContract = await ssvViews.deploy(); + await viewsContract.deployed(); + + await ssvNetworkContract.updateModule(helpers.SSV_MODULES.SSV_VIEWS, viewsContract.address) + + expect(await ssvNetworkViews.getVersion()).to.equal("v1.1.0"); + }); + +}); \ No newline at end of file diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts new file mode 100644 index 00000000..9c4c4703 --- /dev/null +++ b/test/helpers/utils.ts @@ -0,0 +1,64 @@ +declare let network: any; +declare let ethers: any; + +export const strToHex = (str: any) => `0x${Buffer.from(str, 'utf8').toString('hex')}`; + +export const asciiToHex = (str: any) => { + const arr1 = []; + for (let n = 0, l = str.length; n < l; n++) { + const hex = Number(str.charCodeAt(n)).toString(16); + arr1.push(hex); + } + return arr1.join(''); +}; + +export const blockNumber = async function () { + return await ethers.provider.getBlockNumber(); +}; + +export const progress = async function (time: any, blocks: any, func: any = null) { + let snapshot; + + if (func) { + snapshot = await network.provider.send('evm_snapshot'); + } + + if (time) { + await network.provider.send('evm_increaseTime', [time]); + } + await network.provider.send('hardhat_mine', ['0x' + blocks.toString(16)]); + + if (func) { + await func(); + await network.provider.send('evm_revert', [snapshot]); + } +}; + +export const progressTime = async function (time: any, func: any = null) { + return progress(time, 1, func); +}; + +export const progressBlocks = async function (blocks: any, func = null) { + return progress(0, blocks, func); +}; + +export const snapshot = async function (func: any) { + return progress(0, 0, func); +}; + +export const mineOneBlock = async () => network.provider.send('evm_mine', []); + +export const mineChunk = async (amount: number) => + Promise.all( + Array.from({ length: amount }, () => mineOneBlock()) + ) as unknown as Promise; + +export const mine = async (amount: number) => { + if (amount < 0) throw new Error('mine cannot be called with a negative value'); + const MAX_PARALLEL_CALLS = 1000; + // Do it on parallel but do not overflow connections + for (let i = 0; i < Math.floor(amount / MAX_PARALLEL_CALLS); i++) { + await mineChunk(MAX_PARALLEL_CALLS); + } + return mineChunk(amount % MAX_PARALLEL_CALLS); +};