From f445e61f544b01d771cb8eefc449cd9cde397b8d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 14 Oct 2021 15:20:37 +0300 Subject: [PATCH 1/2] Use deposit secrurity module in scenario tests --- .../test_helpers/DepositContractMock.sol | 9 + test/helpers/blockchain.js | 12 ++ test/scenario/helpers/deploy.js | 39 ++++- test/scenario/lido_deposit_iteration_limit.js | 162 ++++++++++++++++-- test/scenario/lido_happy_path.js | 105 +++++++++++- test/scenario/lido_penalties_slashing.js | 131 +++++++++++++- .../lido_rewards_distribution_math.js | 67 +++++++- 7 files changed, 483 insertions(+), 42 deletions(-) create mode 100644 test/helpers/blockchain.js diff --git a/contracts/0.4.24/test_helpers/DepositContractMock.sol b/contracts/0.4.24/test_helpers/DepositContractMock.sol index 8591d57e2..8f5fe9f94 100644 --- a/contracts/0.4.24/test_helpers/DepositContractMock.sol +++ b/contracts/0.4.24/test_helpers/DepositContractMock.sol @@ -20,6 +20,7 @@ contract DepositContractMock is IDepositContract { } Call[] public calls; + bytes32 internal depositRoot; function deposit( bytes /* 48 */ pubkey, @@ -40,4 +41,12 @@ contract DepositContractMock is IDepositContract { function reset() external { calls.length = 0; } + + function get_deposit_root() external view returns (bytes32) { + return depositRoot; + } + + function set_deposit_root(bytes32 _newRoot) external { + depositRoot = _newRoot; + } } diff --git a/test/helpers/blockchain.js b/test/helpers/blockchain.js new file mode 100644 index 000000000..785849e4c --- /dev/null +++ b/test/helpers/blockchain.js @@ -0,0 +1,12 @@ +async function waitBlocks(numBlocksToMine) { + let block + for (let i = 0; i < numBlocksToMine; ++i) { + await network.provider.send('evm_mine') + block = await web3.eth.getBlock('latest') + } + return block +} + +module.exports = { + waitBlocks +} diff --git a/test/scenario/helpers/deploy.js b/test/scenario/helpers/deploy.js index 1ba1d6b7e..0a7d02baa 100644 --- a/test/scenario/helpers/deploy.js +++ b/test/scenario/helpers/deploy.js @@ -4,12 +4,27 @@ const Lido = artifacts.require('LidoMock.sol') const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') const OracleMock = artifacts.require('OracleMock.sol') const DepositContractMock = artifacts.require('DepositContractMock.sol') +const DepositSecurityModule = artifacts.require('DepositSecurityModule.sol') module.exports = { deployDaoAndPool } -async function deployDaoAndPool(appManager, voting, depositor) { +const NETWORK_ID = 1000 +const MAX_DEPOSITS_PER_BLOCK = 100 +const MIN_DEPOSIT_BLOCK_DISTANCE = 20 +const PAUSE_INTENT_VALIDITY_PERIOD_BLOCKS = 10 +const GUARDIAN1 = '0x5Fc0E75BF6502009943590492B02A1d08EAc9C43' +const GUARDIAN2 = '0x8516Cbb5ABe73D775bfc0d21Af226e229F7181A3' +const GUARDIAN3 = '0xdaEAd0E0194abd565d28c1013399801d79627c14' +const GUARDIAN_PRIVATE_KEYS = { + [GUARDIAN1]: '0x3578665169e03e05a26bd5c565ffd12c81a1e0df7d0679f8aee4153110a83c8c', + [GUARDIAN2]: '0x88868f0fb667cfe50261bb385be8987e0ce62faee934af33c3026cf65f25f09e', + [GUARDIAN3]: '0x75e6f508b637327debc90962cd38943ddb9cfc1fc4a8572fc5e3d0984e1261de' +} +const DEPOSIT_ROOT = '0xd151867719c94ad8458feaf491809f9bc8096c702a72747403ecaac30c179137' + +async function deployDaoAndPool(appManager, voting) { // Deploy the DAO, oracle and deposit contract mocks, and base contracts for // Lido (the pool) and NodeOperatorsRegistry (the Node Operators registry) @@ -35,6 +50,18 @@ async function deployDaoAndPool(appManager, voting, depositor) { NodeOperatorsRegistry.at(nodeOperatorRegistryProxyAddress) ]) + const depositSecurityModule = await DepositSecurityModule.new( + pool.address, + depositContractMock.address, + nodeOperatorRegistry.address, + NETWORK_ID, + MAX_DEPOSITS_PER_BLOCK, + MIN_DEPOSIT_BLOCK_DISTANCE, + PAUSE_INTENT_VALIDITY_PERIOD_BLOCKS, + { from: appManager } + ) + await depositSecurityModule.addGuardians([GUARDIAN3, GUARDIAN1, GUARDIAN2], 2, { from: appManager }) + // Initialize the node operators registry and the pool await nodeOperatorRegistry.initialize(pool.address) @@ -74,7 +101,7 @@ async function deployDaoAndPool(appManager, voting, depositor) { acl.createPermission(voting, pool.address, POOL_BURN_ROLE, appManager, { from: appManager }), // Allow depositor to deposit buffered ether - acl.createPermission(depositor, pool.address, DEPOSIT_ROLE, appManager, { from: appManager }), + acl.createPermission(depositSecurityModule.address, pool.address, DEPOSIT_ROLE, appManager, { from: appManager }), // Allow voting to manage node operators registry acl.createPermission(voting, nodeOperatorRegistry.address, NODE_OPERATOR_REGISTRY_MANAGE_SIGNING_KEYS, appManager, { @@ -104,6 +131,7 @@ async function deployDaoAndPool(appManager, voting, depositor) { await oracleMock.setPool(pool.address) await depositContractMock.reset() + await depositContractMock.set_deposit_root(DEPOSIT_ROOT) const [treasuryAddr, insuranceAddr] = await Promise.all([pool.getTreasury(), pool.getInsuranceFund()]) @@ -116,6 +144,11 @@ async function deployDaoAndPool(appManager, voting, depositor) { pool, nodeOperatorRegistry, treasuryAddr, - insuranceAddr + insuranceAddr, + depositSecurityModule, + guardians: { + privateKeys: GUARDIAN_PRIVATE_KEYS, + addresses: [GUARDIAN1, GUARDIAN2, GUARDIAN3] + } } } diff --git a/test/scenario/lido_deposit_iteration_limit.js b/test/scenario/lido_deposit_iteration_limit.js index 2be3682b3..3d44d82b8 100644 --- a/test/scenario/lido_deposit_iteration_limit.js +++ b/test/scenario/lido_deposit_iteration_limit.js @@ -1,10 +1,10 @@ -const { assert } = require('chai') -const { assertBn, assertRevert } = require('@aragon/contract-helpers-test/src/asserts') -const { getEvents, getEventArgument, ZERO_ADDRESS } = require('@aragon/contract-helpers-test') +const { assertBn } = require('@aragon/contract-helpers-test/src/asserts') +const { getEventArgument, ZERO_ADDRESS } = require('@aragon/contract-helpers-test') const { pad, ETH, hexConcat } = require('../helpers/utils') const { deployDaoAndPool } = require('./helpers/deploy') -const { newDao } = require('@aragon/toolkit/dist/dao') +const { signDepositData } = require('../0.8.9/helpers/signatures') +const { waitBlocks } = require('../helpers/blockchain') const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') @@ -18,10 +18,8 @@ contract('Lido: deposit loop iteration limit', (addresses) => { nodeOperator, // users who deposit Ether to the pool user1, - user2, // an unrelated address - nobody, - depositor + nobody ] = addresses // Limits the number of validators assigned in a single transaction, regardless the amount @@ -31,9 +29,10 @@ contract('Lido: deposit loop iteration limit', (addresses) => { const depositIterationLimit = 5 let pool, nodeOperatorRegistry, depositContractMock + let depositSecurityModule, depositRoot, guardians - it('DAO, node operators registry, token, and pool are deployed and initialized', async () => { - const deployed = await deployDaoAndPool(appManager, voting, depositor) + it('DAO, node operators registry, token, pool and deposit security module are deployed and initialized', async () => { + const deployed = await deployDaoAndPool(appManager, voting) // contracts/Lido.sol pool = deployed.pool @@ -44,6 +43,10 @@ contract('Lido: deposit loop iteration limit', (addresses) => { // mocks depositContractMock = deployed.depositContractMock + depositSecurityModule = deployed.depositSecurityModule + guardians = deployed.guardians + depositRoot = await depositContractMock.get_deposit_root() + await pool.setFee(0.01 * 10000, { from: voting }) await pool.setFeeDistribution(0.3 * 10000, 0.2 * 10000, 0.5 * 10000, { from: voting }) await pool.setWithdrawalCredentials(pad('0x0202', 32), { from: voting }) @@ -90,9 +93,32 @@ contract('Lido: deposit loop iteration limit', (addresses) => { assertBn(ether2Stat.depositedValidators, 0, 'deposited validators') }) - it('one can assign the buffered ether to validators by calling depositBufferedEther() and passing deposit iteration limit', async () => { + it('guardians can assign the buffered ether to validators by calling depositBufferedEther() and passing deposit iteration limit', async () => { const depositIterationLimit = 5 - await pool.depositBufferedEther(depositIterationLimit, { from: depositor }) + let bufferedEther = await pool.getBufferedEther() + console.log('Buffered Ether:', bufferedEther.toString()) + + const block = await web3.eth.getBlock('latest') + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther(depositIterationLimit, depositRoot, keysOpIndex, block.number, block.hash, signatures) // no more than depositIterationLimit validators are assigned in a single transaction assertBn(await depositContractMock.totalCalls(), 5, 'total validators assigned') @@ -100,13 +126,57 @@ contract('Lido: deposit loop iteration limit', (addresses) => { const ether2Stat = await pool.getBeaconStat() assertBn(ether2Stat.depositedValidators, 5, 'deposited validators') + bufferedEther = await pool.getBufferedEther() + // the rest of the received Ether is still buffered in the pool assertBn(await pool.getBufferedEther(), ETH(15 * 32), 'buffered ether') }) - it('one can advance the deposit loop further by calling depositBufferedEther() once again', async () => { + it('guardians can advance the deposit loop further by calling depositBufferedEther() once again', async () => { const depositIterationLimit = 10 - await pool.depositBufferedEther(depositIterationLimit, { from: depositor }) + + let block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + let keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + let signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther(depositIterationLimit, depositRoot, keysOpIndex, block.number, block.hash, signatures) + + block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] assertBn(await depositContractMock.totalCalls(), 15, 'total validators assigned') @@ -118,7 +188,27 @@ contract('Lido: deposit loop iteration limit', (addresses) => { it('the number of assigned validators is limited by the remaining ether', async () => { const depositIterationLimit = 10 - await pool.depositBufferedEther(depositIterationLimit, { from: depositor }) + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther(depositIterationLimit, depositRoot, keysOpIndex, block.number, block.hash, signatures) assertBn(await depositContractMock.totalCalls(), 20) @@ -139,7 +229,27 @@ contract('Lido: deposit loop iteration limit', (addresses) => { it('the number of assigned validators is still limited by the number of available validator keys', async () => { const depositIterationLimit = 10 - await pool.depositBufferedEther(depositIterationLimit, { from: depositor }) + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther(depositIterationLimit, depositRoot, keysOpIndex, block.number, block.hash, signatures) assertBn(await depositContractMock.totalCalls(), 21) @@ -152,7 +262,27 @@ contract('Lido: deposit loop iteration limit', (addresses) => { it('depositBufferedEther is a nop if there are no signing keys available', async () => { const depositIterationLimit = 10 - await pool.depositBufferedEther(depositIterationLimit, { from: depositor }) + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther(depositIterationLimit, depositRoot, keysOpIndex, block.number, block.hash, signatures) assertBn(await depositContractMock.totalCalls(), 21, 'total validators assigned') diff --git a/test/scenario/lido_happy_path.js b/test/scenario/lido_happy_path.js index a5b5ab772..008bffd11 100644 --- a/test/scenario/lido_happy_path.js +++ b/test/scenario/lido_happy_path.js @@ -6,6 +6,9 @@ const { getEventArgument } = require('@aragon/contract-helpers-test') const { pad, toBN, ETH, tokens } = require('../helpers/utils') const { deployDaoAndPool } = require('./helpers/deploy') +const { signDepositData } = require('../0.8.9/helpers/signatures') +const { waitBlocks } = require('../helpers/blockchain') + const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') contract('Lido: happy path', (addresses) => { @@ -22,16 +25,16 @@ contract('Lido: happy path', (addresses) => { user2, user3, // unrelated address - nobody, - depositor + nobody ] = addresses let pool, nodeOperatorRegistry, token let oracleMock, depositContractMock - let treasuryAddr, insuranceAddr + let treasuryAddr, insuranceAddr, guardians + let depositSecurityModule, depositRoot - it('DAO, node operators registry, token, and pool are deployed and initialized', async () => { - const deployed = await deployDaoAndPool(appManager, voting, depositor) + it('DAO, node operators registry, token, pool and deposit security module are deployed and initialized', async () => { + const deployed = await deployDaoAndPool(appManager, voting) // contracts/StETH.sol token = deployed.pool @@ -49,6 +52,10 @@ contract('Lido: happy path', (addresses) => { // addresses treasuryAddr = deployed.treasuryAddr insuranceAddr = deployed.insuranceAddr + depositSecurityModule = deployed.depositSecurityModule + guardians = deployed.guardians + + depositRoot = await depositContractMock.get_deposit_root() }) // Fee and its distribution are in basis points, 10000 corresponding to 100% @@ -140,7 +147,34 @@ contract('Lido: happy path', (addresses) => { it('the first user deposits 3 ETH to the pool', async () => { await web3.eth.sendTransaction({ to: pool.address, from: user1, value: ETH(3) }) - await pool.methods['depositBufferedEther()']({ from: depositor }) + const block = await web3.eth.getBlock('latest') + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) // No Ether was deposited yet to the validator contract @@ -164,7 +198,34 @@ contract('Lido: happy path', (addresses) => { it('the second user deposits 30 ETH to the pool', async () => { await web3.eth.sendTransaction({ to: pool.address, from: user2, value: ETH(30) }) - await pool.methods['depositBufferedEther()']({ from: depositor }) + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) // The first 32 ETH chunk was deposited to the deposit contract, // using public key and signature of the only validator of the first operator @@ -248,7 +309,35 @@ contract('Lido: happy path', (addresses) => { it('the third user deposits 64 ETH to the pool', async () => { await web3.eth.sendTransaction({ to: pool.address, from: user3, value: ETH(64) }) - await pool.methods['depositBufferedEther()']({ from: depositor }) + + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) // The first 32 ETH chunk was deposited to the deposit contract, // using public key and signature of the only validator of the second operator diff --git a/test/scenario/lido_penalties_slashing.js b/test/scenario/lido_penalties_slashing.js index f789f92c8..6c4056143 100644 --- a/test/scenario/lido_penalties_slashing.js +++ b/test/scenario/lido_penalties_slashing.js @@ -3,8 +3,10 @@ const { BN } = require('bn.js') const { assertBn, assertRevert } = require('@aragon/contract-helpers-test/src/asserts') const { getEventArgument } = require('@aragon/contract-helpers-test') -const { pad, toBN, ETH, tokens, hexConcat } = require('../helpers/utils') +const { pad, ETH, tokens } = require('../helpers/utils') const { deployDaoAndPool } = require('./helpers/deploy') +const { signDepositData } = require('../0.8.9/helpers/signatures') +const { waitBlocks } = require('../helpers/blockchain') const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') @@ -19,8 +21,6 @@ contract('Lido: penalties, slashing, operator stops', (addresses) => { operator_2, // users who deposit Ether to the pool user1, - user2, - user3, // unrelated address nobody, depositor @@ -28,9 +28,10 @@ contract('Lido: penalties, slashing, operator stops', (addresses) => { let pool, nodeOperatorRegistry, token let oracleMock, depositContractMock - let treasuryAddr, insuranceAddr + let treasuryAddr, insuranceAddr, guardians + let depositSecurityModule, depositRoot - it('DAO, node operators registry, token, and pool are deployed and initialized', async () => { + it('DAO, node operators registry, token, pool and deposit security module are deployed and initialized', async () => { const deployed = await deployDaoAndPool(appManager, voting, depositor) // contracts/StETH.sol @@ -49,6 +50,10 @@ contract('Lido: penalties, slashing, operator stops', (addresses) => { // addresses treasuryAddr = deployed.treasuryAddr insuranceAddr = deployed.insuranceAddr + depositSecurityModule = deployed.depositSecurityModule + guardians = deployed.guardians + + depositRoot = await depositContractMock.get_deposit_root() }) // Fee and its distribution are in basis points, 10000 corresponding to 100% @@ -150,7 +155,34 @@ contract('Lido: penalties, slashing, operator stops', (addresses) => { awaitingTotalShares = new BN(depositAmount) awaitingUser1Balance = new BN(depositAmount) await web3.eth.sendTransaction({ to: pool.address, from: user1, value: depositAmount }) - await pool.methods['depositBufferedEther()']({ from: depositor }) + const block = await web3.eth.getBlock('latest') + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) // No Ether was deposited yet to the validator contract @@ -185,7 +217,34 @@ contract('Lido: penalties, slashing, operator stops', (addresses) => { }) it(`pushes pooled eth to the available validator`, async () => { - await pool.methods['depositBufferedEther()']({ from: depositor }) + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) }) it('new validator gets the 32 ETH deposit from the pool', async () => { @@ -340,7 +399,34 @@ contract('Lido: penalties, slashing, operator stops', (addresses) => { awaitingUser1Balance = awaitingUser1Balance.add(new BN(depositAmount)) const tokenSupplyBefore = await token.totalSupply() await web3.eth.sendTransaction({ to: pool.address, from: user1, value: depositAmount }) - await pool.methods['depositBufferedEther()']({ from: depositor }) + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) assertBn(await depositContractMock.totalCalls(), 2) @@ -477,7 +563,34 @@ contract('Lido: penalties, slashing, operator stops', (addresses) => { awaitingUser1Balance = awaitingUser1Balance.add(new BN(depositAmount)) await web3.eth.sendTransaction({ to: pool.address, from: user1, value: depositAmount }) - await pool.methods['depositBufferedEther()']({ from: depositor }) + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) const ether2Stat = await pool.getBeaconStat() assertBn(ether2Stat.depositedValidators, 2, 'no validators have received the current deposit') diff --git a/test/scenario/lido_rewards_distribution_math.js b/test/scenario/lido_rewards_distribution_math.js index b6d0081a9..3ab9100c3 100644 --- a/test/scenario/lido_rewards_distribution_math.js +++ b/test/scenario/lido_rewards_distribution_math.js @@ -5,6 +5,8 @@ const { getEventArgument, ZERO_ADDRESS } = require('@aragon/contract-helpers-tes const { pad, ETH } = require('../helpers/utils') const { deployDaoAndPool } = require('./helpers/deploy') +const { signDepositData } = require('../0.8.9/helpers/signatures') +const { waitBlocks } = require('../helpers/blockchain') const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') @@ -36,13 +38,14 @@ contract('Lido: rewards distribution math', (addresses) => { user2, user3, // unrelated address - nobody, - depositor + nobody ] = addresses let pool, nodeOperatorRegistry, token let oracleMock - let treasuryAddr, insuranceAddr + let treasuryAddr, insuranceAddr, guardians + let depositSecurityModule, depositRoot + const withdrawalCredentials = pad('0x0202', 32) // Each node operator has its Ethereum 1 address, a name and a set of registered @@ -80,7 +83,7 @@ contract('Lido: rewards distribution math', (addresses) => { } before(async () => { - const deployed = await deployDaoAndPool(appManager, voting, depositor) + const deployed = await deployDaoAndPool(appManager, voting) // contracts/StETH.sol token = deployed.pool @@ -97,6 +100,10 @@ contract('Lido: rewards distribution math', (addresses) => { // addresses treasuryAddr = deployed.treasuryAddr insuranceAddr = deployed.insuranceAddr + depositSecurityModule = deployed.depositSecurityModule + guardians = deployed.guardians + + depositRoot = await deployed.depositContractMock.get_deposit_root() await pool.setFee(totalFeePoints, { from: voting }) await pool.setFeeDistribution(treasuryFeePoints, insuranceFeePoints, nodeOperatorsFeePoints, { from: voting }) @@ -178,7 +185,34 @@ contract('Lido: rewards distribution math', (addresses) => { }) it(`the first deposit gets deployed`, async () => { - await pool.methods['depositBufferedEther()']({ from: depositor }) + const block = await web3.eth.getBlock('latest') + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) assertBn(await nodeOperatorRegistry.getUnusedSigningKeyCount(0), 0, 'no more available keys for the first validator') assertBn(await token.balanceOf(user1), ETH(34), 'user1 balance is equal first reported value + their buffered deposit value') @@ -315,8 +349,29 @@ contract('Lido: rewards distribution math', (addresses) => { }) it(`the second deposit gets deployed`, async () => { + const block = await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + const maxDeposits = await depositSecurityModule.getMaxDeposits() const [_, deltas] = await getSharesTokenDeltas( - () => pool.methods['depositBufferedEther()']({ from: depositor }), + () => depositSecurityModule.depositBufferedEther(maxDeposits, depositRoot, keysOpIndex, block.number, block.hash, signatures), treasuryAddr, insuranceAddr, nodeOperator1.address, From 3866ad08c2149336adbd95bc772ab5600cc35e81 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 14 Oct 2021 16:33:55 +0300 Subject: [PATCH 2/2] Extend happy path with remove signing keys scenario --- test/scenario/lido_happy_path.js | 81 +++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/test/scenario/lido_happy_path.js b/test/scenario/lido_happy_path.js index 008bffd11..71935aa09 100644 --- a/test/scenario/lido_happy_path.js +++ b/test/scenario/lido_happy_path.js @@ -3,7 +3,7 @@ const { BN } = require('bn.js') const { assertBn } = require('@aragon/contract-helpers-test/src/asserts') const { getEventArgument } = require('@aragon/contract-helpers-test') -const { pad, toBN, ETH, tokens } = require('../helpers/utils') +const { pad, toBN, ETH, tokens, hexConcat } = require('../helpers/utils') const { deployDaoAndPool } = require('./helpers/deploy') const { signDepositData } = require('../0.8.9/helpers/signatures') @@ -20,6 +20,7 @@ contract('Lido: happy path', (addresses) => { // node operators operator_1, operator_2, + operator_3, // users who deposit Ether to the pool user1, user2, @@ -449,4 +450,82 @@ contract('Lido: happy path', (addresses) => { .lt(mintedAmount.divn(100)) ) }) + + // node operator with 10 validators + const nodeOperator3 = { + id: 2, + name: 'operator_3', + address: operator_3, + validators: [...Array(10).keys()].map((i) => ({ + key: pad('0xaa01' + i.toString(16), 48), + sig: pad('0x' + i.toString(16), 96) + })) + } + + it('nodeOperator3 registered in NodeOperatorsRegistry and adds 10 signing keys', async () => { + const validatorsCount = 10 + await nodeOperatorRegistry.addNodeOperator(nodeOperator3.name, nodeOperator3.address, { from: voting }) + await nodeOperatorRegistry.setNodeOperatorStakingLimit(nodeOperator3.id, validatorsCount, { from: voting }) + await nodeOperatorRegistry.addSigningKeysOperatorBH( + nodeOperator3.id, + validatorsCount, + hexConcat(...nodeOperator3.validators.map((v) => v.key)), + hexConcat(...nodeOperator3.validators.map((v) => v.sig)), + { + from: nodeOperator3.address + } + ) + }) + + it('nodeOperator3 removes signing key with id 5', async () => { + const signingKeyIndexToRemove = 5 + await nodeOperatorRegistry.removeSigningKeyOperatorBH(nodeOperator3.id, signingKeyIndexToRemove, { from: nodeOperator3.address }) + const nodeOperatorInfo = await nodeOperatorRegistry.getNodeOperator(nodeOperator3.id, false) + assertBn(nodeOperatorInfo.stakingLimit, 5) + }) + + it('deposit to nodeOperator3 validators', async () => { + const amountToDeposit = ETH(32 * 10) + await web3.eth.sendTransaction({ to: pool.address, from: user1, value: amountToDeposit }) + await waitBlocks(await depositSecurityModule.getMinDepositBlockDistance()) + const block = await web3.eth.getBlock('latest') + const keysOpIndex = await nodeOperatorRegistry.getKeysOpIndex() + const signatures = [ + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[0]] + ), + signDepositData( + await depositSecurityModule.ATTEST_MESSAGE_PREFIX(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + guardians.privateKeys[guardians.addresses[1]] + ) + ] + await depositSecurityModule.depositBufferedEther( + await depositSecurityModule.getMaxDeposits(), + depositRoot, + keysOpIndex, + block.number, + block.hash, + signatures + ) + let nodeOperatorInfo = await nodeOperatorRegistry.getNodeOperator(nodeOperator3.id, false) + + // validate that only 5 signing keys used after key removing + assertBn(nodeOperatorInfo.stakingLimit, nodeOperatorInfo.usedSigningKeys) + assertBn(nodeOperatorInfo.totalSigningKeys, 9) + + // validate that all other validators used and pool still has buffered ether + nodeOperatorInfo = await nodeOperatorRegistry.getNodeOperator(nodeOperator1.id, false) + assertBn(nodeOperatorInfo.totalSigningKeys, nodeOperatorInfo.usedSigningKeys) + nodeOperatorInfo = await nodeOperatorRegistry.getNodeOperator(nodeOperator2.id, false) + assertBn(nodeOperatorInfo.totalSigningKeys, nodeOperatorInfo.usedSigningKeys) + }) })