diff --git a/.circleci/config.yml b/.circleci/config.yml index bdee3890..2738f086 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -100,20 +100,24 @@ jobs: sed -i "s|`pwd`/||g" /tmp/workspace/log/test-results/mocha/test-results.xml fi -# FIXME: https://github.com/sc-forks/solidity-coverage/issues/574 -# coverage: -# executor: default -# steps: -# - checkout -# - attach_workspace: -# at: /tmp/workspace -# - run: -# name: Run coverage tests -# command: yarn coverage -# - run: -# name: Upload coverage -# command: | -# bash <(curl -s https://codecov.io/bash) + coverage: + executor: default + resource_class: large + parallelism: 16 + steps: + - checkout + - attach_workspace: + at: /tmp/workspace + - run: + name: Run coverage tests + command: | + circleci tests glob 'test/**/*.test.js' | + circleci tests split | + xargs -I {} yarn coverage --testfiles "{}" + - run: + name: Upload coverage + command: | + bash <(curl -s https://codecov.io/bash) workflows: version: 2 @@ -132,6 +136,6 @@ workflows: - report-gas: requires: - test -# - coverage: -# requires: -# - compile + - coverage: + requires: + - compile diff --git a/.solcover.js b/.solcover.js index 0b15a0b4..cc5d2429 100644 --- a/.solcover.js +++ b/.solcover.js @@ -2,11 +2,11 @@ module.exports = { silent: true, providerOptions: { total_accounts: 5000, - default_balance_ether: 10000000000000, // extra zero just in case (coverage consumes more gas) + default_balance_ether: 1000000000000000000000000, // extra zero just in case (coverage consumes more gas) gasLimit: 0x1fffffffffffff, }, mocha: { - timeout: 180000, + timeout: 1000000, }, skipFiles: [ 'mocks', diff --git a/abi/Oracles.json b/abi/Oracles.json index e8f10197..6ebf6adc 100644 --- a/abi/Oracles.json +++ b/abi/Oracles.json @@ -1,17 +1,4 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "rewardsNonce", - "type": "uint256" - } - ], - "name": "Initialized", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -97,23 +84,11 @@ "name": "sender", "type": "address" }, - { - "indexed": true, - "internalType": "address", - "name": "oracle", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "operator", - "type": "address" - }, { "indexed": false, - "internalType": "bytes", - "name": "publicKey", - "type": "bytes" + "internalType": "address[]", + "name": "oracles", + "type": "address[]" }, { "indexed": false, @@ -122,7 +97,7 @@ "type": "uint256" } ], - "name": "RegisterValidatorVoteSubmitted", + "name": "RegisterValidatorsVoteSubmitted", "type": "event" }, { @@ -458,44 +433,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "admin", - "type": "address" - }, - { - "internalType": "address", - "name": "oraclesV1", - "type": "address" - }, - { - "internalType": "address", - "name": "_rewardEthToken", - "type": "address" - }, - { - "internalType": "address", - "name": "_pool", - "type": "address" - }, - { - "internalType": "address", - "name": "_poolValidators", - "type": "address" - }, - { - "internalType": "address", - "name": "_merkleDistributor", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -616,14 +553,14 @@ "type": "bytes" } ], - "internalType": "struct IPoolValidators.DepositData", + "internalType": "struct IPoolValidators.DepositData[]", "name": "depositData", - "type": "tuple" + "type": "tuple[]" }, { - "internalType": "bytes32[]", - "name": "merkleProof", - "type": "bytes32[]" + "internalType": "bytes32[][]", + "name": "merkleProofs", + "type": "bytes32[][]" }, { "internalType": "bytes32", @@ -636,7 +573,7 @@ "type": "bytes[]" } ], - "name": "registerValidator", + "name": "registerValidators", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/audits/2022-05-06-Quantstamp.pdf b/audits/2022-05-06-Quantstamp.pdf new file mode 100644 index 00000000..5e9b1220 Binary files /dev/null and b/audits/2022-05-06-Quantstamp.pdf differ diff --git a/contracts/Oracles.sol b/contracts/Oracles.sol index dd5542b1..4afaac20 100644 --- a/contracts/Oracles.sol +++ b/contracts/Oracles.sol @@ -13,7 +13,6 @@ import "./interfaces/IPool.sol"; import "./interfaces/IOracles.sol"; import "./interfaces/IMerkleDistributor.sol"; import "./interfaces/IPoolValidators.sol"; -import "./interfaces/IOraclesV1.sol"; /** * @title Oracles @@ -53,43 +52,6 @@ contract Oracles is IOracles, OwnablePausableUpgradeable { _; } - /** - * @dev See {IOracles-initialize}. - */ - function initialize( - address admin, - address oraclesV1, - address _rewardEthToken, - address _pool, - address _poolValidators, - address _merkleDistributor - ) - external override initializer - { - require(admin != address(0), "Pool: invalid admin address"); - require(_rewardEthToken != address(0), "Pool: invalid RewardEthToken address"); - require(_pool != address(0), "Pool: invalid Pool address"); - require(_poolValidators != address(0), "Pool: invalid PoolValidators address"); - require(_merkleDistributor != address(0), "Pool: invalid MerkleDistributor address"); - - __OwnablePausableUpgradeable_init(admin); - - // migrate data from previous Oracles contract - rewardsNonce._value = IOraclesV1(oraclesV1).currentNonce().add(1000); - uint256 oraclesCount = AccessControlUpgradeable(oraclesV1).getRoleMemberCount(ORACLE_ROLE); - for (uint256 i = 0; i < oraclesCount; i++) { - address oracle = AccessControlUpgradeable(oraclesV1).getRoleMember(ORACLE_ROLE, i); - _setupRole(ORACLE_ROLE, oracle); - emit OracleAdded(oracle); - } - - rewardEthToken = IRewardEthToken(_rewardEthToken); - pool = IPool(_pool); - poolValidators = IPoolValidators(_poolValidators); - merkleDistributor = IMerkleDistributor(_merkleDistributor); - emit Initialized(rewardsNonce.current()); - } - /** * @dev See {IOracles-currentRewardsNonce}. */ @@ -115,6 +77,7 @@ contract Oracles is IOracles, OwnablePausableUpgradeable { * @dev See {IOracles-addOracle}. */ function addOracle(address account) external override { + require(account != address(0), "Oracles: invalid oracle address"); grantRole(ORACLE_ROLE, account); emit OracleAdded(account); } @@ -140,7 +103,8 @@ contract Oracles is IOracles, OwnablePausableUpgradeable { * @param signaturesCount - number of signatures. */ function isEnoughSignatures(uint256 signaturesCount) internal view returns (bool) { - return signaturesCount.mul(3) > getRoleMemberCount(ORACLE_ROLE).mul(2); + uint256 totalOracles = getRoleMemberCount(ORACLE_ROLE); + return totalOracles >= signaturesCount && signaturesCount.mul(3) > totalOracles.mul(2); } /** @@ -228,11 +192,11 @@ contract Oracles is IOracles, OwnablePausableUpgradeable { } /** - * @dev See {IOracles-registerValidator}. - */ - function registerValidator( - IPoolValidators.DepositData calldata depositData, - bytes32[] calldata merkleProof, + * @dev See {IOracles-registerValidators}. + */ + function registerValidators( + IPoolValidators.DepositData[] calldata depositData, + bytes32[][] calldata merkleProofs, bytes32 validatorsDepositRoot, bytes[] calldata signatures ) @@ -247,10 +211,10 @@ contract Oracles is IOracles, OwnablePausableUpgradeable { // calculate candidate ID hash uint256 nonce = validatorsNonce.current(); bytes32 candidateId = ECDSAUpgradeable.toEthSignedMessageHash( - keccak256(abi.encode(nonce, depositData.publicKey, depositData.operator, validatorsDepositRoot)) + keccak256(abi.encode(nonce, depositData, validatorsDepositRoot)) ); - // check signatures and calculate number of submitted oracle votes + // check signatures are correct address[] memory signedOracles = new address[](signatures.length); for (uint256 i = 0; i < signatures.length; i++) { bytes memory signature = signatures[i]; @@ -261,19 +225,20 @@ contract Oracles is IOracles, OwnablePausableUpgradeable { require(signedOracles[j] != signer, "Oracles: repeated signature"); } signedOracles[i] = signer; - emit RegisterValidatorVoteSubmitted( - msg.sender, - signer, - depositData.operator, - depositData.publicKey, - nonce - ); } - // increment nonce for future signatures - validatorsNonce.increment(); + uint256 depositDataLength = depositData.length; + require(merkleProofs.length == depositDataLength, "Oracles: invalid merkle proofs length"); - // register validator - poolValidators.registerValidator(depositData, merkleProof); + // submit deposit data + for (uint256 i = 0; i < depositDataLength; i++) { + // register validator + poolValidators.registerValidator(depositData[i], merkleProofs[i]); + } + + emit RegisterValidatorsVoteSubmitted(msg.sender, signedOracles, nonce); + + // increment nonce for future registrations + validatorsNonce.increment(); } } diff --git a/contracts/interfaces/IOracles.sol b/contracts/interfaces/IOracles.sol index 39fdc181..eed704f4 100644 --- a/contracts/interfaces/IOracles.sol +++ b/contracts/interfaces/IOracles.sol @@ -9,12 +9,6 @@ pragma abicoder v2; * @dev Interface of the Oracles contract. */ interface IOracles { - /** - * @dev Event for tracking the Oracles contract initialization. - * @param rewardsNonce - rewards nonce the contract was initialized with. - */ - event Initialized(uint256 rewardsNonce); - /** * @dev Event for tracking oracle rewards votes. * @param sender - address of the transaction sender. @@ -48,18 +42,14 @@ interface IOracles { ); /** - * @dev Event for tracking validator registration votes. + * @dev Event for tracking validators registration vote. * @param sender - address of the transaction sender. - * @param oracle - address of the signed oracle. - * @param operator - address of the operator the vote was sent for. - * @param publicKey - public key of the validator the vote was sent for. - * @param nonce - validator registration nonce. + * @param oracles - addresses of the signed oracles. + * @param nonce - validators registration nonce. */ - event RegisterValidatorVoteSubmitted( + event RegisterValidatorsVoteSubmitted( address indexed sender, - address indexed oracle, - address indexed operator, - bytes publicKey, + address[] oracles, uint256 nonce ); @@ -75,24 +65,6 @@ interface IOracles { */ event OracleRemoved(address indexed oracle); - /** - * @dev Constructor for initializing the Oracles contract. - * @param admin - address of the contract admin. - * @param oraclesV1 - address of the Oracles V1 contract. - * @param _rewardEthToken - address of the RewardEthToken contract. - * @param _pool - address of the Pool contract. - * @param _poolValidators - address of the PoolValidators contract. - * @param _merkleDistributor - address of the MerkleDistributor contract. - */ - function initialize( - address admin, - address oraclesV1, - address _rewardEthToken, - address _pool, - address _poolValidators, - address _merkleDistributor - ) external; - /** * @dev Function for checking whether an account has an oracle role. * @param account - account to check. @@ -155,16 +127,16 @@ interface IOracles { ) external; /** - * @dev Function for submitting registration of the new validator. + * @dev Function for submitting registrations of the new validators. * The quorum of signatures over the same data is required to register. - * @param depositData - the deposit data for the registration. - * @param merkleProof - an array of hashes to verify whether the deposit data is part of the deposit data merkle root. + * @param depositData - an array of deposit data to register. + * @param merkleProofs - an array of hashes to verify whether the every deposit data is part of the merkle root. * @param validatorsDepositRoot - validators deposit root to protect from malicious operators. * @param signatures - oracles' signatures. */ - function registerValidator( - IPoolValidators.DepositData calldata depositData, - bytes32[] calldata merkleProof, + function registerValidators( + IPoolValidators.DepositData[] calldata depositData, + bytes32[][] calldata merkleProofs, bytes32 validatorsDepositRoot, bytes[] calldata signatures ) external; diff --git a/contracts/interfaces/IOraclesV1.sol b/contracts/interfaces/IOraclesV1.sol deleted file mode 100644 index 00de7469..00000000 --- a/contracts/interfaces/IOraclesV1.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only - -pragma solidity 0.7.5; - -pragma abicoder v2; - -/** - * @dev Interface of the Oracles V1 contract. - */ -interface IOraclesV1 { - /** - * @dev Function for retrieving current rewards nonce. - */ - function currentNonce() external view returns (uint256); -} diff --git a/deployments/index.js b/deployments/index.js index 83339ee5..2259668a 100644 --- a/deployments/index.js +++ b/deployments/index.js @@ -1,10 +1,32 @@ -const { contracts } = require('./settings'); +const { white, green } = require('chalk'); +const { ethers, upgrades, config } = require('hardhat'); +const { contracts, contractSettings } = require('./settings'); + +function log(message) { + if (config != null && config.suppressLogs !== true) { + console.log(message); + } +} + +async function upgradeOracles() { + const signer = await ethers.provider.getSigner(contractSettings.admin); + const Oracles = await ethers.getContractFactory('Oracles', signer); + + // upgrade Oracles to new implementation + const proxy = await upgrades.upgradeProxy(contracts.oracles, Oracles); + return proxy.deployed(); +} async function deployContracts() { - return contracts; + const Oracles = await ethers.getContractFactory('Oracles'); + let impl = await upgrades.prepareUpgrade(contracts.oracles, Oracles); + log(white(`Deployed Oracles implementation contract: ${green(impl)}`)); } async function upgradeContracts() { + await deployContracts(); + await upgradeOracles(); + log(white('Upgraded Oracles contract')); return contracts; } diff --git a/hardhat.config.js b/hardhat.config.js index 6d00a299..450096fe 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -10,7 +10,7 @@ require('hardhat-contract-sizer'); require('hardhat-abi-exporter'); require('@nomiclabs/hardhat-etherscan'); -const BLOCK_NUMBER = 13952000; +const BLOCK_NUMBER = 14754000; const OPTIMIZER_RUNS = 5000000; const log = (...text) => console.log(gray(...['└─> [DEBUG]'].concat(text))); @@ -105,6 +105,9 @@ module.exports = { url: process.env.HARDHAT_FORK_API_URL, blockNumber: BLOCK_NUMBER, }, + accounts: { + accountsBalance: '1000000000000000000000000', + }, }, local: { url: 'http://localhost:8545', @@ -145,6 +148,6 @@ module.exports = { apiKey: 'api key goes here', }, mocha: { - timeout: 20000, + timeout: 1000000, }, }; diff --git a/package.json b/package.json index 2b6f6f55..4d415ec8 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "lint-staged": "12.1.2", "prettier": "^2.5.1", "solhint": "^3.3.6", - "solidity-coverage": "^0.7.17", + "solidity-coverage": "^0.7.18", "web3": "^1.6.1" }, "dependencies": {} diff --git a/test/oracles/Oracles.test.js b/test/oracles/Oracles.test.js index d7ce0452..a951777a 100644 --- a/test/oracles/Oracles.test.js +++ b/test/oracles/Oracles.test.js @@ -14,7 +14,7 @@ const { setupOracleAccounts, setTotalRewards, } = require('../utils'); -const { contractSettings, contracts } = require('../../deployments/settings'); +const { contractSettings } = require('../../deployments/settings'); const { upgradeContracts } = require('../../deployments'); const { depositDataMerkleRoot, @@ -31,7 +31,12 @@ const iDepositContract = artifacts.require('IDepositContract'); contract('Oracles', ([_, anyone, operator, ...accounts]) => { let admin = contractSettings.admin; - let oracles, rewardEthToken, pool, merkleDistributor, poolValidators; + let oracles, + rewardEthToken, + pool, + merkleDistributor, + poolValidators, + contracts; let [oracle, anotherOracle] = accounts; after(async () => stopImpersonatingAccount(admin)); @@ -40,13 +45,12 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { await impersonateAccount(admin); await send.ether(anyone, admin, ether('5')); - let upgradedContracts = await upgradeContracts(); - - oracles = await Oracles.at(upgradedContracts.oracles); + contracts = await upgradeContracts(); + oracles = await Oracles.at(contracts.oracles); pool = await Pool.at(contracts.pool); rewardEthToken = await RewardEthToken.at(contracts.rewardEthToken); merkleDistributor = await MerkleDistributor.at(contracts.merkleDistributor); - poolValidators = await PoolValidators.at(upgradedContracts.poolValidators); + poolValidators = await PoolValidators.at(contracts.poolValidators); }); afterEach(async () => resetFork()); @@ -214,12 +218,12 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { }); it('fails to submit with repeated signature', async () => { - signatures.push(signatures[0]); + let signature = signatures[0]; await expectRevert( oracles.submitRewards( newTotalRewards, newActivatedValidators, - signatures, + Array(oracleAccounts.length).fill(signature), { from: oracleAccounts[0], } @@ -364,11 +368,16 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { }); it('fails to submit with repeated signature', async () => { - signatures.push(signatures[0]); + let signature = signatures[0]; await expectRevert( - oracles.submitMerkleRoot(merkleRoot, merkleProofs, signatures, { - from: oracleAccounts[0], - }), + oracles.submitMerkleRoot( + merkleRoot, + merkleProofs, + Array(oracleAccounts.length).fill(signature), + { + from: oracleAccounts[0], + } + ), 'Oracles: repeated signature' ); }); @@ -475,25 +484,28 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { describe('validator voting', () => { const depositDataMerkleProofs = 'ipfs://QmehR8yCaKdHqHSxZMSJA5q2SWc8jTVCSKuVgbtqDEdXCH'; - let { - publicKey, - signature, - depositDataRoot, - withdrawalCredentials, - merkleProof, - } = depositData[0]; let currentNonce, oracleAccounts, candidateId, signatures, validatorsDepositRoot; - let validatorData = { - operator, - withdrawalCredentials, - depositDataRoot, - publicKey, - signature, - }; + let validatorsDepositData = [ + { + operator, + withdrawalCredentials: depositData[0].withdrawalCredentials, + depositDataRoot: depositData[0].depositDataRoot, + publicKey: depositData[0].publicKey, + signature: depositData[0].signature, + }, + { + operator, + withdrawalCredentials: depositData[1].withdrawalCredentials, + depositDataRoot: depositData[1].depositDataRoot, + publicKey: depositData[1].publicKey, + signature: depositData[1].signature, + }, + ]; + let merkleProofs = [depositData[0].merkleProof, depositData[1].merkleProof]; beforeEach(async () => { await poolValidators.addOperator( @@ -516,8 +528,12 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { validatorsDepositRoot = await depositContract.get_deposit_root(); let encoded = defaultAbiCoder.encode( - ['uint256', 'bytes', 'address', 'bytes32'], - [currentNonce.toString(), publicKey, operator, validatorsDepositRoot] + [ + 'uint256', + 'tuple(address operator,bytes32 withdrawalCredentials,bytes32 depositDataRoot,bytes publicKey,bytes signature)[]', + 'bytes32', + ], + [currentNonce.toString(), validatorsDepositData, validatorsDepositRoot] ); candidateId = keccak256(encoded); @@ -532,9 +548,9 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { expect(await oracles.paused()).equal(true); await expectRevert( - oracles.registerValidator( - validatorData, - merkleProof, + oracles.registerValidators( + validatorsDepositData, + merkleProofs, validatorsDepositRoot, signatures, { @@ -547,9 +563,9 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { it('fails to submit with not enough signatures', async () => { await expectRevert( - oracles.registerValidator( - validatorData, - merkleProof, + oracles.registerValidators( + validatorsDepositData, + merkleProofs, validatorsDepositRoot, signatures.slice(signatures.length - 1), { @@ -563,9 +579,9 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { it('fails to submit with invalid signature', async () => { signatures[0] = await web3.eth.sign(candidateId, anyone); await expectRevert( - oracles.registerValidator( - validatorData, - merkleProof, + oracles.registerValidators( + validatorsDepositData, + merkleProofs, validatorsDepositRoot, signatures, { @@ -577,13 +593,13 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { }); it('fails to submit with repeated signature', async () => { - signatures.push(signatures[0]); + let signature = signatures[0]; await expectRevert( - oracles.registerValidator( - validatorData, - merkleProof, + oracles.registerValidators( + validatorsDepositData, + merkleProofs, validatorsDepositRoot, - signatures, + Array(oracleAccounts.length).fill(signature), { from: oracleAccounts[0], } @@ -594,9 +610,9 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { it('fails to submit without oracle role assigned', async () => { await expectRevert( - oracles.registerValidator( - validatorData, - merkleProof, + oracles.registerValidators( + validatorsDepositData, + merkleProofs, validatorsDepositRoot, signatures, { @@ -606,5 +622,26 @@ contract('Oracles', ([_, anyone, operator, ...accounts]) => { 'Oracles: access denied' ); }); + + it('can vote for multiple validators', async () => { + await pool.stake({ + from: anyone, + value: ether('64'), + }); + let receipt = await oracles.registerValidators( + validatorsDepositData, + merkleProofs, + validatorsDepositRoot, + signatures, + { + from: oracleAccounts[0], + } + ); + await expectEvent(receipt, 'RegisterValidatorsVoteSubmitted', { + sender: oracleAccounts[0], + oracles: oracleAccounts, + nonce: currentNonce, + }); + }); }); }); diff --git a/test/pool/PoolValidators.test.js b/test/pool/PoolValidators.test.js index f4dc767a..c2dbf397 100644 --- a/test/pool/PoolValidators.test.js +++ b/test/pool/PoolValidators.test.js @@ -5,12 +5,13 @@ const { balance, send, constants, + BN, } = require('@openzeppelin/test-helpers'); const { keccak256, defaultAbiCoder } = require('ethers/lib/utils'); const { upgradeContracts } = require('../../deployments'); -const { contractSettings, contracts } = require('../../deployments/settings'); +const { contractSettings } = require('../../deployments/settings'); const { - registerValidator, + registerValidators, setupOracleAccounts, stopImpersonatingAccount, impersonateAccount, @@ -25,7 +26,7 @@ const { const Pool = artifacts.require('Pool'); const PoolValidators = artifacts.require('PoolValidators'); const Oracles = artifacts.require('Oracles'); -const iDepositContract = artifacts.require('IDepositContract'); +const IDepositContract = artifacts.require('IDepositContract'); contract('Pool Validators', (accounts) => { const admin = contractSettings.admin; @@ -48,8 +49,8 @@ contract('Pool Validators', (accounts) => { await send.ether(anyone, admin, ether('5')); let upgradedContracts = await upgradeContracts(); - pool = await Pool.at(contracts.pool); - depositContract = await iDepositContract.at( + pool = await Pool.at(upgradedContracts.pool); + depositContract = await IDepositContract.at( await pool.validatorRegistration() ); validatorDepositAmount = await pool.VALIDATOR_TOTAL_DEPOSIT(); @@ -272,14 +273,15 @@ contract('Pool Validators', (accounts) => { }); }); - describe('register validator', () => { - let { - publicKey, - signature, - withdrawalCredentials, - merkleProof, - depositDataRoot, - } = depositData[0]; + describe('register validators', () => { + let validatorDepositData = { + operator, + withdrawalCredentials: depositData[0].withdrawalCredentials, + depositDataRoot: depositData[0].depositDataRoot, + publicKey: depositData[0].publicKey, + signature: depositData[0].signature, + }; + let merkleProof = depositData[0].merkleProof; beforeEach(async () => { await validators.addOperator( @@ -294,34 +296,20 @@ contract('Pool Validators', (accounts) => { it('fails to register validator by not oracles', async () => { await expectRevert( - validators.registerValidator( - { - operator, - withdrawalCredentials, - depositDataRoot, - publicKey, - signature, - }, - merkleProof, - { - from: anyone, - } - ), + validators.registerValidator(validatorDepositData, merkleProof, { + from: anyone, + }), 'PoolValidators: access denied' ); }); it('fails to register validator for not committed operator', async () => { await expectRevert( - registerValidator({ - operator, - merkleProof, - signature, - publicKey, - depositDataRoot, + registerValidators({ + depositData: [validatorDepositData], + merkleProofs: [merkleProof], oracles, oracleAccounts, - withdrawalCredentials, validatorsDepositRoot, }), 'PoolValidators: invalid operator' @@ -332,29 +320,14 @@ contract('Pool Validators', (accounts) => { await validators.commitOperator({ from: operator, }); - await registerValidator({ - operator, - merkleProof, - signature, - publicKey, - withdrawalCredentials, - depositDataRoot, - oracles, - oracleAccounts, - validatorsDepositRoot, - }); await expectRevert( - registerValidator({ - operator, - merkleProof, - signature, - publicKey, - withdrawalCredentials, - depositDataRoot, + registerValidators({ + depositData: [validatorDepositData, validatorDepositData], + merkleProofs: [merkleProof, merkleProof], oracles, oracleAccounts, - validatorsDepositRoot: await depositContract.get_deposit_root(), + validatorsDepositRoot, }), 'PoolValidators: validator already registered' ); @@ -364,14 +337,11 @@ contract('Pool Validators', (accounts) => { await validators.commitOperator({ from: operator, }); + await expectRevert( - registerValidator({ - operator: anyone, - merkleProof, - signature, - publicKey, - withdrawalCredentials, - depositDataRoot, + registerValidators({ + depositData: [{ ...validatorDepositData, operator: anyone }], + merkleProofs: [merkleProof], oracles, oracleAccounts, validatorsDepositRoot, @@ -385,15 +355,16 @@ contract('Pool Validators', (accounts) => { from: operator, }); await expectRevert( - registerValidator({ - operator, - merkleProof, - signature, - publicKey, - depositDataRoot: constants.ZERO_BYTES32, + registerValidators({ + depositData: [ + { + ...validatorDepositData, + depositDataRoot: constants.ZERO_BYTES32, + }, + ], + merkleProofs: [merkleProof], oracles, oracleAccounts, - withdrawalCredentials, validatorsDepositRoot, }), 'PoolValidators: invalid merkle proof' @@ -405,46 +376,40 @@ contract('Pool Validators', (accounts) => { from: operator, }); await expectRevert( - registerValidator({ - operator, - merkleProof, - signature, - publicKey, - depositDataRoot, + registerValidators({ + depositData: [validatorDepositData], + merkleProofs: [merkleProof], oracles, oracleAccounts, - withdrawalCredentials, validatorsDepositRoot: keccak256('0x6be4000000000000'), }), 'Oracles: invalid validators deposit root' ); }); - it('oracles can register validator', async () => { + it('oracles can register one validator', async () => { await validators.commitOperator({ from: operator, }); let poolBalance = await balance.current(pool.address); - let receipt = await registerValidator({ - operator, - merkleProof, - signature, - publicKey, - depositDataRoot, + let receipt = await registerValidators({ + depositData: [validatorDepositData], + merkleProofs: [merkleProof], oracles, oracleAccounts, - withdrawalCredentials, validatorsDepositRoot, }); await expectEvent.inTransaction(receipt.tx, Pool, 'ValidatorRegistered', { operator, - publicKey, + publicKey: validatorDepositData.publicKey, }); expect( await validators.isValidatorRegistered( - keccak256(defaultAbiCoder.encode(['bytes'], [publicKey])) + keccak256( + defaultAbiCoder.encode(['bytes'], [validatorDepositData.publicKey]) + ) ) ).to.equal(true); let _operator = await validators.getOperator(operator); @@ -454,11 +419,77 @@ contract('Pool Validators', (accounts) => { ); await checkValidatorRegistered({ transaction: receipt.tx, - pubKey: publicKey, - withdrawalCredentials, - signature, + pubKey: validatorDepositData.publicKey, + withdrawalCredentials: validatorDepositData.withdrawalCredentials, + signature: validatorDepositData.signature, validatorDepositAmount, }); }); + + it('oracles can register multiple validators', async () => { + await validators.commitOperator({ + from: operator, + }); + await pool.stake({ + from: anyone, + value: ether('32').mul(new BN(depositData.length)), + }); + + let poolBalance = await balance.current(pool.address); + let validatorsDepositData = []; + let merkleProofs = []; + for (let i = 0; i < depositData.length; i++) { + validatorsDepositData.push({ + operator, + withdrawalCredentials: depositData[i].withdrawalCredentials, + depositDataRoot: depositData[i].depositDataRoot, + publicKey: depositData[i].publicKey, + signature: depositData[i].signature, + }); + merkleProofs.push(depositData[i].merkleProof); + } + let receipt = await registerValidators({ + depositData: validatorsDepositData, + merkleProofs, + oracles, + oracleAccounts, + validatorsDepositRoot, + }); + + for (let i = 0; i < depositData.length; i++) { + await expectEvent.inTransaction( + receipt.tx, + Pool, + 'ValidatorRegistered', + { + operator, + publicKey: validatorsDepositData[i].publicKey, + } + ); + expect( + await validators.isValidatorRegistered( + keccak256( + defaultAbiCoder.encode( + ['bytes'], + [validatorsDepositData[i].publicKey] + ) + ) + ) + ).to.equal(true); + await checkValidatorRegistered({ + transaction: receipt.tx, + pubKey: validatorsDepositData[i].publicKey, + withdrawalCredentials: validatorsDepositData[i].withdrawalCredentials, + signature: validatorsDepositData[i].signature, + validatorDepositAmount, + }); + } + + let _operator = await validators.getOperator(operator); + expect(_operator[1]).to.equal(true); + expect(await balance.current(pool.address)).to.bignumber.equal( + poolBalance.sub(validatorDepositAmount.mul(new BN(depositData.length))) + ); + }); }); }); diff --git a/test/pool/settings.test.js b/test/pool/settings.test.js index 179e40ef..5f3424e8 100644 --- a/test/pool/settings.test.js +++ b/test/pool/settings.test.js @@ -11,11 +11,14 @@ const { resetFork, setActivatedValidators, setupOracleAccounts, - registerValidator, + registerValidators, } = require('../utils'); const { upgradeContracts } = require('../../deployments'); -const { contractSettings, contracts } = require('../../deployments/settings'); -const { depositDataMerkleRoot } = require('./depositDataMerkleRoot'); +const { contractSettings } = require('../../deployments/settings'); +const { + depositData, + depositDataMerkleRoot, +} = require('./depositDataMerkleRoot'); const Pool = artifacts.require('Pool'); const Oracles = artifacts.require('Oracles'); @@ -25,7 +28,7 @@ const iDepositContract = artifacts.require('IDepositContract'); contract('Pool (settings)', ([operator, anyone, ...otherAccounts]) => { const admin = contractSettings.admin; - let pool, oracles, oracleAccounts, rewardEthToken, validatorsDepositRoot; + let pool, oracles, oracleAccounts, validatorsDepositRoot, contracts; after(async () => stopImpersonatingAccount(admin)); @@ -33,8 +36,8 @@ contract('Pool (settings)', ([operator, anyone, ...otherAccounts]) => { await impersonateAccount(admin); await send.ether(anyone, admin, ether('5')); - let upgradedContracts = await upgradeContracts(); - let validators = await PoolValidators.at(upgradedContracts.poolValidators); + contracts = await upgradeContracts(); + let validators = await PoolValidators.at(contracts.poolValidators); await validators.addOperator( operator, depositDataMerkleRoot, @@ -51,7 +54,7 @@ contract('Pool (settings)', ([operator, anyone, ...otherAccounts]) => { await pool.validatorRegistration() ); validatorsDepositRoot = await depositContract.get_deposit_root(); - oracles = await Oracles.at(upgradedContracts.oracles); + oracles = await Oracles.at(contracts.oracles); rewardEthToken = await RewardEthToken.at(contracts.rewardEthToken); oracleAccounts = await setupOracleAccounts({ admin, @@ -156,8 +159,17 @@ contract('Pool (settings)', ([operator, anyone, ...otherAccounts]) => { from: anyone, value: ether('32'), }); - await registerValidator({ - operator, + await registerValidators({ + depositData: [ + { + operator, + withdrawalCredentials: depositData[0].withdrawalCredentials, + depositDataRoot: depositData[0].depositDataRoot, + publicKey: depositData[0].publicKey, + signature: depositData[0].signature, + }, + ], + merkleProofs: [depositData[0].merkleProof], oracles, oracleAccounts, validatorsDepositRoot, diff --git a/test/pool/stake.test.js b/test/pool/stake.test.js index 7e878472..d7ce9e08 100644 --- a/test/pool/stake.test.js +++ b/test/pool/stake.test.js @@ -13,7 +13,7 @@ const { impersonateAccount, resetFork, getDepositAmount, - registerValidator, + registerValidators, setupOracleAccounts, } = require('../utils'); const { upgradeContracts } = require('../../deployments'); @@ -117,8 +117,9 @@ contract('Pool (stake)', (accounts) => { }); it('mints tokens for users with deposit less than min activating', async () => { + let maxAmount = ether('0.01'); + await pool.setMinActivatingDeposit(maxAmount, { from: admin }); // User 1 creates a deposit - let maxAmount = await pool.minActivatingDeposit(); let depositAmount1 = getDepositAmount({ max: maxAmount, }); @@ -409,25 +410,31 @@ contract('Pool (stake)', (accounts) => { from: sender1, value: depositAmount, }); - poolBalance = poolBalance.add(depositAmount); + poolBalance = await balance.current(pool.address); validatorIndex = activatedValidators .add(pendingValidators) .add(poolBalance.div(ether('32'))); - for (let i = 0; i < validatorIndex.sub(activatedValidators); i++) { + for ( + let i = 0; + i < validatorIndex.sub(activatedValidators).sub(pendingValidators); + i++ + ) { validatorsDepositRoot = await depositContract.get_deposit_root(); - await registerValidator({ - admin, - validators, + await registerValidators({ + depositData: [ + { + operator, + withdrawalCredentials: depositData[i].withdrawalCredentials, + depositDataRoot: depositData[i].depositDataRoot, + publicKey: depositData[i].publicKey, + signature: depositData[i].signature, + }, + ], + merkleProofs: [depositData[i].merkleProof], oracles, oracleAccounts, - operator, validatorsDepositRoot, - merkleProof: depositData[i].merkleProof, - signature: depositData[i].signature, - publicKey: depositData[i].publicKey, - withdrawalCredentials: depositData[i].withdrawalCredentials, - depositDataRoot: depositData[i].depositDataRoot, }); } }); @@ -532,20 +539,26 @@ contract('Pool (stake)', (accounts) => { .add(pendingValidators) .add(poolBalance.div(ether('32'))); - for (let i = 0; i < validatorIndex2.sub(activatedValidators); i++) { + for ( + let i = 0; + i < validatorIndex2.sub(activatedValidators).sub(pendingValidators); + i++ + ) { validatorsDepositRoot = await depositContract.get_deposit_root(); - await registerValidator({ - admin, - validators, + await registerValidators({ + depositData: [ + { + operator, + withdrawalCredentials: depositData[i].withdrawalCredentials, + depositDataRoot: depositData[i].depositDataRoot, + publicKey: depositData[i].publicKey, + signature: depositData[i].signature, + }, + ], + merkleProofs: [depositData[i].merkleProof], oracles, oracleAccounts, - operator, validatorsDepositRoot, - merkleProof: depositData[i].merkleProof, - signature: depositData[i].signature, - publicKey: depositData[i].publicKey, - withdrawalCredentials: depositData[i].withdrawalCredentials, - depositDataRoot: depositData[i].depositDataRoot, }); } }); diff --git a/test/tokens/RewardEthToken.test.js b/test/tokens/RewardEthToken.test.js index fdd30075..f5c76894 100644 --- a/test/tokens/RewardEthToken.test.js +++ b/test/tokens/RewardEthToken.test.js @@ -195,7 +195,7 @@ contract('RewardEthToken', ([sender, merkleDistributor, ...accounts]) => { { periodRewards: newTotalRewards.sub(prevTotalRewards), totalRewards: newTotalRewards, - protocolReward: new BN(0), + protocolReward: ether('1'), } ); }); diff --git a/test/utils.js b/test/utils.js index 2d1784bc..246f0266 100644 --- a/test/utils.js +++ b/test/utils.js @@ -2,7 +2,6 @@ const { expect } = require('chai'); const hre = require('hardhat'); const { hexlify, keccak256, defaultAbiCoder } = require('ethers/lib/utils'); const { BN, ether, expectEvent } = require('@openzeppelin/test-helpers'); -const { depositData } = require('./pool/depositDataMerkleRoot'); const iDepositContract = artifacts.require('IDepositContract'); @@ -196,21 +195,21 @@ async function setMerkleRoot({ }); } -async function registerValidator({ - operator, - merkleProof = depositData[0].merkleProof, - signature = depositData[0].signature, - publicKey = depositData[0].publicKey, - withdrawalCredentials = depositData[0].withdrawalCredentials, - depositDataRoot = depositData[0].depositDataRoot, +async function registerValidators({ + depositData, + merkleProofs, oracles, oracleAccounts, validatorsDepositRoot, }) { let nonce = await oracles.currentValidatorsNonce(); let encoded = defaultAbiCoder.encode( - ['uint256', 'bytes', 'address', 'bytes32'], - [nonce.toString(), publicKey, operator, validatorsDepositRoot] + [ + 'uint256', + 'tuple(address operator,bytes32 withdrawalCredentials,bytes32 depositDataRoot,bytes publicKey,bytes signature)[]', + 'bytes32', + ], + [nonce.toString(), depositData, validatorsDepositRoot] ); let candidateId = hexlify(keccak256(encoded)); @@ -223,9 +222,9 @@ async function registerValidator({ } // register validator - return oracles.registerValidator( - { operator, withdrawalCredentials, depositDataRoot, publicKey, signature }, - merkleProof, + return oracles.registerValidators( + depositData, + merkleProofs, validatorsDepositRoot, signatures, { @@ -299,5 +298,5 @@ module.exports = { setTotalRewards, setMerkleRoot, setupOracleAccounts, - registerValidator, + registerValidators, }; diff --git a/yarn.lock b/yarn.lock index b3a98979..11b2d646 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4058,7 +4058,7 @@ functional-red-black-tree@^1.0.1, functional-red-black-tree@~1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -ganache-cli@^6.1.0, ganache-cli@^6.12.2: +ganache-cli@^6.1.0: version "6.12.2" resolved "https://registry.yarnpkg.com/ganache-cli/-/ganache-cli-6.12.2.tgz#c0920f7db0d4ac062ffe2375cb004089806f627a" integrity sha512-bnmwnJDBDsOWBUP8E/BExWf85TsdDEFelQSzihSJm9VChVO1SHp94YXLP5BlA4j/OTxp0wR4R1Tje9OHOuAJVw== @@ -7769,18 +7769,17 @@ solidity-ast@^0.4.15: resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.28.tgz#5589998512b9a3602e6ba612cbe7fed7401294f4" integrity sha512-RtZCP5tSvZMadVtg9/IfLmAMKDOnQEvG2HA6VnPuoTMxqxsbbn4lQy8jgH3RVbqW0eO1hd7cSCKecb72/OeOIw== -solidity-coverage@^0.7.17: - version "0.7.17" - resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.17.tgz#5139de8f6666d4755d88f453d8e35632a7bb3444" - integrity sha512-Erw2hd2xdACAvDX8jUdYkmgJlIIazGznwDJA5dhRaw4def2SisXN9jUjneeyOZnl/E7j6D3XJYug4Zg9iwodsg== +solidity-coverage@^0.7.18: + version "0.7.21" + resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.7.21.tgz#20c5615a3a543086b243c2ca36e2951a75316b40" + integrity sha512-O8nuzJ9yXiKUx3NdzVvHrUW0DxoNVcGzq/I7NzewNO9EZE3wYAQ4l8BwcnV64r4aC/HB6Vnw/q2sF0BQHv/3fg== dependencies: - "@solidity-parser/parser" "^0.13.2" + "@solidity-parser/parser" "^0.14.0" "@truffle/provider" "^0.2.24" chalk "^2.4.2" death "^1.1.0" detect-port "^1.3.0" fs-extra "^8.1.0" - ganache-cli "^6.12.2" ghost-testrpc "^0.0.2" global-modules "^2.0.0" globby "^10.0.1"