From 26584a47ad547d39fe64991167785924e3fa97e2 Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Mon, 4 Mar 2024 16:58:15 +0300 Subject: [PATCH 1/9] feat: mev boost relay list --- abi/MEVBoostRelayAllowedList.json | 1 + configs/extra-deployed-goerli.json | 6 +- configs/extra-deployed-holesky.json | 3 + configs/extra-deployed-mainnet.json | 6 +- contracts/allowed-list.ts | 7 ++ contracts/index.ts | 1 + programs/allowed-list.ts | 121 ++++++++++++++++++++++++++++ programs/index.ts | 1 + utils/bool.ts | 5 ++ utils/index.ts | 1 + 10 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 abi/MEVBoostRelayAllowedList.json create mode 100644 contracts/allowed-list.ts create mode 100644 programs/allowed-list.ts create mode 100644 utils/bool.ts diff --git a/abi/MEVBoostRelayAllowedList.json b/abi/MEVBoostRelayAllowedList.json new file mode 100644 index 0000000..1eda372 --- /dev/null +++ b/abi/MEVBoostRelayAllowedList.json @@ -0,0 +1 @@ +[{"name":"RelayAdded","inputs":[{"name":"uri_hash","type":"string","indexed":true},{"name":"relay","type":"tuple","components":[{"name":"uri","type":"string"},{"name":"operator","type":"string"},{"name":"is_mandatory","type":"bool"},{"name":"description","type":"string"}],"indexed":false}],"anonymous":false,"type":"event"},{"name":"RelayRemoved","inputs":[{"name":"uri_hash","type":"string","indexed":true},{"name":"uri","type":"string","indexed":false}],"anonymous":false,"type":"event"},{"name":"AllowedListUpdated","inputs":[{"name":"allowed_list_version","type":"uint256","indexed":true}],"anonymous":false,"type":"event"},{"name":"OwnerChanged","inputs":[{"name":"new_owner","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"ManagerChanged","inputs":[{"name":"new_manager","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true},{"name":"amount","type":"uint256","indexed":false},{"name":"recipient","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"owner","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"get_relays_amount","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_owner","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_manager","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_relays","inputs":[],"outputs":[{"name":"","type":"tuple[]","components":[{"name":"uri","type":"string"},{"name":"operator","type":"string"},{"name":"is_mandatory","type":"bool"},{"name":"description","type":"string"}]}]},{"stateMutability":"view","type":"function","name":"get_relay_by_uri","inputs":[{"name":"relay_uri","type":"string"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"uri","type":"string"},{"name":"operator","type":"string"},{"name":"is_mandatory","type":"bool"},{"name":"description","type":"string"}]}]},{"stateMutability":"view","type":"function","name":"get_allowed_list_version","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"add_relay","inputs":[{"name":"uri","type":"string"},{"name":"operator","type":"string"},{"name":"is_mandatory","type":"bool"},{"name":"description","type":"string"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"remove_relay","inputs":[{"name":"uri","type":"string"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"change_owner","inputs":[{"name":"owner","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_manager","inputs":[{"name":"manager","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"dismiss_manager","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"recover_erc20","inputs":[{"name":"token","type":"address"},{"name":"amount","type":"uint256"},{"name":"recipient","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"fallback"}] diff --git a/configs/extra-deployed-goerli.json b/configs/extra-deployed-goerli.json index 0967ef4..1df332d 100644 --- a/configs/extra-deployed-goerli.json +++ b/configs/extra-deployed-goerli.json @@ -1 +1,5 @@ -{} +{ + "allowedRelayList": { + "address": "0xeabE95AC5f3D64aE16AcBB668Ed0efcd81B721Bc" + } +} diff --git a/configs/extra-deployed-holesky.json b/configs/extra-deployed-holesky.json index 09f764e..2209fff 100644 --- a/configs/extra-deployed-holesky.json +++ b/configs/extra-deployed-holesky.json @@ -1,5 +1,8 @@ { "unlimitedStake": { "address": "0xc589bfa706a94fc0fc48720d62e0f442dee0cb5d" + }, + "allowedRelayList": { + "address": "0x2d86C5855581194a386941806E38cA119E50aEA3" } } diff --git a/configs/extra-deployed-mainnet.json b/configs/extra-deployed-mainnet.json index 0967ef4..b29429d 100644 --- a/configs/extra-deployed-mainnet.json +++ b/configs/extra-deployed-mainnet.json @@ -1 +1,5 @@ -{} +{ + "allowedRelayList": { + "address": "0xF95f069F9AD107938F6ba802a3da87892298610E" + } +} diff --git a/contracts/allowed-list.ts b/contracts/allowed-list.ts new file mode 100644 index 0000000..ed77db1 --- /dev/null +++ b/contracts/allowed-list.ts @@ -0,0 +1,7 @@ +import { Contract } from 'ethers'; +import { wallet } from '@providers'; +import { getOptionalDeployedAddress } from '@configs'; +import abi from 'abi/MEVBoostRelayAllowedList.json'; + +export const allowedListAddress = getOptionalDeployedAddress('allowedRelayList.address'); +export const allowedListContract = new Contract(allowedListAddress, abi, wallet); diff --git a/contracts/index.ts b/contracts/index.ts index 8fae346..0c12664 100644 --- a/contracts/index.ts +++ b/contracts/index.ts @@ -1,3 +1,4 @@ +export * from './allowed-list'; export * from './app-proxy'; export * from './aragon'; export * from './burner'; diff --git a/programs/allowed-list.ts b/programs/allowed-list.ts new file mode 100644 index 0000000..066641f --- /dev/null +++ b/programs/allowed-list.ts @@ -0,0 +1,121 @@ +import { program } from '@command'; +import { allowedListContract } from '@contracts'; +import { addLogsCommands, addParsingCommands } from './common'; +import { authorizedCall, isTrue, logger } from '@utils'; +import { Result } from 'ethers'; + +const allowedList = program + .command('allowed-list') + .aliases(['relay-list', 'allowed-relay-list']) + .description('interact with MEV boost allowed relay list contract'); +addParsingCommands(allowedList, allowedListContract); +addLogsCommands(allowedList, allowedListContract); + +allowedList + .command('get-relays') + .aliases(['relays']) + .description('returns the list of allowed relays') + .action(async () => { + const relays = await allowedListContract.get_relays(); + + if (relays.length === 0) { + logger.warn('No relays found'); + return; + } + + relays.forEach((relay: Result) => { + logger.log(relay.toObject()); + }); + }); + +allowedList + .command('get-relay-by-uri') + .aliases(['relay-by-uri', 'relay', 'get-relay']) + .description('returns the relay by URI') + .argument('', 'relay URI') + .action(async (uri) => { + const relay = await allowedListContract.get_relay_by_uri(uri); + logger.log(relay.toObject()); + }); + +allowedList + .command('get-relays-amount') + .aliases(['relays-amount', 'amount']) + .description('returns the amount of relays') + .action(async () => { + const amount = await allowedListContract.get_relays_amount(); + logger.log('Amount', Number(amount)); + }); + +allowedList + .command('get-allowed-list-version') + .aliases(['version', 'allowed-list-version', 'get-version', 'list-version']) + .description('returns the version of the allowed list') + .action(async () => { + const version = await allowedListContract.get_allowed_list_version(); + logger.log('Version', Number(version)); + }); + +allowedList + .command('add-relay') + .aliases(['add']) + .description('adds a relay to the allowed list') + .argument('', 'relay URI') + .argument('', 'relay operator name') + .argument('', 'is relay mandatory (true or false)') + .argument('', 'relay description') + .action(async (URI, operator, isMandatoryStr, description) => { + const isMandatory = isTrue(isMandatoryStr); + logger.table({ URI, operator, isMandatory, description }); + await authorizedCall(allowedListContract, 'add_relay', [URI, operator, isMandatory, description]); + }); + +allowedList + .command('remove-relay') + .aliases(['remove']) + .description('removes a relay from the allowed list') + .argument('', 'relay URI') + .action(async (URI) => { + await authorizedCall(allowedListContract, 'remove_relay', [URI]); + }); + +allowedList + .command('get-manager') + .aliases(['manager']) + .description('returns the address of the relay list manager') + .action(async () => { + const manager = await allowedListContract.get_manager(); + logger.log('Manager', manager); + }); + +allowedList + .command('set-manager') + .description('sets the address of the relay list manager') + .argument('', 'new manager address') + .action(async (manager) => { + await authorizedCall(allowedListContract, 'set_manager', [manager]); + }); + +allowedList + .command('dissmiss-manager') + .description('dismisses the relay list manager') + .action(async () => { + await authorizedCall(allowedListContract, 'dismiss_manager', []); + }); + +allowedList + .command('get-owner') + .aliases(['owner']) + .description('returns the address of the relay list owner') + .action(async () => { + const owner = await allowedListContract.get_owner(); + logger.log('Owner', owner); + }); + +allowedList + .command('change-owner') + .description('change owner') + .argument('', 'new owner') + .action(async (newOwner) => { + await authorizedCall(allowedListContract, 'change_owner', [newOwner]); + }); diff --git a/programs/index.ts b/programs/index.ts index e90e894..252dd78 100644 --- a/programs/index.ts +++ b/programs/index.ts @@ -1,6 +1,7 @@ export * from './accounting-consensus'; export * from './accounting-oracle'; export * from './accounts'; +export * from './allowed-list'; export * from './burner'; export * from './deposit-contract'; export * from './dsm'; diff --git a/utils/bool.ts b/utils/bool.ts new file mode 100644 index 0000000..91ff649 --- /dev/null +++ b/utils/bool.ts @@ -0,0 +1,5 @@ +export const isTrue = (value: string | number) => { + const lowerValue = value.toLocaleString().toLocaleLowerCase(); + const trueValues = ['true', '1', 'yes', 'y']; + return trueValues.includes(lowerValue); +}; diff --git a/utils/index.ts b/utils/index.ts index 3933564..babc7fb 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,6 +1,7 @@ export * from './abi'; export * from './authorized-call'; export * from './block'; +export * from './bool'; export * from './call-tx'; export * from './compare-calls'; export * from './contract'; From b357e80977eaa808085309ffa91af48a465dc70d Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Mon, 4 Mar 2024 17:16:14 +0300 Subject: [PATCH 2/9] feat: testnet warning --- utils/print-tx.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/utils/print-tx.ts b/utils/print-tx.ts index 01f467c..12d9b41 100644 --- a/utils/print-tx.ts +++ b/utils/print-tx.ts @@ -8,6 +8,7 @@ import { splitArgsAndOverrides } from './split-args-and-overrides'; const title = chalk.gray; const chain = chalk.green.bold; const value = chalk.blue.bold; +const warn = chalk.red.bold; export const printTx = async (contract: Contract, method: string, argsWithOverrides: unknown[] = []) => { const provider = getProvider(contract); @@ -30,4 +31,10 @@ export const printTx = async (contract: Contract, method: string, argsWithOverri if (overrides.value) { logger.log(title('Value:'), value(`${overrides.value.toString()} (${formatEther(overrides.value)} ETH)`)); } + + logger.log(''); + logger.log(warn('--------------------------------------------------------------------------------------------')); + logger.log(warn('If you make any changes on the testnet, please inform the stakeholders in the Discord thread')); + logger.log(warn('--------------------------------------------------------------------------------------------')); + logger.log(''); }; From 2f4e7c646a82d58ada373ec3956f6bea472d842a Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Wed, 6 Mar 2024 15:02:08 +0300 Subject: [PATCH 3/9] feat: deposit data validation --- consensus/constants.ts | 2 + consensus/index.ts | 2 + consensus/ssz-types.ts | 5 ++ consensus/utils.ts | 1 + programs/common/curated-module.ts | 20 +++++- programs/deposit-data.ts | 16 +++++ programs/index.ts | 1 + utils/deposit-data.ts | 111 ++++++++++++++++++++++++++++++ utils/index.ts | 2 + utils/join-hex.ts | 8 +++ utils/to-hex-string.ts | 15 ++++ 11 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 consensus/constants.ts create mode 100644 consensus/utils.ts create mode 100644 programs/deposit-data.ts create mode 100644 utils/deposit-data.ts create mode 100644 utils/join-hex.ts create mode 100644 utils/to-hex-string.ts diff --git a/consensus/constants.ts b/consensus/constants.ts new file mode 100644 index 0000000..6752821 --- /dev/null +++ b/consensus/constants.ts @@ -0,0 +1,2 @@ +export const ZERO_HASH = Buffer.alloc(32, 0); +export const DOMAIN_DEPOSIT = Uint8Array.from([3, 0, 0, 0]); diff --git a/consensus/index.ts b/consensus/index.ts index c296d19..9120b06 100644 --- a/consensus/index.ts +++ b/consensus/index.ts @@ -1,4 +1,6 @@ export * from './attestation'; +export * from './constants'; export * from './domain'; export * from './signing-root'; export * from './ssz-types'; +export * from './utils'; diff --git a/consensus/ssz-types.ts b/consensus/ssz-types.ts index 8fdd066..04e43b0 100644 --- a/consensus/ssz-types.ts +++ b/consensus/ssz-types.ts @@ -53,3 +53,8 @@ export const AttestationDataBigint = new ContainerType( }, { typeName: 'AttestationData', jsonCase: 'eth2', cachePermanentRootStruct: true }, ); + +export const DepositMessage = new ContainerType( + { pubkey: BLSPubkey, withdrawalCredentials: Bytes32, amount: UintNum64 }, + { typeName: 'DepositMessage', jsonCase: 'eth2' }, +); diff --git a/consensus/utils.ts b/consensus/utils.ts new file mode 100644 index 0000000..6087246 --- /dev/null +++ b/consensus/utils.ts @@ -0,0 +1 @@ +export { fromHexString } from '@chainsafe/ssz'; diff --git a/programs/common/curated-module.ts b/programs/common/curated-module.ts index 65dc183..f667efc 100644 --- a/programs/common/curated-module.ts +++ b/programs/common/curated-module.ts @@ -1,8 +1,9 @@ import { Command } from 'commander'; import { Contract, EventLog, concat, toBeHex } from 'ethers'; -import { authorizedCall, contractCallTxWithConfirm, formatDate, getLatestBlock, logger } from '@utils'; +import { authorizedCall, contractCallTxWithConfirm, formatDate, getLatestBlock, joinHex, logger } from '@utils'; import { getPenalizedOperators } from '../staking-module'; import { aclContract } from '@contracts'; +import { DepositData, supplementAndVerifyDepositDataArray } from 'utils/deposit-data'; export const addCuratedModuleSubCommands = (command: Command, contract: Contract) => { command @@ -90,6 +91,23 @@ export const addCuratedModuleSubCommands = (command: Command, contract: Contract await authorizedCall(contract, 'addSigningKeys', [operatorId, count, publicKeys, signatures]); }); + command + .command('add-keys-from-file') + .description('adds signing keys from deposit data file') + .argument('', 'node operator id') + .argument('', 'file path') + .action(async (operatorId, filePath) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const depositData: DepositData[] = require(filePath); + await supplementAndVerifyDepositDataArray(depositData); + + const count = depositData.length; + const publicKeys = joinHex(depositData.map(({ pubkey }) => pubkey)); + const signatures = joinHex(depositData.map(({ signature }) => signature)); + + await authorizedCall(contract, 'addSigningKeys', [operatorId, count, publicKeys, signatures]); + }); + command .command('remove-keys') .description('removes signing keys') diff --git a/programs/deposit-data.ts b/programs/deposit-data.ts new file mode 100644 index 0000000..f2910e6 --- /dev/null +++ b/programs/deposit-data.ts @@ -0,0 +1,16 @@ +import { program } from '@command'; +import { logger } from '@utils'; +import { supplementAndVerifyDepositDataArray } from 'utils/deposit-data'; + +const keys = program.command('deposit-data').aliases(['keys']).description('deposit-data utils'); + +keys + .command('verify') + .description('verify deposit data') + .argument('', 'path to the deposit data file') + .action(async (filePath) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const depositDataArray = require(filePath); + await supplementAndVerifyDepositDataArray(depositDataArray); + logger.log('Deposit data is valid, keys checked', depositDataArray.length); + }); diff --git a/programs/index.ts b/programs/index.ts index 252dd78..c827c36 100644 --- a/programs/index.ts +++ b/programs/index.ts @@ -4,6 +4,7 @@ export * from './accounts'; export * from './allowed-list'; export * from './burner'; export * from './deposit-contract'; +export * from './deposit-data'; export * from './dsm'; export * from './exit-bus-consensus'; export * from './exit-bus-oracle'; diff --git a/utils/deposit-data.ts b/utils/deposit-data.ts new file mode 100644 index 0000000..e5cf910 --- /dev/null +++ b/utils/deposit-data.ts @@ -0,0 +1,111 @@ +import { + BLSPubkey, + Bytes32, + DOMAIN_DEPOSIT, + DepositMessage, + UintNum64, + ZERO_HASH, + computeDomain, + computeSigningRoot, + fromHexString, +} from '@consensus'; +import { fetchSpec } from '@providers'; +import { stakingRouterContract } from '@contracts'; +import { toHexString } from './to-hex-string'; + +export type DepositData = { + pubkey: string; + withdrawalCredentials: string; + amount: bigint; + signature: string; + depositMessageRoot?: string; + depositDataRoot?: string; + forkVersion?: string; + networkName?: string; + depositCliVersion?: string; +}; + +export const supplementAndVerifyDepositDataArray = async (depositDataArray: DepositData[]): Promise => { + const suplplementedDepositDataArray = await supplementDepositDataArray(depositDataArray.map(normilizeDepositDta)); + return suplplementedDepositDataArray.every(verifyDepositData); +}; + +export const normilizeDepositDta = (depositData: DepositData & Record): DepositData => { + const { + withdrawal_credentials, + deposit_message_root, + deposit_data_root, + fork_version, + network_name, + deposit_cli_version, + } = depositData; + + const { + pubkey, + withdrawalCredentials, + amount, + signature, + depositMessageRoot, + depositDataRoot, + forkVersion, + networkName, + depositCliVersion, + } = depositData; + + return { + pubkey: toHexString(pubkey), + withdrawalCredentials: toHexString(withdrawal_credentials || withdrawalCredentials), + amount: BigInt(amount), + signature: toHexString(signature), + depositMessageRoot: toHexString(deposit_message_root || depositMessageRoot), + depositDataRoot: toHexString(deposit_data_root || depositDataRoot), + forkVersion: toHexString(fork_version || forkVersion), + networkName: toHexString(network_name || networkName), + depositCliVersion: toHexString(deposit_cli_version || depositCliVersion), + }; +}; + +export const supplementDepositDataArray = async (depositDataArray: DepositData[]): Promise => { + const withdrawalCredentials = await stakingRouterContract.getWithdrawalCredentials(); + const spec = await fetchSpec(); + const forkVersion = spec.GENESIS_FORK_VERSION; + + return depositDataArray.map((depositData) => { + if (depositData.withdrawalCredentials && depositData.withdrawalCredentials !== withdrawalCredentials) { + throw new Error('Withdrawal credentials do not match the withdrawal credentials from the contract'); + } + + if (depositData.forkVersion && depositData.forkVersion !== forkVersion) { + throw new Error('Fork version mismatch the CL one'); + } + + return { ...depositData, forkVersion, withdrawalCredentials }; + }); +}; + +export const verifyDepositData = (depositData: DepositData): boolean => { + if (!depositData.pubkey) throw new Error('Pubkey is not defined'); + if (!depositData.withdrawalCredentials) throw new Error('Withdrawal credentials is not defined'); + if (!depositData.amount) throw new Error('Amount is not defined'); + if (!depositData.signature) throw new Error('Signature is not defined'); + if (!depositData.forkVersion) throw new Error('Fork version is not defined'); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const blst = require('@chainsafe/blst'); + + const { pubkey, withdrawalCredentials, amount, signature, forkVersion } = depositData; + + const depositMessage = { + pubkey: BLSPubkey.fromJson(toHexString(pubkey)), + withdrawalCredentials: Bytes32.fromJson(toHexString(withdrawalCredentials)), + amount: UintNum64.fromJson(toHexString(amount)), + }; + + const domain = computeDomain(DOMAIN_DEPOSIT, fromHexString(toHexString(forkVersion)), ZERO_HASH); + const signingRoot = computeSigningRoot(DepositMessage, depositMessage, domain); + + const blsPublicKey = blst.PublicKey.fromBytes(depositMessage.pubkey); + const blsSignature = blst.Signature.fromBytes(fromHexString(toHexString(signature))); + + return blst.verify(signingRoot, blsPublicKey, blsSignature); +}; diff --git a/utils/index.ts b/utils/index.ts index babc7fb..666da68 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -8,6 +8,7 @@ export * from './contract'; export * from './csv'; export * from './format-date'; export * from './get-value'; +export * from './join-hex'; export * from './logger'; export * from './modules'; export * from './parse-method-call'; @@ -17,6 +18,7 @@ export * from './scripts'; export * from './sleep'; export * from './staking-limit'; export * from './stringify'; +export * from './to-hex-string'; export * from './trace'; export * from './tree'; export * from './validators'; diff --git a/utils/join-hex.ts b/utils/join-hex.ts new file mode 100644 index 0000000..ba8d93f --- /dev/null +++ b/utils/join-hex.ts @@ -0,0 +1,8 @@ +export const joinHex = (hexStrings: string[]): string => { + const formattedString = hexStrings.map((str) => { + if (str.startsWith('0x')) return str.slice(2); + return str; + }); + + return `0x${formattedString.join('')}`; +}; diff --git a/utils/to-hex-string.ts b/utils/to-hex-string.ts new file mode 100644 index 0000000..10984c8 --- /dev/null +++ b/utils/to-hex-string.ts @@ -0,0 +1,15 @@ +export const toHexString = (value: unknown): string => { + if (typeof value === 'string' && !value.startsWith('0x')) { + return `0x${value}`; + } + + if (typeof value === 'string') { + return value; + } + + if (typeof value === 'number' || typeof value === 'bigint') { + return `0x${value.toString(16)}`; + } + + throw new Error('Unsupported value type'); +}; From 47072dcf74f6a0c73b388f8a11fec0bad9d20396 Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Wed, 6 Mar 2024 15:24:16 +0300 Subject: [PATCH 4/9] feat: keys --- programs/common/curated-module.ts | 42 ++++++++++++++++++++++++++++++- utils/index.ts | 1 + utils/split-hex.ts | 21 ++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 utils/split-hex.ts diff --git a/programs/common/curated-module.ts b/programs/common/curated-module.ts index f667efc..29b118a 100644 --- a/programs/common/curated-module.ts +++ b/programs/common/curated-module.ts @@ -1,6 +1,14 @@ import { Command } from 'commander'; import { Contract, EventLog, concat, toBeHex } from 'ethers'; -import { authorizedCall, contractCallTxWithConfirm, formatDate, getLatestBlock, joinHex, logger } from '@utils'; +import { + authorizedCall, + contractCallTxWithConfirm, + formatDate, + getLatestBlock, + joinHex, + logger, + splitHex, +} from '@utils'; import { getPenalizedOperators } from '../staking-module'; import { aclContract } from '@contracts'; import { DepositData, supplementAndVerifyDepositDataArray } from 'utils/deposit-data'; @@ -79,6 +87,38 @@ export const addCuratedModuleSubCommands = (command: Command, contract: Contract logger.log('Key', keyData); }); + command + .command('keys') + .description('returns signing keys') + .argument('', 'operator id') + .argument('[from-index]', 'from index') + .argument('[count]', 'keys count') + .action(async (operatorId, fromIndex, count) => { + if (fromIndex == null && count == null) { + const total = await contract.getNodeOperator(operatorId, true); + + fromIndex = 0; + count = total.totalAddedValidators; + } + + const [pubkeys, signatures, used] = await contract.getSigningKeys( + Number(operatorId), + Number(fromIndex), + Number(count), + ); + + const pubkeysArray = splitHex(pubkeys, 48 * 2); + const signaturesArray = splitHex(signatures, 96 * 2); + + const keysData = pubkeysArray.map((pubkey: string, index: number) => ({ + pubkey, + signature: signaturesArray[index], + used: used[index], + })); + + logger.log('Keys', keysData); + }); + command .command('add-keys') .description('adds signing keys') diff --git a/utils/index.ts b/utils/index.ts index 666da68..96b55e6 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -16,6 +16,7 @@ export * from './print-tx'; export * from './role-hash'; export * from './scripts'; export * from './sleep'; +export * from './split-hex'; export * from './staking-limit'; export * from './stringify'; export * from './to-hex-string'; diff --git a/utils/split-hex.ts b/utils/split-hex.ts new file mode 100644 index 0000000..fcfd330 --- /dev/null +++ b/utils/split-hex.ts @@ -0,0 +1,21 @@ +export const splitHex = (hexString: string, chunkLength: number) => { + if (!Number.isInteger(chunkLength) || chunkLength < 1) { + throw new RangeError('chunkLength should be positive integer'); + } + + if (typeof hexString !== 'string' || !hexString.match(/^0x[0-9A-Fa-f]*$/)) { + throw new Error('hexString is not a hex-like string'); + } + + const parts: string[] = []; + let part = ''; + // start from index 2 because each record beginning from 0x + for (let i = 2; i < hexString.length; i++) { + part += hexString[i]; + if (part.length === chunkLength) { + parts.push(`0x${part}`); + part = ''; + } + } + return parts; +}; From 832a3e74409a05b76d6dbdfc411ca154bfdd327e Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Sat, 23 Mar 2024 16:54:32 +0300 Subject: [PATCH 5/9] feat: sdvt reward address checks --- programs/simple-dvt.ts | 121 ++---------------- programs/staking-module/index.ts | 1 + programs/staking-module/simple-dvt.ts | 176 ++++++++++++++++++++++++++ utils/chain-id.ts | 6 + utils/index.ts | 1 + 5 files changed, 194 insertions(+), 111 deletions(-) create mode 100644 programs/staking-module/simple-dvt.ts create mode 100644 utils/chain-id.ts diff --git a/programs/simple-dvt.ts b/programs/simple-dvt.ts index 09e9c6a..964158f 100644 --- a/programs/simple-dvt.ts +++ b/programs/simple-dvt.ts @@ -1,9 +1,8 @@ import { program } from '@command'; -import { lidoAddress, simpleDVTContract, wstethAddress } from '@contracts'; +import { simpleDVTContract } from '@contracts'; import { addAragonAppSubCommands, addCuratedModuleSubCommands, addLogsCommands, addParsingCommands } from './common'; -import { provider } from '@providers'; -import { Contract } from 'ethers'; -import { logger } from '@utils'; +import { getLatestBlock, logger } from '@utils'; +import { check0xSplit, checkWrapperContract } from './staking-module'; const simpleDVT = program .command('simple-dvt') @@ -18,113 +17,13 @@ simpleDVT .command('check-reward-address') .description('check split contracts') .argument('
', 'address of the reward address') - .action(async (wrapperAddress) => { - // wrapper checks - - const wrapperABI = [ - 'function feeRecipient() view returns (address)', - 'function feeShare() view returns (uint256)', - 'function splitWallet() view returns (address)', - 'function stETH() view returns (address)', - 'function wstETH() view returns (address)', - ]; - - const wrapperContract = new Contract(wrapperAddress, wrapperABI, provider); - - const [feeRecipient, feeShare, splitWallet, stETH, wstETH] = await Promise.all([ - wrapperContract.feeRecipient(), - wrapperContract.feeShare(), - wrapperContract.splitWallet(), - wrapperContract.stETH(), - wrapperContract.wstETH(), - ]); - - logger.log('Wrapper contract info:'); - logger.table({ - feeShare: Number(feeShare), - feeRecipient, - splitWallet, - stETH, - wstETH, - }); - - // 0x splitter checks - - const splitABI = ['function splitMain() view returns (address)']; - const splitMainABI = [ - 'function createSplit(address[] accounts,uint32[] percentAllocations,uint32 distributorFee,address controller)', - 'event CreateSplit(address indexed split)', - ]; - - const splitContract = new Contract(splitWallet, splitABI, provider); - const splitMainAddress = await splitContract.splitMain(); - - const splitMainContract = new Contract(splitMainAddress, splitMainABI, provider); - const deployEvent = splitMainContract.filters.CreateSplit(splitWallet); - const toBlock = await provider.getBlockNumber(); - const [splitMainEvent] = await splitMainContract.queryFilter(deployEvent, 0, toBlock); - const { transactionHash } = splitMainEvent; - - const tx = await provider.getTransaction(transactionHash); - - if (!tx) { - logger.error('Transaction not found'); - return; - } - - const parsedTx = splitMainContract.interface.parseTransaction(tx); - - if (!parsedTx) { - logger.error('Transaction parse failed'); - return; - } + .option('-b, --blocks ', 'blocks', '1000000000') + .action(async (wrapperAddress, { blocks }) => { + const latestBlock = await getLatestBlock(); + const toBlock = latestBlock.number; + const fromBlock = Math.max(toBlock - Number(blocks), 0); + const { splitWalletAddress } = await checkWrapperContract(wrapperAddress, fromBlock, toBlock); logger.log(''); - logger.log('Split contract info:'); - logger.log('Accounts:'); - logger.table(parsedTx.args[0]); - logger.log('Percent allocations:'); - logger.table(parsedTx.args[1]); - - logger.log('Common info:'); - logger.table({ - transactionHash, - splitMainAddress, - splitWallet, - distributorFee: parsedTx.args[2], - controller: parsedTx.args[3], - }); - - // addresses checks - - const splitFactoryAddresses: Record = { - 1: '0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE', - 17000: '0x2ed6c4b5da6378c7897ac67ba9e43102feb694ee', - }; - - const { chainId } = await provider.getNetwork(); - const expectedSplitMainAddress = splitFactoryAddresses[Number(chainId)]; - - logger.log('Addresses checks:'); - - const expectedStETHAddress = lidoAddress; - const expectedWstETHAddress = wstethAddress; - - if (stETH.toLocaleLowerCase() === expectedStETHAddress.toLocaleLowerCase()) { - logger.success('stETH address matches'); - } else { - logger.error(`stETH address mismatch: ${stETH} !== ${expectedStETHAddress}`); - } - - if (wstETH.toLocaleLowerCase() === expectedWstETHAddress.toLocaleLowerCase()) { - logger.success('wstETH address matches'); - } else { - logger.error(`wstETH address mismatch: ${wstETH} !== ${expectedWstETHAddress}`); - } - - if (splitMainAddress.toLocaleLowerCase() === expectedSplitMainAddress.toLocaleLowerCase()) { - logger.success('splitMain address matches'); - } else { - logger.error(`splitMain address mismatch: ${splitMainAddress} !== ${expectedSplitMainAddress}`); - } + await check0xSplit(splitWalletAddress, fromBlock, toBlock); }); diff --git a/programs/staking-module/index.ts b/programs/staking-module/index.ts index bb2ea2b..e2a1e03 100644 --- a/programs/staking-module/index.ts +++ b/programs/staking-module/index.ts @@ -1,3 +1,4 @@ export * from './modules'; export * from './nor'; export * from './operators'; +export * from './simple-dvt'; diff --git a/programs/staking-module/simple-dvt.ts b/programs/staking-module/simple-dvt.ts new file mode 100644 index 0000000..1264ae4 --- /dev/null +++ b/programs/staking-module/simple-dvt.ts @@ -0,0 +1,176 @@ +import { provider } from '@providers'; +import { getChainId, logger } from '@utils'; +import { Contract, EventLog, ZeroAddress } from 'ethers'; +import { lidoAddress, wstethAddress } from '@contracts'; +import chalk from 'chalk'; + +const header = chalk.white.bold; +const checkPass = (text: string) => chalk.green.bold(text) + ' ✅'; +const checkFail = (text: string) => chalk.red.bold(text) + ' ❌'; +const formatCheck = (text: string, check: boolean) => (check ? checkPass(text) : checkFail(text)); + +const expectedStETHAddress = lidoAddress; +const expectedWstETHAddress = wstethAddress; + +export const checkWrapperContract = async (wrapperAddress: string, fromBlock: number, toBlock: number) => { + const chainId = await getChainId(); + + // TODO: move to constants + const wrapperFactoryAddresses: Record> = { + 1: { + obol: '0xA9d94139A310150Ca1163b5E23f3E1dbb7D9E2A6', + ssv: '0x3df147bd18854bfa03291034666469237504d4ca', + }, + 17000: { + obol: '0x934ec6b68ce7cc3b3e6106c686b5ad808ed26449', + ssv: '0xB7f465f1bd6B2f8DAbA3FcA36c5F5E49E0812F37', + }, + }; + + const factoryAddresses = wrapperFactoryAddresses[chainId]; + + if (!factoryAddresses) { + throw new Error('Unsupported chain id'); + } + + const factoryABI = ['event CreateObolLidoSplit(address split)']; + const wrapperABI = [ + 'function feeRecipient() view returns (address)', + 'function feeShare() view returns (uint256)', + 'function splitWallet() view returns (address)', + 'function stETH() view returns (address)', + 'function wstETH() view returns (address)', + ]; + + const factoryObolContract = new Contract(factoryAddresses.obol, factoryABI, provider); + const wrapperFactorySsvContract = new Contract(factoryAddresses.ssv, factoryABI, provider); + const wrapperContract = new Contract(wrapperAddress, wrapperABI, provider); + + const [feeRecipient, feeShare, splitWallet, stETH, wstETH] = await Promise.all([ + wrapperContract.feeRecipient(), + wrapperContract.feeShare(), + wrapperContract.splitWallet(), + wrapperContract.stETH(), + wrapperContract.wstETH(), + ]); + + logger.log(''); + logger.log(header('Wrapper contract')); + logger.log(''); + + const obolWrapperDeployFilter = factoryObolContract.filters.CreateObolLidoSplit(); + const ssvWrapperDeployFilter = wrapperFactorySsvContract.filters.CreateObolLidoSplit(); + + const deployEvents = ( + await Promise.all([ + factoryObolContract.queryFilter(obolWrapperDeployFilter, fromBlock, toBlock), + wrapperFactorySsvContract.queryFilter(ssvWrapperDeployFilter, fromBlock, toBlock), + ]) + ) + .flat() + .filter((event) => event instanceof EventLog && event.args?.split === wrapperAddress); + + if (deployEvents.length == 0) throw new Error('Wrapper contract is deployed from unkown factory'); + if (deployEvents.length > 1) throw new Error('Multiple deployment events found'); + + const deployEvent = deployEvents[0]; + const factoryAddress = deployEvent.address; + + const isObol = factoryAddress.toLocaleLowerCase() == factoryAddresses.obol.toLocaleLowerCase(); + const isSSV = factoryAddress.toLocaleLowerCase() == factoryAddresses.ssv.toLocaleLowerCase(); + const factoryName = isObol ? 'Obol' : isSSV ? 'SSV' : null; + + const { transactionHash } = deployEvent; + + const isStETH = stETH.toLocaleLowerCase() === expectedStETHAddress.toLocaleLowerCase(); + const isWstETH = wstETH.toLocaleLowerCase() === expectedWstETHAddress.toLocaleLowerCase(); + + logger.log('Factory: ', formatCheck(factoryName ?? 'Unknown', factoryName != null)); + logger.log('Factory address:', factoryAddress); + logger.log('Deploy tx: ', transactionHash); + logger.log('Fee share: ', Number(feeShare)); + logger.log('Fee recipient: ', feeRecipient); + logger.log('Split wallet: ', splitWallet); + logger.log('stETH address: ', formatCheck(stETH, isStETH)); + logger.log('wstETH address: ', formatCheck(wstETH, isWstETH)); + + if (!isStETH) logger.error(`stETH address mismatch: ${stETH} !== ${expectedStETHAddress}`); + if (!isWstETH) logger.error(`wstETH address mismatch: ${wstETH} !== ${expectedWstETHAddress}`); + + return { splitWalletAddress: splitWallet }; +}; + +export const check0xSplit = async (splitWalletAddress: string, fromBlock: number, toBlock: number) => { + const chainId = await getChainId(); + + // todo: move to constants + const splitABI = ['function splitMain() view returns (address)']; + const splitMainABI = [ + 'function createSplit(address[] accounts,uint32[] percentAllocations,uint32 distributorFee,address controller)', + 'event CreateSplit(address indexed split)', + ]; + + const splitFactoryAddresses: Record = { + 1: '0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE', + 17000: '0x2ed6c4b5da6378c7897ac67ba9e43102feb694ee', + }; + + const factoryAddress = splitFactoryAddresses[chainId]; + if (!factoryAddress) { + throw new Error('Unsupported chain id'); + } + + const splitContract = new Contract(splitWalletAddress, splitABI, provider); + const splitMainAddress = await splitContract.splitMain(); + + const splitMainContract = new Contract(splitMainAddress, splitMainABI, provider); + const deployEvent = splitMainContract.filters.CreateSplit(splitWalletAddress); + const [splitMainEvent] = await splitMainContract.queryFilter(deployEvent, fromBlock, toBlock); + const { transactionHash } = splitMainEvent; + + const tx = await provider.getTransaction(transactionHash); + if (!tx) throw new Error('Split deploy transaction not found'); + + const parsedTx = splitMainContract.interface.parseTransaction(tx); + if (!parsedTx) throw new Error('Split deploy transaction parse failed'); + + logger.log(''); + logger.log(header('Split contract')); + logger.log(''); + + const accounts = parsedTx.args[0]; + const allocations = parsedTx.args[1].map(Number); + const distributorFee = parsedTx.args[2]; + const controller = parsedTx.args[3]; + + const expectedSplitMainAddress = factoryAddress; + const isSplitMain = splitMainAddress.toLocaleLowerCase() === expectedSplitMainAddress.toLocaleLowerCase(); + const isZeroFee = Number(distributorFee) === 0; + const isZeroController = controller === ZeroAddress; + + if (accounts.length !== allocations.length) { + throw new Error('Accounts and allocations lengths mismatch'); + } + + logger.log('Deploy tx: ', transactionHash); + logger.log('Split wallet: ', splitWalletAddress); + logger.log('Split main: ', formatCheck(splitMainAddress, isSplitMain)); + logger.log('Controller: ', formatCheck(controller, isZeroController)); + logger.log('Distributor fee:', formatCheck(String(distributorFee), isZeroFee)); + + const maxAllocation = Math.max(...allocations); + const minAllocation = Math.min(...allocations); + const isFairAllocation = maxAllocation - minAllocation <= 1; + + logger.log(''); + logger.log('Account Share'); + + parsedTx.args[0].forEach((account: string, index: number) => { + const allocation = parsedTx.args[1][index]; + logger.log(account, formatCheck(String(allocation), isFairAllocation)); + }); + + if (!isSplitMain) { + logger.error(`splitMain address mismatch: ${splitMainAddress} !== ${expectedSplitMainAddress}`); + } +}; diff --git a/utils/chain-id.ts b/utils/chain-id.ts new file mode 100644 index 0000000..e76698b --- /dev/null +++ b/utils/chain-id.ts @@ -0,0 +1,6 @@ +import { provider } from '@providers'; + +export const getChainId = async (): Promise => { + const network = await provider.getNetwork(); + return Number(network.chainId); +}; diff --git a/utils/index.ts b/utils/index.ts index 96b55e6..0a30cec 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -3,6 +3,7 @@ export * from './authorized-call'; export * from './block'; export * from './bool'; export * from './call-tx'; +export * from './chain-id'; export * from './compare-calls'; export * from './contract'; export * from './csv'; From 077bcfc81e7e8bcdf0ae2a0f9b7d0a00a93ea783 Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Sat, 23 Mar 2024 19:04:58 +0300 Subject: [PATCH 6/9] feat: gnosis safe manager address checks for sdvt --- abi/GnosisSafe.json | 1 + abi/GnosisSafeProxyFactory.json | 1 + abi/ObolLidoSplit.json | 1 + abi/ObolLidoSplitFactory.json | 1 + abi/SplitMain.json | 1 + abi/SplitWallet.json | 1 + configs/extra-deployed-holesky.json | 23 +++ configs/extra-deployed-mainnet.json | 23 +++ contracts/gnosis.ts | 17 +++ contracts/index.ts | 3 + contracts/obol-lido-split.ts | 13 ++ contracts/split-main.ts | 10 ++ programs/simple-dvt.ts | 30 ++-- programs/staking-module/simple-dvt.ts | 200 ++++++++++++++------------ utils/block.ts | 8 ++ utils/chain-id.ts | 6 - utils/index.ts | 1 - 17 files changed, 231 insertions(+), 109 deletions(-) create mode 100644 abi/GnosisSafe.json create mode 100644 abi/GnosisSafeProxyFactory.json create mode 100644 abi/ObolLidoSplit.json create mode 100644 abi/ObolLidoSplitFactory.json create mode 100644 abi/SplitMain.json create mode 100644 abi/SplitWallet.json create mode 100644 contracts/gnosis.ts create mode 100644 contracts/obol-lido-split.ts create mode 100644 contracts/split-main.ts delete mode 100644 utils/chain-id.ts diff --git a/abi/GnosisSafe.json b/abi/GnosisSafe.json new file mode 100644 index 0000000..79ac9c9 --- /dev/null +++ b/abi/GnosisSafe.json @@ -0,0 +1 @@ +[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"AddedOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"approvedHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"owner","type":"address"}],"name":"ApproveHash","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"handler","type":"address"}],"name":"ChangedFallbackHandler","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"guard","type":"address"}],"name":"ChangedGuard","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"threshold","type":"uint256"}],"name":"ChangedThreshold","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"module","type":"address"}],"name":"DisabledModule","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"module","type":"address"}],"name":"EnabledModule","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"txHash","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"payment","type":"uint256"}],"name":"ExecutionFailure","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"module","type":"address"}],"name":"ExecutionFromModuleFailure","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"module","type":"address"}],"name":"ExecutionFromModuleSuccess","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"txHash","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"payment","type":"uint256"}],"name":"ExecutionSuccess","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"RemovedOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"SafeReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"initiator","type":"address"},{"indexed":false,"internalType":"address[]","name":"owners","type":"address[]"},{"indexed":false,"internalType":"uint256","name":"threshold","type":"uint256"},{"indexed":false,"internalType":"address","name":"initializer","type":"address"},{"indexed":false,"internalType":"address","name":"fallbackHandler","type":"address"}],"name":"SafeSetup","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"msgHash","type":"bytes32"}],"name":"SignMsg","type":"event"},{"stateMutability":"nonpayable","type":"fallback"},{"inputs":[],"name":"VERSION","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"_threshold","type":"uint256"}],"name":"addOwnerWithThreshold","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hashToApprove","type":"bytes32"}],"name":"approveHash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"approvedHashes","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_threshold","type":"uint256"}],"name":"changeThreshold","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"bytes","name":"signatures","type":"bytes"},{"internalType":"uint256","name":"requiredSignatures","type":"uint256"}],"name":"checkNSignatures","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"bytes","name":"signatures","type":"bytes"}],"name":"checkSignatures","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"prevModule","type":"address"},{"internalType":"address","name":"module","type":"address"}],"name":"disableModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"domainSeparator","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"module","type":"address"}],"name":"enableModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"},{"internalType":"uint256","name":"safeTxGas","type":"uint256"},{"internalType":"uint256","name":"baseGas","type":"uint256"},{"internalType":"uint256","name":"gasPrice","type":"uint256"},{"internalType":"address","name":"gasToken","type":"address"},{"internalType":"address","name":"refundReceiver","type":"address"},{"internalType":"uint256","name":"_nonce","type":"uint256"}],"name":"encodeTransactionData","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"},{"internalType":"uint256","name":"safeTxGas","type":"uint256"},{"internalType":"uint256","name":"baseGas","type":"uint256"},{"internalType":"uint256","name":"gasPrice","type":"uint256"},{"internalType":"address","name":"gasToken","type":"address"},{"internalType":"address payable","name":"refundReceiver","type":"address"},{"internalType":"bytes","name":"signatures","type":"bytes"}],"name":"execTransaction","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"}],"name":"execTransactionFromModule","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"}],"name":"execTransactionFromModuleReturnData","outputs":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getChainId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"start","type":"address"},{"internalType":"uint256","name":"pageSize","type":"uint256"}],"name":"getModulesPaginated","outputs":[{"internalType":"address[]","name":"array","type":"address[]"},{"internalType":"address","name":"next","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOwners","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"offset","type":"uint256"},{"internalType":"uint256","name":"length","type":"uint256"}],"name":"getStorageAt","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getThreshold","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"},{"internalType":"uint256","name":"safeTxGas","type":"uint256"},{"internalType":"uint256","name":"baseGas","type":"uint256"},{"internalType":"uint256","name":"gasPrice","type":"uint256"},{"internalType":"address","name":"gasToken","type":"address"},{"internalType":"address","name":"refundReceiver","type":"address"},{"internalType":"uint256","name":"_nonce","type":"uint256"}],"name":"getTransactionHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"module","type":"address"}],"name":"isModuleEnabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"isOwner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"prevOwner","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"_threshold","type":"uint256"}],"name":"removeOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"enum Enum.Operation","name":"operation","type":"uint8"}],"name":"requiredTxGas","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"handler","type":"address"}],"name":"setFallbackHandler","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"guard","type":"address"}],"name":"setGuard","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_owners","type":"address[]"},{"internalType":"uint256","name":"_threshold","type":"uint256"},{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"address","name":"fallbackHandler","type":"address"},{"internalType":"address","name":"paymentToken","type":"address"},{"internalType":"uint256","name":"payment","type":"uint256"},{"internalType":"address payable","name":"paymentReceiver","type":"address"}],"name":"setup","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"signedMessages","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"targetContract","type":"address"},{"internalType":"bytes","name":"calldataPayload","type":"bytes"}],"name":"simulateAndRevert","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"prevOwner","type":"address"},{"internalType":"address","name":"oldOwner","type":"address"},{"internalType":"address","name":"newOwner","type":"address"}],"name":"swapOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] diff --git a/abi/GnosisSafeProxyFactory.json b/abi/GnosisSafeProxyFactory.json new file mode 100644 index 0000000..4772715 --- /dev/null +++ b/abi/GnosisSafeProxyFactory.json @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"},{"indexed":false,"internalType":"address","name":"singleton","type":"address"}],"name":"ProxyCreation","type":"event"},{"inputs":[{"internalType":"address","name":"_singleton","type":"address"},{"internalType":"bytes","name":"initializer","type":"bytes"},{"internalType":"uint256","name":"saltNonce","type":"uint256"}],"name":"calculateCreateProxyWithNonceAddress","outputs":[{"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"singleton","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"createProxy","outputs":[{"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_singleton","type":"address"},{"internalType":"bytes","name":"initializer","type":"bytes"},{"internalType":"uint256","name":"saltNonce","type":"uint256"},{"internalType":"contract IProxyCreationCallback","name":"callback","type":"address"}],"name":"createProxyWithCallback","outputs":[{"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_singleton","type":"address"},{"internalType":"bytes","name":"initializer","type":"bytes"},{"internalType":"uint256","name":"saltNonce","type":"uint256"}],"name":"createProxyWithNonce","outputs":[{"internalType":"contract GnosisSafeProxy","name":"proxy","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"proxyCreationCode","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"proxyRuntimeCode","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"pure","type":"function"}] diff --git a/abi/ObolLidoSplit.json b/abi/ObolLidoSplit.json new file mode 100644 index 0000000..f28b0e7 --- /dev/null +++ b/abi/ObolLidoSplit.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_feeRecipient","type":"address"},{"internalType":"uint256","name":"_feeShare","type":"uint256"},{"internalType":"contract ERC20","name":"_stETH","type":"address"},{"internalType":"contract ERC20","name":"_wstETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"Invalid_Address","type":"error"},{"inputs":[],"name":"Invalid_FeeRecipient","type":"error"},{"inputs":[{"internalType":"uint256","name":"fee","type":"uint256"}],"name":"Invalid_FeeShare","type":"error"},{"inputs":[],"name":"distribute","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"feeRecipient","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeShare","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"rescueFunds","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"splitWallet","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"stETH","outputs":[{"internalType":"contract ERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"wstETH","outputs":[{"internalType":"contract ERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"}] diff --git a/abi/ObolLidoSplitFactory.json b/abi/ObolLidoSplitFactory.json new file mode 100644 index 0000000..c2f423b --- /dev/null +++ b/abi/ObolLidoSplitFactory.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_feeRecipient","type":"address"},{"internalType":"uint256","name":"_feeShare","type":"uint256"},{"internalType":"contract ERC20","name":"_stETH","type":"address"},{"internalType":"contract ERC20","name":"_wstETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"Invalid_Wallet","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"split","type":"address"}],"name":"CreateObolLidoSplit","type":"event"},{"inputs":[{"internalType":"address","name":"splitWallet","type":"address"}],"name":"createSplit","outputs":[{"internalType":"address","name":"lidoSplit","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"lidoSplitImpl","outputs":[{"internalType":"contract ObolLidoSplit","name":"","type":"address"}],"stateMutability":"view","type":"function"}] diff --git a/abi/SplitMain.json b/abi/SplitMain.json new file mode 100644 index 0000000..6ab2c04 --- /dev/null +++ b/abi/SplitMain.json @@ -0,0 +1 @@ +[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"Create2Error","type":"error"},{"inputs":[],"name":"CreateError","type":"error"},{"inputs":[{"internalType":"address","name":"newController","type":"address"}],"name":"InvalidNewController","type":"error"},{"inputs":[{"internalType":"uint256","name":"accountsLength","type":"uint256"},{"internalType":"uint256","name":"allocationsLength","type":"uint256"}],"name":"InvalidSplit__AccountsAndAllocationsMismatch","type":"error"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"InvalidSplit__AccountsOutOfOrder","type":"error"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"InvalidSplit__AllocationMustBePositive","type":"error"},{"inputs":[{"internalType":"uint32","name":"allocationsSum","type":"uint32"}],"name":"InvalidSplit__InvalidAllocationsSum","type":"error"},{"inputs":[{"internalType":"uint32","name":"distributorFee","type":"uint32"}],"name":"InvalidSplit__InvalidDistributorFee","type":"error"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"}],"name":"InvalidSplit__InvalidHash","type":"error"},{"inputs":[{"internalType":"uint256","name":"accountsLength","type":"uint256"}],"name":"InvalidSplit__TooFewAccounts","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"Unauthorized","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"}],"name":"CancelControlTransfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"},{"indexed":true,"internalType":"address","name":"previousController","type":"address"},{"indexed":true,"internalType":"address","name":"newController","type":"address"}],"name":"ControlTransfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"}],"name":"CreateSplit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"},{"indexed":true,"internalType":"contract ERC20","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"address","name":"distributorAddress","type":"address"}],"name":"DistributeERC20","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"address","name":"distributorAddress","type":"address"}],"name":"DistributeETH","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"},{"indexed":true,"internalType":"address","name":"newPotentialController","type":"address"}],"name":"InitiateControlTransfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"}],"name":"UpdateSplit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"ethAmount","type":"uint256"},{"indexed":false,"internalType":"contract ERC20[]","name":"tokens","type":"address[]"},{"indexed":false,"internalType":"uint256[]","name":"tokenAmounts","type":"uint256[]"}],"name":"Withdrawal","type":"event"},{"inputs":[],"name":"PERCENTAGE_SCALE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"}],"name":"acceptControl","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"}],"name":"cancelControlTransfer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"accounts","type":"address[]"},{"internalType":"uint32[]","name":"percentAllocations","type":"uint32[]"},{"internalType":"uint32","name":"distributorFee","type":"uint32"},{"internalType":"address","name":"controller","type":"address"}],"name":"createSplit","outputs":[{"internalType":"address","name":"split","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"},{"internalType":"contract ERC20","name":"token","type":"address"},{"internalType":"address[]","name":"accounts","type":"address[]"},{"internalType":"uint32[]","name":"percentAllocations","type":"uint32[]"},{"internalType":"uint32","name":"distributorFee","type":"uint32"},{"internalType":"address","name":"distributorAddress","type":"address"}],"name":"distributeERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"},{"internalType":"address[]","name":"accounts","type":"address[]"},{"internalType":"uint32[]","name":"percentAllocations","type":"uint32[]"},{"internalType":"uint32","name":"distributorFee","type":"uint32"},{"internalType":"address","name":"distributorAddress","type":"address"}],"name":"distributeETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"}],"name":"getController","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"contract ERC20","name":"token","type":"address"}],"name":"getERC20Balance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"getETHBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"}],"name":"getHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"}],"name":"getNewPotentialController","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"}],"name":"makeSplitImmutable","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"accounts","type":"address[]"},{"internalType":"uint32[]","name":"percentAllocations","type":"uint32[]"},{"internalType":"uint32","name":"distributorFee","type":"uint32"}],"name":"predictImmutableSplitAddress","outputs":[{"internalType":"address","name":"split","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"},{"internalType":"address","name":"newController","type":"address"}],"name":"transferControl","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"},{"internalType":"contract ERC20","name":"token","type":"address"},{"internalType":"address[]","name":"accounts","type":"address[]"},{"internalType":"uint32[]","name":"percentAllocations","type":"uint32[]"},{"internalType":"uint32","name":"distributorFee","type":"uint32"},{"internalType":"address","name":"distributorAddress","type":"address"}],"name":"updateAndDistributeERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"},{"internalType":"address[]","name":"accounts","type":"address[]"},{"internalType":"uint32[]","name":"percentAllocations","type":"uint32[]"},{"internalType":"uint32","name":"distributorFee","type":"uint32"},{"internalType":"address","name":"distributorAddress","type":"address"}],"name":"updateAndDistributeETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"split","type":"address"},{"internalType":"address[]","name":"accounts","type":"address[]"},{"internalType":"uint32[]","name":"percentAllocations","type":"uint32[]"},{"internalType":"uint32","name":"distributorFee","type":"uint32"}],"name":"updateSplit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"walletImplementation","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"withdrawETH","type":"uint256"},{"internalType":"contract ERC20[]","name":"tokens","type":"address[]"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] diff --git a/abi/SplitWallet.json b/abi/SplitWallet.json new file mode 100644 index 0000000..26a5411 --- /dev/null +++ b/abi/SplitWallet.json @@ -0,0 +1 @@ +[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"Unauthorized","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"split","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ReceiveETH","type":"event"},{"inputs":[{"internalType":"contract ERC20","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"sendERC20ToMain","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"sendETHToMain","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"splitMain","outputs":[{"internalType":"contract ISplitMain","name":"","type":"address"}],"stateMutability":"view","type":"function"}] diff --git a/configs/extra-deployed-holesky.json b/configs/extra-deployed-holesky.json index 2209fff..5283b3d 100644 --- a/configs/extra-deployed-holesky.json +++ b/configs/extra-deployed-holesky.json @@ -4,5 +4,28 @@ }, "allowedRelayList": { "address": "0x2d86C5855581194a386941806E38cA119E50aEA3" + }, + "obolLidoSplit": { + "factory": { + "obol": { + "address": "0x934ec6b68ce7cc3b3e6106c686b5ad808ed26449" + }, + "ssv": { + "address": "0xB7f465f1bd6B2f8DAbA3FcA36c5F5E49E0812F37" + } + } + }, + "0xSplit": { + "splitMain": { + "address": "0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE" + } + }, + "gnosis": { + "factory": { + "address": "0xa6b71e26c5e0845f74c812102ca7114b6a896ab2" + }, + "singleton": { + "address": "0xd9db270c1b5e3bd161e8c8503c55ceabee709552" + } } } diff --git a/configs/extra-deployed-mainnet.json b/configs/extra-deployed-mainnet.json index b29429d..c405a2a 100644 --- a/configs/extra-deployed-mainnet.json +++ b/configs/extra-deployed-mainnet.json @@ -1,5 +1,28 @@ { "allowedRelayList": { "address": "0xF95f069F9AD107938F6ba802a3da87892298610E" + }, + "obolLidoSplit": { + "factory": { + "obol": { + "address": "0xA9d94139A310150Ca1163b5E23f3E1dbb7D9E2A6" + }, + "ssv": { + "address": "0x3df147bd18854bfa03291034666469237504d4ca" + } + } + }, + "0xSplit": { + "splitMain": { + "address": "0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE" + } + }, + "gnosis": { + "factory": { + "address": "0xa6b71e26c5e0845f74c812102ca7114b6a896ab2" + }, + "singleton": { + "address": "0xd9db270c1b5e3bd161e8c8503c55ceabee709552" + } } } diff --git a/contracts/gnosis.ts b/contracts/gnosis.ts new file mode 100644 index 0000000..0e270c6 --- /dev/null +++ b/contracts/gnosis.ts @@ -0,0 +1,17 @@ +import { Contract } from 'ethers'; +import { wallet } from '@providers'; +import { getOptionalDeployedAddress } from '@configs'; +import gnosisSafeAbi from 'abi/GnosisSafe.json'; +import gnosisSafeProxyFactoryAbi from 'abi/GnosisSafeProxyFactory.json'; + +export const gnosisSafeProxyFactoryAddress = getOptionalDeployedAddress('gnosis.factory.address'); +export const gnosisSafeProxyFactoryContract = new Contract( + gnosisSafeProxyFactoryAddress, + gnosisSafeProxyFactoryAbi, + wallet, +); + +export const gnosisSafeSingletonAddress = getOptionalDeployedAddress('gnosis.singleton.address'); +export const gnosisSafeSingletonContract = new Contract(gnosisSafeSingletonAddress, gnosisSafeAbi, wallet); + +export { gnosisSafeAbi }; diff --git a/contracts/index.ts b/contracts/index.ts index 0c12664..b7c4355 100644 --- a/contracts/index.ts +++ b/contracts/index.ts @@ -5,10 +5,12 @@ export * from './burner'; export * from './deposit-contract'; export * from './dsm'; export * from './ens'; +export * from './gnosis'; export * from './ldo'; export * from './lido'; export * from './locator'; export * from './nor'; +export * from './obol-lido-split'; export * from './oracles'; export * from './proxy'; export * from './public-resolver'; @@ -16,6 +18,7 @@ export * from './repo'; export * from './sandbox'; export * from './sanity-checker'; export * from './simple-dvt'; +export * from './split-main'; export * from './staking-module'; export * from './staking-router'; export * from './token-manager'; diff --git a/contracts/obol-lido-split.ts b/contracts/obol-lido-split.ts new file mode 100644 index 0000000..58d2e2d --- /dev/null +++ b/contracts/obol-lido-split.ts @@ -0,0 +1,13 @@ +import { Contract } from 'ethers'; +import { wallet } from '@providers'; +import { getOptionalDeployedAddress } from '@configs'; +import factoryAbi from 'abi/ObolLidoSplitFactory.json'; +import obolLidoSplitAbi from 'abi/ObolLidoSplit.json'; + +export const obolLidoSplitFactoryObolAddress = getOptionalDeployedAddress('obolLidoSplit.factory.obol.address'); +export const obolLidoSplitFactoryObolContract = new Contract(obolLidoSplitFactoryObolAddress, factoryAbi, wallet); + +export const obolLidoSplitFactorySSVAddress = getOptionalDeployedAddress('obolLidoSplit.factory.ssv.address'); +export const obolLidoSplitFactorySSVContract = new Contract(obolLidoSplitFactorySSVAddress, factoryAbi, wallet); + +export { obolLidoSplitAbi }; diff --git a/contracts/split-main.ts b/contracts/split-main.ts new file mode 100644 index 0000000..37e704b --- /dev/null +++ b/contracts/split-main.ts @@ -0,0 +1,10 @@ +import { Contract } from 'ethers'; +import { wallet } from '@providers'; +import { getOptionalDeployedAddress } from '@configs'; +import splitMainAbi from 'abi/SplitMain.json'; +import splitWalletAbi from 'abi/SplitWallet.json'; + +export const splitMainAddress = getOptionalDeployedAddress('0xSplit.splitMain.address'); +export const splitMainContract = new Contract(splitMainAddress, splitMainAbi, wallet); + +export { splitWalletAbi }; diff --git a/programs/simple-dvt.ts b/programs/simple-dvt.ts index 964158f..0bac0ff 100644 --- a/programs/simple-dvt.ts +++ b/programs/simple-dvt.ts @@ -1,8 +1,8 @@ import { program } from '@command'; import { simpleDVTContract } from '@contracts'; import { addAragonAppSubCommands, addCuratedModuleSubCommands, addLogsCommands, addParsingCommands } from './common'; -import { getLatestBlock, logger } from '@utils'; -import { check0xSplit, checkWrapperContract } from './staking-module'; +import { getLatestBlockRange } from '@utils'; +import { check0xSplit, checkGnosisSafe, checkWrapperContract } from './staking-module'; const simpleDVT = program .command('simple-dvt') @@ -16,14 +16,26 @@ addCuratedModuleSubCommands(simpleDVT, simpleDVTContract); simpleDVT .command('check-reward-address') .description('check split contracts') - .argument('
', 'address of the reward address') + .argument('', 'cluster reward address') .option('-b, --blocks ', 'blocks', '1000000000') - .action(async (wrapperAddress, { blocks }) => { - const latestBlock = await getLatestBlock(); - const toBlock = latestBlock.number; - const fromBlock = Math.max(toBlock - Number(blocks), 0); + .action(async (rewardAddress, { blocks }) => { + const [fromBlock, toBlock] = await getLatestBlockRange(blocks); - const { splitWalletAddress } = await checkWrapperContract(wrapperAddress, fromBlock, toBlock); - logger.log(''); + const { splitWalletAddress } = await checkWrapperContract(rewardAddress, fromBlock, toBlock); await check0xSplit(splitWalletAddress, fromBlock, toBlock); }); + +simpleDVT + .command('check-cluster-addresses') + .description('check split contracts') + .argument('', 'cluster reward address') + .argument('', 'cluster manager address') + .option('-b, --blocks ', 'blocks', '1000000000') + .action(async (rewardAddress, managerAddress, { blocks }) => { + const [fromBlock, toBlock] = await getLatestBlockRange(blocks); + + const { splitWalletAddress } = await checkWrapperContract(rewardAddress, fromBlock, toBlock); + const { splitWalletAccounts } = await check0xSplit(splitWalletAddress, fromBlock, toBlock); + + await checkGnosisSafe(managerAddress, splitWalletAccounts); + }); diff --git a/programs/staking-module/simple-dvt.ts b/programs/staking-module/simple-dvt.ts index 1264ae4..d5abee3 100644 --- a/programs/staking-module/simple-dvt.ts +++ b/programs/staking-module/simple-dvt.ts @@ -1,50 +1,44 @@ import { provider } from '@providers'; -import { getChainId, logger } from '@utils'; +import { logger } from '@utils'; import { Contract, EventLog, ZeroAddress } from 'ethers'; -import { lidoAddress, wstethAddress } from '@contracts'; +import { + lidoAddress, + wstethAddress, + obolLidoSplitAbi, + obolLidoSplitFactoryObolContract, + obolLidoSplitFactorySSVContract, + obolLidoSplitFactoryObolAddress, + obolLidoSplitFactorySSVAddress, + splitWalletAbi, + splitMainContract, + splitMainAddress, + gnosisSafeAbi, + gnosisSafeProxyFactoryContract, +} from '@contracts'; import chalk from 'chalk'; const header = chalk.white.bold; + +const match = chalk.green; +const unmatch = chalk.gray; + const checkPass = (text: string) => chalk.green.bold(text) + ' ✅'; const checkFail = (text: string) => chalk.red.bold(text) + ' ❌'; -const formatCheck = (text: string, check: boolean) => (check ? checkPass(text) : checkFail(text)); +const checkWarn = (text: string) => chalk.yellow.bold(text); +const passOrFail = (text: string, check: boolean) => (check ? checkPass(text) : checkFail(text)); +const passOrWarn = (text: string, check: boolean) => (check ? checkPass(text) : checkWarn(text)); const expectedStETHAddress = lidoAddress; const expectedWstETHAddress = wstethAddress; +const expectedSplitMainAddress = splitMainAddress; export const checkWrapperContract = async (wrapperAddress: string, fromBlock: number, toBlock: number) => { - const chainId = await getChainId(); - - // TODO: move to constants - const wrapperFactoryAddresses: Record> = { - 1: { - obol: '0xA9d94139A310150Ca1163b5E23f3E1dbb7D9E2A6', - ssv: '0x3df147bd18854bfa03291034666469237504d4ca', - }, - 17000: { - obol: '0x934ec6b68ce7cc3b3e6106c686b5ad808ed26449', - ssv: '0xB7f465f1bd6B2f8DAbA3FcA36c5F5E49E0812F37', - }, - }; - - const factoryAddresses = wrapperFactoryAddresses[chainId]; - - if (!factoryAddresses) { - throw new Error('Unsupported chain id'); - } + const wrapperContract = new Contract(wrapperAddress, obolLidoSplitAbi, provider); - const factoryABI = ['event CreateObolLidoSplit(address split)']; - const wrapperABI = [ - 'function feeRecipient() view returns (address)', - 'function feeShare() view returns (uint256)', - 'function splitWallet() view returns (address)', - 'function stETH() view returns (address)', - 'function wstETH() view returns (address)', - ]; - - const factoryObolContract = new Contract(factoryAddresses.obol, factoryABI, provider); - const wrapperFactorySsvContract = new Contract(factoryAddresses.ssv, factoryABI, provider); - const wrapperContract = new Contract(wrapperAddress, wrapperABI, provider); + const obolFactoryContract = obolLidoSplitFactoryObolContract; + const ssvFactoryContract = obolLidoSplitFactorySSVContract; + const obolFactoryAddress = obolLidoSplitFactoryObolAddress; + const ssvFactoryAddress = obolLidoSplitFactorySSVAddress; const [feeRecipient, feeShare, splitWallet, stETH, wstETH] = await Promise.all([ wrapperContract.feeRecipient(), @@ -54,17 +48,13 @@ export const checkWrapperContract = async (wrapperAddress: string, fromBlock: nu wrapperContract.wstETH(), ]); - logger.log(''); - logger.log(header('Wrapper contract')); - logger.log(''); - - const obolWrapperDeployFilter = factoryObolContract.filters.CreateObolLidoSplit(); - const ssvWrapperDeployFilter = wrapperFactorySsvContract.filters.CreateObolLidoSplit(); + const obolWrapperDeployFilter = obolFactoryContract.filters.CreateObolLidoSplit(); + const ssvWrapperDeployFilter = ssvFactoryContract.filters.CreateObolLidoSplit(); const deployEvents = ( await Promise.all([ - factoryObolContract.queryFilter(obolWrapperDeployFilter, fromBlock, toBlock), - wrapperFactorySsvContract.queryFilter(ssvWrapperDeployFilter, fromBlock, toBlock), + obolFactoryContract.queryFilter(obolWrapperDeployFilter, fromBlock, toBlock), + ssvFactoryContract.queryFilter(ssvWrapperDeployFilter, fromBlock, toBlock), ]) ) .flat() @@ -76,8 +66,8 @@ export const checkWrapperContract = async (wrapperAddress: string, fromBlock: nu const deployEvent = deployEvents[0]; const factoryAddress = deployEvent.address; - const isObol = factoryAddress.toLocaleLowerCase() == factoryAddresses.obol.toLocaleLowerCase(); - const isSSV = factoryAddress.toLocaleLowerCase() == factoryAddresses.ssv.toLocaleLowerCase(); + const isObol = factoryAddress.toLocaleLowerCase() == obolFactoryAddress.toLocaleLowerCase(); + const isSSV = factoryAddress.toLocaleLowerCase() == ssvFactoryAddress.toLocaleLowerCase(); const factoryName = isObol ? 'Obol' : isSSV ? 'SSV' : null; const { transactionHash } = deployEvent; @@ -85,45 +75,27 @@ export const checkWrapperContract = async (wrapperAddress: string, fromBlock: nu const isStETH = stETH.toLocaleLowerCase() === expectedStETHAddress.toLocaleLowerCase(); const isWstETH = wstETH.toLocaleLowerCase() === expectedWstETHAddress.toLocaleLowerCase(); - logger.log('Factory: ', formatCheck(factoryName ?? 'Unknown', factoryName != null)); + logger.log(''); + logger.log(header('Wrapper contract')); + logger.log(''); + + logger.log('Factory: ', passOrFail(factoryName ?? 'Unknown', factoryName != null)); logger.log('Factory address:', factoryAddress); logger.log('Deploy tx: ', transactionHash); logger.log('Fee share: ', Number(feeShare)); logger.log('Fee recipient: ', feeRecipient); logger.log('Split wallet: ', splitWallet); - logger.log('stETH address: ', formatCheck(stETH, isStETH)); - logger.log('wstETH address: ', formatCheck(wstETH, isWstETH)); - - if (!isStETH) logger.error(`stETH address mismatch: ${stETH} !== ${expectedStETHAddress}`); - if (!isWstETH) logger.error(`wstETH address mismatch: ${wstETH} !== ${expectedWstETHAddress}`); + logger.log('stETH address: ', passOrFail(stETH, isStETH)); + logger.log('wstETH address: ', passOrFail(wstETH, isWstETH)); + logger.log(''); return { splitWalletAddress: splitWallet }; }; export const check0xSplit = async (splitWalletAddress: string, fromBlock: number, toBlock: number) => { - const chainId = await getChainId(); - - // todo: move to constants - const splitABI = ['function splitMain() view returns (address)']; - const splitMainABI = [ - 'function createSplit(address[] accounts,uint32[] percentAllocations,uint32 distributorFee,address controller)', - 'event CreateSplit(address indexed split)', - ]; - - const splitFactoryAddresses: Record = { - 1: '0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE', - 17000: '0x2ed6c4b5da6378c7897ac67ba9e43102feb694ee', - }; - - const factoryAddress = splitFactoryAddresses[chainId]; - if (!factoryAddress) { - throw new Error('Unsupported chain id'); - } - - const splitContract = new Contract(splitWalletAddress, splitABI, provider); + const splitContract = new Contract(splitWalletAddress, splitWalletAbi, provider); const splitMainAddress = await splitContract.splitMain(); - const splitMainContract = new Contract(splitMainAddress, splitMainABI, provider); const deployEvent = splitMainContract.filters.CreateSplit(splitWalletAddress); const [splitMainEvent] = await splitMainContract.queryFilter(deployEvent, fromBlock, toBlock); const { transactionHash } = splitMainEvent; @@ -134,29 +106,26 @@ export const check0xSplit = async (splitWalletAddress: string, fromBlock: number const parsedTx = splitMainContract.interface.parseTransaction(tx); if (!parsedTx) throw new Error('Split deploy transaction parse failed'); - logger.log(''); - logger.log(header('Split contract')); - logger.log(''); + const accounts = (parsedTx.args[0] as unknown[]).map(String); + const allocations = (parsedTx.args[1] as unknown[]).map(Number); + const distributorFee = Number(parsedTx.args[2]); + const controller = String(parsedTx.args[3]); - const accounts = parsedTx.args[0]; - const allocations = parsedTx.args[1].map(Number); - const distributorFee = parsedTx.args[2]; - const controller = parsedTx.args[3]; - - const expectedSplitMainAddress = factoryAddress; const isSplitMain = splitMainAddress.toLocaleLowerCase() === expectedSplitMainAddress.toLocaleLowerCase(); - const isZeroFee = Number(distributorFee) === 0; + const isZeroFee = distributorFee === 0; const isZeroController = controller === ZeroAddress; - if (accounts.length !== allocations.length) { - throw new Error('Accounts and allocations lengths mismatch'); - } + if (accounts.length !== allocations.length) throw new Error('Accounts and allocations lengths mismatch'); + + logger.log(''); + logger.log(header('Split contract')); + logger.log(''); logger.log('Deploy tx: ', transactionHash); logger.log('Split wallet: ', splitWalletAddress); - logger.log('Split main: ', formatCheck(splitMainAddress, isSplitMain)); - logger.log('Controller: ', formatCheck(controller, isZeroController)); - logger.log('Distributor fee:', formatCheck(String(distributorFee), isZeroFee)); + logger.log('Split main: ', passOrFail(splitMainAddress, isSplitMain)); + logger.log('Controller: ', passOrFail(controller, isZeroController)); + logger.log('Distributor fee:', passOrFail(String(distributorFee), isZeroFee)); const maxAllocation = Math.max(...allocations); const minAllocation = Math.min(...allocations); @@ -164,13 +133,58 @@ export const check0xSplit = async (splitWalletAddress: string, fromBlock: number logger.log(''); logger.log('Account Share'); - - parsedTx.args[0].forEach((account: string, index: number) => { - const allocation = parsedTx.args[1][index]; - logger.log(account, formatCheck(String(allocation), isFairAllocation)); + accounts.forEach((account, index) => { + logger.log(account, passOrFail(String(allocations[index]), isFairAllocation)); }); + logger.log(''); + + return { splitWalletAccounts: accounts }; +}; + +export const checkGnosisSafe = async (safeAddress: string, splitAccounts: string[]) => { + const expectedGnosisVersion = '1.3.0'; + const expectedMinThreshold = Math.floor(splitAccounts.length / 2) + 1; + + const safeContract = new Contract(safeAddress, gnosisSafeAbi, provider); + + const [gnosisOwners, version, threshold, referrenceCode, deployedCode] = await Promise.all([ + safeContract.getOwners(), + safeContract.VERSION(), + safeContract.getThreshold(), + gnosisSafeProxyFactoryContract.proxyRuntimeCode(), + safeContract.getDeployedCode(), + ]); + + const sortedSplitAccounts = splitAccounts.map((owner) => owner.toLocaleLowerCase()).sort(); + const sortedGnosisOwners: string[] = gnosisOwners.map((owner: string) => owner.toLocaleLowerCase()).sort(); + const isAmountMatch = gnosisOwners.length === splitAccounts.length; + const isOwnersMatch = JSON.stringify(sortedGnosisOwners) === JSON.stringify(sortedSplitAccounts); + const isCodeMatch = deployedCode === referrenceCode; + const isExpectedVersion = version === expectedGnosisVersion; + const isExpectedThreshold = threshold >= expectedMinThreshold; + + logger.log(''); + logger.log(header('GnosisSafe contract')); + logger.log(''); + + logger.log('Threshold: ', passOrFail(threshold, isExpectedThreshold)); + logger.log('MS version: ', passOrFail(version, isExpectedVersion)); + logger.log('MS bytecode: ', passOrFail(`${isCodeMatch ? '' : 'do not '}match GnosisProxy`, isCodeMatch)); + logger.log('Owners amount: ', passOrFail(`${isAmountMatch ? '' : 'do not '}match Split`, isAmountMatch)); + logger.log('Owners: ', passOrWarn(`${isOwnersMatch ? '' : 'do not '}match Split`, isOwnersMatch)); + + const rows = Math.max(gnosisOwners.length, splitAccounts.length); + const placeholder = ' '.repeat(40); + + logger.log('Split accounts Gnosis owners'); + + for (let i = 0; i < rows; i++) { + const splitAccount = splitAccounts[i] ?? placeholder; + const gnosisOwner = gnosisOwners[i] ?? placeholder; - if (!isSplitMain) { - logger.error(`splitMain address mismatch: ${splitMainAddress} !== ${expectedSplitMainAddress}`); + logger.log( + gnosisOwners.includes(splitAccount) ? match(splitAccount) : unmatch(splitAccount), + splitAccounts.includes(gnosisOwner) ? match(gnosisOwner) : unmatch(gnosisOwner), + ); } }; diff --git a/utils/block.ts b/utils/block.ts index 24c867c..71dfd6d 100644 --- a/utils/block.ts +++ b/utils/block.ts @@ -14,3 +14,11 @@ export const getBlock = async (blockTag: BlockTag) => { return block; }; + +export const getLatestBlockRange = async (limit: number): Promise<[number, number]> => { + const latestBlock = await getLatestBlock(); + const toBlock = latestBlock.number; + const fromBlock = Math.max(toBlock - Number(limit), 0); + + return [fromBlock, toBlock]; +}; diff --git a/utils/chain-id.ts b/utils/chain-id.ts deleted file mode 100644 index e76698b..0000000 --- a/utils/chain-id.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { provider } from '@providers'; - -export const getChainId = async (): Promise => { - const network = await provider.getNetwork(); - return Number(network.chainId); -}; diff --git a/utils/index.ts b/utils/index.ts index 0a30cec..96b55e6 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -3,7 +3,6 @@ export * from './authorized-call'; export * from './block'; export * from './bool'; export * from './call-tx'; -export * from './chain-id'; export * from './compare-calls'; export * from './contract'; export * from './csv'; From 7fabf32d7000cd31f5ce4d258536fc621c06650d Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Sat, 23 Mar 2024 19:59:28 +0300 Subject: [PATCH 7/9] feat: sdvt checks table --- package.json | 1 + programs/staking-module/simple-dvt.ts | 73 ++++++++++++++++++++------- yarn.lock | 16 +++++- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index c616c11..f804519 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@chainsafe/ssz": "^0.13.0", "JSONStream": "^1.3.5", "chalk": "^4", + "cli-table3": "^0.6.4", "commander": "^11.0.0", "ethers": "^6.2.2", "node-fetch": "^2", diff --git a/programs/staking-module/simple-dvt.ts b/programs/staking-module/simple-dvt.ts index d5abee3..ac7c957 100644 --- a/programs/staking-module/simple-dvt.ts +++ b/programs/staking-module/simple-dvt.ts @@ -16,11 +16,12 @@ import { gnosisSafeProxyFactoryContract, } from '@contracts'; import chalk from 'chalk'; +import Table from 'cli-table3'; const header = chalk.white.bold; -const match = chalk.green; -const unmatch = chalk.gray; +const ok = chalk.green.bold; +const warn = chalk.yellow.bold; const checkPass = (text: string) => chalk.green.bold(text) + ' ✅'; const checkFail = (text: string) => chalk.red.bold(text) + ' ❌'; @@ -32,6 +33,8 @@ const expectedStETHAddress = lidoAddress; const expectedWstETHAddress = wstethAddress; const expectedSplitMainAddress = splitMainAddress; +const intl = new Intl.NumberFormat('en-GB', { minimumFractionDigits: 2 }); + export const checkWrapperContract = async (wrapperAddress: string, fromBlock: number, toBlock: number) => { const wrapperContract = new Contract(wrapperAddress, obolLidoSplitAbi, provider); @@ -131,11 +134,33 @@ export const check0xSplit = async (splitWalletAddress: string, fromBlock: number const minAllocation = Math.min(...allocations); const isFairAllocation = maxAllocation - minAllocation <= 1; - logger.log(''); - logger.log('Account Share'); - accounts.forEach((account, index) => { - logger.log(account, passOrFail(String(allocations[index]), isFairAllocation)); + const accountsTable = new Table({ + head: ['Account', 'Share', 'Nonce', 'Balance'], + colAligns: ['left', 'right', 'right', 'right'], + style: { head: ['gray'], compact: true }, }); + + accountsTable.push( + ...(await Promise.all( + accounts.map(async (account, index) => { + const share = passOrFail(String(allocations[index]), isFairAllocation); + + const [txCount, balance] = await Promise.all([ + provider.getTransactionCount(account, 'latest'), + provider.getBalance(account), + ]); + + const balanceEth = intl.format(Number(balance / 10n ** 16n) / 100); + const txCountFormatted = txCount > 0 ? ok(txCount) : warn(txCount); + const balanceFormatted = balance > 0 ? ok(balanceEth) : warn(balanceEth); + + return [account, share, txCountFormatted, balanceFormatted]; + }), + )), + ); + + logger.log(''); + logger.log(accountsTable.toString()); logger.log(''); return { splitWalletAccounts: accounts }; @@ -173,18 +198,32 @@ export const checkGnosisSafe = async (safeAddress: string, splitAccounts: string logger.log('Owners amount: ', passOrFail(`${isAmountMatch ? '' : 'do not '}match Split`, isAmountMatch)); logger.log('Owners: ', passOrWarn(`${isOwnersMatch ? '' : 'do not '}match Split`, isOwnersMatch)); - const rows = Math.max(gnosisOwners.length, splitAccounts.length); - const placeholder = ' '.repeat(40); + const ownersTable = new Table({ + head: ['Account', 'In split', 'Nonce', 'Balance'], + colAligns: ['left', 'right', 'right', 'right'], + style: { head: ['gray'], compact: true }, + }); + + ownersTable.push( + ...(await Promise.all( + sortedGnosisOwners.map(async (account) => { + const [txCount, balance] = await Promise.all([ + provider.getTransactionCount(account, 'latest'), + provider.getBalance(account), + ]); - logger.log('Split accounts Gnosis owners'); + const balanceEth = intl.format(Number(balance / 10n ** 16n) / 100); + const txCountFormatted = txCount > 0 ? ok(txCount) : warn(txCount); + const balanceFormatted = balance > 0 ? ok(balanceEth) : warn(balanceEth); - for (let i = 0; i < rows; i++) { - const splitAccount = splitAccounts[i] ?? placeholder; - const gnosisOwner = gnosisOwners[i] ?? placeholder; + const isInSplit = sortedSplitAccounts.includes(account) ? ok('yes') : warn('no'); - logger.log( - gnosisOwners.includes(splitAccount) ? match(splitAccount) : unmatch(splitAccount), - splitAccounts.includes(gnosisOwner) ? match(gnosisOwner) : unmatch(gnosisOwner), - ); - } + return [account, isInSplit, txCountFormatted, balanceFormatted]; + }), + )), + ); + + logger.log(''); + logger.log(ownersTable.toString()); + logger.log(''); }; diff --git a/yarn.lock b/yarn.lock index 78bc19a..a1ccead 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,6 +58,11 @@ "@chainsafe/as-sha256" "^0.4.1" "@chainsafe/persistent-merkle-tree" "^0.6.1" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -656,6 +661,15 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-table3@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.4.tgz#d1c536b8a3f2e7bec58f67ac9e5769b1b30088b0" + integrity sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1867,7 +1881,7 @@ string-argv@^0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== From 105bc56b20971fa6f6c1b5bff0547c4a436c3f5b Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Sat, 23 Mar 2024 20:24:21 +0300 Subject: [PATCH 8/9] refactor: sdvt account info --- programs/staking-module/simple-dvt.ts | 46 ++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/programs/staking-module/simple-dvt.ts b/programs/staking-module/simple-dvt.ts index ac7c957..80f2d41 100644 --- a/programs/staking-module/simple-dvt.ts +++ b/programs/staking-module/simple-dvt.ts @@ -35,6 +35,26 @@ const expectedSplitMainAddress = splitMainAddress; const intl = new Intl.NumberFormat('en-GB', { minimumFractionDigits: 2 }); +const formatETH = (wei: bigint) => { + return intl.format(Number(wei / 10n ** 15n) / 1000); +}; + +const getFormattedAccountInfo = async (account: string) => { + const [txCount, balance] = await Promise.all([ + provider.getTransactionCount(account, 'latest'), + provider.getBalance(account), + ]); + + const balanceEth = formatETH(balance); + const txCountFormatted = txCount > 0 ? ok(txCount) : warn(txCount); + const balanceFormatted = balance > 0 ? ok(balanceEth) : warn(balanceEth); + + return { + nonce: txCountFormatted, + balance: balanceFormatted, + }; +}; + export const checkWrapperContract = async (wrapperAddress: string, fromBlock: number, toBlock: number) => { const wrapperContract = new Contract(wrapperAddress, obolLidoSplitAbi, provider); @@ -144,17 +164,8 @@ export const check0xSplit = async (splitWalletAddress: string, fromBlock: number ...(await Promise.all( accounts.map(async (account, index) => { const share = passOrFail(String(allocations[index]), isFairAllocation); - - const [txCount, balance] = await Promise.all([ - provider.getTransactionCount(account, 'latest'), - provider.getBalance(account), - ]); - - const balanceEth = intl.format(Number(balance / 10n ** 16n) / 100); - const txCountFormatted = txCount > 0 ? ok(txCount) : warn(txCount); - const balanceFormatted = balance > 0 ? ok(balanceEth) : warn(balanceEth); - - return [account, share, txCountFormatted, balanceFormatted]; + const { nonce, balance } = await getFormattedAccountInfo(account); + return [account, share, nonce, balance]; }), )), ); @@ -207,18 +218,9 @@ export const checkGnosisSafe = async (safeAddress: string, splitAccounts: string ownersTable.push( ...(await Promise.all( sortedGnosisOwners.map(async (account) => { - const [txCount, balance] = await Promise.all([ - provider.getTransactionCount(account, 'latest'), - provider.getBalance(account), - ]); - - const balanceEth = intl.format(Number(balance / 10n ** 16n) / 100); - const txCountFormatted = txCount > 0 ? ok(txCount) : warn(txCount); - const balanceFormatted = balance > 0 ? ok(balanceEth) : warn(balanceEth); - const isInSplit = sortedSplitAccounts.includes(account) ? ok('yes') : warn('no'); - - return [account, isInSplit, txCountFormatted, balanceFormatted]; + const { nonce, balance } = await getFormattedAccountInfo(account); + return [account, isInSplit, nonce, balance]; }), )), ); From 3cc70a3fa23e64219669606f543112eacbbc2ffe Mon Sep 17 00:00:00 2001 From: George Avsetsin Date: Sat, 23 Mar 2024 22:47:23 +0300 Subject: [PATCH 9/9] fix: number formatting --- programs/staking-module/simple-dvt.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/staking-module/simple-dvt.ts b/programs/staking-module/simple-dvt.ts index 80f2d41..76c1dc5 100644 --- a/programs/staking-module/simple-dvt.ts +++ b/programs/staking-module/simple-dvt.ts @@ -33,10 +33,10 @@ const expectedStETHAddress = lidoAddress; const expectedWstETHAddress = wstethAddress; const expectedSplitMainAddress = splitMainAddress; -const intl = new Intl.NumberFormat('en-GB', { minimumFractionDigits: 2 }); +const intl = new Intl.NumberFormat('en-GB', { minimumFractionDigits: 5, maximumFractionDigits: 5 }); const formatETH = (wei: bigint) => { - return intl.format(Number(wei / 10n ** 15n) / 1000); + return intl.format(Number(wei) / 1e18); }; const getFormattedAccountInfo = async (account: string) => {