diff --git a/SCRATH_DEPLOY.md b/SCRATCH_DEPLOY.md similarity index 100% rename from SCRATH_DEPLOY.md rename to SCRATCH_DEPLOY.md diff --git a/apps/lido/app/src/App.js b/apps/lido/app/src/App.js index 04203f3ac..069b0a361 100644 --- a/apps/lido/app/src/App.js +++ b/apps/lido/app/src/App.js @@ -85,6 +85,7 @@ export default function App() { nodeOperatorsRegistry, depositContract, oracle, + executionLayerRewardsVault, // operators, // treasury, // insuranceFund, @@ -249,6 +250,10 @@ export default function App() { label: 'Oracle', content: , }, + { + label: 'Execution layer rewards Vault', + content: , + }, ] }, [ appState, diff --git a/apps/lido/app/src/index.js b/apps/lido/app/src/index.js index 036e81d37..fc4b50742 100644 --- a/apps/lido/app/src/index.js +++ b/apps/lido/app/src/index.js @@ -22,6 +22,7 @@ const defaultState = { nodeOperatorsRegistry: defaultValue, depositContract: defaultValue, oracle: defaultValue, + executionLayerRewardsVault: defaultValue, operators: defaultValue, treasury: defaultValue, insuranceFund: defaultValue, diff --git a/apps/lido/app/src/script.js b/apps/lido/app/src/script.js index 437e61a7c..321be96e5 100644 --- a/apps/lido/app/src/script.js +++ b/apps/lido/app/src/script.js @@ -62,6 +62,7 @@ function initializeState() { nodeOperatorsRegistry: await getNodeOperatorsRegistry(), depositContract: await getDepositContract(), oracle: await getOracle(), + executionLayerRewardsVault: await getExecutionLayerRewardsVault(), // operators: await getOperators(), // treasury: await getTreasury(), // insuranceFund: await getInsuranceFund(), @@ -107,6 +108,10 @@ function getOracle() { return app.call('getOracle').toPromise() } +function getExecutionLayerRewardsVault() { + return app.call('getExecutionLayerRewardsVault').toPromise() +} + // async function getOperators() { // return await app.call('getOperators').toPromise() // } diff --git a/arapp.json b/arapp.json index 8dc75a4ce..cc55079a1 100644 --- a/arapp.json +++ b/arapp.json @@ -19,6 +19,11 @@ "registry": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", "appName": "dao.lido.eth", "network": "mainnet" + }, + "kiln": { + "registry": "0xD3A23B83902066baC61e82bCe449fE1d3154Ab5D", + "appName": "dao.lido.eth", + "network": "kiln" } }, "appName": "dao.lido.eth" diff --git a/contracts/0.4.24/Lido.sol b/contracts/0.4.24/Lido.sol index 60b8d7cd1..90e5d55a4 100644 --- a/contracts/0.4.24/Lido.sol +++ b/contracts/0.4.24/Lido.sol @@ -8,15 +8,26 @@ pragma solidity 0.4.24; import "@aragon/os/contracts/apps/AragonApp.sol"; import "@aragon/os/contracts/lib/math/SafeMath.sol"; import "@aragon/os/contracts/lib/math/SafeMath64.sol"; -import "@aragon/os/contracts/common/IsContract.sol"; import "solidity-bytes-utils/contracts/BytesLib.sol"; import "./interfaces/ILido.sol"; import "./interfaces/INodeOperatorsRegistry.sol"; import "./interfaces/IDepositContract.sol"; +import "./interfaces/ILidoExecutionLayerRewardsVault.sol"; import "./StETH.sol"; +import "./lib/StakeLimitUtils.sol"; + + +interface IERC721 { + /// @notice Transfer ownership of an NFT + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; +} + /** * @title Liquid staking pool implementation @@ -25,28 +36,38 @@ import "./StETH.sol"; * until transfers become available in Ethereum 2.0. * Whitepaper: https://lido.fi/static/Lido:Ethereum-Liquid-Staking.pdf * -* NOTE: the code below assumes moderate amount of node operators, e.g. up to 50. +* NOTE: the code below assumes moderate amount of node operators, e.g. up to 200. * * Since balances of all token holders change when the amount of total pooled Ether * changes, this token cannot fully implement ERC20 standard: it only emits `Transfer` * events upon explicit transfer between holders. In contrast, when Lido oracle reports * rewards, no Transfer events are generated: doing so would require emitting an event * for each token holder and thus running an unbounded loop. +* +* At the moment withdrawals are not possible in the beacon chain and there's no workaround. +* Pool will be upgraded to an actual implementation when withdrawals are enabled +* (Phase 1.5 or 2 of Eth2 launch, likely late 2022 or 2023). */ -contract Lido is ILido, IsContract, StETH, AragonApp { +contract Lido is ILido, StETH, AragonApp { using SafeMath for uint256; - using SafeMath64 for uint64; using UnstructuredStorage for bytes32; + using StakeLimitUnstructuredStorage for bytes32; + using StakeLimitUtils for StakeLimitState.Data; /// ACL bytes32 constant public PAUSE_ROLE = keccak256("PAUSE_ROLE"); + bytes32 constant public RESUME_ROLE = keccak256("RESUME_ROLE"); + bytes32 constant public STAKING_PAUSE_ROLE = keccak256("STAKING_PAUSE_ROLE"); + bytes32 constant public STAKING_CONTROL_ROLE = keccak256("STAKING_CONTROL_ROLE"); bytes32 constant public MANAGE_FEE = keccak256("MANAGE_FEE"); bytes32 constant public MANAGE_WITHDRAWAL_KEY = keccak256("MANAGE_WITHDRAWAL_KEY"); - bytes32 constant public SET_ORACLE = keccak256("SET_ORACLE"); + bytes32 constant public MANAGE_PROTOCOL_CONTRACTS_ROLE = keccak256("MANAGE_PROTOCOL_CONTRACTS_ROLE"); bytes32 constant public BURN_ROLE = keccak256("BURN_ROLE"); - bytes32 constant public SET_TREASURY = keccak256("SET_TREASURY"); - bytes32 constant public SET_INSURANCE_FUND = keccak256("SET_INSURANCE_FUND"); bytes32 constant public DEPOSIT_ROLE = keccak256("DEPOSIT_ROLE"); + bytes32 constant public SET_EL_REWARDS_VAULT_ROLE = keccak256("SET_EL_REWARDS_VAULT_ROLE"); + bytes32 constant public SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE = keccak256( + "SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE" + ); uint256 constant public PUBKEY_LENGTH = 48; uint256 constant public WITHDRAWAL_CREDENTIALS_LENGTH = 32; @@ -55,6 +76,7 @@ contract Lido is ILido, IsContract, StETH, AragonApp { uint256 constant public DEPOSIT_SIZE = 32 ether; uint256 internal constant DEPOSIT_AMOUNT_UNIT = 1000000000 wei; + uint256 internal constant TOTAL_BASIS_POINTS = 10000; /// @dev default value for maximum number of Ethereum 2.0 validators registered in a single depositBufferedEther call uint256 internal constant DEFAULT_MAX_DEPOSITS_PER_CALL = 150; @@ -69,7 +91,10 @@ contract Lido is ILido, IsContract, StETH, AragonApp { bytes32 internal constant NODE_OPERATORS_REGISTRY_POSITION = keccak256("lido.Lido.nodeOperatorsRegistry"); bytes32 internal constant TREASURY_POSITION = keccak256("lido.Lido.treasury"); bytes32 internal constant INSURANCE_FUND_POSITION = keccak256("lido.Lido.insuranceFund"); + bytes32 internal constant EL_REWARDS_VAULT_POSITION = keccak256("lido.Lido.executionLayerRewardsVault"); + /// @dev storage slot position of the staking rate limit structure + bytes32 internal constant STAKING_STATE_POSITION = keccak256("lido.Lido.stakeLimit"); /// @dev amount of Ether (on the current Ethereum side) buffered on this smart contract balance bytes32 internal constant BUFFERED_ETHER_POSITION = keccak256("lido.Lido.bufferedEther"); /// @dev number of deposited validators (incrementing counter of deposit operations). @@ -79,17 +104,27 @@ contract Lido is ILido, IsContract, StETH, AragonApp { /// @dev number of Lido's validators available in the Beacon state bytes32 internal constant BEACON_VALIDATORS_POSITION = keccak256("lido.Lido.beaconValidators"); + /// @dev percent in basis points of total pooled ether allowed to withdraw from LidoExecutionLayerRewardsVault per LidoOracle report + bytes32 internal constant EL_REWARDS_WITHDRAWAL_LIMIT_POSITION = keccak256("lido.Lido.ELRewardsWithdrawalLimit"); + + /// @dev Just a counter of total amount of execution layer rewards received by Lido contract + /// Not used in the logic + bytes32 internal constant TOTAL_EL_REWARDS_COLLECTED_POSITION = keccak256("lido.Lido.totalELRewardsCollected"); + /// @dev Credentials which allows the DAO to withdraw Ether on the 2.0 side bytes32 internal constant WITHDRAWAL_CREDENTIALS_POSITION = keccak256("lido.Lido.withdrawalCredentials"); /** * @dev As AragonApp, Lido contract must be initialized with following variables: - * @param depositContract official ETH2 Deposit contract + * @param _depositContract official ETH2 Deposit contract * @param _oracle oracle contract * @param _operators instance of Node Operators Registry + * @param _treasury treasury contract + * @param _insuranceFund insurance fund contract + * NB: by default, staking and the whole Lido pool are in paused state */ function initialize( - IDepositContract depositContract, + IDepositContract _depositContract, address _oracle, INodeOperatorsRegistry _operators, address _treasury, @@ -97,15 +132,143 @@ contract Lido is ILido, IsContract, StETH, AragonApp { ) public onlyInit { - _setDepositContract(depositContract); - _setOracle(_oracle); - _setOperators(_operators); - _setTreasury(_treasury); - _setInsuranceFund(_insuranceFund); + NODE_OPERATORS_REGISTRY_POSITION.setStorageAddress(address(_operators)); + DEPOSIT_CONTRACT_POSITION.setStorageAddress(address(_depositContract)); + + _setProtocolContracts(_oracle, _treasury, _insuranceFund); initialized(); } + /** + * @notice Stops accepting new Ether to the protocol + * + * @dev While accepting new Ether is stopped, calls to the `submit` function, + * as well as to the default payable function, will revert. + * + * Emits `StakingPaused` event. + */ + function pauseStaking() external { + _auth(STAKING_PAUSE_ROLE); + + _pauseStaking(); + } + + /** + * @notice Resumes accepting new Ether to the protocol (if `pauseStaking` was called previously) + * NB: Staking could be rate-limited by imposing a limit on the stake amount + * at each moment in time, see `setStakingLimit()` and `removeStakingLimit()` + * + * @dev Preserves staking limit if it was set previously + * + * Emits `StakingResumed` event + */ + function resumeStaking() external { + _auth(STAKING_CONTROL_ROLE); + + _resumeStaking(); + } + + /** + * @notice Sets the staking rate limit + * + * ▲ Stake limit + * │..... ..... ........ ... .... ... Stake limit = max + * │ . . . . . . . . . + * │ . . . . . . . . . + * │ . . . . . + * │──────────────────────────────────────────────────> Time + * │ ^ ^ ^ ^^^ ^ ^ ^ ^^^ ^ Stake events + * + * @dev Reverts if: + * - `_maxStakeLimit` == 0 + * - `_maxStakeLimit` >= 2^96 + * - `_maxStakeLimit` < `_stakeLimitIncreasePerBlock` + * - `_maxStakeLimit` / `_stakeLimitIncreasePerBlock` >= 2^32 (only if `_stakeLimitIncreasePerBlock` != 0) + * + * Emits `StakingLimitSet` event + * + * @param _maxStakeLimit max stake limit value + * @param _stakeLimitIncreasePerBlock stake limit increase per single block + */ + function setStakingLimit(uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock) external { + _auth(STAKING_CONTROL_ROLE); + + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + STAKING_STATE_POSITION.getStorageStakeLimitStruct().setStakingLimit( + _maxStakeLimit, + _stakeLimitIncreasePerBlock + ) + ); + + emit StakingLimitSet(_maxStakeLimit, _stakeLimitIncreasePerBlock); + } + + /** + * @notice Removes the staking rate limit + * + * Emits `StakingLimitRemoved` event + */ + function removeStakingLimit() external { + _auth(STAKING_CONTROL_ROLE); + + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + STAKING_STATE_POSITION.getStorageStakeLimitStruct().removeStakingLimit() + ); + + emit StakingLimitRemoved(); + } + + /** + * @notice Check staking state: whether it's paused or not + */ + function isStakingPaused() external view returns (bool) { + return STAKING_STATE_POSITION.getStorageStakeLimitStruct().isStakingPaused(); + } + + /** + * @notice Returns how much Ether can be staked in the current block + * @dev Special return values: + * - 2^256 - 1 if staking is unlimited; + * - 0 if staking is paused or if limit is exhausted. + */ + function getCurrentStakeLimit() public view returns (uint256) { + return _getCurrentStakeLimit(STAKING_STATE_POSITION.getStorageStakeLimitStruct()); + } + + /** + * @notice Returns full info about current stake limit params and state + * @dev Might be used for the advanced integration requests. + * @return isStakingPaused staking pause state (equivalent to return of isStakingPaused()) + * @return isStakingLimitSet whether the stake limit is set + * @return currentStakeLimit current stake limit (equivalent to return of getCurrentStakeLimit()) + * @return maxStakeLimit max stake limit + * @return maxStakeLimitGrowthBlocks blocks needed to restore max stake limit from the fully exhausted state + * @return prevStakeLimit previously reached stake limit + * @return prevStakeBlockNumber previously seen block number + */ + function getStakeLimitFullInfo() external view returns ( + bool isStakingPaused, + bool isStakingLimitSet, + uint256 currentStakeLimit, + uint256 maxStakeLimit, + uint256 maxStakeLimitGrowthBlocks, + uint256 prevStakeLimit, + uint256 prevStakeBlockNumber + ) { + StakeLimitState.Data memory stakeLimitData = STAKING_STATE_POSITION.getStorageStakeLimitStruct(); + + isStakingPaused = stakeLimitData.isStakingPaused(); + isStakingLimitSet = stakeLimitData.isStakingLimitSet(); + + currentStakeLimit = _getCurrentStakeLimit(stakeLimitData); + + maxStakeLimit = stakeLimitData.maxStakeLimit; + maxStakeLimitGrowthBlocks = stakeLimitData.maxStakeLimitGrowthBlocks; + prevStakeLimit = stakeLimitData.prevStakeLimit; + prevStakeBlockNumber = stakeLimitData.prevStakeBlockNumber; + } + /** * @notice Send funds to the pool * @dev Users are able to submit their funds by transacting to the fallback function. @@ -128,19 +291,37 @@ contract Lido is ILido, IsContract, StETH, AragonApp { return _submit(_referral); } + /** + * @notice A payable function for execution layer rewards. Can be called only by ExecutionLayerRewardsVault contract + * @dev We need a dedicated function because funds received by the default payable function + * are treated as a user deposit + */ + function receiveELRewards() external payable { + require(msg.sender == EL_REWARDS_VAULT_POSITION.getStorageAddress()); + + TOTAL_EL_REWARDS_COLLECTED_POSITION.setStorageUint256( + TOTAL_EL_REWARDS_COLLECTED_POSITION.getStorageUint256().add(msg.value)); + + emit ELRewardsReceived(msg.value); + } + /** * @notice Deposits buffered ethers to the official DepositContract. * @dev This function is separated from submit() to reduce the cost of sending funds. */ - function depositBufferedEther() external auth(DEPOSIT_ROLE) { + function depositBufferedEther() external { + _auth(DEPOSIT_ROLE); + return _depositBufferedEther(DEFAULT_MAX_DEPOSITS_PER_CALL); } /** - * @notice Deposits buffered ethers to the official DepositContract, making no more than `_maxDeposits` deposit calls. - * @dev This function is separated from submit() to reduce the cost of sending funds. - */ - function depositBufferedEther(uint256 _maxDeposits) external auth(DEPOSIT_ROLE) { + * @notice Deposits buffered ethers to the official DepositContract, making no more than `_maxDeposits` deposit calls. + * @dev This function is separated from submit() to reduce the cost of sending funds. + */ + function depositBufferedEther(uint256 _maxDeposits) external { + _auth(DEPOSIT_ROLE); + return _depositBufferedEther(_maxDeposits); } @@ -153,40 +334,58 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @notice Stop pool routine operations - */ - function stop() external auth(PAUSE_ROLE) { + * @notice Stop pool routine operations + */ + function stop() external { + _auth(PAUSE_ROLE); + _stop(); + _pauseStaking(); } /** - * @notice Resume pool routine operations - */ - function resume() external auth(PAUSE_ROLE) { + * @notice Resume pool routine operations + * @dev Staking should be resumed manually after this call using the desired limits + */ + function resume() external { + _auth(RESUME_ROLE); + _resume(); + _resumeStaking(); } /** - * @notice Set fee rate to `_feeBasisPoints` basis points. The fees are accrued when oracles report staking results - * @param _feeBasisPoints Fee rate, in basis points - */ - function setFee(uint16 _feeBasisPoints) external auth(MANAGE_FEE) { + * @notice Set fee rate to `_feeBasisPoints` basis points. + * The fees are accrued when: + * - oracles report staking results (beacon chain balance increase) + * - validators gain execution layer rewards (priority fees and MEV) + * @param _feeBasisPoints Fee rate, in basis points + */ + function setFee(uint16 _feeBasisPoints) external { + _auth(MANAGE_FEE); + _setBPValue(FEE_POSITION, _feeBasisPoints); emit FeeSet(_feeBasisPoints); } /** - * @notice Set fee distribution: `_treasuryFeeBasisPoints` basis points go to the treasury, `_insuranceFeeBasisPoints` basis points go to the insurance fund, `_operatorsFeeBasisPoints` basis points go to node operators. The sum has to be 10 000. - */ + * @notice Set fee distribution + * @param _treasuryFeeBasisPoints basis points go to the treasury, + * @param _insuranceFeeBasisPoints basis points go to the insurance fund, + * @param _operatorsFeeBasisPoints basis points go to node operators. + * @dev The sum has to be 10 000. + */ function setFeeDistribution( uint16 _treasuryFeeBasisPoints, uint16 _insuranceFeeBasisPoints, uint16 _operatorsFeeBasisPoints ) - external auth(MANAGE_FEE) + external { + _auth(MANAGE_FEE); + require( - 10000 == uint256(_treasuryFeeBasisPoints) + TOTAL_BASIS_POINTS == uint256(_treasuryFeeBasisPoints) .add(uint256(_insuranceFeeBasisPoints)) .add(uint256(_operatorsFeeBasisPoints)), "FEES_DONT_ADD_UP" @@ -200,40 +399,36 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @notice Set authorized oracle contract address to `_oracle` - * @dev Contract specified here is allowed to make periodical updates of beacon states - * by calling pushBeacon. - * @param _oracle oracle contract - */ - function setOracle(address _oracle) external auth(SET_ORACLE) { - _setOracle(_oracle); - } + * @notice Set Lido protocol contracts (oracle, treasury, insurance fund). + * + * @dev Oracle contract specified here is allowed to make + * periodical updates of beacon stats + * by calling pushBeacon. Treasury contract specified here is used + * to accumulate the protocol treasury fee. Insurance fund contract + * specified here is used to accumulate the protocol insurance fee. + * + * @param _oracle oracle contract + * @param _treasury treasury contract + * @param _insuranceFund insurance fund contract + */ + function setProtocolContracts( + address _oracle, + address _treasury, + address _insuranceFund + ) external { + _auth(MANAGE_PROTOCOL_CONTRACTS_ROLE); - /** - * @notice Set treasury contract address to `_treasury` - * @dev Contract specified here is used to accumulate the protocol treasury fee. - * @param _treasury contract which accumulates treasury fee. - */ - function setTreasury(address _treasury) external auth(SET_TREASURY) { - _setTreasury(_treasury); + _setProtocolContracts(_oracle, _treasury, _insuranceFund); } /** - * @notice Set insuranceFund contract address to `_insuranceFund` - * @dev Contract specified here is used to accumulate the protocol insurance fee. - * @param _insuranceFund contract which accumulates insurance fee. - */ - function setInsuranceFund(address _insuranceFund) external auth(SET_INSURANCE_FUND) { - _setInsuranceFund(_insuranceFund); - } + * @notice Set credentials to withdraw ETH on ETH 2.0 side after the phase 2 is launched to `_withdrawalCredentials` + * @dev Note that setWithdrawalCredentials discards all unused signing keys as the signatures are invalidated. + * @param _withdrawalCredentials withdrawal credentials field as defined in the Ethereum PoS consensus specs + */ + function setWithdrawalCredentials(bytes32 _withdrawalCredentials) external { + _auth(MANAGE_WITHDRAWAL_KEY); - /** - * @notice Set credentials to withdraw ETH on ETH 2.0 side after the phase 2 is launched to `_withdrawalCredentials` - * @dev Note that setWithdrawalCredentials discards all unused signing keys as the signatures are invalidated. - * @param _withdrawalCredentials hash of withdrawal multisignature key as accepted by - * the deposit_contract.deposit function - */ - function setWithdrawalCredentials(bytes32 _withdrawalCredentials) external auth(MANAGE_WITHDRAWAL_KEY) { WITHDRAWAL_CREDENTIALS_POSITION.setStorageBytes32(_withdrawalCredentials); getOperators().trimUnusedKeys(); @@ -241,23 +436,35 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @notice Issues withdrawal request. Not implemented. - * @param _amount Amount of StETH to withdraw - * @param _pubkeyHash Receiving address - */ - function withdraw(uint256 _amount, bytes32 _pubkeyHash) external whenNotStopped { /* solhint-disable-line no-unused-vars */ - //will be upgraded to an actual implementation when withdrawals are enabled (Phase 1.5 or 2 of Eth2 launch, likely late 2021 or 2022). - //at the moment withdrawals are not possible in the beacon chain and there's no workaround - revert("NOT_IMPLEMENTED_YET"); + * @dev Sets the address of LidoExecutionLayerRewardsVault contract + * @param _executionLayerRewardsVault Execution layer rewards vault contract address + */ + function setELRewardsVault(address _executionLayerRewardsVault) external { + _auth(SET_EL_REWARDS_VAULT_ROLE); + + EL_REWARDS_VAULT_POSITION.setStorageAddress(_executionLayerRewardsVault); + + emit ELRewardsVaultSet(_executionLayerRewardsVault); + } + + /** + * @dev Sets limit on amount of ETH to withdraw from execution layer rewards vault per LidoOracle report + * @param _limitPoints limit in basis points to amount of ETH to withdraw per LidoOracle report + */ + function setELRewardsWithdrawalLimit(uint16 _limitPoints) external { + _auth(SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE); + + _setBPValue(EL_REWARDS_WITHDRAWAL_LIMIT_POSITION, _limitPoints); + emit ELRewardsWithdrawalLimitSet(_limitPoints); } /** - * @notice Updates the number of Lido-controlled keys in the beacon validators set and their total balance. + * @notice Updates beacon stats, collects rewards from LidoExecutionLayerRewardsVault and distributes all rewards if beacon balance increased * @dev periodically called by the Oracle contract * @param _beaconValidators number of Lido's keys in the beacon state - * @param _beaconBalance simmarized balance of Lido-controlled keys in wei + * @param _beaconBalance summarized balance of Lido-controlled keys in wei */ - function pushBeacon(uint256 _beaconValidators, uint256 _beaconBalance) external whenNotStopped { + function handleOracleReport(uint256 _beaconValidators, uint256 _beaconBalance) external whenNotStopped { require(msg.sender == getOracle(), "APP_AUTH_FAILED"); uint256 depositedValidators = DEPOSITED_VALIDATORS_POSITION.getStorageUint256(); @@ -276,34 +483,54 @@ contract Lido is ILido, IsContract, StETH, AragonApp { uint256 rewardBase = (appearedValidators.mul(DEPOSIT_SIZE)).add(BEACON_BALANCE_POSITION.getStorageUint256()); // Save the current beacon balance and validators to - // calcuate rewards on the next push + // calculate rewards on the next push BEACON_BALANCE_POSITION.setStorageUint256(_beaconBalance); BEACON_VALIDATORS_POSITION.setStorageUint256(_beaconValidators); + // If LidoExecutionLayerRewardsVault address is not set just do as if there were no execution layer rewards at all + // Otherwise withdraw all rewards and put them to the buffer + // Thus, execution layer rewards are handled the same way as beacon rewards + + uint256 executionLayerRewards; + address executionLayerRewardsVaultAddress = getELRewardsVault(); + + if (executionLayerRewardsVaultAddress != address(0)) { + executionLayerRewards = ILidoExecutionLayerRewardsVault(executionLayerRewardsVaultAddress).withdrawRewards( + (_getTotalPooledEther() * EL_REWARDS_WITHDRAWAL_LIMIT_POSITION.getStorageUint256()) / TOTAL_BASIS_POINTS + ); + + if (executionLayerRewards != 0) { + BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().add(executionLayerRewards)); + } + } + + // Don’t mint/distribute any protocol fee on the non-profitable Lido oracle report + // (when beacon chain balance delta is zero or negative). + // See ADR #3 for details: https://research.lido.fi/t/rewards-distribution-after-the-merge-architecture-decision-record/1535 if (_beaconBalance > rewardBase) { uint256 rewards = _beaconBalance.sub(rewardBase); - distributeRewards(rewards); + distributeFee(rewards.add(executionLayerRewards)); } } /** - * @notice Send funds to recovery Vault. Overrides default AragonApp behaviour. - * @param _token Token to be sent to recovery vault. - */ + * @notice Send funds to recovery Vault. Overrides default AragonApp behaviour + * @param _token Token to be sent to recovery vault + */ function transferToVault(address _token) external { require(allowRecoverability(_token), "RECOVER_DISALLOWED"); address vault = getRecoveryVault(); - require(isContract(vault), "RECOVER_VAULT_NOT_CONTRACT"); + require(vault != address(0), "RECOVER_VAULT_ZERO"); uint256 balance; if (_token == ETH) { balance = _getUnaccountedEther(); - // Transfer replaced by call to prevent transfer gas amount issue + // Transfer replaced by call to prevent transfer gas amount issue require(vault.call.value(balance)(), "RECOVER_TRANSFER_FAILED"); } else { ERC20 token = ERC20(_token); balance = token.staticBalanceOf(this); - // safeTransfer comes from overriden default implementation + // safeTransfer comes from overridden default implementation require(token.safeTransfer(vault, balance), "RECOVER_TOKEN_TRANSFER_FAILED"); } @@ -311,17 +538,17 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @notice Returns staking rewards fee rate - */ - function getFee() external view returns (uint16 feeBasisPoints) { - return _getFee(); + * @notice Returns staking rewards fee rate + */ + function getFee() public view returns (uint16 feeBasisPoints) { + return uint16(FEE_POSITION.getStorageUint256()); } /** - * @notice Returns fee distribution proportion - */ + * @notice Returns fee distribution proportion + */ function getFeeDistribution() - external + public view returns ( uint16 treasuryFeeBasisPoints, @@ -329,12 +556,14 @@ contract Lido is ILido, IsContract, StETH, AragonApp { uint16 operatorsFeeBasisPoints ) { - return _getFeeDistribution(); + treasuryFeeBasisPoints = uint16(TREASURY_FEE_POSITION.getStorageUint256()); + insuranceFeeBasisPoints = uint16(INSURANCE_FEE_POSITION.getStorageUint256()); + operatorsFeeBasisPoints = uint16(NODE_OPERATORS_FEE_POSITION.getStorageUint256()); } /** - * @notice Returns current credentials to withdraw ETH on ETH 2.0 side after the phase 2 is launched - */ + * @notice Returns current credentials to withdraw ETH on ETH 2.0 side after the phase 2 is launched + */ function getWithdrawalCredentials() public view returns (bytes32) { return WITHDRAWAL_CREDENTIALS_POSITION.getStorageBytes32(); } @@ -343,15 +572,33 @@ contract Lido is ILido, IsContract, StETH, AragonApp { * @notice Get the amount of Ether temporary buffered on this contract balance * @dev Buffered balance is kept on the contract from the moment the funds are received from user * until the moment they are actually sent to the official Deposit contract. - * @return uint256 of buffered funds in wei + * @return amount of buffered funds in wei */ function getBufferedEther() external view returns (uint256) { return _getBufferedEther(); } /** - * @notice Gets deposit contract handle - */ + * @notice Get total amount of execution layer rewards collected to Lido contract + * @dev Ether got through LidoExecutionLayerRewardsVault is kept on this contract's balance the same way + * as other buffered Ether is kept (until it gets deposited) + * @return amount of funds received as execution layer rewards (in wei) + */ + function getTotalELRewardsCollected() external view returns (uint256) { + return TOTAL_EL_REWARDS_COLLECTED_POSITION.getStorageUint256(); + } + + /** + * @notice Get limit in basis points to amount of ETH to withdraw per LidoOracle report + * @return limit in basis points to amount of ETH to withdraw per LidoOracle report + */ + function getELRewardsWithdrawalLimit() external view returns (uint256) { + return EL_REWARDS_WITHDRAWAL_LIMIT_POSITION.getStorageUint256(); + } + + /** + * @notice Gets deposit contract handle + */ function getDepositContract() public view returns (IDepositContract) { return IDepositContract(DEPOSIT_CONTRACT_POSITION.getStorageAddress()); } @@ -365,22 +612,22 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @notice Gets node operators registry interface handle - */ + * @notice Gets node operators registry interface handle + */ function getOperators() public view returns (INodeOperatorsRegistry) { return INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY_POSITION.getStorageAddress()); } /** - * @notice Returns the treasury address - */ + * @notice Returns the treasury address + */ function getTreasury() public view returns (address) { return TREASURY_POSITION.getStorageAddress(); } /** - * @notice Returns the insurance fund address - */ + * @notice Returns the insurance fund address + */ function getInsuranceFund() public view returns (address) { return INSURANCE_FUND_POSITION.getStorageAddress(); } @@ -398,40 +645,26 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @dev Sets the address of Deposit contract - * @param _contract the address of Deposit contract + * @notice Returns address of the contract set as LidoExecutionLayerRewardsVault */ - function _setDepositContract(IDepositContract _contract) internal { - require(isContract(address(_contract)), "NOT_A_CONTRACT"); - DEPOSIT_CONTRACT_POSITION.setStorageAddress(address(_contract)); + function getELRewardsVault() public view returns (address) { + return EL_REWARDS_VAULT_POSITION.getStorageAddress(); } /** * @dev Internal function to set authorized oracle address * @param _oracle oracle contract */ - function _setOracle(address _oracle) internal { - require(isContract(_oracle), "NOT_A_CONTRACT"); - ORACLE_POSITION.setStorageAddress(_oracle); - } - - /** - * @dev Internal function to set node operator registry address - * @param _r registry of node operators - */ - function _setOperators(INodeOperatorsRegistry _r) internal { - require(isContract(_r), "NOT_A_CONTRACT"); - NODE_OPERATORS_REGISTRY_POSITION.setStorageAddress(_r); - } + function _setProtocolContracts(address _oracle, address _treasury, address _insuranceFund) internal { + require(_oracle != address(0), "ORACLE_ZERO_ADDRESS"); + require(_treasury != address(0), "TREASURY_ZERO_ADDRESS"); + require(_insuranceFund != address(0), "INSURANCE_FUND_ZERO_ADDRESS"); - function _setTreasury(address _treasury) internal { - require(_treasury != address(0), "SET_TREASURY_ZERO_ADDRESS"); + ORACLE_POSITION.setStorageAddress(_oracle); TREASURY_POSITION.setStorageAddress(_treasury); - } - - function _setInsuranceFund(address _insuranceFund) internal { - require(_insuranceFund != address(0), "SET_INSURANCE_FUND_ZERO_ADDRESS"); INSURANCE_FUND_POSITION.setStorageAddress(_insuranceFund); + + emit ProtocolContactsSet(_oracle, _treasury, _insuranceFund); } /** @@ -439,29 +672,44 @@ contract Lido is ILido, IsContract, StETH, AragonApp { * @param _referral address of referral. * @return amount of StETH shares generated */ - function _submit(address _referral) internal whenNotStopped returns (uint256) { - address sender = msg.sender; - uint256 deposit = msg.value; - require(deposit != 0, "ZERO_DEPOSIT"); + function _submit(address _referral) internal returns (uint256) { + require(msg.value != 0, "ZERO_DEPOSIT"); + + StakeLimitState.Data memory stakeLimitData = STAKING_STATE_POSITION.getStorageStakeLimitStruct(); + require(!stakeLimitData.isStakingPaused(), "STAKING_PAUSED"); + + if (stakeLimitData.isStakingLimitSet()) { + uint256 currentStakeLimit = stakeLimitData.calculateCurrentStakeLimit(); + + require(msg.value <= currentStakeLimit, "STAKE_LIMIT"); + + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + stakeLimitData.updatePrevStakeLimit(currentStakeLimit - msg.value) + ); + } - uint256 sharesAmount = getSharesByPooledEth(deposit); + uint256 sharesAmount = getSharesByPooledEth(msg.value); if (sharesAmount == 0) { // totalControlledEther is 0: either the first-ever deposit or complete slashing // assume that shares correspond to Ether 1-to-1 - sharesAmount = deposit; + sharesAmount = msg.value; } - _mintShares(sender, sharesAmount); - _submitted(sender, deposit, _referral); - _emitTransferAfterMintingShares(sender, sharesAmount); + _mintShares(msg.sender, sharesAmount); + + BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().add(msg.value)); + emit Submitted(msg.sender, msg.value, _referral); + + _emitTransferAfterMintingShares(msg.sender, sharesAmount); return sharesAmount; } /** - * @dev Emits an {Transfer} event where from is 0 address. Indicates mint events. - */ + * @dev Emits {Transfer} and {TransferShares} events where `from` is 0 address. Indicates mint events. + */ function _emitTransferAfterMintingShares(address _to, uint256 _sharesAmount) internal { emit Transfer(address(0), _to, getPooledEthByShares(_sharesAmount)); + emit TransferShares(address(0), _to, _sharesAmount); } /** @@ -547,14 +795,14 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @dev Distributes rewards by minting and distributing corresponding amount of liquid tokens. + * @dev Distributes fee portion of the rewards by minting and distributing corresponding amount of liquid tokens. * @param _totalRewards Total rewards accrued on the Ethereum 2.0 side in wei */ - function distributeRewards(uint256 _totalRewards) internal { + function distributeFee(uint256 _totalRewards) internal { // We need to take a defined percentage of the reported reward as a fee, and we do // this by minting new token shares and assigning them to the fee recipients (see // StETH docs for the explanation of the shares mechanics). The staking rewards fee - // is defined in basis points (1 basis point is equal to 0.01%, 10000 is 100%). + // is defined in basis points (1 basis point is equal to 0.01%, 10000 (TOTAL_BASIS_POINTS) is 100%). // // Since we've increased totalPooledEther by _totalRewards (which is already // performed by the time this function is called), the combined cost of all holders' @@ -564,23 +812,23 @@ contract Lido is ILido, IsContract, StETH, AragonApp { // Now we want to mint new shares to the fee recipient, so that the total cost of the // newly-minted shares exactly corresponds to the fee taken: // - // shares2mint * newShareCost = (_totalRewards * feeBasis) / 10000 + // shares2mint * newShareCost = (_totalRewards * feeBasis) / TOTAL_BASIS_POINTS // newShareCost = newTotalPooledEther / (prevTotalShares + shares2mint) // // which follows to: // // _totalRewards * feeBasis * prevTotalShares // shares2mint = -------------------------------------------------------------- - // (newTotalPooledEther * 10000) - (feeBasis * _totalRewards) + // (newTotalPooledEther * TOTAL_BASIS_POINTS) - (feeBasis * _totalRewards) // // The effect is that the given percentage of the reward goes to the fee recipient, and // the rest of the reward is distributed between token holders proportionally to their // token shares. - uint256 feeBasis = _getFee(); + uint256 feeBasis = getFee(); uint256 shares2mint = ( _totalRewards.mul(feeBasis).mul(_getTotalShares()) .div( - _getTotalPooledEther().mul(10000) + _getTotalPooledEther().mul(TOTAL_BASIS_POINTS) .sub(feeBasis.mul(_totalRewards)) ) ); @@ -589,15 +837,15 @@ contract Lido is ILido, IsContract, StETH, AragonApp { // balances of the holders, as if the fee was taken in parts from each of them. _mintShares(address(this), shares2mint); - (,uint16 insuranceFeeBasisPoints, uint16 operatorsFeeBasisPoints) = _getFeeDistribution(); + (,uint16 insuranceFeeBasisPoints, uint16 operatorsFeeBasisPoints) = getFeeDistribution(); - uint256 toInsuranceFund = shares2mint.mul(insuranceFeeBasisPoints).div(10000); + uint256 toInsuranceFund = shares2mint.mul(insuranceFeeBasisPoints).div(TOTAL_BASIS_POINTS); address insuranceFund = getInsuranceFund(); _transferShares(address(this), insuranceFund, toInsuranceFund); _emitTransferAfterMintingShares(insuranceFund, toInsuranceFund); uint256 distributedToOperatorsShares = _distributeNodeOperatorsReward( - shares2mint.mul(operatorsFeeBasisPoints).div(10000) + shares2mint.mul(operatorsFeeBasisPoints).div(TOTAL_BASIS_POINTS) ); // Transfer the rest of the fee to treasury @@ -608,6 +856,11 @@ contract Lido is ILido, IsContract, StETH, AragonApp { _emitTransferAfterMintingShares(treasury, toTreasury); } + /** + * @dev Internal function to distribute reward to node operators + * @param _sharesToDistribute amount of shares to distribute + * @return actual amount of shares that was transferred to node operators as a reward + */ function _distributeNodeOperatorsReward(uint256 _sharesToDistribute) internal returns (uint256 distributed) { (address[] memory recipients, uint256[] memory shares) = getOperators().getRewardsDistribution(_sharesToDistribute); @@ -626,21 +879,9 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @dev Records a deposit made by a user with optional referral - * @param _sender sender's address - * @param _value Deposit value in wei - * @param _referral address of the referral + * @dev Records a deposit to the deposit_contract.deposit function + * @param _amount Total amount deposited to the ETH 2.0 side */ - function _submitted(address _sender, uint256 _value, address _referral) internal { - BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().add(_value)); - - emit Submitted(_sender, _value, _referral); - } - - /** - * @dev Records a deposit to the deposit_contract.deposit function. - * @param _amount Total amount deposited to the ETH 2.0 side - */ function _markAsUnbuffered(uint256 _amount) internal { BUFFERED_ETHER_POSITION.setStorageUint256( BUFFERED_ETHER_POSITION.getStorageUint256().sub(_amount)); @@ -649,43 +890,16 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @dev Write a value nominated in basis points - */ + * @dev Write a value nominated in basis points + */ function _setBPValue(bytes32 _slot, uint16 _value) internal { - require(_value <= 10000, "VALUE_OVER_100_PERCENT"); + require(_value <= TOTAL_BASIS_POINTS, "VALUE_OVER_100_PERCENT"); _slot.setStorageUint256(uint256(_value)); } /** - * @dev Returns staking rewards fee rate - */ - function _getFee() internal view returns (uint16) { - return _readBPValue(FEE_POSITION); - } - - /** - * @dev Returns fee distribution proportion - */ - function _getFeeDistribution() internal view - returns (uint16 treasuryFeeBasisPoints, uint16 insuranceFeeBasisPoints, uint16 operatorsFeeBasisPoints) - { - treasuryFeeBasisPoints = _readBPValue(TREASURY_FEE_POSITION); - insuranceFeeBasisPoints = _readBPValue(INSURANCE_FEE_POSITION); - operatorsFeeBasisPoints = _readBPValue(NODE_OPERATORS_FEE_POSITION); - } - - /** - * @dev Read a value nominated in basis points - */ - function _readBPValue(bytes32 _slot) internal view returns (uint16) { - uint256 v = _slot.getStorageUint256(); - assert(v <= 10000); - return uint16(v); - } - - /** - * @dev Gets the amount of Ether temporary buffered on this contract balance - */ + * @dev Gets the amount of Ether temporary buffered on this contract balance + */ function _getBufferedEther() internal view returns (uint256) { uint256 buffered = BUFFERED_ETHER_POSITION.getStorageUint256(); assert(address(this).balance >= buffered); @@ -694,8 +908,8 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @dev Gets unaccounted (excess) Ether on this contract balance - */ + * @dev Gets unaccounted (excess) Ether on this contract balance + */ function _getUnaccountedEther() internal view returns (uint256) { return address(this).balance.sub(_getBufferedEther()); } @@ -710,8 +924,7 @@ contract Lido is ILido, IsContract, StETH, AragonApp { uint256 beaconValidators = BEACON_VALIDATORS_POSITION.getStorageUint256(); // beaconValidators can never be less than deposited ones. assert(depositedValidators >= beaconValidators); - uint256 transientValidators = depositedValidators.sub(beaconValidators); - return transientValidators.mul(DEPOSIT_SIZE); + return depositedValidators.sub(beaconValidators).mul(DEPOSIT_SIZE); } /** @@ -719,16 +932,15 @@ contract Lido is ILido, IsContract, StETH, AragonApp { * @return total balance in wei */ function _getTotalPooledEther() internal view returns (uint256) { - uint256 bufferedBalance = _getBufferedEther(); - uint256 beaconBalance = BEACON_BALANCE_POSITION.getStorageUint256(); - uint256 transientBalance = _getTransientBalance(); - return bufferedBalance.add(beaconBalance).add(transientBalance); + return _getBufferedEther().add( + BEACON_BALANCE_POSITION.getStorageUint256() + ).add(_getTransientBalance()); } /** - * @dev Padding memory array with zeroes up to 64 bytes on the right - * @param _b Memory array of size 32 .. 64 - */ + * @dev Padding memory array with zeroes up to 64 bytes on the right + * @param _b Memory array of size 32 .. 64 + */ function _pad64(bytes memory _b) internal pure returns (bytes memory) { assert(_b.length >= 32 && _b.length <= 64); if (64 == _b.length) @@ -744,9 +956,9 @@ contract Lido is ILido, IsContract, StETH, AragonApp { } /** - * @dev Converting value to little endian bytes and padding up to 32 bytes on the right - * @param _value Number less than `2**64` for compatibility reasons - */ + * @dev Converting value to little endian bytes and padding up to 32 bytes on the right + * @param _value Number less than `2**64` for compatibility reasons + */ function _toLittleEndian64(uint256 _value) internal pure returns (uint256 result) { result = 0; uint256 temp_value = _value; @@ -759,8 +971,38 @@ contract Lido is ILido, IsContract, StETH, AragonApp { result <<= (24 * 8); } - function to64(uint256 v) internal pure returns (uint64) { - assert(v <= uint256(uint64(-1))); - return uint64(v); + function _pauseStaking() internal { + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + STAKING_STATE_POSITION.getStorageStakeLimitStruct().setStakeLimitPauseState(true) + ); + + emit StakingPaused(); + } + + function _resumeStaking() internal { + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + STAKING_STATE_POSITION.getStorageStakeLimitStruct().setStakeLimitPauseState(false) + ); + + emit StakingResumed(); + } + + function _getCurrentStakeLimit(StakeLimitState.Data memory _stakeLimitData) internal view returns(uint256) { + if (_stakeLimitData.isStakingPaused()) { + return 0; + } + if (!_stakeLimitData.isStakingLimitSet()) { + return uint256(-1); + } + + return _stakeLimitData.calculateCurrentStakeLimit(); + } + + /** + * @dev Size-efficient analog of the `auth(_role)` modifier + * @param _role Permission name + */ + function _auth(bytes32 _role) internal view auth(_role) { + // no-op } } diff --git a/contracts/0.4.24/StETH.sol b/contracts/0.4.24/StETH.sol index c510cf959..970c83658 100644 --- a/contracts/0.4.24/StETH.sol +++ b/contracts/0.4.24/StETH.sol @@ -42,7 +42,7 @@ import "./lib/Pausable.sol"; * emitting an event for each token holder and thus running an unbounded loop. * * The token inherits from `Pausable` and uses `whenNotStopped` modifier for methods - * which change `shares` or `allowances`. `_stop` and `_resume` functions are overriden + * which change `shares` or `allowances`. `_stop` and `_resume` functions are overridden * in `Lido.sol` and might be called by an account with the `PAUSE_ROLE` assigned by the * DAO. This is useful for emergency scenarios, e.g. a protocol bug, where one might want * to freeze all token transfers and approvals until the emergency is resolved. @@ -81,6 +81,36 @@ contract StETH is IERC20, Pausable { */ bytes32 internal constant TOTAL_SHARES_POSITION = keccak256("lido.StETH.totalShares"); + /** + * @notice An executed shares transfer from `sender` to `recipient`. + * + * @dev emitted in pair with an ERC20-defined `Transfer` event. + */ + event TransferShares( + address indexed from, + address indexed to, + uint256 sharesValue + ); + + /** + * @notice An executed `burnShares` request + * + * @dev Reports simultaneously burnt shares amount + * and corresponding stETH amount. + * The stETH amount is calculated twice: before and after the burning incurred rebase. + * + * @param account holder of the burnt shares + * @param preRebaseTokenAmount amount of stETH the burnt shares corresponded to before the burn + * @param postRebaseTokenAmount amount of stETH the burnt shares corresponded to after the burn + * @param sharesAmount amount of burnt shares + */ + event SharesBurnt( + address indexed account, + uint256 preRebaseTokenAmount, + uint256 postRebaseTokenAmount, + uint256 sharesAmount + ); + /** * @return the name of the token. */ @@ -137,6 +167,7 @@ contract StETH is IERC20, Pausable { * * @return a boolean value indicating whether the operation succeeded. * Emits a `Transfer` event. + * Emits a `TransferShares` event. * * Requirements: * @@ -187,6 +218,7 @@ contract StETH is IERC20, Pausable { * @return a boolean value indicating whether the operation succeeded. * * Emits a `Transfer` event. + * Emits a `TransferShares` event. * Emits an `Approval` event indicating the updated allowance. * * Requirements: @@ -291,9 +323,32 @@ contract StETH is IERC20, Pausable { } } + /** + * @notice Moves `_sharesAmount` token shares from the caller's account to the `_recipient` account. + * + * @return amount of transferred tokens. + * Emits a `TransferShares` event. + * Emits a `Transfer` event. + * + * Requirements: + * + * - `_recipient` cannot be the zero address. + * - the caller must have at least `_sharesAmount` shares. + * - the contract must not be paused. + * + * @dev The `_sharesAmount` argument is the amount of shares, not tokens. + */ + function transferShares(address _recipient, uint256 _sharesAmount) public returns (uint256) { + _transferShares(msg.sender, _recipient, _sharesAmount); + emit TransferShares(msg.sender, _recipient, _sharesAmount); + uint256 tokensAmount = getPooledEthByShares(_sharesAmount); + emit Transfer(msg.sender, _recipient, tokensAmount); + return tokensAmount; + } + /** * @return the total amount (in wei) of Ether controlled by the protocol. - * @dev This is used for calaulating tokens from shares and vice versa. + * @dev This is used for calculating tokens from shares and vice versa. * @dev This function is required to be implemented in a derived contract. */ function _getTotalPooledEther() internal view returns (uint256); @@ -301,11 +356,13 @@ contract StETH is IERC20, Pausable { /** * @notice Moves `_amount` tokens from `_sender` to `_recipient`. * Emits a `Transfer` event. + * Emits a `TransferShares` event. */ function _transfer(address _sender, address _recipient, uint256 _amount) internal { uint256 _sharesToTransfer = getSharesByPooledEth(_amount); _transferShares(_sender, _recipient, _sharesToTransfer); emit Transfer(_sender, _recipient, _amount); + emit TransferShares(_sender, _recipient, _sharesToTransfer); } /** @@ -403,15 +460,23 @@ contract StETH is IERC20, Pausable { uint256 accountShares = shares[_account]; require(_sharesAmount <= accountShares, "BURN_AMOUNT_EXCEEDS_BALANCE"); + uint256 preRebaseTokenAmount = getPooledEthByShares(_sharesAmount); + newTotalShares = _getTotalShares().sub(_sharesAmount); TOTAL_SHARES_POSITION.setStorageUint256(newTotalShares); shares[_account] = accountShares.sub(_sharesAmount); + uint256 postRebaseTokenAmount = getPooledEthByShares(_sharesAmount); + + emit SharesBurnt(_account, preRebaseTokenAmount, postRebaseTokenAmount, _sharesAmount); + // Notice: we're not emitting a Transfer event to the zero address here since shares burn // works by redistributing the amount of tokens corresponding to the burned shares between // all other token holders. The total supply of the token doesn't change as the result. // This is equivalent to performing a send from `address` to each other token holder address, // but we cannot reflect this as it would require sending an unbounded number of events. + + // We're emitting `SharesBurnt` event to provide an explicit rebase log record nonetheless. } } diff --git a/contracts/0.4.24/interfaces/ILido.sol b/contracts/0.4.24/interfaces/ILido.sol index 647fa3cb1..17b5427cc 100644 --- a/contracts/0.4.24/interfaces/ILido.sol +++ b/contracts/0.4.24/interfaces/ILido.sol @@ -13,11 +13,12 @@ pragma solidity 0.4.24; * and stakes it via the deposit_contract.sol contract. It doesn't hold ether on it's balance, * only a small portion (buffer) of it. * It also mints new tokens for rewards generated at the ETH 2.0 side. + * + * At the moment withdrawals are not possible in the beacon chain and there's no workaround. + * Pool will be upgraded to an actual implementation when withdrawals are enabled + * (Phase 1.5 or 2 of Eth2 launch, likely late 2022 or 2023). */ interface ILido { - /** - * @dev From ISTETH interface, because "Interfaces cannot inherit". - */ function totalSupply() external view returns (uint256); function getTotalShares() external view returns (uint256); @@ -31,24 +32,127 @@ interface ILido { */ function resume() external; + /** + * @notice Stops accepting new Ether to the protocol + * + * @dev While accepting new Ether is stopped, calls to the `submit` function, + * as well as to the default payable function, will revert. + * + * Emits `StakingPaused` event. + */ + function pauseStaking() external; + + /** + * @notice Resumes accepting new Ether to the protocol (if `pauseStaking` was called previously) + * NB: Staking could be rate-limited by imposing a limit on the stake amount + * at each moment in time, see `setStakingLimit()` and `removeStakingLimit()` + * + * @dev Preserves staking limit if it was set previously + * + * Emits `StakingResumed` event + */ + function resumeStaking() external; + + /** + * @notice Sets the staking rate limit + * + * @dev Reverts if: + * - `_maxStakeLimit` == 0 + * - `_maxStakeLimit` >= 2^96 + * - `_maxStakeLimit` < `_stakeLimitIncreasePerBlock` + * - `_maxStakeLimit` / `_stakeLimitIncreasePerBlock` >= 2^32 (only if `_stakeLimitIncreasePerBlock` != 0) + * + * Emits `StakingLimitSet` event + * + * @param _maxStakeLimit max stake limit value + * @param _stakeLimitIncreasePerBlock stake limit increase per single block + */ + function setStakingLimit(uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock) external; + + /** + * @notice Removes the staking rate limit + * + * Emits `StakingLimitRemoved` event + */ + function removeStakingLimit() external; + + /** + * @notice Check staking state: whether it's paused or not + */ + function isStakingPaused() external view returns (bool); + + /** + * @notice Returns how much Ether can be staked in the current block + * @dev Special return values: + * - 2^256 - 1 if staking is unlimited; + * - 0 if staking is paused or if limit is exhausted. + */ + function getCurrentStakeLimit() external view returns (uint256); + + /** + * @notice Returns full info about current stake limit params and state + * @dev Might be used for the advanced integration requests. + * @return isStakingPaused staking pause state (equivalent to return of isStakingPaused()) + * @return isStakingLimitSet whether the stake limit is set + * @return currentStakeLimit current stake limit (equivalent to return of getCurrentStakeLimit()) + * @return maxStakeLimit max stake limit + * @return maxStakeLimitGrowthBlocks blocks needed to restore max stake limit from the fully exhausted state + * @return prevStakeLimit previously reached stake limit + * @return prevStakeBlockNumber previously seen block number + */ + function getStakeLimitFullInfo() external view returns ( + bool isStakingPaused, + bool isStakingLimitSet, + uint256 currentStakeLimit, + uint256 maxStakeLimit, + uint256 maxStakeLimitGrowthBlocks, + uint256 prevStakeLimit, + uint256 prevStakeBlockNumber + ); + event Stopped(); event Resumed(); + event StakingPaused(); + event StakingResumed(); + event StakingLimitSet(uint256 maxStakeLimit, uint256 stakeLimitIncreasePerBlock); + event StakingLimitRemoved(); + + /** + * @notice Set Lido protocol contracts (oracle, treasury, insurance fund). + * @param _oracle oracle contract + * @param _treasury treasury contract + * @param _insuranceFund insurance fund contract + */ + function setProtocolContracts( + address _oracle, + address _treasury, + address _insuranceFund + ) external; + + event ProtocolContactsSet(address oracle, address treasury, address insuranceFund); /** - * @notice Set fee rate to `_feeBasisPoints` basis points. The fees are accrued when oracles report staking results + * @notice Set fee rate to `_feeBasisPoints` basis points. + * The fees are accrued when: + * - oracles report staking results (beacon chain balance increase) + * - validators gain execution layer rewards (priority fees and MEV) * @param _feeBasisPoints Fee rate, in basis points */ function setFee(uint16 _feeBasisPoints) external; /** - * @notice Set fee distribution: `_treasuryFeeBasisPoints` basis points go to the treasury, `_insuranceFeeBasisPoints` basis points go to the insurance fund, `_operatorsFeeBasisPoints` basis points go to node operators. The sum has to be 10 000. + * @notice Set fee distribution + * @param _treasuryFeeBasisPoints basis points go to the treasury, + * @param _insuranceFeeBasisPoints basis points go to the insurance fund, + * @param _operatorsFeeBasisPoints basis points go to node operators. + * @dev The sum has to be 10 000. */ function setFeeDistribution( uint16 _treasuryFeeBasisPoints, uint16 _insuranceFeeBasisPoints, - uint16 _operatorsFeeBasisPoints) - external; + uint16 _operatorsFeeBasisPoints + ) external; /** * @notice Returns staking rewards fee rate @@ -58,19 +162,39 @@ interface ILido { /** * @notice Returns fee distribution proportion */ - function getFeeDistribution() external view returns (uint16 treasuryFeeBasisPoints, uint16 insuranceFeeBasisPoints, - uint16 operatorsFeeBasisPoints); + function getFeeDistribution() external view returns ( + uint16 treasuryFeeBasisPoints, + uint16 insuranceFeeBasisPoints, + uint16 operatorsFeeBasisPoints + ); event FeeSet(uint16 feeBasisPoints); event FeeDistributionSet(uint16 treasuryFeeBasisPoints, uint16 insuranceFeeBasisPoints, uint16 operatorsFeeBasisPoints); + /** + * @notice A payable function supposed to be called only by LidoExecutionLayerRewardsVault contract + * @dev We need a dedicated function because funds received by the default payable function + * are treated as a user deposit + */ + function receiveELRewards() external payable; + + // The amount of ETH withdrawn from LidoExecutionLayerRewardsVault contract to Lido contract + event ELRewardsReceived(uint256 amount); + + /** + * @dev Sets limit on amount of ETH to withdraw from execution layer rewards vault per LidoOracle report + * @param _limitPoints limit in basis points to amount of ETH to withdraw per LidoOracle report + */ + function setELRewardsWithdrawalLimit(uint16 _limitPoints) external; + + // Percent in basis points of total pooled ether allowed to withdraw from LidoExecutionLayerRewardsVault per LidoOracle report + event ELRewardsWithdrawalLimitSet(uint256 limitPoints); /** * @notice Set credentials to withdraw ETH on ETH 2.0 side after the phase 2 is launched to `_withdrawalCredentials` * @dev Note that setWithdrawalCredentials discards all unused signing keys as the signatures are invalidated. - * @param _withdrawalCredentials hash of withdrawal multisignature key as accepted by - * the deposit_contract.deposit function + * @param _withdrawalCredentials withdrawal credentials field as defined in the Ethereum PoS consensus specs */ function setWithdrawalCredentials(bytes32 _withdrawalCredentials) external; @@ -79,16 +203,23 @@ interface ILido { */ function getWithdrawalCredentials() external view returns (bytes); - event WithdrawalCredentialsSet(bytes32 withdrawalCredentials); + /** + * @dev Sets the address of LidoExecutionLayerRewardsVault contract + * @param _executionLayerRewardsVault Execution layer rewards vault contract address + */ + function setELRewardsVault(address _executionLayerRewardsVault) external; + + // The `executionLayerRewardsVault` was set as the execution layer rewards vault for Lido + event ELRewardsVaultSet(address executionLayerRewardsVault); /** * @notice Ether on the ETH 2.0 side reported by the oracle * @param _epoch Epoch id * @param _eth2balance Balance in wei on the ETH 2.0 side */ - function pushBeacon(uint256 _epoch, uint256 _eth2balance) external; + function handleOracleReport(uint256 _epoch, uint256 _eth2balance) external; // User functions @@ -102,16 +233,9 @@ interface ILido { // Records a deposit made by a user event Submitted(address indexed sender, uint256 amount, address referral); - // The `_amount` of ether was sent to the deposit_contract.deposit function. + // The `amount` of ether was sent to the deposit_contract.deposit function event Unbuffered(uint256 amount); - /** - * @notice Issues withdrawal request. Large withdrawals will be processed only after the phase 2 launch. - * @param _amount Amount of StETH to burn - * @param _pubkeyHash Receiving address - */ - function withdraw(uint256 _amount, bytes32 _pubkeyHash) external; - // Requested withdrawal of `etherAmount` to `pubkeyHash` on the ETH 2.0 side, `tokenAmount` burned by `sender`, // `sentFromBuffer` was sent on the current Ethereum side. event Withdrawal(address indexed sender, uint256 tokenAmount, uint256 sentFromBuffer, diff --git a/contracts/0.4.24/interfaces/ILidoExecutionLayerRewardsVault.sol b/contracts/0.4.24/interfaces/ILidoExecutionLayerRewardsVault.sol new file mode 100644 index 000000000..701643ef6 --- /dev/null +++ b/contracts/0.4.24/interfaces/ILidoExecutionLayerRewardsVault.sol @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2021 Lido + +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.4.24; + + +interface ILidoExecutionLayerRewardsVault { + + /** + * @notice Withdraw all accumulated execution layer rewards to Lido contract + * @param _maxAmount Max amount of ETH to withdraw + * @return amount of funds received as execution layer rewards (in wei) + */ + function withdrawRewards(uint256 _maxAmount) external returns (uint256 amount); +} diff --git a/contracts/0.4.24/interfaces/ILidoOracle.sol b/contracts/0.4.24/interfaces/ILidoOracle.sol index 11864bbeb..8847dd08d 100644 --- a/contracts/0.4.24/interfaces/ILidoOracle.sol +++ b/contracts/0.4.24/interfaces/ILidoOracle.sol @@ -183,16 +183,33 @@ interface ILidoOracle { uint256 timeElapsed ); - /** - * @notice Initialize the contract v2 data, with sanity check bounds - * (`_allowedBeaconBalanceAnnualRelativeIncrease`, `_allowedBeaconBalanceRelativeDecrease`) - * @dev Original initialize function removed from v2 because it is invoked only once - */ - function initialize_v2( + + /** + * @notice Initialize the contract (version 3 for now) from scratch + * @dev For details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md + * @param _lido Address of Lido contract + * @param _epochsPerFrame Number of epochs per frame + * @param _slotsPerEpoch Number of slots per epoch + * @param _secondsPerSlot Number of seconds per slot + * @param _genesisTime Genesis time + * @param _allowedBeaconBalanceAnnualRelativeIncrease Allowed beacon balance annual relative increase (e.g. 1000 means 10% yearly increase) + * @param _allowedBeaconBalanceRelativeDecrease Allowed beacon balance moment descreat (e.g. 500 means 5% moment decrease) + */ + function initialize( + address _lido, + uint64 _epochsPerFrame, + uint64 _slotsPerEpoch, + uint64 _secondsPerSlot, + uint64 _genesisTime, uint256 _allowedBeaconBalanceAnnualRelativeIncrease, uint256 _allowedBeaconBalanceRelativeDecrease - ) - external; + ) external; + + /** + * @notice A function to finalize upgrade to v3 (from v1). Can be called only once + * @dev For more details see _initialize_v3() + */ + function finalizeUpgrade_v3() external; /** * @notice Add `_member` to the oracle member committee list diff --git a/contracts/0.4.24/interfaces/ISTETH.sol b/contracts/0.4.24/interfaces/ISTETH.sol deleted file mode 100644 index 6222d559e..000000000 --- a/contracts/0.4.24/interfaces/ISTETH.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Lido - -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.4.24; - - -/** - * @title A liquid version of ETH 2.0 native token - * - * ERC20 token which supports stop/resume mechanics. The token is operated by `ILido`. - * - * Since balances of all token holders change when the amount of total controlled Ether - * changes, this token cannot fully implement ERC20 standard: it only emits `Transfer` - * events upon explicit transfer between holders. In contrast, when Lido oracle reports - * rewards, no Transfer events are generated: doing so would require emitting an event - * for each token holder and thus running an unbounded loop. - */ -interface ISTETH /* is IERC20 */ { - function totalSupply() external view returns (uint256); - - /** - * @notice Stop transfers - */ - function stop() external; - - /** - * @notice Resume transfers - */ - function resume() external; - - /** - * @notice Returns true if the token is stopped - */ - function isStopped() external view returns (bool); - - event Stopped(); - event Resumed(); - - /** - * @notice Increases shares of a given address by the specified amount. Called by Lido - * contract in two cases: 1) when a user submits an ETH1.0 deposit; 2) when - * ETH2.0 rewards are reported by the oracle. Upon user deposit, Lido contract - * mints the amount of shares that corresponds to the submitted Ether, so - * token balances of other token holders don't change. Upon rewards report, - * Lido contract mints new shares to distribute fee, effectively diluting the - * amount of Ether that would otherwise correspond to each share. - * - * @param _to Receiver of new shares - * @param _sharesAmount Amount of shares to mint - * @return The total amount of all holders' shares after new shares are minted - */ - function mintShares(address _to, uint256 _sharesAmount) external returns (uint256); - - /** - * @notice Burn is called by Lido contract when a user withdraws their Ether. - * @param _account Account which tokens are to be burnt - * @param _sharesAmount Amount of shares to burn - * @return The total amount of all holders' shares after the shares are burned - */ - function burnShares(address _account, uint256 _sharesAmount) external returns (uint256); - - - function balanceOf(address owner) external view returns (uint256); - - function transfer(address to, uint256 value) external returns (bool); - - function getTotalShares() external view returns (uint256); - - function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); - function getSharesByPooledEth(uint256 _pooledEthAmount) external view returns (uint256); -} diff --git a/contracts/0.4.24/lib/StakeLimitUtils.sol b/contracts/0.4.24/lib/StakeLimitUtils.sol new file mode 100644 index 000000000..f9353e255 --- /dev/null +++ b/contracts/0.4.24/lib/StakeLimitUtils.sol @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2022 Lido + +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +pragma solidity 0.4.24; + +import "@aragon/os/contracts/common/UnstructuredStorage.sol"; + +// +// We need to pack four variables into the same 256bit-wide storage slot +// to lower the costs per each staking request. +// +// As a result, slot's memory aligned as follows: +// +// MSB ------------------------------------------------------------------------------> LSB +// 256____________160_________________________128_______________32_____________________ 0 +// |_______________|___________________________|________________|_______________________| +// | maxStakeLimit | maxStakeLimitGrowthBlocks | prevStakeLimit | prevStakeBlockNumber | +// |<-- 96 bits -->|<---------- 32 bits ------>|<-- 96 bits --->|<----- 32 bits ------->| +// +// +// NB: Internal representation conventions: +// +// - the `maxStakeLimitGrowthBlocks` field above represented as follows: +// `maxStakeLimitGrowthBlocks` = `maxStakeLimit` / `stakeLimitIncreasePerBlock` +// 32 bits 96 bits 96 bits +// +// +// - the "staking paused" state is encoded by `prevStakeBlockNumber` being zero, +// - the "staking unlimited" state is encoded by `maxStakeLimit` being zero and `prevStakeBlockNumber` being non-zero. +// + +/** +* @notice Library for the internal structs definitions +* @dev solidity <0.6 doesn't support top-level structs +* using the library to have a proper namespace +*/ +library StakeLimitState { + /** + * @dev Internal representation struct (slot-wide) + */ + struct Data { + uint32 prevStakeBlockNumber; + uint96 prevStakeLimit; + uint32 maxStakeLimitGrowthBlocks; + uint96 maxStakeLimit; + } +} + +library StakeLimitUnstructuredStorage { + using UnstructuredStorage for bytes32; + + /// @dev Storage offset for `maxStakeLimit` (bits) + uint256 internal constant MAX_STAKE_LIMIT_OFFSET = 160; + /// @dev Storage offset for `maxStakeLimitGrowthBlocks` (bits) + uint256 internal constant MAX_STAKE_LIMIT_GROWTH_BLOCKS_OFFSET = 128; + /// @dev Storage offset for `prevStakeLimit` (bits) + uint256 internal constant PREV_STAKE_LIMIT_OFFSET = 32; + /// @dev Storage offset for `prevStakeBlockNumber` (bits) + uint256 internal constant PREV_STAKE_BLOCK_NUMBER_OFFSET = 0; + + /** + * @dev Read stake limit state from the unstructured storage position + * @param _position storage offset + */ + function getStorageStakeLimitStruct(bytes32 _position) internal view returns (StakeLimitState.Data memory stakeLimit) { + uint256 slotValue = _position.getStorageUint256(); + + stakeLimit.prevStakeBlockNumber = uint32(slotValue >> PREV_STAKE_BLOCK_NUMBER_OFFSET); + stakeLimit.prevStakeLimit = uint96(slotValue >> PREV_STAKE_LIMIT_OFFSET); + stakeLimit.maxStakeLimitGrowthBlocks = uint32(slotValue >> MAX_STAKE_LIMIT_GROWTH_BLOCKS_OFFSET); + stakeLimit.maxStakeLimit = uint96(slotValue >> MAX_STAKE_LIMIT_OFFSET); + } + + /** + * @dev Write stake limit state to the unstructured storage position + * @param _position storage offset + * @param _data stake limit state structure instance + */ + function setStorageStakeLimitStruct(bytes32 _position, StakeLimitState.Data memory _data) internal { + _position.setStorageUint256( + uint256(_data.prevStakeBlockNumber) << PREV_STAKE_BLOCK_NUMBER_OFFSET + | uint256(_data.prevStakeLimit) << PREV_STAKE_LIMIT_OFFSET + | uint256(_data.maxStakeLimitGrowthBlocks) << MAX_STAKE_LIMIT_GROWTH_BLOCKS_OFFSET + | uint256(_data.maxStakeLimit) << MAX_STAKE_LIMIT_OFFSET + ); + } +} + +/** +* @notice Interface library with helper functions to deal with stake limit struct in a more high-level approach. +*/ +library StakeLimitUtils { + /** + * @notice Calculate stake limit for the current block. + */ + function calculateCurrentStakeLimit(StakeLimitState.Data memory _data) internal view returns(uint256 limit) { + uint256 stakeLimitIncPerBlock; + if (_data.maxStakeLimitGrowthBlocks != 0) { + stakeLimitIncPerBlock = _data.maxStakeLimit / _data.maxStakeLimitGrowthBlocks; + } + + limit = _data.prevStakeLimit + ((block.number - _data.prevStakeBlockNumber) * stakeLimitIncPerBlock); + if (limit > _data.maxStakeLimit) { + limit = _data.maxStakeLimit; + } + } + + /** + * @notice check if staking is on pause + */ + function isStakingPaused(StakeLimitState.Data memory _data) internal pure returns(bool) { + return _data.prevStakeBlockNumber == 0; + } + + /** + * @notice check if staking limit is set (otherwise staking is unlimited) + */ + function isStakingLimitSet(StakeLimitState.Data memory _data) internal pure returns(bool) { + return _data.maxStakeLimit != 0; + } + + /** + * @notice update stake limit repr with the desired limits + * @dev input `_data` param is mutated and the func returns effectively the same pointer + * @param _data stake limit state struct + * @param _maxStakeLimit stake limit max value + * @param _stakeLimitIncreasePerBlock stake limit increase (restoration) per block + */ + function setStakingLimit( + StakeLimitState.Data memory _data, + uint256 _maxStakeLimit, + uint256 _stakeLimitIncreasePerBlock + ) internal view returns (StakeLimitState.Data memory) { + require(_maxStakeLimit != 0, "ZERO_MAX_STAKE_LIMIT"); + require(_maxStakeLimit <= uint96(-1), "TOO_LARGE_MAX_STAKE_LIMIT"); + require(_maxStakeLimit >= _stakeLimitIncreasePerBlock, "TOO_LARGE_LIMIT_INCREASE"); + require( + (_stakeLimitIncreasePerBlock == 0) + || (_maxStakeLimit / _stakeLimitIncreasePerBlock <= uint32(-1)), + "TOO_SMALL_LIMIT_INCREASE" + ); + + // if staking was paused or unlimited previously, + // or new limit is lower than previous, then + // reset prev stake limit to the new max stake limit + if ((_data.maxStakeLimit == 0) || (_maxStakeLimit < _data.prevStakeLimit)) { + _data.prevStakeLimit = uint96(_maxStakeLimit); + } + _data.maxStakeLimitGrowthBlocks = _stakeLimitIncreasePerBlock != 0 ? uint32(_maxStakeLimit / _stakeLimitIncreasePerBlock) : 0; + + _data.maxStakeLimit = uint96(_maxStakeLimit); + + if (_data.prevStakeBlockNumber != 0) { + _data.prevStakeBlockNumber = uint32(block.number); + } + + return _data; + } + + /** + * @notice update stake limit repr to remove the limit + * @dev input `_data` param is mutated and the func returns effectively the same pointer + * @param _data stake limit state struct + */ + function removeStakingLimit( + StakeLimitState.Data memory _data + ) internal view returns (StakeLimitState.Data memory) { + _data.maxStakeLimit = 0; + + return _data; + } + + /** + * @notice update stake limit repr after submitting user's eth + * @dev input `_data` param is mutated and the func returns effectively the same pointer + * @param _data stake limit state struct + * @param _newPrevStakeLimit new value for the `prevStakeLimit` field + */ + function updatePrevStakeLimit( + StakeLimitState.Data memory _data, + uint256 _newPrevStakeLimit + ) internal view returns (StakeLimitState.Data memory) { + assert(_newPrevStakeLimit <= uint96(-1)); + assert(_data.prevStakeBlockNumber != 0); + + _data.prevStakeLimit = uint96(_newPrevStakeLimit); + _data.prevStakeBlockNumber = uint32(block.number); + + return _data; + } + + /** + * @notice set stake limit pause state (on or off) + * @dev input `_data` param is mutated and the func returns effectively the same pointer + * @param _data stake limit state struct + * @param _isPaused pause state flag + */ + function setStakeLimitPauseState( + StakeLimitState.Data memory _data, + bool _isPaused + ) internal view returns (StakeLimitState.Data memory) { + _data.prevStakeBlockNumber = uint32(_isPaused ? 0 : block.number); + + return _data; + } +} diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index bc12ca570..e5d7d8a89 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -20,7 +20,7 @@ import "../lib/MemUtils.sol"; * * See the comment of `INodeOperatorsRegistry`. * - * NOTE: the code below assumes moderate amount of node operators, e.g. up to 50. + * NOTE: the code below assumes moderate amount of node operators, i.e. up to `MAX_NODE_OPERATORS_COUNT`. */ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp { using SafeMath for uint256; @@ -38,12 +38,12 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp uint256 constant public PUBKEY_LENGTH = 48; uint256 constant public SIGNATURE_LENGTH = 96; + uint256 constant public MAX_NODE_OPERATORS_COUNT = 200; uint256 internal constant UINT64_MAX = uint256(uint64(-1)); bytes32 internal constant SIGNING_KEYS_MAPPING_NAME = keccak256("lido.NodeOperatorsRegistry.signingKeysMappingName"); - /// @dev Node Operator parameters and internal state struct NodeOperator { bool active; // a flag indicating if the operator can participate in further staking and reward distribution @@ -118,6 +118,8 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp returns (uint256 id) { id = getNodeOperatorsCount(); + require(id < MAX_NODE_OPERATORS_COUNT, "MAX_NODE_OPERATORS_COUNT_EXCEEDED"); + TOTAL_OPERATORS_COUNT_POSITION.setStorageUint256(id.add(1)); NodeOperator storage operator = operators[id]; @@ -142,14 +144,15 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp authP(SET_NODE_OPERATOR_ACTIVE_ROLE, arr(_id, _active ? uint256(1) : uint256(0))) operatorExists(_id) { + require(operators[_id].active != _active, "NODE_OPERATOR_ACTIVITY_ALREADY_SET"); + _increaseKeysOpIndex(); - if (operators[_id].active != _active) { - uint256 activeOperatorsCount = getActiveNodeOperatorsCount(); - if (_active) - ACTIVE_OPERATORS_COUNT_POSITION.setStorageUint256(activeOperatorsCount.add(1)); - else - ACTIVE_OPERATORS_COUNT_POSITION.setStorageUint256(activeOperatorsCount.sub(1)); - } + + uint256 activeOperatorsCount = getActiveNodeOperatorsCount(); + if (_active) + ACTIVE_OPERATORS_COUNT_POSITION.setStorageUint256(activeOperatorsCount.add(1)); + else + ACTIVE_OPERATORS_COUNT_POSITION.setStorageUint256(activeOperatorsCount.sub(1)); operators[_id].active = _active; @@ -163,6 +166,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp authP(SET_NODE_OPERATOR_NAME_ROLE, arr(_id)) operatorExists(_id) { + require(keccak256(operators[_id].name) != keccak256(_name), "NODE_OPERATOR_NAME_IS_THE_SAME"); operators[_id].name = _name; emit NodeOperatorNameSet(_id, _name); } @@ -175,6 +179,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp operatorExists(_id) validAddress(_rewardAddress) { + require(operators[_id].rewardAddress != _rewardAddress, "NODE_OPERATOR_ADDRESS_IS_THE_SAME"); operators[_id].rewardAddress = _rewardAddress; emit NodeOperatorRewardAddressSet(_id, _rewardAddress); } @@ -186,6 +191,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp authP(SET_NODE_OPERATOR_LIMIT_ROLE, arr(_id, uint256(_stakingLimit))) operatorExists(_id) { + require(operators[_id].stakingLimit != _stakingLimit, "NODE_OPERATOR_STAKING_LIMIT_IS_THE_SAME"); _increaseKeysOpIndex(); operators[_id].stakingLimit = _stakingLimit; emit NodeOperatorStakingLimitSet(_id, _stakingLimit); @@ -284,7 +290,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp authP(MANAGE_SIGNING_KEYS, arr(_operator_id)) { // removing from the last index to the highest one, so we won't get outside the array - for (uint256 i = _index + _amount; i > _index ; --i) { + for (uint256 i = _index.add(_amount); i > _index; --i) { _removeSigningKey(_operator_id, i - 1); } } @@ -308,7 +314,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp function removeSigningKeysOperatorBH(uint256 _operator_id, uint256 _index, uint256 _amount) external { require(msg.sender == operators[_operator_id].rewardAddress, "APP_AUTH_FAILED"); // removing from the last index to the highest one, so we won't get outside the array - for (uint256 i = _index + _amount; i > _index ; --i) { + for (uint256 i = _index.add(_amount); i > _index; --i) { _removeSigningKey(_operator_id, i - 1); } } @@ -334,7 +340,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp // Finding the best suitable operator uint256 bestOperatorIdx = cache.length; // 'not found' flag uint256 smallestStake; - // The loop is ligthweight comparing to an ether transfer and .deposit invocation + // The loop is lightweight comparing to an ether transfer and .deposit invocation for (uint256 idx = 0; idx < cache.length; ++idx) { entry = cache[idx]; @@ -386,6 +392,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp for (uint256 keyIndex = entry.initialUsedSigningKeys; keyIndex < entry.usedSigningKeys; ++keyIndex) { (bytes memory pubkey, bytes memory signature) = _loadSigningKey(entry.id, keyIndex); if (numAssignedKeys == 1) { + _increaseKeysOpIndex(); return (pubkey, signature); } else { MemUtils.copyBytes(pubkey, pubkeys, numLoadedKeys * PUBKEY_LENGTH); @@ -399,6 +406,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp } } + _increaseKeysOpIndex(); // numAssignedKeys is guaranteed to be > 0 here assert(numLoadedKeys == numAssignedKeys); return (pubkeys, signatures); } @@ -420,25 +428,25 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp shares = new uint256[](activeCount); uint256 idx = 0; - uint256 effectiveStakeTotal = 0; + uint256 activeValidatorsTotal = 0; for (uint256 operatorId = 0; operatorId < nodeOperatorCount; ++operatorId) { NodeOperator storage operator = operators[operatorId]; if (!operator.active) continue; - uint256 effectiveStake = operator.usedSigningKeys.sub(operator.stoppedValidators); - effectiveStakeTotal = effectiveStakeTotal.add(effectiveStake); + uint256 activeValidators = operator.usedSigningKeys.sub(operator.stoppedValidators); + activeValidatorsTotal = activeValidatorsTotal.add(activeValidators); recipients[idx] = operator.rewardAddress; - shares[idx] = effectiveStake; + shares[idx] = activeValidators; ++idx; } - if (effectiveStakeTotal == 0) + if (activeValidatorsTotal == 0) return (recipients, shares); - uint256 perValidatorReward = _totalRewardShares.div(effectiveStakeTotal); + uint256 perValidatorReward = _totalRewardShares.div(activeValidatorsTotal); for (idx = 0; idx < activeCount; ++idx) { shares[idx] = shares[idx].mul(perValidatorReward); @@ -537,8 +545,6 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp function _isEmptySigningKey(bytes memory _key) internal pure returns (bool) { assert(_key.length == PUBKEY_LENGTH); - // algorithm applicability constraint - assert(PUBKEY_LENGTH >= 32 && PUBKEY_LENGTH <= 64); uint256 k1; uint256 k2; @@ -551,7 +557,7 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp } function to64(uint256 v) internal pure returns (uint64) { - assert(v <= uint256(uint64(-1))); + assert(v <= UINT64_MAX); return uint64(v); } @@ -562,9 +568,6 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp function _storeSigningKey(uint256 _operator_id, uint256 _keyIndex, bytes memory _key, bytes memory _signature) internal { assert(_key.length == PUBKEY_LENGTH); assert(_signature.length == SIGNATURE_LENGTH); - // algorithm applicability constraints - assert(PUBKEY_LENGTH >= 32 && PUBKEY_LENGTH <= 64); - assert(0 == SIGNATURE_LENGTH % 32); // key uint256 offset = _signingKeyOffset(_operator_id, _keyIndex); @@ -642,10 +645,6 @@ contract NodeOperatorsRegistry is INodeOperatorsRegistry, IsContract, AragonApp } function _loadSigningKey(uint256 _operator_id, uint256 _keyIndex) internal view returns (bytes memory key, bytes memory signature) { - // algorithm applicability constraints - assert(PUBKEY_LENGTH >= 32 && PUBKEY_LENGTH <= 64); - assert(0 == SIGNATURE_LENGTH % 32); - uint256 offset = _signingKeyOffset(_operator_id, _keyIndex); // key diff --git a/contracts/0.4.24/nos/test_helpers/ERC721Mock.sol b/contracts/0.4.24/nos/test_helpers/ERC721Mock.sol new file mode 100644 index 000000000..e3f85f44a --- /dev/null +++ b/contracts/0.4.24/nos/test_helpers/ERC721Mock.sol @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2022 Lido + +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.4.24; + +import "openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; + +contract ERC721Mock is ERC721 { + constructor() ERC721() {} + + function mint(address _account, uint256 _tokenId) public { + _mint(_account, _tokenId); + } +} diff --git a/contracts/0.4.24/nos/test_helpers/PoolMock.sol b/contracts/0.4.24/nos/test_helpers/PoolMock.sol index 7828c4fed..0045bbe95 100644 --- a/contracts/0.4.24/nos/test_helpers/PoolMock.sol +++ b/contracts/0.4.24/nos/test_helpers/PoolMock.sol @@ -8,6 +8,7 @@ import "../../interfaces/INodeOperatorsRegistry.sol"; */ contract PoolMock { event KeysAssigned(bytes pubkeys, bytes signatures); + event KeysOpIndexSet(uint256 keysOpIndex); INodeOperatorsRegistry private operators; diff --git a/contracts/0.4.24/oracle/LidoOracle.sol b/contracts/0.4.24/oracle/LidoOracle.sol index d0159f525..46bccbd40 100644 --- a/contracts/0.4.24/oracle/LidoOracle.sol +++ b/contracts/0.4.24/oracle/LidoOracle.sol @@ -7,6 +7,7 @@ pragma solidity 0.4.24; import "@aragon/os/contracts/apps/AragonApp.sol"; import "@aragon/os/contracts/lib/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/introspection/ERC165Checker.sol"; import "../interfaces/IBeaconReportReceiver.sol"; import "../interfaces/ILido.sol"; @@ -14,6 +15,7 @@ import "../interfaces/ILidoOracle.sol"; import "./ReportUtils.sol"; + /** * @title Implementation of an ETH 2.0 -> ETH oracle * @@ -31,6 +33,7 @@ import "./ReportUtils.sol"; contract LidoOracle is ILidoOracle, AragonApp { using SafeMath for uint256; using ReportUtils for uint256; + using ERC165Checker for address; struct BeaconSpec { uint64 epochsPerFrame; @@ -73,7 +76,12 @@ contract LidoOracle is ILidoOracle, AragonApp { bytes32 internal constant BEACON_SPEC_POSITION = 0x805e82d53a51be3dfde7cfed901f1f96f5dad18e874708b082adb8841e8ca909; // keccak256("lido.LidoOracle.beaconSpec") - /// Version of the initialized contract data, v1 is 0 + /// Version of the initialized contract data + /// NB: Contract versioning starts from 1. + /// The version stored in CONTRACT_VERSION_POSITION equals to + /// - 0 right after deployment when no initializer is invoked yet + /// - N after calling initialize() during deployment from scratch, where N is the current contract version + /// - N after upgrading contract from the previous version (after calling finalize_vN()) bytes32 internal constant CONTRACT_VERSION_POSITION = 0x75be19a3f314d89bd1f84d30a6c84e2f1cd7afc7b6ca21876564c265113bb7e4; // keccak256("lido.LidoOracle.contractVersion") @@ -112,7 +120,8 @@ contract LidoOracle is ILidoOracle, AragonApp { bytes32 internal constant ALLOWED_BEACON_BALANCE_RELATIVE_DECREASE_POSITION = 0x92ba7776ed6c5d13cf023555a94e70b823a4aebd56ed522a77345ff5cd8a9109; // keccak256("lido.LidoOracle.allowedBeaconBalanceDecrease") - /// This variable is from v1: the last reported epoch, used only in the initializer + /// This is a dead variable: it was used only in v1 and in upgrade v1 --> v2 + /// Just keep in mind that storage at this position is occupied but with no actual usage bytes32 internal constant V1_LAST_REPORTED_EPOCH_ID_POSITION = 0xfe0250ed0c5d8af6526c6d133fccb8e5a55dd6b1aa6696ed0c327f8e517b5a94; // keccak256("lido.LidoOracle.lastReportedEpochId") @@ -177,6 +186,14 @@ contract LidoOracle is ILidoOracle, AragonApp { * @dev Specify 0 to disable this functionality */ function setBeaconReportReceiver(address _addr) external auth(SET_BEACON_REPORT_RECEIVER) { + if(_addr != address(0)) { + IBeaconReportReceiver iBeacon; + require( + _addr._supportsInterface(iBeacon.processLidoOracleReport.selector), + "BAD_BEACON_REPORT_RECEIVER" + ); + } + BEACON_REPORT_RECEIVER_POSITION.setStorageUint256(uint256(_addr)); emit BeaconReportReceiverSet(_addr); } @@ -330,20 +347,47 @@ contract LidoOracle is ILidoOracle, AragonApp { } /** - * @notice Initialize the contract v2 data, with sanity check bounds - * (`_allowedBeaconBalanceAnnualRelativeIncrease`, `_allowedBeaconBalanceRelativeDecrease`) - * @dev Original initialize function removed from v2 because it is invoked only once + * @notice Initialize the contract (version 3 for now) from scratch + * @dev For details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md + * @param _lido Address of Lido contract + * @param _epochsPerFrame Number of epochs per frame + * @param _slotsPerEpoch Number of slots per epoch + * @param _secondsPerSlot Number of seconds per slot + * @param _genesisTime Genesis time + * @param _allowedBeaconBalanceAnnualRelativeIncrease Allowed beacon balance annual relative increase (e.g. 1000 means 10% increase) + * @param _allowedBeaconBalanceRelativeDecrease Allowed beacon balance instantaneous decrease (e.g. 500 means 5% decrease) */ - function initialize_v2( + function initialize( + address _lido, + uint64 _epochsPerFrame, + uint64 _slotsPerEpoch, + uint64 _secondsPerSlot, + uint64 _genesisTime, uint256 _allowedBeaconBalanceAnnualRelativeIncrease, uint256 _allowedBeaconBalanceRelativeDecrease ) - external + external onlyInit { - require(CONTRACT_VERSION_POSITION.getStorageUint256() == 0, "ALREADY_INITIALIZED"); - CONTRACT_VERSION_POSITION.setStorageUint256(1); - emit ContractVersionSet(1); + assert(1 == ((1 << (MAX_MEMBERS - 1)) >> (MAX_MEMBERS - 1))); // static assert + + // We consider storage state right after deployment (no initialize() called yet) as version 0 + + // Initializations for v0 --> v1 + require(CONTRACT_VERSION_POSITION.getStorageUint256() == 0, "BASE_VERSION_MUST_BE_ZERO"); + + _setBeaconSpec( + _epochsPerFrame, + _slotsPerEpoch, + _secondsPerSlot, + _genesisTime + ); + + LIDO_POSITION.setStorageAddress(_lido); + QUORUM_POSITION.setStorageUint256(1); + emit QuorumChanged(1); + + // Initializations for v1 --> v2 ALLOWED_BEACON_BALANCE_ANNUAL_RELATIVE_INCREASE_POSITION .setStorageUint256(_allowedBeaconBalanceAnnualRelativeIncrease); emit AllowedBeaconBalanceAnnualRelativeIncreaseSet(_allowedBeaconBalanceAnnualRelativeIncrease); @@ -352,16 +396,39 @@ contract LidoOracle is ILidoOracle, AragonApp { .setStorageUint256(_allowedBeaconBalanceRelativeDecrease); emit AllowedBeaconBalanceRelativeDecreaseSet(_allowedBeaconBalanceRelativeDecrease); - // set last completed epoch as V1's contract last reported epoch, in the vast majority of - // cases this is true, in others the error is within a frame - uint256 lastReportedEpoch = V1_LAST_REPORTED_EPOCH_ID_POSITION.getStorageUint256(); - LAST_COMPLETED_EPOCH_ID_POSITION.setStorageUint256(lastReportedEpoch); - // set expected epoch to the first epoch for the next frame BeaconSpec memory beaconSpec = _getBeaconSpec(); - uint256 expectedEpoch = _getFrameFirstEpochId(lastReportedEpoch, beaconSpec) + beaconSpec.epochsPerFrame; + uint256 expectedEpoch = _getFrameFirstEpochId(0, beaconSpec) + beaconSpec.epochsPerFrame; EXPECTED_EPOCH_ID_POSITION.setStorageUint256(expectedEpoch); emit ExpectedEpochIdUpdated(expectedEpoch); + + // Initializations for v2 --> v3 + _initialize_v3(); + + // Needed to finish the Aragon part of initialization (otherwise auth() modifiers will fail) + initialized(); + } + + /** + * @notice A function to finalize upgrade to v3 (from v1). Can be called only once + * @dev Value 2 in CONTRACT_VERSION_POSITION is skipped due to change in numbering + * For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md + */ + function finalizeUpgrade_v3() external { + require(CONTRACT_VERSION_POSITION.getStorageUint256() == 1, "WRONG_BASE_VERSION"); + + _initialize_v3(); + } + + /** + * @notice A dummy incremental v1/v2 --> v3 initialize function. Just corrects version number in storage + * @dev This function is introduced just to set in correspondence version number in storage, + * semantic version of the contract and number N used in naming of _initialize_nN/finalizeUpgrade_vN. + * NB, that thus version 2 is skipped + */ + function _initialize_v3() internal { + CONTRACT_VERSION_POSITION.setStorageUint256(3); + emit ContractVersionSet(3); } /** @@ -370,9 +437,10 @@ contract LidoOracle is ILidoOracle, AragonApp { function addOracleMember(address _member) external auth(MANAGE_MEMBERS) { require(address(0) != _member, "BAD_ARGUMENT"); require(MEMBER_NOT_FOUND == _getMemberId(_member), "MEMBER_EXISTS"); + require(members.length < MAX_MEMBERS, "TOO_MANY_MEMBERS"); members.push(_member); - require(members.length < MAX_MEMBERS, "TOO_MANY_MEMBERS"); + emit MemberAdded(_member); } @@ -567,7 +635,7 @@ contract LidoOracle is ILidoOracle, AragonApp { // report to the Lido and collect stats ILido lido = getLido(); uint256 prevTotalPooledEther = lido.totalSupply(); - lido.pushBeacon(_beaconValidators, _beaconBalanceEth1); + lido.handleOracleReport(_beaconValidators, _beaconBalanceEth1); uint256 postTotalPooledEther = lido.totalSupply(); PRE_COMPLETED_TOTAL_POOLED_ETHER_POSITION.setStorageUint256(prevTotalPooledEther); diff --git a/contracts/0.4.24/template/LidoTemplate.sol b/contracts/0.4.24/template/LidoTemplate.sol index 64d9e5f4b..5f8fa4138 100644 --- a/contracts/0.4.24/template/LidoTemplate.sol +++ b/contracts/0.4.24/template/LidoTemplate.sol @@ -359,16 +359,15 @@ contract LidoTemplate is IsContract { noInit )); - // TODO: uncomment on merge of branch 'redo-oracle-initialization' - // state.oracle.initialize( - // state.lido, - // _beaconSpec[0], // epochsPerFrame - // _beaconSpec[1], // slotsPerEpoch - // _beaconSpec[2], // secondsPerSlot - // _beaconSpec[3], // genesisTime - // 100000, - // 50000 - // ); + state.oracle.initialize( + state.lido, + _beaconSpec[0], // epochsPerFrame + _beaconSpec[1], // slotsPerEpoch + _beaconSpec[2], // secondsPerSlot + _beaconSpec[3], // genesisTime + 100000, + 50000 + ); state.operators.initialize(state.lido); @@ -395,7 +394,7 @@ contract LidoTemplate is IsContract { uint64 _vestingCliff, uint64 _vestingEnd, bool _vestingRevokable, - uint256 _extectedFinalTotalSupply + uint256 _expectedFinalTotalSupply ) onlyOwner external @@ -415,7 +414,7 @@ contract LidoTemplate is IsContract { _vestingCliff, _vestingEnd, _vestingRevokable, - _extectedFinalTotalSupply + _expectedFinalTotalSupply ); emit TmplTokensIssued(totalAmount); @@ -623,7 +622,7 @@ contract LidoTemplate is IsContract { } // using loops to save contract size - bytes32[7] memory perms; + bytes32[10] memory perms; // Oracle perms[0] = _state.oracle.MANAGE_MEMBERS(); @@ -653,12 +652,15 @@ contract LidoTemplate is IsContract { perms[0] = _state.lido.PAUSE_ROLE(); perms[1] = _state.lido.MANAGE_FEE(); perms[2] = _state.lido.MANAGE_WITHDRAWAL_KEY(); - perms[3] = _state.lido.SET_ORACLE(); + perms[3] = _state.lido.MANAGE_PROTOCOL_CONTRACTS_ROLE(); perms[4] = _state.lido.BURN_ROLE(); - perms[5] = _state.lido.SET_TREASURY(); - perms[6] = _state.lido.SET_INSURANCE_FUND(); + perms[5] = _state.lido.RESUME_ROLE(); + perms[6] = _state.lido.STAKING_PAUSE_ROLE(); + perms[7] = _state.lido.STAKING_CONTROL_ROLE(); + perms[8] = _state.lido.SET_EL_REWARDS_VAULT_ROLE(); + perms[9] = _state.lido.SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE(); - for (i = 0; i < 7; ++i) { + for (i = 0; i < 10; ++i) { _createPermissionForVoting(acl, _state.lido, perms[i], voting); } } diff --git a/contracts/0.4.24/test_helpers/BeaconReportReceiverMock.sol b/contracts/0.4.24/test_helpers/BeaconReportReceiverMock.sol index dd560c0e9..4a5305f5f 100644 --- a/contracts/0.4.24/test_helpers/BeaconReportReceiverMock.sol +++ b/contracts/0.4.24/test_helpers/BeaconReportReceiverMock.sol @@ -4,15 +4,22 @@ pragma solidity 0.4.24; +import "openzeppelin-solidity/contracts/introspection/ERC165.sol"; + import "../interfaces/IBeaconReportReceiver.sol"; -contract BeaconReportReceiverMock is IBeaconReportReceiver { +contract BeaconReportReceiverMock is IBeaconReportReceiver, ERC165 { uint256 public postTotalPooledEther; uint256 public preTotalPooledEther; uint256 public timeElapsed; uint256 public gas; - + + constructor() { + IBeaconReportReceiver iBeacon; + _registerInterface(iBeacon.processLidoOracleReport.selector); + } + function processLidoOracleReport(uint256 _postTotalPooledEther, uint256 _preTotalPooledEther, uint256 _timeElapsed) external { @@ -22,3 +29,10 @@ contract BeaconReportReceiverMock is IBeaconReportReceiver { timeElapsed = _timeElapsed; } } + +contract BeaconReportReceiverMockWithoutERC165 is IBeaconReportReceiver { + function processLidoOracleReport(uint256 _postTotalPooledEther, + uint256 _preTotalPooledEther, + uint256 _timeElapsed) external { + } +} diff --git a/contracts/0.4.24/test_helpers/LidoMock.sol b/contracts/0.4.24/test_helpers/LidoMock.sol index 8ae94289a..be7dc3844 100644 --- a/contracts/0.4.24/test_helpers/LidoMock.sol +++ b/contracts/0.4.24/test_helpers/LidoMock.sol @@ -13,14 +13,14 @@ import "./VaultMock.sol"; */ contract LidoMock is Lido { function initialize( - IDepositContract depositContract, + IDepositContract _depositContract, address _oracle, INodeOperatorsRegistry _operators ) public { super.initialize( - depositContract, + _depositContract, _oracle, _operators, new VaultMock(), @@ -28,6 +28,7 @@ contract LidoMock is Lido { ); _resume(); + _resumeStaking(); } /** @@ -53,22 +54,6 @@ contract LidoMock is Lido { return _toLittleEndian64(_value); } - /** - * @dev Public wrapper of internal fun. Internal function sets the address of Deposit contract - * @param _contract the address of Deposit contract - */ - function setDepositContract(IDepositContract _contract) public { - _setDepositContract(_contract); - } - - /** - * @dev Public wrapper of internal fun. Internal function sets node operator registry address - * @param _r registry of node operators - */ - function setOperators(INodeOperatorsRegistry _r) public { - _setOperators(_r); - } - /** * @dev Only for testing recovery vault */ diff --git a/contracts/0.4.24/test_helpers/LidoMockForOracle.sol b/contracts/0.4.24/test_helpers/LidoMockForOracle.sol index 695d22f81..c9b1299e9 100644 --- a/contracts/0.4.24/test_helpers/LidoMockForOracle.sol +++ b/contracts/0.4.24/test_helpers/LidoMockForOracle.sol @@ -15,7 +15,7 @@ contract LidoMockForOracle { return totalPooledEther; } - function pushBeacon(uint256 /*_beaconValidators*/, uint256 _beaconBalance) external { + function handleOracleReport(uint256 /*_beaconValidators*/, uint256 _beaconBalance) external { totalPooledEther = _beaconBalance; } diff --git a/contracts/0.4.24/test_helpers/LidoOracleMock.sol b/contracts/0.4.24/test_helpers/LidoOracleMock.sol index 90adbe2c4..6b10eb231 100644 --- a/contracts/0.4.24/test_helpers/LidoOracleMock.sol +++ b/contracts/0.4.24/test_helpers/LidoOracleMock.sol @@ -13,29 +13,6 @@ import "../oracle/LidoOracle.sol"; contract LidoOracleMock is LidoOracle { uint256 private time; - // Original initialize function from v1 - function initialize( - address _lido, - uint64 _epochsPerFrame, - uint64 _slotsPerEpoch, - uint64 _secondsPerSlot, - uint64 _genesisTime - ) - public onlyInit - { - assert(1 == ((1 << (MAX_MEMBERS - 1)) >> (MAX_MEMBERS - 1))); // static assert - _setBeaconSpec( - _epochsPerFrame, - _slotsPerEpoch, - _secondsPerSlot, - _genesisTime - ); - LIDO_POSITION.setStorageAddress(_lido); - QUORUM_POSITION.setStorageUint256(1); - emit QuorumChanged(1); - initialized(); - } - function setV1LastReportedEpochForTest(uint256 _epoch) public { V1_LAST_REPORTED_EPOCH_ID_POSITION.setStorageUint256(_epoch); } diff --git a/contracts/0.4.24/test_helpers/LidoPushableMock.sol b/contracts/0.4.24/test_helpers/LidoPushableMock.sol index b5e29823b..704b3d346 100644 --- a/contracts/0.4.24/test_helpers/LidoPushableMock.sol +++ b/contracts/0.4.24/test_helpers/LidoPushableMock.sol @@ -9,12 +9,12 @@ import "./VaultMock.sol"; /** - * @dev Mock for unit-testing pushBeacon and how reward get calculated + * @dev Mock for unit-testing handleOracleReport and how reward get calculated */ contract LidoPushableMock is Lido { uint256 public totalRewards; - bool public distributeRewardsCalled; + bool public distributeFeeCalled; function initialize( IDepositContract depositContract, @@ -52,18 +52,18 @@ contract LidoPushableMock is Lido { } function initialize(address _oracle) public onlyInit { - _setOracle(_oracle); + _setProtocolContracts(_oracle, _oracle, _oracle); _resume(); initialized(); } - function resetDistributeRewards() public { + function resetDistributeFee() public { totalRewards = 0; - distributeRewardsCalled = false; + distributeFeeCalled = false; } - function distributeRewards(uint256 _totalRewards) internal { + function distributeFee(uint256 _totalRewards) internal { totalRewards = _totalRewards; - distributeRewardsCalled = true; + distributeFeeCalled = true; } } diff --git a/contracts/0.4.24/test_helpers/OracleMock.sol b/contracts/0.4.24/test_helpers/OracleMock.sol index bcbcab128..4293d430f 100644 --- a/contracts/0.4.24/test_helpers/OracleMock.sol +++ b/contracts/0.4.24/test_helpers/OracleMock.sol @@ -19,7 +19,7 @@ contract OracleMock { } function reportBeacon(uint256 _epochId, uint128 _beaconValidators, uint128 _beaconBalance) external { - pool.pushBeacon(_beaconValidators, _beaconBalance); + pool.handleOracleReport(_beaconValidators, _beaconBalance); } function setBeaconReportReceiver(address _receiver) { diff --git a/contracts/0.4.24/test_helpers/StETHMock.sol b/contracts/0.4.24/test_helpers/StETHMock.sol index 5d5f4c81e..f93b7d8e2 100644 --- a/contracts/0.4.24/test_helpers/StETHMock.sol +++ b/contracts/0.4.24/test_helpers/StETHMock.sol @@ -52,5 +52,6 @@ contract StETHMock is StETH { internal { emit Transfer(address(0), _to, getPooledEthByShares(_sharesAmount)); + emit TransferShares(address(0), _to, _sharesAmount); } } diff --git a/contracts/0.4.24/test_helpers/StakeLimitUtilsMock.sol b/contracts/0.4.24/test_helpers/StakeLimitUtilsMock.sol new file mode 100644 index 000000000..3b3f4d244 --- /dev/null +++ b/contracts/0.4.24/test_helpers/StakeLimitUtilsMock.sol @@ -0,0 +1,95 @@ + +// SPDX-FileCopyrightText: 2022 Lido + +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.4.24; + +import "../lib/StakeLimitUtils.sol"; + +contract StakeLimitUtilsMock { + using UnstructuredStorage for bytes32; + using StakeLimitUnstructuredStorage for bytes32; + using StakeLimitUtils for StakeLimitState.Data; + + bytes32 internal constant STAKING_STATE_POSITION = keccak256("abcdef"); + + function getStorageStakeLimit(uint256 _slotValue) public view returns ( + uint32 prevStakeBlockNumber, + uint96 prevStakeLimit, + uint32 maxStakeLimitGrowthBlocks, + uint96 maxStakeLimit + ) { + STAKING_STATE_POSITION.setStorageUint256(_slotValue); + StakeLimitState.Data memory data = STAKING_STATE_POSITION.getStorageStakeLimitStruct(); + + prevStakeBlockNumber = data.prevStakeBlockNumber; + prevStakeLimit = data.prevStakeLimit; + maxStakeLimitGrowthBlocks = data.maxStakeLimitGrowthBlocks; + maxStakeLimit = data.maxStakeLimit; + } + + function setStorageStakeLimitStruct( + uint32 _prevStakeBlockNumber, + uint96 _prevStakeLimit, + uint32 _maxStakeLimitGrowthBlocks, + uint96 _maxStakeLimit + ) public view returns (uint256 ret) { + StakeLimitState.Data memory data; + data.prevStakeBlockNumber = _prevStakeBlockNumber; + data.prevStakeLimit = _prevStakeLimit; + data.maxStakeLimitGrowthBlocks = _maxStakeLimitGrowthBlocks; + data.maxStakeLimit = _maxStakeLimit; + + STAKING_STATE_POSITION.setStorageStakeLimitStruct(data); + return STAKING_STATE_POSITION.getStorageUint256(); + } + + function calculateCurrentStakeLimit(uint256 _slotValue) public view returns(uint256 limit) { + STAKING_STATE_POSITION.setStorageUint256(_slotValue); + return STAKING_STATE_POSITION.getStorageStakeLimitStruct().calculateCurrentStakeLimit(); + } + + function isStakingPaused(uint256 _slotValue) public view returns(bool) { + STAKING_STATE_POSITION.setStorageUint256(_slotValue); + return STAKING_STATE_POSITION.getStorageStakeLimitStruct().isStakingPaused(); + } + + function isStakingLimitSet(uint256 _slotValue) public view returns(bool) { + STAKING_STATE_POSITION.setStorageUint256(_slotValue); + return STAKING_STATE_POSITION.getStorageStakeLimitStruct().isStakingLimitSet(); + } + + function setStakingLimit(uint256 _slotValue, uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock) public view { + STAKING_STATE_POSITION.setStorageUint256(_slotValue); + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + STAKING_STATE_POSITION.getStorageStakeLimitStruct().setStakingLimit( + _maxStakeLimit, _stakeLimitIncreasePerBlock + ) + ); + } + + function removeStakingLimit(uint256 _slotValue) public view returns(uint256) { + STAKING_STATE_POSITION.setStorageUint256(_slotValue); + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + STAKING_STATE_POSITION.getStorageStakeLimitStruct().removeStakingLimit() + ); + return STAKING_STATE_POSITION.getStorageUint256(); + } + + function updatePrevStakeLimit(uint256 _slotValue, uint256 _newPrevLimit) public view returns(uint256) { + STAKING_STATE_POSITION.setStorageUint256(_slotValue); + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + STAKING_STATE_POSITION.getStorageStakeLimitStruct().updatePrevStakeLimit(_newPrevLimit) + ); + return STAKING_STATE_POSITION.getStorageUint256(); + } + + function setStakeLimitPauseState(uint256 _slotValue, bool _isPaused) public view returns(uint256) { + STAKING_STATE_POSITION.setStorageUint256(_slotValue); + STAKING_STATE_POSITION.setStorageStakeLimitStruct( + STAKING_STATE_POSITION.getStorageStakeLimitStruct().setStakeLimitPauseState(_isPaused) + ); + return STAKING_STATE_POSITION.getStorageUint256(); + } +} diff --git a/contracts/0.8.9/CompositePostRebaseBeaconReceiver.sol b/contracts/0.8.9/CompositePostRebaseBeaconReceiver.sol index 231791e99..bd332d752 100644 --- a/contracts/0.8.9/CompositePostRebaseBeaconReceiver.sol +++ b/contracts/0.8.9/CompositePostRebaseBeaconReceiver.sol @@ -5,6 +5,7 @@ /* See contracts/COMPILERS.md */ pragma solidity 0.8.9; +import "@openzeppelin/contracts-v4.4/utils/introspection/ERC165.sol"; import "./OrderedCallbacksArray.sol"; import "./interfaces/IBeaconReportReceiver.sol"; @@ -14,7 +15,7 @@ import "./interfaces/IBeaconReportReceiver.sol"; * Contract adds permission modifiers. * Only the `ORACLE` address can invoke `processLidoOracleReport` function. */ -contract CompositePostRebaseBeaconReceiver is OrderedCallbacksArray, IBeaconReportReceiver { +contract CompositePostRebaseBeaconReceiver is OrderedCallbacksArray, IBeaconReportReceiver, ERC165 { address public immutable ORACLE; modifier onlyOracle() { @@ -25,7 +26,7 @@ contract CompositePostRebaseBeaconReceiver is OrderedCallbacksArray, IBeaconRepo constructor( address _voting, address _oracle - ) OrderedCallbacksArray(_voting) { + ) OrderedCallbacksArray(_voting, type(IBeaconReportReceiver).interfaceId) { require(_oracle != address(0), "ORACLE_ZERO_ADDRESS"); ORACLE = _oracle; @@ -35,7 +36,7 @@ contract CompositePostRebaseBeaconReceiver is OrderedCallbacksArray, IBeaconRepo uint256 _postTotalPooledEther, uint256 _preTotalPooledEther, uint256 _timeElapsed - ) external override onlyOracle { + ) external virtual override onlyOracle { uint256 callbacksLen = callbacksLength(); for (uint256 brIndex = 0; brIndex < callbacksLen; brIndex++) { @@ -47,4 +48,11 @@ contract CompositePostRebaseBeaconReceiver is OrderedCallbacksArray, IBeaconRepo ); } } + + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(IBeaconReportReceiver).interfaceId + || super.supportsInterface(_interfaceId) + ); + } } diff --git a/contracts/0.8.9/DepositSecurityModule.sol b/contracts/0.8.9/DepositSecurityModule.sol index dc6ad889f..3f0d59c63 100644 --- a/contracts/0.8.9/DepositSecurityModule.sol +++ b/contracts/0.8.9/DepositSecurityModule.sol @@ -74,6 +74,8 @@ contract DepositSecurityModule { uint256 _minDepositBlockDistance, uint256 _pauseIntentValidityPeriodBlocks ) { + require(_lido != address(0), "LIDO_ZERO_ADDRESS"); + require(_depositContract != address(0), "DEPOSIT_CONTRACT_ZERO_ADDRESS"); LIDO = _lido; DEPOSIT_CONTRACT = _depositContract; @@ -94,12 +96,8 @@ contract DepositSecurityModule { _setMaxDeposits(_maxDepositsPerBlock); _setMinDepositBlockDistance(_minDepositBlockDistance); _setPauseIntentValidityPeriodBlocks(_pauseIntentValidityPeriodBlocks); - - paused = false; - lastDepositBlock = 0; } - /** * Returns the owner address. */ @@ -147,14 +145,14 @@ contract DepositSecurityModule { /** - * Returns `PAUSE_INTENT_VALIDITY_PERIOD_BLOCKS` (see `pauseDeposits`). + * Returns current `pauseIntentValidityPeriodBlocks` contract parameter (see `pauseDeposits`). */ function getPauseIntentValidityPeriodBlocks() external view returns (uint256) { return pauseIntentValidityPeriodBlocks; } /** - * Sets `PAUSE_INTENT_VALIDITY_PERIOD_BLOCKS`. Only callable by the owner. + * Sets `pauseIntentValidityPeriodBlocks`. Only callable by the owner. */ function setPauseIntentValidityPeriodBlocks(uint256 newValue) external onlyOwner { _setPauseIntentValidityPeriodBlocks(newValue); @@ -168,14 +166,14 @@ contract DepositSecurityModule { /** - * Returns `MAX_DEPOSITS_PER_BLOCK` (see `depositBufferedEther`). + * Returns `maxDepositsPerBlock` (see `depositBufferedEther`). */ function getMaxDeposits() external view returns (uint256) { return maxDepositsPerBlock; } /** - * Sets `MAX_DEPOSITS_PER_BLOCK`. Only callable by the owner. + * Sets `maxDepositsPerBlock`. Only callable by the owner. */ function setMaxDeposits(uint256 newValue) external onlyOwner { _setMaxDeposits(newValue); @@ -188,14 +186,14 @@ contract DepositSecurityModule { /** - * Returns `MIN_DEPOSIT_BLOCK_DISTANCE` (see `depositBufferedEther`). + * Returns `minDepositBlockDistance` (see `depositBufferedEther`). */ function getMinDepositBlockDistance() external view returns (uint256) { return minDepositBlockDistance; } /** - * Sets `MIN_DEPOSIT_BLOCK_DISTANCE`. Only callable by the owner. + * Sets `minDepositBlockDistance`. Only callable by the owner. */ function setMinDepositBlockDistance(uint256 newValue) external onlyOwner { _setMinDepositBlockDistance(newValue); @@ -203,8 +201,10 @@ contract DepositSecurityModule { function _setMinDepositBlockDistance(uint256 newValue) internal { require(newValue > 0, "invalid value for minDepositBlockDistance: must be greater then 0"); - minDepositBlockDistance = newValue; - emit MinDepositBlockDistanceChanged(newValue); + if (newValue != minDepositBlockDistance) { + minDepositBlockDistance = newValue; + emit MinDepositBlockDistanceChanged(newValue); + } } @@ -220,7 +220,7 @@ contract DepositSecurityModule { } function _setGuardianQuorum(uint256 newValue) internal { - // we're intentionally allowing setting quorum value higher than the number of quardians + // we're intentionally allowing setting quorum value higher than the number of guardians quorum = newValue; emit GuardianQuorumChanged(newValue); } @@ -280,6 +280,7 @@ contract DepositSecurityModule { } function _addGuardian(address addr) internal { + require(addr != address(0), "guardian zero address"); require(!_isGuardian(addr), "duplicate address"); guardians.push(addr); guardianIndicesOneBased[addr] = guardians.length; @@ -327,7 +328,7 @@ contract DepositSecurityModule { * is a valid signature by the guardian with index guardianIndex of the data * defined below. * - * 2. block.number - blockNumber <= PAUSE_INTENT_VALIDITY_PERIOD_BLOCKS + * 2. block.number - blockNumber <= pauseIntentValidityPeriodBlocks * * The signature, if present, must be produced for keccak256 hash of the following * message (each component taking 32 bytes): @@ -335,6 +336,10 @@ contract DepositSecurityModule { * | PAUSE_MESSAGE_PREFIX | blockNumber */ function pauseDeposits(uint256 blockNumber, Signature memory sig) external { + // In case of an emergency function `pauseDeposits` is supposed to be called + // by all guardians. Thus only the first call will do the actual change. But + // the other calls would be OK operations from the point of view of protocol’s logic. + // Thus we prefer not to use “error” semantics which is implied by `require`. if (paused) { return; } @@ -379,6 +384,14 @@ contract DepositSecurityModule { } + /** + * Sets `lastDepositBlock`. Only callable by the owner. + */ + function setLastDepositBlock(uint256 newLastDepositBlock) external onlyOwner { + lastDepositBlock = newLastDepositBlock; + } + + /** * Returns whether depositBufferedEther can be called, given that the caller will provide * guardian attestations of non-stale deposit root and `keysOpIndex`, and the number of @@ -390,14 +403,14 @@ contract DepositSecurityModule { /** - * Calls Lido.depositBufferedEther(MAX_DEPOSITS_PER_BLOCK). + * Calls Lido.depositBufferedEther(maxDepositsPerBlock). * * Reverts if any of the following is true: * 1. IDepositContract.get_deposit_root() != depositRoot. * 2. INodeOperatorsRegistry.getKeysOpIndex() != keysOpIndex. * 3. The number of guardian signatures is less than getGuardianQuorum(). * 4. An invalid or non-guardian signature received. - * 5. block.number - getLastDepositBlock() < MIN_DEPOSIT_BLOCK_DISTANCE. + * 5. block.number - getLastDepositBlock() < minDepositBlockDistance. * 6. blockhash(blockNumber) != blockHash. * * Signatures must be sorted in ascending order by index of the guardian. Each signature must diff --git a/contracts/0.8.9/LidoExecutionLayerRewardsVault.sol b/contracts/0.8.9/LidoExecutionLayerRewardsVault.sol new file mode 100644 index 000000000..2d1250be2 --- /dev/null +++ b/contracts/0.8.9/LidoExecutionLayerRewardsVault.sol @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2021 Lido + +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +pragma solidity 0.8.9; + +import "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; + +interface ILido { + /** + * @notice A payable function supposed to be called only by LidoExecLayerRewardsVault contract + * @dev We need a dedicated function because funds received by the default payable function + * are treated as a user deposit + */ + function receiveELRewards() external payable; +} + + +/** + * @title A vault for temporary storage of execution layer rewards (MEV and tx priority fee) + */ +contract LidoExecutionLayerRewardsVault { + using SafeERC20 for IERC20; + + address public immutable LIDO; + address public immutable TREASURY; + + /** + * Emitted when the ERC20 `token` recovered (i.e. transferred) + * to the Lido treasury address by `requestedBy` sender. + */ + event ERC20Recovered( + address indexed requestedBy, + address indexed token, + uint256 amount + ); + + /** + * Emitted when the ERC721-compatible `token` (NFT) recovered (i.e. transferred) + * to the Lido treasury address by `requestedBy` sender. + */ + event ERC721Recovered( + address indexed requestedBy, + address indexed token, + uint256 tokenId + ); + + /** + * Emitted when the vault received ETH + */ + event ETHReceived( + uint256 amount + ); + + /** + * Ctor + * + * @param _lido the Lido token (stETH) address + * @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces) + */ + constructor(address _lido, address _treasury) { + require(_lido != address(0), "LIDO_ZERO_ADDRESS"); + require(_treasury != address(0), "TREASURY_ZERO_ADDRESS"); + + LIDO = _lido; + TREASURY = _treasury; + } + + /** + * @notice Allows the contract to receive ETH + * @dev execution layer rewards may be sent as plain ETH transfers + */ + receive() external payable { + emit ETHReceived(msg.value); + } + + /** + * @notice Withdraw all accumulated rewards to Lido contract + * @dev Can be called only by the Lido contract + * @param _maxAmount Max amount of ETH to withdraw + * @return amount of funds received as execution layer rewards (in wei) + */ + function withdrawRewards(uint256 _maxAmount) external returns (uint256 amount) { + require(msg.sender == LIDO, "ONLY_LIDO_CAN_WITHDRAW"); + + uint256 balance = address(this).balance; + amount = (balance > _maxAmount) ? _maxAmount : balance; + if (amount > 0) { + ILido(LIDO).receiveELRewards{value: amount}(); + } + return amount; + } + + /** + * Transfers a given `_amount` of an ERC20-token (defined by the `_token` contract address) + * currently belonging to the burner contract address to the Lido treasury address. + * + * @param _token an ERC20-compatible token + * @param _amount token amount + */ + function recoverERC20(address _token, uint256 _amount) external { + require(_amount > 0, "ZERO_RECOVERY_AMOUNT"); + + emit ERC20Recovered(msg.sender, _token, _amount); + + IERC20(_token).safeTransfer(TREASURY, _amount); + } + + /** + * Transfers a given token_id of an ERC721-compatible NFT (defined by the token contract address) + * currently belonging to the burner contract address to the Lido treasury address. + * + * @param _token an ERC721-compatible token + * @param _tokenId minted token id + */ + function recoverERC721(address _token, uint256 _tokenId) external { + emit ERC721Recovered(msg.sender, _token, _tokenId); + + IERC721(_token).transferFrom(address(this), TREASURY, _tokenId); + } +} diff --git a/contracts/0.8.9/OrderedCallbacksArray.sol b/contracts/0.8.9/OrderedCallbacksArray.sol index c13e8e36b..be684e120 100644 --- a/contracts/0.8.9/OrderedCallbacksArray.sol +++ b/contracts/0.8.9/OrderedCallbacksArray.sol @@ -5,18 +5,24 @@ /* See contracts/COMPILERS.md */ pragma solidity 0.8.9; +import "@openzeppelin/contracts-v4.4/utils/introspection/ERC165Checker.sol"; + import "./interfaces/IOrderedCallbacksArray.sol"; /** * @title Contract defining an ordered callbacks array supporting add/insert/remove ops * - * Contract adds permission modifiers ontop of `IOderedCallbacksArray` interface functions. + * Contract adds permission modifiers atop of `IOrderedCallbacksArray` interface functions. * Only the `VOTING` address can invoke storage mutating (add/insert/remove) functions. */ contract OrderedCallbacksArray is IOrderedCallbacksArray { + using ERC165Checker for address; + uint256 public constant MAX_CALLBACKS_COUNT = 16; + bytes4 constant INVALID_INTERFACE_ID = 0xffffffff; address public immutable VOTING; + bytes4 public immutable REQUIRED_INTERFACE; address[] public callbacks; @@ -25,10 +31,12 @@ contract OrderedCallbacksArray is IOrderedCallbacksArray { _; } - constructor(address _voting) { + constructor(address _voting, bytes4 _requiredIface) { + require(_requiredIface != INVALID_INTERFACE_ID, "INVALID_IFACE"); require(_voting != address(0), "VOTING_ZERO_ADDRESS"); VOTING = _voting; + REQUIRED_INTERFACE = _requiredIface; } function callbacksLength() public view override returns (uint256) { @@ -58,6 +66,7 @@ contract OrderedCallbacksArray is IOrderedCallbacksArray { function _insertCallback(address _callback, uint256 _atIndex) private { require(_callback != address(0), "CALLBACK_ZERO_ADDRESS"); + require(_callback.supportsInterface(REQUIRED_INTERFACE), "BAD_CALLBACK_INTERFACE"); uint256 oldCArrayLength = callbacks.length; require(_atIndex <= oldCArrayLength, "INDEX_IS_OUT_OF_RANGE"); diff --git a/contracts/0.8.9/SelfOwnedStETHBurner.sol b/contracts/0.8.9/SelfOwnedStETHBurner.sol index 8a26a5bd6..1894f77cc 100644 --- a/contracts/0.8.9/SelfOwnedStETHBurner.sol +++ b/contracts/0.8.9/SelfOwnedStETHBurner.sol @@ -7,8 +7,11 @@ pragma solidity 0.8.9; import "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-v4.4/utils/introspection/ERC165.sol"; import "@openzeppelin/contracts-v4.4/utils/math/Math.sol"; import "./interfaces/IBeaconReportReceiver.sol"; +import "./interfaces/ISelfOwnedStETHBurner.sol"; /** * @title Interface defining a Lido liquid staking pool @@ -73,7 +76,9 @@ interface IOracle { * * @dev Burning stETH means 'decrease total underlying shares amount to perform stETH token rebase' */ -contract SelfOwnedStETHBurner is IBeaconReportReceiver { +contract SelfOwnedStETHBurner is ISelfOwnedStETHBurner, IBeaconReportReceiver, ERC165 { + using SafeERC20 for IERC20; + uint256 private constant MAX_BASIS_POINTS = 10000; uint256 private coverSharesBurnRequested; @@ -115,7 +120,7 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver { ); /** - * Emitted when the excessive stETH `amount` (corresponding to `sharesAmount` shares) recovered (e.g. transferred) + * Emitted when the excessive stETH `amount` (corresponding to `sharesAmount` shares) recovered (i.e. transferred) * to the Lido treasure address by `requestedBy` sender. */ event ExcessStETHRecovered( @@ -125,7 +130,7 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver { ); /** - * Emitted when the ERC20 `token` recovered (e.g. transferred) + * Emitted when the ERC20 `token` recovered (i.e. transferred) * to the Lido treasure address by `requestedBy` sender. */ event ERC20Recovered( @@ -135,7 +140,7 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver { ); /** - * Emitted when the ERC721-compatible `token` (NFT) recovered (e.g. transferred) + * Emitted when the ERC721-compatible `token` (NFT) recovered (i.e. transferred) * to the Lido treasure address by `requestedBy` sender. */ event ERC721Recovered( @@ -265,7 +270,7 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver { emit ERC20Recovered(msg.sender, _token, _amount); - require(IERC20(_token).transfer(TREASURY, _amount)); + IERC20(_token).safeTransfer(TREASURY, _amount); } /** @@ -287,7 +292,7 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver { * Resets `coverSharesBurnRequested` and `nonCoverSharesBurnRequested` counters to zero. * Does nothing if there are no pending burning requests. */ - function processLidoOracleReport(uint256, uint256, uint256) external override { + function processLidoOracleReport(uint256, uint256, uint256) external virtual override { uint256 memCoverSharesBurnRequested = coverSharesBurnRequested; uint256 memNonCoverSharesBurnRequested = nonCoverSharesBurnRequested; @@ -346,14 +351,14 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver { /** * Returns the total cover shares ever burnt. */ - function getCoverSharesBurnt() external view returns (uint256) { + function getCoverSharesBurnt() external view virtual override returns (uint256) { return totalCoverSharesBurnt; } /** * Returns the total non-cover shares ever burnt. */ - function getNonCoverSharesBurnt() external view returns (uint256) { + function getNonCoverSharesBurnt() external view virtual override returns (uint256) { return totalNonCoverSharesBurnt; } @@ -379,6 +384,14 @@ contract SelfOwnedStETHBurner is IBeaconReportReceiver { return ILido(LIDO).getPooledEthByShares(totalShares - sharesBurnRequested); } + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(IBeaconReportReceiver).interfaceId + || _interfaceId == type(ISelfOwnedStETHBurner).interfaceId + || super.supportsInterface(_interfaceId) + ); + } + function _requestBurnMyStETH(uint256 _stETH2Burn, bool _isCover) private { require(_stETH2Burn > 0, "ZERO_BURN_AMOUNT"); require(msg.sender == VOTING, "MSG_SENDER_MUST_BE_VOTING"); diff --git a/contracts/0.8.9/interfaces/ISelfOwnedStETHBurner.sol b/contracts/0.8.9/interfaces/ISelfOwnedStETHBurner.sol new file mode 100644 index 000000000..6c66044c4 --- /dev/null +++ b/contracts/0.8.9/interfaces/ISelfOwnedStETHBurner.sol @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2022 Lido + +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + +/** + * @title Interface defining a "client-side" of the `SelfOwnedStETHBurner` contract. + */ +interface ISelfOwnedStETHBurner { + /** + * Returns the total cover shares ever burnt. + */ + function getCoverSharesBurnt() external view returns (uint256); + + /** + * Returns the total non-cover shares ever burnt. + */ + function getNonCoverSharesBurnt() external view returns (uint256); +} diff --git a/contracts/0.8.9/test_helpers/BeaconReceiverMock.sol b/contracts/0.8.9/test_helpers/BeaconReceiverMock.sol index 99397b58c..1aaeaed0d 100644 --- a/contracts/0.8.9/test_helpers/BeaconReceiverMock.sol +++ b/contracts/0.8.9/test_helpers/BeaconReceiverMock.sol @@ -5,6 +5,7 @@ /* See contracts/COMPILERS.md */ pragma solidity 0.8.9; +import "@openzeppelin/contracts-v4.4/utils/introspection/ERC165.sol"; import "../interfaces/IBeaconReportReceiver.sol"; /** @@ -12,7 +13,7 @@ import "../interfaces/IBeaconReportReceiver.sol"; * * @dev DON'T USE THIS CODE IN A PRODUCTION */ -contract BeaconReceiverMock is IBeaconReportReceiver { +contract BeaconReceiverMock is IBeaconReportReceiver, ERC165 { uint256 public immutable id; uint256 public processedCounter; @@ -20,7 +21,20 @@ contract BeaconReceiverMock is IBeaconReportReceiver { id = _id; } - function processLidoOracleReport(uint256, uint256, uint256) external override { + function processLidoOracleReport(uint256, uint256, uint256) external virtual override { processedCounter++; } + + function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + return ( + _interfaceId == type(IBeaconReportReceiver).interfaceId + || super.supportsInterface(_interfaceId) + ); + } +} + +contract BeaconReceiverMockWithoutERC165 is IBeaconReportReceiver { + function processLidoOracleReport(uint256, uint256, uint256) external virtual override { + + } } diff --git a/contracts/0.8.9/test_helpers/ETHForwarderMock.sol b/contracts/0.8.9/test_helpers/ETHForwarderMock.sol index a83b791c5..7d00cedfe 100644 --- a/contracts/0.8.9/test_helpers/ETHForwarderMock.sol +++ b/contracts/0.8.9/test_helpers/ETHForwarderMock.sol @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2021 Lido -// SPDX-License-Identifier: GPL-3.0 +//SPDX-License-Identifier: GPL-3.0 + pragma solidity 0.8.9; contract ETHForwarderMock { diff --git a/deployed-goerli.json b/deployed-goerli.json index d9bd5bc50..ec634865f 100644 --- a/deployed-goerli.json +++ b/deployed-goerli.json @@ -16,14 +16,14 @@ "0x70B3284fD016a570146cE48fC54D7A81bCd94BBa" ], "daoTemplateDeployTx": "0xda8746bca23c7a4f7c58e1a3370cb1ffa1e250ace90fd4684dbb1842ee6ac921", - "lidoBaseDeployTx": "0xedff0d82dd32fdafa30a62388898e35b8c98759c15aa92ab1ba87b779d86c0bb", - "oracleBaseDeployTx": "0x4ff91791cc0f969cf9fe0cc9a6241593d26b20ee27f785a2eb002258e25f4e43", + "lidoBaseDeployTx": "0x754584f974624d32310d68e6bda260b7200c31d4086b1416d421ad7b31f4ad0c", + "oracleBaseDeployTx": "0x171d09bc9bea4421db6c614c994c218c894dc028d807f8e7edd759a4ba148bbf", "nodeOperatorsRegistryBaseDeployTx": "0x25cf4db41a4159e94826fd0495bafe02e4a015a9bcf0ffbc0e684a5cafe6eefc", "compositePostRebaseBeaconReceiverDeployTx": "0xf0f2d207ac3693abb8aa58e885aeff12371bb033d64376a409a8dd43d28d85ea", "selfOwnedStETHBurnerDeployTx": "0xa6267ebc56579f0c9e3f3b55908b851db971a83d5f97d4d2df9984396ae03b2f", "daoTemplateAddress": "0x9A4a36B5fe517f98BD6529d4317B0b723F600AaD", "app:lido": { - "baseAddress": "0x25C29642Aa0263B8Bf0A621a264Be6b6238B90E6", + "baseAddress": "0xd2deB2210C1C4BcA5cb20081B7383278d84E009B", "ipfsCid": "QmbmPW5r9HMdyUARNJjjE7MNqBUGrXashwoWvqRZhc1t5b", "contentURI": "0x697066733a516d626d5057357239484d64795541524e4a6a6a45374d4e714255477258617368776f577671525a686331743562", "name": "lido", @@ -32,7 +32,7 @@ "proxyAddress": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F" }, "app:oracle": { - "baseAddress": "0x869E3cB508200D3bE0e946613a8986E8eb3E64d7", + "baseAddress": "0x0fCdBaaC5dfEC479c8A9dEEaFe0B1BBc42E04706", "ipfsCid": "QmfACH9oSHFWgV81Dh8RSVcgaVBdhkZuHhZY2iZv5syBJK", "contentURI": "0x697066733a516d66414348396f5348465767563831446838525356636761564264686b5a7548685a5932695a76357379424a4b", "name": "oracle", diff --git a/deployed-kiln.json b/deployed-kiln.json new file mode 100644 index 000000000..f8ca73cb9 --- /dev/null +++ b/deployed-kiln.json @@ -0,0 +1,230 @@ +{ + "networkId": 1337802, + "ipfsAPI": "https://ipfs.infura.io:5001/api/v0", + "multisigAddress": "0x8c84909DE05702e1bA637fe05fddebA0FB3e3b21", + "lidoApmEnsName": "lidopm.eth", + "lidoApmEnsRegDurationSec": 94608000, + "daoAragonId": "lido-dao", + "daoInitialSettings": { + "voting": { + "minSupportRequired": "500000000000000000", + "minAcceptanceQuorum": "50000000000000000", + "voteDuration": 14400 + }, + "beaconSpec": { + "depositContractAddress": "0x4242424242424242424242424242424242424242", + "epochsPerFrame": 225, + "slotsPerEpoch": 32, + "secondsPerSlot": 12, + "genesisTime": 1647007200 + }, + "fee": { + "totalPercent": 10, + "treasuryPercent": 0, + "insurancePercent": 50, + "nodeOperatorsPercent": 50 + }, + "token": { + "name": "TEST Lido DAO Token", + "symbol": "TLDO" + } + }, + "vestingParams": { + "unvestedTokensAmount": "230000000000000000000000", + "holders": { + "0xa5F1d7D49F581136Cf6e58B32cBE9a2039C48bA1": "530000000000000000000000", + "0x00444797Ba158A7BdB8302e72da98dCbCCef0Fbc": "15000000000000000000000", + "0x9F188809b2ba2e29653693C4069c212a95C1B8c4": "15000000000000000000000", + "0xeC2d2E81721A477798e938924266f92c4E5Fc54e": "15000000000000000000000", + "0x401FD888B5E41113B7c0C47725A742bbc3A083EF": "15000000000000000000000", + "0x36c648351274bb4455ba6aAabF3F976824a93aF4": "15000000000000000000000", + "0xB6d0A3120c0e13749c211F48fccD9458Dfc99BD0": "15000000000000000000000", + "0x75936b355942D89d9B59798dacD70d70Ded93B78": "15000000000000000000000", + "0x39ceC2b3ba293CC15f15a3876dB8D356a1670789": "15000000000000000000000", + "0xC37F899BB002D93fdcbD9D91d42889AC0e29B704": "15000000000000000000000", + "0xc69f1606e0615F788Cb99982848528f39f188486": "15000000000000000000000", + "0xd355521db7f30F74059dFa7aE0A5C6543dFA8215": "15000000000000000000000", + "0xAA566f0eB53F26db5E15aC5B42685361B4e0d9F4": "15000000000000000000000", + "0xCd3975FC68Aa71eDebfef7442cF8e91ce407DEEE": "15000000000000000000000", + "0xfd0dd5c3b72528e8f581a8544309706d1c2d9206": "15000000000000000000000", + "0x161EE6a03D52ee176a514C88Cf07dcAcF248155f": "15000000000000000000000", + "0x11D7D72876cfC3d7CFC3d1a2C9c35EA4591694a7": "15000000000000000000000" + }, + "start": 1648627400, + "cliff": 1648628400, + "end": 1648628400, + "revokable": false + }, + "owner": "0x8c84909DE05702e1bA637fe05fddebA0FB3e3b21", + "aragonEnsLabelName": "aragonpm", + "ensAddress": "0xD3A23B83902066baC61e82bCe449fE1d3154Ab5D", + "ensFactoryAddress": "0x66Ca44dDb0B9Ba49C7a153A4108F46C95E4693dD", + "ensFactoryConstructorArgs": [], + "kernelBaseAddress": "0xf76235B924b6749cB62129b45c69771996D078B1", + "kernelBaseConstructorArgs": [true], + "aclBaseAddress": "0x31BbA6CCf72aA04f725602474614068fff2B3330", + "aclBaseConstructorArgs": [], + "evmScriptRegistryFactoryAddress": "0x19b424c26b36f2E7A9C6bA6d99243a2ECae7919F", + "evmScriptRegistryFactoryConstructorArgs": [], + "daoFactoryAddress": "0xF19b955B1aC41238eBc7411f3b75a157fe250df0", + "daoFactoryConstructorArgs": [ + "0xf76235B924b6749cB62129b45c69771996D078B1", + "0x31BbA6CCf72aA04f725602474614068fff2B3330", + "0x19b424c26b36f2E7A9C6bA6d99243a2ECae7919F" + ], + "apmRegistryBaseAddress": "0xfdac8a612d078c2F77b75992905275f2a06F0814", + "apmRepoBaseAddress": "0xD37598A90c531E5D35faa69D3706ECB6CE589E3A", + "ensSubdomainRegistrarBaseAddress": "0x79A5dcf2eC088639726aa3960B7Ea540466d4A42", + "apmRegistryFactoryAddress": "0xA160e5529Aa3cA0dE313B77C88671DD91D1C6B84", + "apmRegistryFactoryConstructorArgs": [ + "0xF19b955B1aC41238eBc7411f3b75a157fe250df0", + "0xfdac8a612d078c2F77b75992905275f2a06F0814", + "0xD37598A90c531E5D35faa69D3706ECB6CE589E3A", + "0x79A5dcf2eC088639726aa3960B7Ea540466d4A42", + "0xD3A23B83902066baC61e82bCe449fE1d3154Ab5D", + "0x0000000000000000000000000000000000000000" + ], + "aragonApmRegistryAddress": "0x1618d119c028f418Ab96D8096172aa8848DDBe36", + "aragonEnsNodeName": "aragonpm.eth", + "aragonEnsNode": "0x9065c3e7f7b7ef1ef4e53d2d0b8e0cef02874ab020c1ece79d5f0d3d0111c0ba", + "miniMeTokenFactoryAddress": "0x7DcaFCB80fDB46faEb501A5c5695771F938F47DC", + "miniMeTokenFactoryConstructorArgs": [], + "aragonIDAddress": "0x5B1510775E7186177b3f1fB3d36161276FE52C55", + "aragonIDConstructorArgs": [ + "0xD3A23B83902066baC61e82bCe449fE1d3154Ab5D", + "0xfa6574301a64d05838FE48A32bB415D5D35caF09", + "0x7e74a86b6e146964fb965db04dc2590516da77f720bb6759337bf5632415fd86" + ], + "aragonIDEnsNodeName": "aragonid.eth", + "aragonIDEnsNode": "0x7e74a86b6e146964fb965db04dc2590516da77f720bb6759337bf5632415fd86", + "app:agent": { + "fullName": "agent.aragonpm.eth", + "name": "agent", + "id": "0x9ac98dc5f995bf0211ed589ef022719d1487e5cb2bab505676f0d084c07cf89a", + "baseAddress": "0xa6b2449ef19191e06fde16bbd9a6A780820BBe01", + "ipfsCid": "Qmd32Lx4BbyfWKAvs9QprTDjWDnroWwwm9uRfRdwXTgS7T" + }, + "app:finance": { + "fullName": "finance.aragonpm.eth", + "name": "finance", + "id": "0xbf8491150dafc5dcaee5b861414dca922de09ccffa344964ae167212e8c673ae", + "baseAddress": "0x8E33218b90fD89c291882A3F26547Ecb76Fa6DFe", + "ipfsCid": "QmVWUzVwUV8gQFEdpw4Y5tAsN1zmPFqHtUUbNAD9upADsF" + }, + "app:token-manager": { + "fullName": "token-manager.aragonpm.eth", + "name": "token-manager", + "id": "0x6b20a3010614eeebf2138ccec99f028a61c811b3b1a3343b6ff635985c75c91f", + "baseAddress": "0x3F3A9f7E807C5CdFDeD57322907f8CD8D26C3Ab1", + "ipfsCid": "QmVA6TDcrEVHathVXLvYmRP944YLZpXsXUEoWqD9UwCVET" + }, + "app:vault": { + "fullName": "vault.aragonpm.eth", + "name": "vault", + "id": "0x7e852e0fcfce6551c13800f1e7476f982525c2b5277ba14b24339c68416336d1", + "baseAddress": "0x6b054aa056a381eFA094b4e96e39bb3F1856F108", + "ipfsCid": "QmQb95FxdzDZPX5rEnmdKzK2JLcntQWf2k7qvsPdmqr3Do" + }, + "app:voting": { + "fullName": "voting.aragonpm.eth", + "name": "voting", + "id": "0x9fa3927f639745e587912d4b0fea7ef9013bf93fb907d29faeab57417ba6e1d4", + "baseAddress": "0x48787380A54BDD6F058E4CFF3c35c1D546aa018B", + "ipfsCid": "QmZF7TCstYWRTr9QUV6CJ8bM2Zo5YMXqzagtf5VqY9teLe" + }, + "depositorParams": { + "maxDepositsPerBlock": 100, + "minDepositBlockDistance": 14, + "pauseIntentValidityPeriodBlocks": 10, + "guardians": [ + "0x3dc4cf780f2599b528f37dedb34449fb65ef7d4a", + "0x401fd888b5e41113b7c0c47725a742bbc3a083ef", + "0x3db460f33838fdc91e804d7fce2adf9e61e9d654", + "0x96fd3d127abd0d77724d49b7bddecdc89f684bb6", + "0xda1a296f9df18d04e0aefcff658b80b3ef824ec9", + "0xc34d33b95d7d6df2255d6051ddb12f8bd7aef64c", + "0xad5eecc863d88d5d848b89aa8517388a3ee432c8", + "0x6c83f70ed019fdcafe6c6f78cb33ce15943e4cb9", + "0x43464fe06c18848a2e2e913194d64c1970f4326a", + "0xf060ab3d5dcfdc6a0dfd5ca0645ac569b8f105ca", + "0x79a132be0c25ced09e745629d47cf05e531bb2bb" + ], + "quorum": 1 + }, + "depositContractAddress": "0x4242424242424242424242424242424242424242", + "daoTemplateConstructorArgs": [ + "0x8c84909DE05702e1bA637fe05fddebA0FB3e3b21", + "0xF19b955B1aC41238eBc7411f3b75a157fe250df0", + "0xD3A23B83902066baC61e82bCe449fE1d3154Ab5D", + "0x7DcaFCB80fDB46faEb501A5c5695771F938F47DC", + "0x5B1510775E7186177b3f1fB3d36161276FE52C55", + "0xA160e5529Aa3cA0dE313B77C88671DD91D1C6B84" + ], + "daoTemplateDeployTx": "0x4437237c1b23538d48416ce38043e1030d71aa15a51e4e7af2205e7e08e64e33", + "lidoBaseDeployTx": "0xc1780accef35fab68aa7b62b09f8ae269690c8455d2704d1f2e5de367b89544e", + "oracleBaseDeployTx": "0x5680258689872a551dee4b67b117fff0bf75633e045b574c881d228357a5a190", + "nodeOperatorsRegistryBaseDeployTx": "0xe7449e5f98a6a20a44f5f70416a7a4191b559fcc5bc8ef7ab52cf178b0604dce", + "daoTemplateAddress": "0xaD07B585F279A6b4F693fB5c81be3920b76a45EC", + "daoTemplateDeployBlock": 161005, + "app:lido": { + "baseAddress": "0xD9f9Da698b1F09BcfC46Ec24A489ff0b1FCF74f0", + "fullName": "lido.lidopm.eth", + "name": "lido", + "id": "0x3ca7c3e38968823ccb4c78ea688df41356f182ae1d159e4ee608d30d68cef320", + "ipfsCid": "QmTtpuoZdv3i72DDfcHxoYywVNATVJEsBv6iipaEKDSi5d", + "contentURI": "0x697066733a516d547470756f5a6476336937324444666348786f597977564e4154564a457342763669697061454b4453693564", + "proxyAddress": "0x3E50180cf438e185ec52Ade55855718584541476" + }, + "app:oracle": { + "baseAddress": "0x92dfD7E1643A3Ab7eBBC68F399F4a62FabAaEa23", + "fullName": "oracle.lidopm.eth", + "name": "oracle", + "id": "0x8b47ba2a8454ec799cd91646e7ec47168e91fd139b23f017455f3e5898aaba93", + "ipfsCid": "QmYJVQW83CGmYpRtZNREz7mioXddzXxdd1z6g62oRtxWB8", + "contentURI": "0x697066733a516d594a565157383343476d597052745a4e52457a376d696f5864647a58786464317a366736326f527478574238", + "proxyAddress": "0x9633df55696561C4Cf8A260316A6e08113FA2164" + }, + "app:node-operators-registry": { + "baseAddress": "0xaF87d30A3463280b486B239C7DecC4390C84DCBC", + "fullName": "node-operators-registry.lidopm.eth", + "name": "node-operators-registry", + "id": "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d", + "ipfsCid": "QmdS4HqNbRumzR1g5aebPzTf5K7A8MrA6Y4CV8ZuGfcUFq", + "contentURI": "0x697066733a516d64533448714e6252756d7a52316735616562507a5466354b3741384d72413659344356385a75476663554671", + "proxyAddress": "0xb849C5b35DC45277Bad5c6F2c6C77183d842367c" + }, + "lidoApmDeployArguments": [ + "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae", + "0x90a9580abeb24937fc658e497221c81ce8553b560304f9525821f32b17dbdaec" + ], + "lidoApmDeployTx": "0x26af347abc33a16678a9f45b55cdfa8241f0d48de9862ece61da3e06158616c6", + "lidoApmAddress": "0xaC1719341a8b0AC8fbd058Ccf40e0e2eA20Cc06e", + "createAppReposTx": "0x056ea8c943ada1fbf8dd27f3322d5fc9dfa3b6e9de206f2128b58a1865712e9b", + "newDaoTx": "0x257d02e1cc52e4cf1d66da79ab23b0a256f3180950389174e5aa4c29dfc4f1af", + "daoAddress": "0x1c7E1226D5D7f276b80c1e42cf53F753a431ea9e", + "daoTokenAddress": "0xA463D98281b5126D501d22F0719a13BfE092688d", + "app:aragon-agent": { + "name": "aragon-agent", + "fullName": "aragon-agent.lidopm.eth", + "id": "0x701a4fd1f5174d12a0f1d9ad2c88d0ad11ab6aad0ac72b7d9ce621815f8016a9", + "proxyAddress": "0xb98B21f9725F1deF45500c5D2D4D48eC5DF4B6AF" + }, + "app:aragon-finance": { + "name": "aragon-finance", + "fullName": "aragon-finance.lidopm.eth", + "id": "0x5c9918c99c4081ca9459c178381be71d9da40e49e151687da55099c49a4237f1", + "proxyAddress": "0xA9024A812da051D4c947dcbBB745FD8051edF6e5" + }, + "app:aragon-token-manager": { + "name": "aragon-token-manager", + "fullName": "aragon-token-manager.lidopm.eth", + "id": "0xcd567bdf93dd0f6acc3bc7f2155f83244d56a65abbfbefb763e015420102c67b", + "proxyAddress": "0x6AB36E8B1c6Cb572F910A7e50204F41f98D5de74" + }, + "app:aragon-voting": { + "name": "aragon-voting", + "fullName": "aragon-voting.lidopm.eth", + "id": "0x0abcd104777321a82b010357f20887d61247493d89d2e987ff57bcecbde00e1e", + "proxyAddress": "0x84633bf8e3AE2508a81F09F46a47774F5aFD16B9" + } +} diff --git a/deployed-kintsugi.json b/deployed-kintsugi.json index e39918bda..07328eccf 100644 --- a/deployed-kintsugi.json +++ b/deployed-kintsugi.json @@ -250,7 +250,5 @@ }, "lidoBaseDeployTx": "0x99f814a256ddedc706bb4b699fee8106ee3d12d2fa78da6d79544efd398aeac4", "oracleBaseDeployTx": "0x195866659be58414ac4a8095c5f6c1b1edc8f24baa48384bc4baa4c77c555af3", - "nodeOperatorsRegistryBaseDeployTx": "0x8e16572b1b3fe7ea5b8d67d2af9176566169b706f470d4cbd4fd1d256e339a25", - "mevTxFeeVaultDeployTx": "0x3d5375b7a19c6c8391fe65b3dbaabce4d5cc95cdeb910230c516ff434f686889", - "mevTxFeeVaultAddress": "0xcDB3885e9680105c02FcC740B184eA98Ef14af42" + "nodeOperatorsRegistryBaseDeployTx": "0x8e16572b1b3fe7ea5b8d67d2af9176566169b706f470d4cbd4fd1d256e339a25" } diff --git a/docker-compose.kiln.yml b/docker-compose.kiln.yml new file mode 100644 index 000000000..e6c181a49 --- /dev/null +++ b/docker-compose.kiln.yml @@ -0,0 +1,14 @@ +version: '3.7' +services: + aragon: + build: + context: ./docker/aragon/ + args: + - ARAGON_IPFS_GATEWAY=https://ipfs.infura.io/ipfs + - ARAGON_DEFAULT_ETH_NODE=ws://34.159.167.0:8546 + - ARAGON_APP_LOCATOR=ipfs + - ARAGON_ETH_NETWORK_TYPE=local + - ARAGON_ENS_REGISTRY_ADDRESS=0xD3A23B83902066baC61e82bCe449fE1d3154Ab5D + container_name: aragon + ports: + - 3000:8080 diff --git a/docs/protocol-levers.md b/docs/protocol-levers.md index f8f5e152a..32a64a3b9 100644 --- a/docs/protocol-levers.md +++ b/docs/protocol-levers.md @@ -100,8 +100,10 @@ if the amount of the buffered Ether becomes sufficiently large. ### Pausing -* Mutators: `stop()`, `resume()` +* Mutator: `stop()` * Permission required: `PAUSE_ROLE` +* Mutator: `resume()` + * Permission required: `RESUME_ROLE` * Accessor: `isStopped() returns (bool)` When paused, `Lido` doesn't accept user submissions, doesn't allow user withdrawals and oracle @@ -112,7 +114,7 @@ allowances) are allowed. The following transactions revert: * calls to `submit(address)`; * calls to `depositBufferedEther(uint256)`; * calls to `withdraw(uint256, bytes32)` (withdrawals are not implemented yet). -* calls to `pushBeacon(uint256, uint256)`; +* calls to `handleOracleReport(uint256, uint256)`; * calls to `burnShares(address, uint256)` * calls to `transfer(address, uint256)` * calls to `transferFrom(address, address, uint256)` diff --git a/hardhat.config.js b/hardhat.config.js index 26381f4cb..a2999340e 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -9,6 +9,7 @@ require('@nomiclabs/hardhat-ganache') require('@nomiclabs/hardhat-etherscan') require('hardhat-gas-reporter') require('solidity-coverage') +require('hardhat-contract-sizer') const NETWORK_NAME = getNetworkName() const ETH_ACCOUNT_NAME = process.env.ETH_ACCOUNT_NAME @@ -55,6 +56,14 @@ const getNetConfig = (networkName, ethAccountName) => { // gas: 10000000, gasPrice: 2000000000 }, + kiln: { + ...base, + accounts: accounts.eth.kiln, + url: 'http://34.159.167.0:8545', + chainId: 1337802, + // gas: 10000000, + gasPrice: 2000000000 + }, // local local: { ...base, @@ -68,6 +77,7 @@ const getNetConfig = (networkName, ethAccountName) => { blockGasLimit: 20000000, gasPrice: 0, initialBaseFeePerGas: 0, + allowUnlimitedContractSize: true, accounts: { mnemonic: 'hardhat', count: 20, diff --git a/lib/abi/CompositePostRebaseBeaconReceiver.json b/lib/abi/CompositePostRebaseBeaconReceiver.json index c1bd83495..05fc8f647 100644 --- a/lib/abi/CompositePostRebaseBeaconReceiver.json +++ b/lib/abi/CompositePostRebaseBeaconReceiver.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"_voting","type":"address"},{"internalType":"address","name":"_oracle","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"callback","type":"address"},{"indexed":false,"internalType":"uint256","name":"atIndex","type":"uint256"}],"name":"CallbackAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"callback","type":"address"},{"indexed":false,"internalType":"uint256","name":"atIndex","type":"uint256"}],"name":"CallbackRemoved","type":"event"},{"inputs":[],"name":"MAX_CALLBACKS_COUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOTING","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_callback","type":"address"}],"name":"addCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"callbacks","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"callbacksLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_callback","type":"address"},{"internalType":"uint256","name":"_atIndex","type":"uint256"}],"name":"insertCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_postTotalPooledEther","type":"uint256"},{"internalType":"uint256","name":"_preTotalPooledEther","type":"uint256"},{"internalType":"uint256","name":"_timeElapsed","type":"uint256"}],"name":"processLidoOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_atIndex","type":"uint256"}],"name":"removeCallback","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_voting","type":"address"},{"internalType":"address","name":"_oracle","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"callback","type":"address"},{"indexed":false,"internalType":"uint256","name":"atIndex","type":"uint256"}],"name":"CallbackAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"callback","type":"address"},{"indexed":false,"internalType":"uint256","name":"atIndex","type":"uint256"}],"name":"CallbackRemoved","type":"event"},{"inputs":[],"name":"MAX_CALLBACKS_COUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUIRED_INTERFACE","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOTING","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_callback","type":"address"}],"name":"addCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"callbacks","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"callbacksLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_callback","type":"address"},{"internalType":"uint256","name":"_atIndex","type":"uint256"}],"name":"insertCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_postTotalPooledEther","type":"uint256"},{"internalType":"uint256","name":"_preTotalPooledEther","type":"uint256"},{"internalType":"uint256","name":"_timeElapsed","type":"uint256"}],"name":"processLidoOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_atIndex","type":"uint256"}],"name":"removeCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"_interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/IERC721.json b/lib/abi/IERC721.json new file mode 100644 index 000000000..b777358c2 --- /dev/null +++ b/lib/abi/IERC721.json @@ -0,0 +1 @@ +[{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"payable":true,"stateMutability":"payable","type":"function"}] \ No newline at end of file diff --git a/lib/abi/ILido.json b/lib/abi/ILido.json index 6f7d89520..f08bfb199 100644 --- a/lib/abi/ILido.json +++ b/lib/abi/ILido.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"uint256","name":"maxDeposits","type":"uint256"}],"name":"depositBufferedEther","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_account","type":"address"},{"internalType":"uint256","name":"_sharesAmount","type":"uint256"}],"name":"burnShares","outputs":[{"internalType":"uint256","name":"newTotalShares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getOracle","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_sharesAmount","type":"uint256"}],"name":"getPooledEthByShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_pooledEthAmount","type":"uint256"}],"name":"getSharesByPooledEth","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTotalShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"sharesOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/IOracle.json b/lib/abi/IOracle.json new file mode 100644 index 000000000..b04d6d368 --- /dev/null +++ b/lib/abi/IOracle.json @@ -0,0 +1 @@ +[{"inputs":[],"name":"getBeaconReportReceiver","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/Lido.json b/lib/abi/Lido.json index b5c287d9f..d6fcd21bd 100644 --- a/lib/abi/Lido.json +++ b/lib/abi/Lido.json @@ -1 +1 @@ -[{"constant":false,"inputs":[],"name":"resume","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[],"name":"stop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"depositContract","type":"address"},{"name":"_oracle","type":"address"},{"name":"_operators","type":"address"},{"name":"_treasury","type":"address"},{"name":"_insuranceFund","type":"address"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getInsuranceFund","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_ethAmount","type":"uint256"}],"name":"getSharesByPooledEth","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getOperators","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"DEPOSIT_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"DEPOSIT_SIZE","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalPooledEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"PAUSE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTreasury","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_ORACLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isStopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_WITHDRAWAL_KEY","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBufferedEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SIGNATURE_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getWithdrawalCredentials","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getFeeDistribution","outputs":[{"name":"treasuryFeeBasisPoints","type":"uint16"},{"name":"insuranceFeeBasisPoints","type":"uint16"},{"name":"operatorsFeeBasisPoints","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sharesAmount","type":"uint256"}],"name":"getPooledEthByShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_oracle","type":"address"}],"name":"setOracle","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getOracle","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_treasuryFeeBasisPoints","type":"uint16"},{"name":"_insuranceFeeBasisPoints","type":"uint16"},{"name":"_operatorsFeeBasisPoints","type":"uint16"}],"name":"setFeeDistribution","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_feeBasisPoints","type":"uint16"}],"name":"setFee","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_maxDeposits","type":"uint256"}],"name":"depositBufferedEther","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_FEE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SET_TREASURY","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_referral","type":"address"}],"name":"submit","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"WITHDRAWAL_CREDENTIALS_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"PUBKEY_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_amount","type":"uint256"},{"name":"_pubkeyHash","type":"bytes32"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getDepositContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconStat","outputs":[{"name":"depositedValidators","type":"uint256"},{"name":"beaconValidators","type":"uint256"},{"name":"beaconBalance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"BURN_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_insuranceFund","type":"address"}],"name":"setInsuranceFund","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getFee","outputs":[{"name":"feeBasisPoints","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_INSURANCE_FUND","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_withdrawalCredentials","type":"bytes32"}],"name":"setWithdrawalCredentials","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"depositBufferedEther","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_account","type":"address"},{"name":"_sharesAmount","type":"uint256"}],"name":"burnShares","outputs":[{"name":"newTotalShares","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_treasury","type":"address"}],"name":"setTreasury","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_beaconValidators","type":"uint256"},{"name":"_beaconBalance","type":"uint256"}],"name":"pushBeacon","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"sharesOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[],"name":"Stopped","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"feeBasisPoints","type":"uint16"}],"name":"FeeSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"treasuryFeeBasisPoints","type":"uint16"},{"indexed":false,"name":"insuranceFeeBasisPoints","type":"uint16"},{"indexed":false,"name":"operatorsFeeBasisPoints","type":"uint16"}],"name":"FeeDistributionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"withdrawalCredentials","type":"bytes32"}],"name":"WithdrawalCredentialsSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"referral","type":"address"}],"name":"Submitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Unbuffered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"sender","type":"address"},{"indexed":false,"name":"tokenAmount","type":"uint256"},{"indexed":false,"name":"sentFromBuffer","type":"uint256"},{"indexed":true,"name":"pubkeyHash","type":"bytes32"},{"indexed":false,"name":"etherAmount","type":"uint256"}],"name":"Withdrawal","type":"event"}] \ No newline at end of file +[{"constant":false,"inputs":[],"name":"resume","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[],"name":"stop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"receiveMevTxFee","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_depositContract","type":"address"},{"name":"_oracle","type":"address"},{"name":"_operators","type":"address"},{"name":"_treasury","type":"address"},{"name":"_insuranceFund","type":"address"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getInsuranceFund","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_ethAmount","type":"uint256"}],"name":"getSharesByPooledEth","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isStakingPaused","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getOperators","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"RESUME_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"DEPOSIT_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalMevTxFeeCollected","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"DEPOSIT_SIZE","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalPooledEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"PAUSE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTreasury","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isStopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_WITHDRAWAL_KEY","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBufferedEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"},{"name":"_tokenId","type":"uint256"}],"name":"transferERC721ToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SIGNATURE_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getWithdrawalCredentials","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_beaconValidators","type":"uint256"},{"name":"_beaconBalance","type":"uint256"}],"name":"handleOracleReport","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SET_MEV_TX_FEE_VAULT_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getMevTxFeeVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getFeeDistribution","outputs":[{"name":"treasuryFeeBasisPoints","type":"uint16"},{"name":"insuranceFeeBasisPoints","type":"uint16"},{"name":"operatorsFeeBasisPoints","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_maxStakeLimit","type":"uint96"},{"name":"_stakeLimitIncreasePerBlock","type":"uint96"}],"name":"resumeStaking","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_sharesAmount","type":"uint256"}],"name":"getPooledEthByShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_PROTOCOL_CONTRACTS_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getOracle","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"calculateCurrentStakeLimit","outputs":[{"name":"currentStakeLimit","type":"uint256"},{"name":"maxStakeLimit","type":"uint256"},{"name":"stakeLimitIncreasePerBlock","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_treasuryFeeBasisPoints","type":"uint16"},{"name":"_insuranceFeeBasisPoints","type":"uint16"},{"name":"_operatorsFeeBasisPoints","type":"uint16"}],"name":"setFeeDistribution","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_feeBasisPoints","type":"uint16"}],"name":"setFee","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_sharesAmount","type":"uint256"}],"name":"transferShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_maxDeposits","type":"uint256"}],"name":"depositBufferedEther","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_FEE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_referral","type":"address"}],"name":"submit","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"WITHDRAWAL_CREDENTIALS_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"PUBKEY_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_amount","type":"uint256"},{"name":"_pubkeyHash","type":"bytes32"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getDepositContract","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_mevTxFeeVault","type":"address"}],"name":"setMevTxFeeVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconStat","outputs":[{"name":"depositedValidators","type":"uint256"},{"name":"beaconValidators","type":"uint256"},{"name":"beaconBalance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"BURN_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_MEV_TX_FEE_WITHDRAWAL_LIMIT_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getFee","outputs":[{"name":"feeBasisPoints","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_oracle","type":"address"},{"name":"_treasury","type":"address"},{"name":"_insuranceFund","type":"address"}],"name":"setProtocolContracts","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_limitPoints","type":"uint16"}],"name":"setMevTxFeeWithdrawalLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_withdrawalCredentials","type":"bytes32"}],"name":"setWithdrawalCredentials","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"STAKING_PAUSE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"depositBufferedEther","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_account","type":"address"},{"name":"_sharesAmount","type":"uint256"}],"name":"burnShares","outputs":[{"name":"newTotalShares","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"sharesOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getMevTxFeeWithdrawalLimitPoints","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"STAKING_RESUME_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pauseStaking","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"sharesValue","type":"uint256"}],"name":"TransferShares","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"sharesAmount","type":"uint256"}],"name":"SharesBurnt","type":"event"},{"anonymous":false,"inputs":[],"name":"Stopped","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[],"name":"StakingPaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"maxStakeLimit","type":"uint96"},{"indexed":false,"name":"stakeLimitIncreasePerBlock","type":"uint96"}],"name":"StakingResumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"oracle","type":"address"},{"indexed":false,"name":"treasury","type":"address"},{"indexed":false,"name":"insuranceFund","type":"address"}],"name":"ProtocolContactsSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"feeBasisPoints","type":"uint16"}],"name":"FeeSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"treasuryFeeBasisPoints","type":"uint16"},{"indexed":false,"name":"insuranceFeeBasisPoints","type":"uint16"},{"indexed":false,"name":"operatorsFeeBasisPoints","type":"uint16"}],"name":"FeeDistributionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"MevTxFeeReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"limitPoints","type":"uint256"}],"name":"MevTxFeeWithdrawalLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"withdrawalCredentials","type":"bytes32"}],"name":"WithdrawalCredentialsSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"mevTxFeeVault","type":"address"}],"name":"LidoMevTxFeeVaultSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"referral","type":"address"}],"name":"Submitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Unbuffered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"sender","type":"address"},{"indexed":false,"name":"tokenAmount","type":"uint256"},{"indexed":false,"name":"sentFromBuffer","type":"uint256"},{"indexed":true,"name":"pubkeyHash","type":"bytes32"},{"indexed":false,"name":"etherAmount","type":"uint256"}],"name":"Withdrawal","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"tokenId","type":"uint256"}],"name":"RecoverERC721ToVault","type":"event"}] \ No newline at end of file diff --git a/lib/abi/LidoMevTxFeeVault.json b/lib/abi/LidoMevTxFeeVault.json new file mode 100644 index 000000000..a337b331a --- /dev/null +++ b/lib/abi/LidoMevTxFeeVault.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_lido","type":"address"},{"internalType":"address","name":"_treasury","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ERC20Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721Recovered","type":"event"},{"inputs":[],"name":"LIDO","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TREASURY","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"recoverERC721","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxAmount","type":"uint256"}],"name":"withdrawRewards","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] \ No newline at end of file diff --git a/lib/abi/LidoOracle.json b/lib/abi/LidoOracle.json index da58bc87e..d875e1943 100644 --- a/lib/abi/LidoOracle.json +++ b/lib/abi/LidoOracle.json @@ -1 +1 @@ -[{"constant":true,"inputs":[],"name":"getCurrentOraclesReportStatus","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"setAllowedBeaconBalanceAnnualRelativeIncrease","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_QUORUM","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_epochId","type":"uint256"},{"name":"_beaconBalance","type":"uint64"},{"name":"_beaconValidators","type":"uint32"}],"name":"reportBeacon","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getAllowedBeaconBalanceAnnualRelativeIncrease","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getAllowedBeaconBalanceRelativeDecrease","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getExpectedEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLastCompletedReportDelta","outputs":[{"name":"postTotalPooledEther","type":"uint256"},{"name":"preTotalPooledEther","type":"uint256"},{"name":"timeElapsed","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLido","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_BEACON_REPORT_RECEIVER","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_MEMBERS","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentFrame","outputs":[{"name":"frameEpochId","type":"uint256"},{"name":"frameStartTime","type":"uint256"},{"name":"frameEndTime","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_index","type":"uint256"}],"name":"getCurrentReportVariant","outputs":[{"name":"beaconBalance","type":"uint64"},{"name":"beaconValidators","type":"uint32"},{"name":"count","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLastCompletedEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_allowedBeaconBalanceAnnualRelativeIncrease","type":"uint256"},{"name":"_allowedBeaconBalanceRelativeDecrease","type":"uint256"}],"name":"initialize_v2","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"}],"name":"setBeaconReportReceiver","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SET_BEACON_SPEC","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_member","type":"address"}],"name":"addOracleMember","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconReportReceiver","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_REPORT_BOUNDARIES","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_quorum","type":"uint256"}],"name":"setQuorum","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getQuorum","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getOracleMembers","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"setAllowedBeaconBalanceRelativeDecrease","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconSpec","outputs":[{"name":"epochsPerFrame","type":"uint64"},{"name":"slotsPerEpoch","type":"uint64"},{"name":"secondsPerSlot","type":"uint64"},{"name":"genesisTime","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_epochsPerFrame","type":"uint64"},{"name":"_slotsPerEpoch","type":"uint64"},{"name":"_secondsPerSlot","type":"uint64"},{"name":"_genesisTime","type":"uint64"}],"name":"setBeaconSpec","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MAX_MEMBERS","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentReportVariantsSize","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_member","type":"address"}],"name":"removeOracleMember","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"AllowedBeaconBalanceAnnualRelativeIncreaseSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"AllowedBeaconBalanceRelativeDecreaseSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"callback","type":"address"}],"name":"BeaconReportReceiverSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"member","type":"address"}],"name":"MemberAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"member","type":"address"}],"name":"MemberRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"quorum","type":"uint256"}],"name":"QuorumChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochId","type":"uint256"}],"name":"ExpectedEpochIdUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochsPerFrame","type":"uint64"},{"indexed":false,"name":"slotsPerEpoch","type":"uint64"},{"indexed":false,"name":"secondsPerSlot","type":"uint64"},{"indexed":false,"name":"genesisTime","type":"uint64"}],"name":"BeaconSpecSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochId","type":"uint256"},{"indexed":false,"name":"beaconBalance","type":"uint128"},{"indexed":false,"name":"beaconValidators","type":"uint128"},{"indexed":false,"name":"caller","type":"address"}],"name":"BeaconReported","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochId","type":"uint256"},{"indexed":false,"name":"beaconBalance","type":"uint128"},{"indexed":false,"name":"beaconValidators","type":"uint128"}],"name":"Completed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"postTotalPooledEther","type":"uint256"},{"indexed":false,"name":"preTotalPooledEther","type":"uint256"},{"indexed":false,"name":"timeElapsed","type":"uint256"},{"indexed":false,"name":"totalShares","type":"uint256"}],"name":"PostTotalShares","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"}] \ No newline at end of file +[{"constant":true,"inputs":[],"name":"getCurrentOraclesReportStatus","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"setAllowedBeaconBalanceAnnualRelativeIncrease","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_QUORUM","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_epochId","type":"uint256"},{"name":"_beaconBalance","type":"uint64"},{"name":"_beaconValidators","type":"uint32"}],"name":"reportBeacon","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getAllowedBeaconBalanceAnnualRelativeIncrease","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getAllowedBeaconBalanceRelativeDecrease","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getExpectedEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLastCompletedReportDelta","outputs":[{"name":"postTotalPooledEther","type":"uint256"},{"name":"preTotalPooledEther","type":"uint256"},{"name":"timeElapsed","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_lido","type":"address"},{"name":"_epochsPerFrame","type":"uint64"},{"name":"_slotsPerEpoch","type":"uint64"},{"name":"_secondsPerSlot","type":"uint64"},{"name":"_genesisTime","type":"uint64"},{"name":"_allowedBeaconBalanceAnnualRelativeIncrease","type":"uint256"},{"name":"_allowedBeaconBalanceRelativeDecrease","type":"uint256"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getLido","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_BEACON_REPORT_RECEIVER","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"finalizeUpgrade_v3","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_MEMBERS","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentFrame","outputs":[{"name":"frameEpochId","type":"uint256"},{"name":"frameStartTime","type":"uint256"},{"name":"frameEndTime","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_index","type":"uint256"}],"name":"getCurrentReportVariant","outputs":[{"name":"beaconBalance","type":"uint64"},{"name":"beaconValidators","type":"uint32"},{"name":"count","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLastCompletedEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"}],"name":"setBeaconReportReceiver","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SET_BEACON_SPEC","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_member","type":"address"}],"name":"addOracleMember","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconReportReceiver","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_REPORT_BOUNDARIES","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_quorum","type":"uint256"}],"name":"setQuorum","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getQuorum","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getOracleMembers","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"setAllowedBeaconBalanceRelativeDecrease","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconSpec","outputs":[{"name":"epochsPerFrame","type":"uint64"},{"name":"slotsPerEpoch","type":"uint64"},{"name":"secondsPerSlot","type":"uint64"},{"name":"genesisTime","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_epochsPerFrame","type":"uint64"},{"name":"_slotsPerEpoch","type":"uint64"},{"name":"_secondsPerSlot","type":"uint64"},{"name":"_genesisTime","type":"uint64"}],"name":"setBeaconSpec","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MAX_MEMBERS","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentReportVariantsSize","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_member","type":"address"}],"name":"removeOracleMember","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"AllowedBeaconBalanceAnnualRelativeIncreaseSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"AllowedBeaconBalanceRelativeDecreaseSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"callback","type":"address"}],"name":"BeaconReportReceiverSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"member","type":"address"}],"name":"MemberAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"member","type":"address"}],"name":"MemberRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"quorum","type":"uint256"}],"name":"QuorumChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochId","type":"uint256"}],"name":"ExpectedEpochIdUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochsPerFrame","type":"uint64"},{"indexed":false,"name":"slotsPerEpoch","type":"uint64"},{"indexed":false,"name":"secondsPerSlot","type":"uint64"},{"indexed":false,"name":"genesisTime","type":"uint64"}],"name":"BeaconSpecSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochId","type":"uint256"},{"indexed":false,"name":"beaconBalance","type":"uint128"},{"indexed":false,"name":"beaconValidators","type":"uint128"},{"indexed":false,"name":"caller","type":"address"}],"name":"BeaconReported","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochId","type":"uint256"},{"indexed":false,"name":"beaconBalance","type":"uint128"},{"indexed":false,"name":"beaconValidators","type":"uint128"}],"name":"Completed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"postTotalPooledEther","type":"uint256"},{"indexed":false,"name":"preTotalPooledEther","type":"uint256"},{"indexed":false,"name":"timeElapsed","type":"uint256"},{"indexed":false,"name":"totalShares","type":"uint256"}],"name":"PostTotalShares","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"}] \ No newline at end of file diff --git a/lib/abi/NodeOperatorsRegistry.json b/lib/abi/NodeOperatorsRegistry.json index a0356f8b7..0cc9f4db7 100644 --- a/lib/abi/NodeOperatorsRegistry.json +++ b/lib/abi/NodeOperatorsRegistry.json @@ -1 +1 @@ -[{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_quantity","type":"uint256"},{"name":"_pubkeys","type":"bytes"},{"name":"_signatures","type":"bytes"}],"name":"addSigningKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_numKeys","type":"uint256"}],"name":"assignNextSigningKeys","outputs":[{"name":"pubkeys","type":"bytes"},{"name":"signatures","type":"bytes"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SIGNATURE_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_ADDRESS_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"},{"name":"_amount","type":"uint256"}],"name":"removeSigningKeysOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_name","type":"string"}],"name":"setNodeOperatorName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_totalRewardShares","type":"uint256"}],"name":"getRewardsDistribution","outputs":[{"name":"recipients","type":"address[]"},{"name":"shares","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_active","type":"bool"}],"name":"setNodeOperatorActive","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_NAME_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"removeSigningKey","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"},{"name":"_amount","type":"uint256"}],"name":"removeSigningKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"ADD_NODE_OPERATOR_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_quantity","type":"uint256"},{"name":"_pubkeys","type":"bytes"},{"name":"_signatures","type":"bytes"}],"name":"addSigningKeysOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getActiveNodeOperatorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_rewardAddress","type":"address"}],"name":"addNodeOperator","outputs":[{"name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_operator_id","type":"uint256"}],"name":"getUnusedSigningKeyCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_rewardAddress","type":"address"}],"name":"setNodeOperatorRewardAddress","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_id","type":"uint256"},{"name":"_fullInfo","type":"bool"}],"name":"getNodeOperator","outputs":[{"name":"active","type":"bool"},{"name":"name","type":"string"},{"name":"rewardAddress","type":"address"},{"name":"stakingLimit","type":"uint64"},{"name":"stoppedValidators","type":"uint64"},{"name":"totalSigningKeys","type":"uint64"},{"name":"usedSigningKeys","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"PUBKEY_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNodeOperatorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_stakingLimit","type":"uint64"}],"name":"setNodeOperatorStakingLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"getSigningKey","outputs":[{"name":"key","type":"bytes"},{"name":"depositSignature","type":"bytes"},{"name":"used","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_stoppedIncrement","type":"uint64"}],"name":"reportStoppedValidators","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_lido","type":"address"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"REPORT_STOPPED_VALIDATORS_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getKeysOpIndex","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_ACTIVE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_LIMIT_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_operator_id","type":"uint256"}],"name":"getTotalSigningKeyCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"removeSigningKeyOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_SIGNING_KEYS","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"trimUnusedKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"name","type":"string"},{"indexed":false,"name":"rewardAddress","type":"address"},{"indexed":false,"name":"stakingLimit","type":"uint64"}],"name":"NodeOperatorAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"active","type":"bool"}],"name":"NodeOperatorActiveSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"name","type":"string"}],"name":"NodeOperatorNameSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"rewardAddress","type":"address"}],"name":"NodeOperatorRewardAddressSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"stakingLimit","type":"uint64"}],"name":"NodeOperatorStakingLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"totalStopped","type":"uint64"}],"name":"NodeOperatorTotalStoppedValidatorsReported","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"totalKeysTrimmed","type":"uint64"}],"name":"NodeOperatorTotalKeysTrimmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"operatorId","type":"uint256"},{"indexed":false,"name":"pubkey","type":"bytes"}],"name":"SigningKeyAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"operatorId","type":"uint256"},{"indexed":false,"name":"pubkey","type":"bytes"}],"name":"SigningKeyRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"keysOpIndex","type":"uint256"}],"name":"KeysOpIndexSet","type":"event"}] \ No newline at end of file +[{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_quantity","type":"uint256"},{"name":"_pubkeys","type":"bytes"},{"name":"_signatures","type":"bytes"}],"name":"addSigningKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_numKeys","type":"uint256"}],"name":"assignNextSigningKeys","outputs":[{"name":"pubkeys","type":"bytes"},{"name":"signatures","type":"bytes"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SIGNATURE_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_ADDRESS_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"},{"name":"_amount","type":"uint256"}],"name":"removeSigningKeysOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_name","type":"string"}],"name":"setNodeOperatorName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_totalRewardShares","type":"uint256"}],"name":"getRewardsDistribution","outputs":[{"name":"recipients","type":"address[]"},{"name":"shares","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_active","type":"bool"}],"name":"setNodeOperatorActive","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_NAME_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"removeSigningKey","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"},{"name":"_amount","type":"uint256"}],"name":"removeSigningKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"ADD_NODE_OPERATOR_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_quantity","type":"uint256"},{"name":"_pubkeys","type":"bytes"},{"name":"_signatures","type":"bytes"}],"name":"addSigningKeysOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getActiveNodeOperatorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_rewardAddress","type":"address"}],"name":"addNodeOperator","outputs":[{"name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_operator_id","type":"uint256"}],"name":"getUnusedSigningKeyCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_rewardAddress","type":"address"}],"name":"setNodeOperatorRewardAddress","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_id","type":"uint256"},{"name":"_fullInfo","type":"bool"}],"name":"getNodeOperator","outputs":[{"name":"active","type":"bool"},{"name":"name","type":"string"},{"name":"rewardAddress","type":"address"},{"name":"stakingLimit","type":"uint64"},{"name":"stoppedValidators","type":"uint64"},{"name":"totalSigningKeys","type":"uint64"},{"name":"usedSigningKeys","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"PUBKEY_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNodeOperatorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_stakingLimit","type":"uint64"}],"name":"setNodeOperatorStakingLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"getSigningKey","outputs":[{"name":"key","type":"bytes"},{"name":"depositSignature","type":"bytes"},{"name":"used","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_stoppedIncrement","type":"uint64"}],"name":"reportStoppedValidators","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_lido","type":"address"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"REPORT_STOPPED_VALIDATORS_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getKeysOpIndex","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_ACTIVE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_LIMIT_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_operator_id","type":"uint256"}],"name":"getTotalSigningKeyCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_NODE_OPERATORS_COUNT","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_operator_id","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"removeSigningKeyOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_SIGNING_KEYS","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"trimUnusedKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"id","type":"uint256"},{"indexed":false,"name":"name","type":"string"},{"indexed":false,"name":"rewardAddress","type":"address"},{"indexed":false,"name":"stakingLimit","type":"uint64"}],"name":"NodeOperatorAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"active","type":"bool"}],"name":"NodeOperatorActiveSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"name","type":"string"}],"name":"NodeOperatorNameSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"rewardAddress","type":"address"}],"name":"NodeOperatorRewardAddressSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"stakingLimit","type":"uint64"}],"name":"NodeOperatorStakingLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"totalStopped","type":"uint64"}],"name":"NodeOperatorTotalStoppedValidatorsReported","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"totalKeysTrimmed","type":"uint64"}],"name":"NodeOperatorTotalKeysTrimmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"operatorId","type":"uint256"},{"indexed":false,"name":"pubkey","type":"bytes"}],"name":"SigningKeyAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"operatorId","type":"uint256"},{"indexed":false,"name":"pubkey","type":"bytes"}],"name":"SigningKeyRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"keysOpIndex","type":"uint256"}],"name":"KeysOpIndexSet","type":"event"}] \ No newline at end of file diff --git a/lib/abi/OrderedCallbacksArray.json b/lib/abi/OrderedCallbacksArray.json index bd33b6f41..881c3bb6c 100644 --- a/lib/abi/OrderedCallbacksArray.json +++ b/lib/abi/OrderedCallbacksArray.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"_voting","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"callback","type":"address"},{"indexed":false,"internalType":"uint256","name":"atIndex","type":"uint256"}],"name":"CallbackAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"callback","type":"address"},{"indexed":false,"internalType":"uint256","name":"atIndex","type":"uint256"}],"name":"CallbackRemoved","type":"event"},{"inputs":[],"name":"MAX_CALLBACKS_COUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOTING","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_callback","type":"address"}],"name":"addCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"callbacks","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"callbacksLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_callback","type":"address"},{"internalType":"uint256","name":"_atIndex","type":"uint256"}],"name":"insertCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_atIndex","type":"uint256"}],"name":"removeCallback","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_voting","type":"address"},{"internalType":"bytes4","name":"_requiredIface","type":"bytes4"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"callback","type":"address"},{"indexed":false,"internalType":"uint256","name":"atIndex","type":"uint256"}],"name":"CallbackAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"callback","type":"address"},{"indexed":false,"internalType":"uint256","name":"atIndex","type":"uint256"}],"name":"CallbackRemoved","type":"event"},{"inputs":[],"name":"MAX_CALLBACKS_COUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUIRED_INTERFACE","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOTING","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_callback","type":"address"}],"name":"addCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"callbacks","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"callbacksLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_callback","type":"address"},{"internalType":"uint256","name":"_atIndex","type":"uint256"}],"name":"insertCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_atIndex","type":"uint256"}],"name":"removeCallback","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/lib/abi/SelfOwnedStETHBurner.json b/lib/abi/SelfOwnedStETHBurner.json index e8779fa00..d70f513d1 100644 --- a/lib/abi/SelfOwnedStETHBurner.json +++ b/lib/abi/SelfOwnedStETHBurner.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"_treasury","type":"address"},{"internalType":"address","name":"_lido","type":"address"},{"internalType":"address","name":"_voting","type":"address"},{"internalType":"uint256","name":"_totalCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_totalNonCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_maxBurnAmountPerRunBasePoints","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ERC20Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"ExcessStETHRecovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxBurnAmountPerRunBasePoints","type":"uint256"}],"name":"MaxBurnAmountPerRunChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"StETHBurnRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"StETHBurnt","type":"event"},{"inputs":[],"name":"LIDO","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TREASURY","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOTING","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getExcessStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMaxBurnAmountPerRunBasePoints","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNonCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"processLidoOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"recoverERC721","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"recoverExcessStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETH2Burn","type":"uint256"}],"name":"requestBurnMyStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETH2Burn","type":"uint256"}],"name":"requestBurnMyStETHForCover","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxBurnAmountPerRunBasePoints","type":"uint256"}],"name":"setMaxBurnAmountPerRunBasePoints","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_treasury","type":"address"},{"internalType":"address","name":"_lido","type":"address"},{"internalType":"address","name":"_voting","type":"address"},{"internalType":"uint256","name":"_totalCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_totalNonCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_maxBurnAmountPerRunBasisPoints","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxBurnAmountPerRunBasisPoints","type":"uint256"}],"name":"BurnAmountPerRunQuotaChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ERC20Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"ExcessStETHRecovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"StETHBurnRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesAmount","type":"uint256"}],"name":"StETHBurnt","type":"event"},{"inputs":[],"name":"LIDO","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TREASURY","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOTING","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBurnAmountPerRunQuota","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getExcessStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNonCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"processLidoOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"recoverERC721","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"recoverExcessStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETH2Burn","type":"uint256"}],"name":"requestBurnMyStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETH2Burn","type":"uint256"}],"name":"requestBurnMyStETHForCover","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxBurnAmountPerRunBasisPoints","type":"uint256"}],"name":"setBurnAmountPerRunQuota","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"_interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}] \ No newline at end of file diff --git a/lib/abi/StETH.json b/lib/abi/StETH.json index d3d352880..9775eefc4 100644 --- a/lib/abi/StETH.json +++ b/lib/abi/StETH.json @@ -1 +1 @@ -[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_ethAmount","type":"uint256"}],"name":"getSharesByPooledEth","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getTotalPooledEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"isStopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sharesAmount","type":"uint256"}],"name":"getPooledEthByShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTotalShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"sharesOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[],"name":"Stopped","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"}] \ No newline at end of file +[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_ethAmount","type":"uint256"}],"name":"getSharesByPooledEth","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getTotalPooledEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"isStopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sharesAmount","type":"uint256"}],"name":"getPooledEthByShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_sharesAmount","type":"uint256"}],"name":"transferShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTotalShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"sharesOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"sharesValue","type":"uint256"}],"name":"TransferShares","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"sharesAmount","type":"uint256"}],"name":"SharesBurnt","type":"event"},{"anonymous":false,"inputs":[],"name":"Stopped","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"}] \ No newline at end of file diff --git a/package.json b/package.json index 902a74bc6..75f453740 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,12 @@ "deploy:kintsugi:apps": "NETWORK_NAME=kintsugi NETWORK_STATE_FILE=deployed-kintsugi.json IPFS_API_URL=https://ipfs.infura.io:5001/api/v0 IPFS_GATEWAY_URL=https://ipfs.io hardhat run --no-compile ./scripts/deploy-lido-apps.js", "deploy:kintsugi:perm": "NETWORK_NAME=kintsugi NETWORK_STATE_FILE=deployed-kintsugi.json hardhat run --no-compile ./scripts/deploy-repo-permissions.js", "deploy:kintsugi:dao": "NETWORK_NAME=kintsugi NETWORK_STATE_FILE=deployed-kintsugi.json hardhat run --no-compile ./scripts/deploy-lido-dao.js", + "deploy:kiln:aragon-env": "NETWORK_NAME=kiln NETWORK_STATE_FILE=deployed-kiln.json hardhat run --no-compile ./scripts/deploy-aragon-env.js", + "deploy:kiln:aragon-std-apps": "NETWORK_NAME=kiln NETWORK_STATE_FILE=deployed-kiln.json IPFS_API_URL=https://ipfs.infura.io:5001/api/v0 IPFS_GATEWAY_URL=https://ipfs.io RELEASE_TYPE=major hardhat run --no-compile ./scripts/deploy-aragon-std-apps.js --config ./hardhat.config.aragon-apps.js", + "deploy:kiln:apm-and-template": "NETWORK_NAME=kiln NETWORK_STATE_FILE=deployed-kiln.json hardhat run --no-compile ./scripts/deploy-lido-apm-and-template.js", + "deploy:kiln:apps": "NETWORK_NAME=kiln NETWORK_STATE_FILE=deployed-kiln.json IPFS_API_URL=https://ipfs.infura.io:5001/api/v0 IPFS_GATEWAY_URL=https://ipfs.io hardhat run --no-compile ./scripts/deploy-lido-apps.js", + "deploy:kiln:perm": "NETWORK_NAME=kiln NETWORK_STATE_FILE=deployed-kiln.json hardhat run --no-compile ./scripts/deploy-repo-permissions.js", + "deploy:kiln:dao": "NETWORK_NAME=kiln NETWORK_STATE_FILE=deployed-kiln.json hardhat run --no-compile ./scripts/deploy-lido-dao.js", "deploy:local:all": "yarn compile && yarn deploy:local:aragon-env && yarn deploy:local:aragon-std-apps && yarn deploy:local:apm-and-template && yarn deploy:local:apps && yarn deploy:local:dao", "deploy:local:aragon-env": "NETWORK_NAME=local NETWORK_STATE_FILE=deployed-local.json hardhat run --no-compile ./scripts/deploy-aragon-env.js", "deploy:local:aragon-std-apps": "NETWORK_NAME=local NETWORK_STATE_FILE=deployed-local.json IPFS_API_URL=http://127.0.0.1:5001/api/v0 IPFS_GATEWAY_URL=http://127.0.0.1:8080 RELEASE_TYPE=major hardhat run --no-compile ./scripts/deploy-aragon-std-apps.js --config ./hardhat.config.aragon-apps.js", @@ -102,8 +108,9 @@ "eth-ens-namehash": "^2.0.8", "eth-gas-reporter": "latest", "ethereumjs-testrpc-sc": "^6.5.1-sc.1", - "hardhat": "^2.7.0", - "hardhat-gas-reporter": "1.0.7", + "hardhat": "^2.8.0", + "hardhat-contract-sizer": "^2.5.0", + "hardhat-gas-reporter": "1.0.8", "husky": "^4.3.0", "ipfs-http-client": "^55.0.0", "lerna": "^3.22.1", diff --git a/scripts/deploy-aragon-std-apps.js b/scripts/deploy-aragon-std-apps.js index 7032308bc..61a58326b 100644 --- a/scripts/deploy-aragon-std-apps.js +++ b/scripts/deploy-aragon-std-apps.js @@ -11,7 +11,7 @@ const { filterObject } = require('./helpers/collections') const { readAppName } = require('./helpers/aragon') const { gitCloneRepo } = require('./helpers/git') -const { uploadDirToIpfs } = require('./helpers/ipfs') +const { uploadDirToIpfs } = require('@aragon/buidler-aragon/dist/src/utils/ipfs') const { toContentUri } = require('@aragon/buidler-aragon/dist/src/utils/apm/utils') const APPS = ['agent', 'finance', 'token-manager', 'vault', 'voting'] diff --git a/scripts/helpers/ipfs.js b/scripts/helpers/ipfs.js deleted file mode 100644 index 5207f4128..000000000 --- a/scripts/helpers/ipfs.js +++ /dev/null @@ -1,23 +0,0 @@ -const { create, globSource } = require('ipfs-http-client') - -const globSourceOptions = { - recursive: true -} - -const addOptions = { - pin: true, - wrapWithDirectory: true, - timeout: 10000 -} - -async function uploadDirToIpfs({ apiUrl, dirPath }) { - const ipfs = await create(apiUrl) - - const results = [] - for await (const result of ipfs.addAll(globSource(dirPath, '*', globSourceOptions), addOptions)) { - results.push(result) - } - return results.find((r) => r.path === '').cid.toString() -} - -module.exports = { uploadDirToIpfs } diff --git a/scripts/multisig/04-publish-app-frontends.js b/scripts/multisig/04-publish-app-frontends.js index 0459c7b03..72814d586 100644 --- a/scripts/multisig/04-publish-app-frontends.js +++ b/scripts/multisig/04-publish-app-frontends.js @@ -17,7 +17,7 @@ const { readJSON } = require('../helpers/fs') require('@aragon/buidler-aragon/dist/bootstrap-paths') const { generateArtifacts } = require('@aragon/buidler-aragon/dist/src/utils/artifact/generateArtifacts') -const { uploadDirToIpfs } = require('../helpers/ipfs') +const { uploadDirToIpfs } = require('@aragon/buidler-aragon/dist/src/utils/ipfs') const { toContentUri } = require('@aragon/buidler-aragon/dist/src/utils/apm/utils') const { APP_NAMES } = require('./constants') diff --git a/scripts/multisig/12-check-dao.js b/scripts/multisig/12-check-dao.js index 805268f38..e11f3f191 100644 --- a/scripts/multisig/12-check-dao.js +++ b/scripts/multisig/12-check-dao.js @@ -566,7 +566,19 @@ async function assertDaoPermissions({ kernel, lido, oracle, nopsRegistry, agent, manager: voting, groups: [ { - roleNames: ['PAUSE_ROLE', 'MANAGE_FEE', 'MANAGE_WITHDRAWAL_KEY', 'SET_ORACLE', 'BURN_ROLE', 'SET_TREASURY', 'SET_INSURANCE_FUND'], + roleNames: [ + 'PAUSE_ROLE', + 'RESUME_ROLE', + 'MANAGE_FEE', + 'MANAGE_WITHDRAWAL_KEY', + 'SET_ORACLE', 'BURN_ROLE', + 'SET_TREASURY', + 'SET_INSURANCE_FUND', + 'STAKING_PAUSE_ROLE', + 'STAKING_CONTROL_ROLE', + 'SET_EL_REWARDS_VAULT_ROLE', + 'SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE' + ], grantee: voting } ] diff --git a/scripts/multisig/17-obtain-deployed-depositor.js b/scripts/multisig/17-obtain-deployed-depositor.js index 0245a942e..68089deeb 100644 --- a/scripts/multisig/17-obtain-deployed-depositor.js +++ b/scripts/multisig/17-obtain-deployed-depositor.js @@ -36,9 +36,16 @@ async function obtainInstance({ web3, artifacts, appName = APP }) { await assertParams(state.depositorParams, depositor, appArtifact) await assertAddresses({ lidoAddress, nosAddress, depositContractAddress }, depositor, appArtifact) - persistNetworkState(network.name, netId, state, { + + // If the depositor is already deployed save its previous address + const depositorCurrentAddress = state['depositorAddress'] + const newDepositorState = { depositorAddress: depositor.address - }) + } + if (depositorCurrentAddress !== undefined && depositor.address !== depositorCurrentAddress) { + newDepositorState['depositorPreviousAddress'] = depositorCurrentAddress + } + persistNetworkState(network.name, netId, state, newDepositorState) } async function assertParams({ maxDepositsPerBlock, minDepositBlockDistance, pauseIntentValidityPeriodBlocks }, instance, desc) { diff --git a/scripts/multisig/18-depositor-add-guardians.js b/scripts/multisig/18-depositor-add-guardians.js index 06d5c98ae..d807f1c30 100644 --- a/scripts/multisig/18-depositor-add-guardians.js +++ b/scripts/multisig/18-depositor-add-guardians.js @@ -11,15 +11,18 @@ async function obtainInstance({ web3, artifacts }) { const appArtifact = 'DepositSecurityModule' const netId = await web3.eth.net.getId() - logWideSplitter() - log(`Network ID:`, yl(netId)) - const state = readNetworkState(network.name, netId) assertRequiredNetworkState(state, REQUIRED_NET_STATE) - const depositor = await artifacts.require(appArtifact).at(state.depositorAddress) const { guardians = [], quorum = 1 } = state.depositorParams + + logWideSplitter() + log(`Network ID:`, yl(netId)) + console.log("Going to set these params in addGuardians(guardians, quorum):") + console.log({ guardians, quorum }) + console.log() + const depositor = await artifacts.require(appArtifact).at(state.depositorAddress) await saveCallTxData(`Set guardians`, depositor, 'addGuardians', `tx-18-depositor-add-guardians.json`, { arguments: [guardians, quorum], from: DEPLOYER || state.multisigAddress diff --git a/scripts/multisig/20-mitigating-deposit-front-running.js b/scripts/multisig/20-mitigating-deposit-front-running.js index 40cea0f49..08afed405 100644 --- a/scripts/multisig/20-mitigating-deposit-front-running.js +++ b/scripts/multisig/20-mitigating-deposit-front-running.js @@ -99,9 +99,9 @@ async function upgradeAppImpl({ web3, artifacts }) { const txName = `tx-20-mitigating-deposit-front-running.json` const votingDesc = `1) Publishing new implementation in lido app APM repo -2) Updating implementaion of lido app with new one +2) Updating implementation of lido app with new one 3) Publishing new implementation in node operators registry app APM repo -4) Updating implementaion of node operators registry app with new one +4) Updating implementation of node operators registry app with new one 5) Granting new permission DEPOSIT_ROLE for ${depositorAddress} ${nosIncreaseLimitsDesc.map((desc, index) => `${index + 6}) ${desc}`).join('\n')} ` diff --git a/scripts/multisig/26-deploy-exec-layer-rewards-vault.js b/scripts/multisig/26-deploy-exec-layer-rewards-vault.js new file mode 100644 index 000000000..93f4285ad --- /dev/null +++ b/scripts/multisig/26-deploy-exec-layer-rewards-vault.js @@ -0,0 +1,36 @@ +const runOrWrapScript = require('../helpers/run-or-wrap-script') +const { log, logSplitter, logWideSplitter, yl, gr } = require('../helpers/log') +const { saveDeployTx } = require('../helpers/deploy') +const { readNetworkState, assertRequiredNetworkState, persistNetworkState } = require('../helpers/persisted-network-state') + +const { APP_NAMES } = require('./constants') + +const DEPLOYER = process.env.DEPLOYER || '' +const REQUIRED_NET_STATE = ['daoInitialSettings', 'depositorParams', `app:${APP_NAMES.LIDO}`, `app:${APP_NAMES.NODE_OPERATORS_REGISTRY}`] + +async function deployELRewardsVault({ web3, artifacts }) { + const appArtifact = 'LidoExecutionLayerRewardsVault' + const netId = await web3.eth.net.getId() + + logWideSplitter() + log(`Network ID:`, yl(netId)) + + const state = readNetworkState(network.name, netId) + assertRequiredNetworkState(state, REQUIRED_NET_STATE) + const lidoAddress = state[`app:${APP_NAMES.LIDO}`].proxyAddress + log(`Using Lido contract address:`, yl(lidoAddress)) + + const lido = await artifacts.require('Lido').at(lidoAddress) + const treasuryAddr = await lido.getTreasury() + + log(`Using Lido Treasury contract address:`, yl(lidoAddress)) + logSplitter() + + const args = [lidoAddress, treasuryAddr] + await saveDeployTx(appArtifact, `tx-26-deploy-execution-layer-rewards-vault.json`, { + arguments: args, + from: DEPLOYER || state.multisigAddress + }) +} + +module.exports = runOrWrapScript(deployELRewardsVault, module) diff --git a/scripts/multisig/27-obtain-deployed-exec-layer-rewards-vault.js b/scripts/multisig/27-obtain-deployed-exec-layer-rewards-vault.js new file mode 100644 index 000000000..0864c67ee --- /dev/null +++ b/scripts/multisig/27-obtain-deployed-exec-layer-rewards-vault.js @@ -0,0 +1,41 @@ +const runOrWrapScript = require('../helpers/run-or-wrap-script') +const { log, logSplitter, logWideSplitter, logHeader, yl } = require('../helpers/log') +const { useOrGetDeployed, assertDeployedBytecode } = require('../helpers/deploy') +const { assert } = require('../helpers/assert') +const { readNetworkState, persistNetworkState, assertRequiredNetworkState } = require('../helpers/persisted-network-state') + +const { APP_NAMES, APP_ARTIFACTS } = require('./constants') + +const REQUIRED_NET_STATE = [ + 'executionLayerRewardsVaultDeployTx', + `app:${APP_NAMES.LIDO}`, +] + +async function obtainInstance({ web3, artifacts }) { + // convert dash-ed appName to camel case-d + const appArtifact = 'LidoExecutionLayerRewardsVault' + const netId = await web3.eth.net.getId() + + logWideSplitter() + log(`Network ID:`, yl(netId)) + + const state = readNetworkState(network.name, netId) + assertRequiredNetworkState(state, REQUIRED_NET_STATE) + + logHeader(`${appArtifact} app base`) + const vault = await useOrGetDeployed(appArtifact, null, state.executionLayerRewardsVaultDeployTx) + log(`Checking...`) + const lidoAddress = state[`app:${APP_NAMES.LIDO}`].proxyAddress + await assertAddresses({ lidoAddress }, vault, appArtifact) + persistNetworkState(network.name, netId, state, { + executionLayerRewardsVaultAddress: vault.address + }) +} + + +async function assertAddresses({ lidoAddress }, instance, desc) { + assert.equal(await instance.LIDO(), lidoAddress, `${desc}: wrong lido`) + log.success(`Lido address is correct`) +} + +module.exports = runOrWrapScript(obtainInstance, module) diff --git a/scripts/multisig/28-vote-merge-ready-first-pack-upgrade.js b/scripts/multisig/28-vote-merge-ready-first-pack-upgrade.js new file mode 100644 index 000000000..cf06ca6f5 --- /dev/null +++ b/scripts/multisig/28-vote-merge-ready-first-pack-upgrade.js @@ -0,0 +1,223 @@ +const { hash: namehash } = require('eth-ens-namehash') +const { encodeCallScript } = require('@aragon/contract-helpers-test/src/aragon-os') +const { BN } = require('bn.js') + +const runOrWrapScript = require('../helpers/run-or-wrap-script') +const { log, logSplitter, logWideSplitter, yl, gr } = require('../helpers/log') +const { saveCallTxData } = require('../helpers/tx-data') +const { readNetworkState, assertRequiredNetworkState, persistNetworkState } = require('../helpers/persisted-network-state') +const { resolveEnsAddress } = require('../components/ens') + +const { APP_NAMES } = require('./constants') + +const ETH = (value) => web3.utils.toWei(value + '', 'ether') + +const DEPLOYER = process.env.DEPLOYER || '' +const REQUIRED_NET_STATE = [ + 'lidoApmEnsName', + 'ensAddress', + 'daoAddress', + `app:${APP_NAMES.ARAGON_VOTING}`, + `app:${APP_NAMES.ARAGON_TOKEN_MANAGER}` +] + +async function createVoting({ web3, artifacts }) { + const netId = await web3.eth.net.getId() + + logWideSplitter() + log(`Network ID:`, yl(netId)) + + const state = readNetworkState(network.name, netId) + assertRequiredNetworkState(state, REQUIRED_NET_STATE.concat([ + 'app:lido', 'app:node-operators-registry', 'app:oracle', 'executionLayerRewardsVaultAddress'])) + + logSplitter() + + const ens = await artifacts.require('ENS').at(state.ensAddress) + const votingAddress = state[`app:${APP_NAMES.ARAGON_VOTING}`].proxyAddress + const tokenManagerAddress = state[`app:${APP_NAMES.ARAGON_TOKEN_MANAGER}`].proxyAddress + const voting = await artifacts.require('Voting').at(votingAddress) + const tokenManager = await artifacts.require('TokenManager').at(tokenManagerAddress) + const kernel = await artifacts.require('Kernel').at(state.daoAddress) + const aclAddress = await kernel.acl() + const acl = await artifacts.require('ACL').at(aclAddress) + const lidoAddress = state[`app:lido`].proxyAddress + const oracleAddress = state[`app:oracle`].proxyAddress + const lido = await artifacts.require('Lido').at(lidoAddress) + const oracle = await artifacts.require('LidoOracle').at(oracleAddress) + const elRewardsVaultAddress = state.executionLayerRewardsVaultAddress + + const elRewardsWithdrawalLimitPoints = 2 // see https://github.com/lidofinance/lido-dao/issues/405 + const dailyStakingLimit = ETH(150000) + const stakeLimitIncreasePerBlock = calcStakeLimitIncreasePerBlock(dailyStakingLimit) + + async function createGrantRoleForLidoAppCallData(roleName) { + return { + to: aclAddress, + calldata: await acl.contract.methods + .createPermission( + votingAddress, + state['app:lido'].proxyAddress, + web3.utils.soliditySha3(roleName), + votingAddress + ) + .encodeABI() + } + } + + + log(`Using ENS:`, yl(state.ensAddress)) + log(`TokenManager address:`, yl(tokenManagerAddress)) + log(`Voting address:`, yl(votingAddress)) + log(`Kernel:`, yl(kernel.address)) + log(`ACL:`, yl(acl.address)) + log(`ELRewardsWithdrawalLimitPoints: `, yl(elRewardsWithdrawalLimitPoints)) + log(`dailyStakeLimit: `, yl(dailyStakingLimit)) + log(`stakeLimitIncreasePerBlock: `, yl(stakeLimitIncreasePerBlock)) + + log.splitter() + + const lidoUpgradeCallData = await buildUpgradeTransaction('lido', state, ens, kernel) + + const nodeOperatorsRegistryUpgradeCallData = await buildUpgradeTransaction('node-operators-registry', state, ens, kernel) + + const oracleUpgradeCallData = await buildUpgradeTransaction('oracle', state, ens, kernel) + + const grantSetELRewardsVaultRoleCallData = await createGrantRoleForLidoAppCallData('SET_EL_REWARDS_VAULT_ROLE') + const grantSetELRewardsWithdrawalLimitRoleCallData = await createGrantRoleForLidoAppCallData('SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE') + const grantResumeRoleCallData = await createGrantRoleForLidoAppCallData('RESUME_ROLE') + const grantStakingPauseRoleCallData = await createGrantRoleForLidoAppCallData('STAKING_PAUSE_ROLE') + const grantStakingControlRoleCallData = await createGrantRoleForLidoAppCallData('STAKING_CONTROL_ROLE') + const grantManageProtocolContractsRoleCallData = await createGrantRoleForLidoAppCallData('MANAGE_PROTOCOL_CONTRACTS_ROLE') + + const setELRewardsVaultCallData = { + to: lidoAddress, + calldata: await lido.contract.methods.setELRewardsVault(elRewardsVaultAddress).encodeABI() + } + + const setELRewardsWithdrawalLimitCallData = { + to: lidoAddress, + calldata: await lido.contract.methods.setELRewardsWithdrawalLimit(elRewardsWithdrawalLimitPoints).encodeABI() + } + + const updateOracleVersionToV3CallData = { + to: oracleAddress, + calldata: await oracle.contract.methods.finalizeUpgrade_v3().encodeABI() + } + + const unpauseStakingCallData = { + to: lidoAddress, + calldata: await lido.contract.methods.resumeStaking(dailyStakingLimit, stakeLimitIncreasePerBlock).encodeABI() + } + + + const encodedUpgradeCallData = encodeCallScript([ + ...lidoUpgradeCallData, + ...nodeOperatorsRegistryUpgradeCallData, + ...oracleUpgradeCallData, + updateOracleVersionToV3CallData, + grantSetELRewardsVaultRoleCallData, + grantSetELRewardsWithdrawalLimitRoleCallData, + grantResumeRoleCallData, + grantStakingPauseRoleCallData, + grantStakingControlRoleCallData, + grantManageProtocolContractsRoleCallData, + setELRewardsVaultCallData, + setELRewardsWithdrawalLimitCallData, + unpauseStakingCallData, + ]) + + log(`encodedUpgradeCallData:`, yl(encodedUpgradeCallData)) + const votingCallData = encodeCallScript([ + { + to: votingAddress, + calldata: await voting.contract.methods.forward(encodedUpgradeCallData).encodeABI() + } + ]) + + // TODO: update the list + const txName = `tx-28-vote-merge-ready-first-pack-upgrade.json` + const votingDesc = `1) Publishing new implementation in lido app APM repo +2) Updating implementation of lido app with new one +3) Publishing new implementation in node-operators-registry app APM repo +4) Updating implementation of node-operators-registry app with new one +5) Publishing new implementation in oracle app APM repo +6) Updating implementation of oracle app with new one +7) Call Oracle's finalizeUpgrade_v3() to update internal version counter +8) Grant role SET_EL_REWARDS_VAULT_ROLE to voting +9) Grant role SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE to voting +10) Grant role RESUME_ROLE to voting +11) Grant role STAKING_PAUSE_ROLE to voting +12) Grant role STAKING_CONTROL_ROLE to voting +13) Grant role MANAGE_PROTOCOL_CONTRACTS_ROLE to voting +14) Set deployed LidoExecutionLayerRewardsVault to Lido contract +15) Set Execution Layer rewards withdrawal limit to ${elRewardsWithdrawalLimitPoints} basis points +16) Unpause staking setting daily limit to ${fromE18ToString(dailyStakingLimit)}` + + await saveCallTxData(votingDesc, tokenManager, 'forward', txName, { + arguments: [votingCallData], + from: DEPLOYER || state.multisigAddress + }) + + log.splitter() + log(gr(`Before continuing the deployment, please send all transactions listed above.`)) + log(gr(`You must complete it positively and execute before continuing with the deployment!`)) + log.splitter() +} + +async function buildUpgradeTransaction(appName, state, ens, kernel) { + const appId = namehash(`${appName}.${state.lidoApmEnsName}`) + const repoAddress = await resolveEnsAddress(artifacts, ens, appId) + const newContractAddress = state[`app:${appName}`].baseAddress + const newContentURI = state[`app:${appName}`].contentURI + const repo = await artifacts.require('Repo').at(repoAddress) + const APP_BASES_NAMESPACE = await kernel.APP_BASES_NAMESPACE() + + const { semanticVersion: currentVersion, contractAddress: currentContractAddress, contentURI: currentContentURI } = await repo.getLatest() + + const versionFrom = currentVersion.map((n) => n.toNumber()) + currentVersion[0] = currentVersion[0].addn(1) + currentVersion[1] = new BN(0) + currentVersion[2] = new BN(0) + const versionTo = currentVersion.map((n) => n.toNumber()) + + log.splitter() + + log(`Upgrading app:`, yl(appName)) + log(`App ID:`, yl(appId)) + log(`Contract implementation:`, yl(currentContractAddress), `->`, yl(newContractAddress)) + log(`Content URI:`, yl(currentContentURI), `->`, yl(newContentURI)) + log(`Bump version:`, yl(versionFrom.join('.')), `->`, yl(versionTo.join('.'))) + log(`Repo:`, yl(repoAddress)) + log(`APP_BASES_NAMESPACE`, yl(APP_BASES_NAMESPACE)) + + log.splitter() + const upgradeCallData = [ + { + // repo.newVersion(versionTo, contractAddress, contentURI) + to: repoAddress, + calldata: await repo.contract.methods.newVersion(versionTo, newContractAddress, newContentURI).encodeABI() + }, + + { + // kernel.setApp(APP_BASES_NAMESPACE, appId, oracle) + to: state.daoAddress, + calldata: await kernel.contract.methods.setApp(APP_BASES_NAMESPACE, appId, newContractAddress).encodeABI() + } + ] + + return upgradeCallData +} + +function calcStakeLimitIncreasePerBlock(dailyLimit) { + const secondsPerBlock = 12 + const secondsPerDay = 24 * 60 * 60 + const blocksPerDay = secondsPerDay / secondsPerBlock + return Math.floor(dailyLimit / blocksPerDay).toString() +} + +function fromE18ToString(x) { + return `${(x / 1e18).toFixed(3)} ETH (${x} wei)` +} + +module.exports = runOrWrapScript(createVoting, module) diff --git a/scripts/multisig/29-deploy-new-depositor-instance.js b/scripts/multisig/29-deploy-new-depositor-instance.js new file mode 100644 index 000000000..097a8c7f1 --- /dev/null +++ b/scripts/multisig/29-deploy-new-depositor-instance.js @@ -0,0 +1,82 @@ +const runOrWrapScript = require('../helpers/run-or-wrap-script') +const { log, logSplitter, logWideSplitter, yl, gr } = require('../helpers/log') +const { saveDeployTx } = require('../helpers/deploy') +const { readNetworkState, assertRequiredNetworkState, persistNetworkState } = require('../helpers/persisted-network-state') +const { assert, strictEqual } = require('../helpers/assert') + +const { APP_NAMES } = require('./constants') + +const DEPLOYER = process.env.DEPLOYER || '' +const REQUIRED_NET_STATE = [ + 'daoInitialSettings', + 'depositorParams', + 'depositorAddress', + `app:${APP_NAMES.LIDO}`, + `app:${APP_NAMES.NODE_OPERATORS_REGISTRY}` +] + +function assertParameterIsTheSame(contractValue, stateValue, paramName) { + // console.log(typeof(contractValue)) + // assert.equal(+contractValue.toString(), stateValue, + assert.equal(contractValue, stateValue, + `Value of ${paramName} in state and in the deployed contract differ`) +} + +async function upgradeApp({ web3, artifacts }) { + const appArtifact = 'DepositSecurityModule' + const netId = await web3.eth.net.getId() + + logWideSplitter() + log(`Network ID:`, yl(netId)) + + const state = readNetworkState(network.name, netId) + assertRequiredNetworkState(state, REQUIRED_NET_STATE) + const lidoAddress = state[`app:${APP_NAMES.LIDO}`].proxyAddress + log(`Using Lido address:`, yl(lidoAddress)) + const nosAddress = state[`app:${APP_NAMES.NODE_OPERATORS_REGISTRY}`].proxyAddress + log(`Using NOS address:`, yl(nosAddress)) + const { depositContractAddress } = state.daoInitialSettings.beaconSpec + log(`Using Deposit Contract:`, yl(depositContractAddress)) + logSplitter() + + const depositor = await artifacts.require(appArtifact).at(state.depositorAddress) + + const { maxDepositsPerBlock, minDepositBlockDistance, pauseIntentValidityPeriodBlocks, quorum, guardians } = state.depositorParams + + const args = [ + lidoAddress, + depositContractAddress, + nosAddress, + netId, + maxDepositsPerBlock, + minDepositBlockDistance, + pauseIntentValidityPeriodBlocks + ] + console.log("Constructor arguments (for use for source code verification): " + args.join(' ')) + + assertParameterIsTheSame(await depositor.LIDO(), lidoAddress, 'lidoAddress') + assertParameterIsTheSame(await depositor.DEPOSIT_CONTRACT(), depositContractAddress, 'depositContractAddress') + assertParameterIsTheSame(await depositor.getMaxDeposits(), maxDepositsPerBlock, 'maxDepositsPerBlock') + assertParameterIsTheSame(await depositor.getMinDepositBlockDistance(), minDepositBlockDistance, 'minDepositBlockDistance') + assertParameterIsTheSame(await depositor.getPauseIntentValidityPeriodBlocks(), pauseIntentValidityPeriodBlocks, 'pauseIntentValidityPeriodBlocks') + + // Also check guardians and quorum in state file correspond to the values in the contract + assertParameterIsTheSame(await depositor.getGuardianQuorum(), quorum, 'quorum') + assertParameterIsTheSame(await depositor.getGuardians(), guardians, 'guardians') + + await saveDeployTx(appArtifact, `tx-29-deploy-new-depositor-instance.json`, { + arguments: args, + from: DEPLOYER || state.multisigAddress + }) + persistNetworkState(network.name, netId, state, { + depositorConstructorArgs: args + }) + + logSplitter() + log(gr(`Before continuing the deployment, please send all contract creation transactions`)) + log(gr(`that you can find in the files listed above. You may use a multisig address`)) + log(gr(`if it supports deploying new contract instances.`)) + logSplitter() +} + +module.exports = runOrWrapScript(upgradeApp, module) diff --git a/scripts/multisig/30-initialize-updated-depositor.js b/scripts/multisig/30-initialize-updated-depositor.js new file mode 100644 index 000000000..a81f2f55b --- /dev/null +++ b/scripts/multisig/30-initialize-updated-depositor.js @@ -0,0 +1,42 @@ +const runOrWrapScript = require('../helpers/run-or-wrap-script') +const { log, logWideSplitter, logHeader, yl, gr } = require('../helpers/log') +const { saveCallTxData } = require('../helpers/tx-data') +const { readNetworkState, assertRequiredNetworkState } = require('../helpers/persisted-network-state') +const { assert } = require('chai') + +const DEPLOYER = process.env.DEPLOYER || '' +const REQUIRED_NET_STATE = ['depositorAddress', 'depositorParams', 'depositorPreviousAddress'] + +async function initializeDepositor({ web3, artifacts }) { + // convert dash-ed appName to camel case-d + const appArtifact = 'DepositSecurityModule' + const netId = await web3.eth.net.getId() + + logWideSplitter() + log(`Network ID:`, yl(netId)) + + const state = readNetworkState(network.name, netId) + assertRequiredNetworkState(state, REQUIRED_NET_STATE) + + const oldDepositor = await artifacts.require(appArtifact).at(state.depositorPreviousAddress) + const guardians = await oldDepositor.getGuardians() + const quorum = +(await oldDepositor.getGuardianQuorum()).toString() + + console.log("Going to set these params in addGuardians(guardians, quorum):") + console.log({ guardians, quorum }) + console.log() + + const depositor = await artifacts.require(appArtifact).at(state.depositorAddress) + assert.notEqual(depositor.getGuardians(), guardians, 'Guardians list on the new contract are supposed to be empty') + + await saveCallTxData(`Set guardians`, depositor, 'addGuardians', `tx-30-initialize-updated-depositor.json`, { + arguments: [guardians, quorum], + from: DEPLOYER || state.multisigAddress + }) + + log.splitter() + log(gr(`Before continuing the deployment, please send all transactions listed above.`)) + log(gr(`You must complete it positively and execute before continuing with the deployment!`)) + log.splitter() +} +module.exports = runOrWrapScript(initializeDepositor, module) diff --git a/test/0.4.24/lido.test.js b/test/0.4.24/lido.test.js index ec52c1efe..e32487be8 100644 --- a/test/0.4.24/lido.test.js +++ b/test/0.4.24/lido.test.js @@ -3,16 +3,21 @@ const { assert } = require('chai') const { newDao, newApp } = require('./helpers/dao') const { getInstalledApp } = require('@aragon/contract-helpers-test/src/aragon-os') const { assertBn, assertRevert, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') -const { ZERO_ADDRESS, bn } = require('@aragon/contract-helpers-test') +const { ZERO_ADDRESS, bn, getEventAt } = require('@aragon/contract-helpers-test') const { BN } = require('bn.js') +const { formatEther } = require('ethers/lib/utils') +const { getEthBalance, formatStEth: formamtStEth, formatBN } = require('../helpers/utils') const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') const Lido = artifacts.require('LidoMock.sol') +const ELRewardsVault = artifacts.require('LidoExecutionLayerRewardsVault.sol') const OracleMock = artifacts.require('OracleMock.sol') const DepositContractMock = artifacts.require('DepositContractMock.sol') const ERC20Mock = artifacts.require('ERC20Mock.sol') +const ERC721Mock = artifacts.require('ERC721Mock.sol') const VaultMock = artifacts.require('AragonVaultMock.sol') +const RewardEmulatorMock = artifacts.require('RewardEmulatorMock.sol') const ADDRESS_1 = '0x0000000000000000000000000000000000000001' const ADDRESS_2 = '0x0000000000000000000000000000000000000002' @@ -20,6 +25,7 @@ const ADDRESS_3 = '0x0000000000000000000000000000000000000003' const ADDRESS_4 = '0x0000000000000000000000000000000000000004' const UNLIMITED = 1000000000 +const TOTAL_BASIS_POINTS = 10000 const pad = (hex, bytesLength) => { const absentZeroes = bytesLength * 2 + 2 - hex.length @@ -35,17 +41,23 @@ const hexConcat = (first, ...rest) => { return result } -// Divides a BN by 1e15 +const assertNoEvent = (receipt, eventName, msg) => { + const event = getEventAt(receipt, eventName) + assert.equal(event, undefined, msg) +} +// Divides a BN by 1e15 const div15 = (bn) => bn.div(new BN('1000000000000000')) const ETH = (value) => web3.utils.toWei(value + '', 'ether') +const STETH = ETH const tokens = ETH contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) => { let appBase, nodeOperatorsRegistryBase, app, oracle, depositContract, operators let treasuryAddr, insuranceAddr let dao, acl + let elRewardsVault, rewarder before('deploy base app', async () => { // Deploy the app's base contract. @@ -71,12 +83,17 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) // Set up the app's permissions. await acl.createPermission(voting, app.address, await app.PAUSE_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.RESUME_ROLE(), appManager, { from: appManager }) await acl.createPermission(voting, app.address, await app.MANAGE_FEE(), appManager, { from: appManager }) await acl.createPermission(voting, app.address, await app.MANAGE_WITHDRAWAL_KEY(), appManager, { from: appManager }) await acl.createPermission(voting, app.address, await app.BURN_ROLE(), appManager, { from: appManager }) - await acl.createPermission(voting, app.address, await app.SET_TREASURY(), appManager, { from: appManager }) - await acl.createPermission(voting, app.address, await app.SET_ORACLE(), appManager, { from: appManager }) - await acl.createPermission(voting, app.address, await app.SET_INSURANCE_FUND(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.MANAGE_PROTOCOL_CONTRACTS_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.SET_EL_REWARDS_VAULT_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(voting, app.address, await app.STAKING_PAUSE_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.STAKING_CONTROL_ROLE(), appManager, { from: appManager }) await acl.createPermission(voting, operators.address, await operators.MANAGE_SIGNING_KEYS(), appManager, { from: appManager }) await acl.createPermission(voting, operators.address, await operators.ADD_NODE_OPERATOR_ROLE(), appManager, { from: appManager }) @@ -93,11 +110,21 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) // Initialize the app's proxy. await app.initialize(depositContract.address, oracle.address, operators.address) + treasuryAddr = await app.getTreasury() insuranceAddr = await app.getInsuranceFund() await oracle.setPool(app.address) await depositContract.reset() + + elRewardsVault = await ELRewardsVault.new(app.address, treasuryAddr) + rewarder = await RewardEmulatorMock.new(elRewardsVault.address) + let receipt = await app.setELRewardsVault(elRewardsVault.address, { from: voting }) + assertEvent(receipt, 'ELRewardsVaultSet', { expectedArgs: { executionLayerRewardsVault: elRewardsVault.address } }) + + const elRewardsWithdrawalLimitPoints = 3 + receipt = await app.setELRewardsWithdrawalLimit(elRewardsWithdrawalLimitPoints, { from: voting }) + assertEvent(receipt, 'ELRewardsWithdrawalLimitSet', { expectedArgs: { limitPoints: elRewardsWithdrawalLimitPoints } }) }) const checkStat = async ({ depositedValidators, beaconValidators, beaconBalance }) => { @@ -124,6 +151,142 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) assertBn(div15(operators_b.add(a1).add(a2).add(a3).add(a4)), operator, 'node operators token balance check') } + async function getStEthBalance(address) { + return formamtStEth(await app.balanceOf(address)) + } + + const logLidoState = async () => { + const elRewardsVaultBalance = await getEthBalance(elRewardsVault.address) + const lidoBalance = await getEthBalance(app.address) + const lidoTotalSupply = formatBN(await app.totalSupply()) + const lidoTotalPooledEther = formatBN(await app.getTotalPooledEther()) + const lidoBufferedEther = formatBN(await app.getBufferedEther()) + const lidoTotalShares = formatBN(await app.getTotalShares()) + const beaconStat = await app.getBeaconStat() + const depositedValidators = beaconStat.depositedValidators.toString() + const beaconValidators = beaconStat.beaconValidators.toString() + const beaconBalance = formatEther(beaconStat.beaconBalance) + + console.log({ + elRewardsVaultBalance, + lidoBalance, + lidoTotalSupply, + lidoTotalPooledEther, + lidoBufferedEther, + lidoTotalShares, + depositedValidators, + beaconValidators, + beaconBalance + }) + } + + const logBalances = async () => { + const user2stEthBalance = await getStEthBalance(user2) + const treasuryStEthBalance = await getStEthBalance(treasuryAddr) + const insuranceStEthBalance = await getStEthBalance(insuranceAddr) + console.log({ user2stEthBalance, treasuryStEthBalance, insuranceStEthBalance }) + } + + const logAll = async () => { + await logLidoState() + await logBalances() + console.log() + } + + const setupNodeOperatorsForELRewardsVaultTests = async (userAddress, initialDepositAmount) => { + await app.setFee(1000, { from: voting }) // 10% + + await operators.addNodeOperator('1', ADDRESS_1, { from: voting }) + await operators.addNodeOperator('2', ADDRESS_2, { from: voting }) + + await operators.setNodeOperatorStakingLimit(0, UNLIMITED, { from: voting }) + await operators.setNodeOperatorStakingLimit(1, UNLIMITED, { from: voting }) + + await app.setWithdrawalCredentials(pad('0x0202', 32), { from: voting }) + await operators.addSigningKeys(0, 1, pad('0x010203', 48), pad('0x01', 96), { from: voting }) + await operators.addSigningKeys( + 0, + 3, + hexConcat(pad('0x010204', 48), pad('0x010205', 48), pad('0x010206', 48)), + hexConcat(pad('0x01', 96), pad('0x01', 96), pad('0x01', 96)), + { from: voting } + ) + + await web3.eth.sendTransaction({ to: app.address, from: userAddress, value: initialDepositAmount }) + await app.methods['depositBufferedEther()']({ from: depositor }) + } + + it('Execution layer rewards distribution works when zero rewards reported', async () => { + const depositAmount = 32 + const elRewards = depositAmount / TOTAL_BASIS_POINTS + const beaconRewards = 0 + + await setupNodeOperatorsForELRewardsVaultTests(user2, ETH(depositAmount)) + await oracle.reportBeacon(100, 1, ETH(depositAmount)) + + await rewarder.reward({ from: user1, value: ETH(elRewards) }) + await oracle.reportBeacon(101, 1, ETH(depositAmount + beaconRewards)) + + assertBn(await app.getTotalPooledEther(), ETH(depositAmount + elRewards + beaconRewards)) + assertBn(await app.getBufferedEther(), ETH(elRewards)) + assertBn(await app.balanceOf(user2), STETH(depositAmount + elRewards)) + assertBn(await app.getTotalELRewardsCollected(), ETH(elRewards)) + }) + + it('Execution layer rewards distribution works when negative rewards reported', async () => { + const depositAmount = 32 + const elRewards = depositAmount / TOTAL_BASIS_POINTS + const beaconRewards = -2 + + await setupNodeOperatorsForELRewardsVaultTests(user2, ETH(depositAmount)) + await oracle.reportBeacon(100, 1, ETH(depositAmount)) + + await rewarder.reward({ from: user1, value: ETH(elRewards) }) + await oracle.reportBeacon(101, 1, ETH(depositAmount + beaconRewards)) + + assertBn(await app.getTotalPooledEther(), ETH(depositAmount + elRewards + beaconRewards)) + assertBn(await app.getBufferedEther(), ETH(elRewards)) + assertBn(await app.balanceOf(user2), STETH(depositAmount + elRewards + beaconRewards)) + assertBn(await app.getTotalELRewardsCollected(), ETH(elRewards)) + }) + + it('Execution layer rewards distribution works when positive rewards reported', async () => { + const depositAmount = 32 + const elRewards = depositAmount / TOTAL_BASIS_POINTS + const beaconRewards = 3 + + await setupNodeOperatorsForELRewardsVaultTests(user2, ETH(depositAmount)) + await oracle.reportBeacon(100, 1, ETH(depositAmount)) + + await rewarder.reward({ from: user1, value: ETH(elRewards) }) + await oracle.reportBeacon(101, 1, ETH(depositAmount + beaconRewards)) + + const protocolFeePoints = await app.getFee() + const shareOfRewardsForStakers = (TOTAL_BASIS_POINTS - protocolFeePoints) / TOTAL_BASIS_POINTS + assertBn(await app.getTotalPooledEther(), ETH(depositAmount + elRewards + beaconRewards)) + assertBn(await app.getBufferedEther(), ETH(elRewards)) + assertBn(await app.balanceOf(user2), STETH(depositAmount + shareOfRewardsForStakers * (elRewards + beaconRewards))) + assertBn(await app.getTotalELRewardsCollected(), ETH(elRewards)) + }) + + it('Attempt to set invalid execution layer rewards withdrawal limit', async () => { + const initialValue = await app.getELRewardsWithdrawalLimit() + + assertEvent(await app.setELRewardsWithdrawalLimit(1, { from: voting }), 'ELRewardsWithdrawalLimitSet', { + expectedArgs: { limitPoints: 1 } + }) + + await assertNoEvent(app.setELRewardsWithdrawalLimit(1, { from: voting }), 'ELRewardsWithdrawalLimitSet') + + await app.setELRewardsWithdrawalLimit(10000, { from: voting }) + await assertRevert(app.setELRewardsWithdrawalLimit(10001, { from: voting }), 'VALUE_OVER_100_PERCENT') + + await app.setELRewardsWithdrawalLimit(initialValue, { from: voting }) + + // unable to receive execution layer rewards from arbitrary account + assertRevert(app.receiveELRewards({ from: user1, value: ETH(1) })) + }) + it('setFee works', async () => { await app.setFee(110, { from: voting }) await assertRevert(app.setFee(110, { from: user1 }), 'APP_AUTH_FAILED') @@ -156,16 +319,10 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) }) it('setOracle works', async () => { - await assertRevert(app.setOracle(user1, { from: voting }), 'NOT_A_CONTRACT') - await app.setOracle(yetAnotherOracle.address, { from: voting }) - }) - - it('_setDepositContract reverts with invalid arg', async () => { - await assertRevert(app.setDepositContract(user1, { from: voting }), 'NOT_A_CONTRACT') - }) - - it('_setOperators reverts with invalid arg', async () => { - await assertRevert(app.setOperators(user1, { from: voting }), 'NOT_A_CONTRACT') + await assertRevert(app.setProtocolContracts(ZERO_ADDRESS, user2, user3, { from: voting }), 'ORACLE_ZERO_ADDRESS') + const receipt = await app.setProtocolContracts(yetAnotherOracle.address, oracle.address, oracle.address, { from: voting }) + assertEvent(receipt, 'ProtocolContactsSet', { expectedArgs: { oracle: yetAnotherOracle.address } }) + assert.equal(await app.getOracle(), yetAnotherOracle.address) }) it('setWithdrawalCredentials resets unused keys', async () => { @@ -404,6 +561,204 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) assertEvent(receipt, 'Submitted', { expectedArgs: { sender: user2, amount: ETH(5), referral: ZERO_ADDRESS } }) }) + const verifyStakeLimitState = async ( + expectedMaxStakeLimit, + expectedLimitIncrease, + expectedCurrentStakeLimit, + expectedIsStakingPaused, + expectedIsStakingLimited + ) => { + currentStakeLimit = await app.getCurrentStakeLimit() + assertBn(currentStakeLimit, expectedCurrentStakeLimit) + + isStakingPaused = await app.isStakingPaused() + assert.equal(isStakingPaused, expectedIsStakingPaused) + ;({ + isStakingPaused, + isStakingLimitSet, + currentStakeLimit, + maxStakeLimit, + maxStakeLimitGrowthBlocks, + prevStakeLimit, + prevStakeBlockNumber + } = await app.getStakeLimitFullInfo()) + + assertBn(currentStakeLimit, expectedCurrentStakeLimit) + assertBn(maxStakeLimit, expectedMaxStakeLimit) + assert.equal(isStakingPaused, expectedIsStakingPaused) + assert.equal(isStakingLimitSet, expectedIsStakingLimited) + + if (isStakingLimitSet) { + assertBn(maxStakeLimitGrowthBlocks, expectedLimitIncrease > 0 ? expectedMaxStakeLimit / expectedLimitIncrease : 0) + } + } + + it('staking pause & unlimited resume works', async () => { + let receipt + + const MAX_UINT256 = bn(2).pow(bn(256)).sub(bn(1)) + await verifyStakeLimitState(bn(0), bn(0), MAX_UINT256, false, false) + receipt = await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(2) }) + assertEvent(receipt, 'Submitted', { expectedArgs: { sender: user2, amount: ETH(2), referral: ZERO_ADDRESS } }) + + await assertRevert(app.pauseStaking(), 'APP_AUTH_FAILED') + receipt = await app.pauseStaking({ from: voting }) + assertEvent(receipt, 'StakingPaused') + verifyStakeLimitState(bn(0), bn(0), bn(0), true, false) + + await assertRevert(web3.eth.sendTransaction({ to: app.address, from: user2, value: ETH(2) }), `STAKING_PAUSED`) + await assertRevert(app.submit(ZERO_ADDRESS, { from: user2, value: ETH(2) }), `STAKING_PAUSED`) + + await assertRevert(app.resumeStaking(), 'APP_AUTH_FAILED') + receipt = await app.resumeStaking({ from: voting }) + assertEvent(receipt, 'StakingResumed') + await verifyStakeLimitState(bn(0), bn(0), MAX_UINT256, false, false) + + await web3.eth.sendTransaction({ to: app.address, from: user2, value: ETH(1.1) }) + receipt = await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(1.4) }) + assertEvent(receipt, 'Submitted', { expectedArgs: { sender: user2, amount: ETH(1.4), referral: ZERO_ADDRESS } }) + }) + + const mineNBlocks = async (n) => { + for (let index = 0; index < n; index++) { + await ethers.provider.send('evm_mine') + } + } + + it('staking resume with a limit works', async () => { + let receipt + + const blocksToReachMaxStakeLimit = 300 + const expectedMaxStakeLimit = ETH(3) + const limitIncreasePerBlock = bn(expectedMaxStakeLimit).div(bn(blocksToReachMaxStakeLimit)) // 1 * 10**16 + + receipt = await app.resumeStaking({ from: voting }) + assertEvent(receipt, 'StakingResumed') + receipt = await app.setStakingLimit(expectedMaxStakeLimit, limitIncreasePerBlock, { from: voting }) + assertEvent(receipt, 'StakingLimitSet', { + expectedArgs: { + maxStakeLimit: expectedMaxStakeLimit, + stakeLimitIncreasePerBlock: limitIncreasePerBlock + } + }) + + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, expectedMaxStakeLimit, false, true) + receipt = await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(2) }) + assertEvent(receipt, 'Submitted', { expectedArgs: { sender: user2, amount: ETH(2), referral: ZERO_ADDRESS } }) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, ETH(1), false, true) + await assertRevert(app.submit(ZERO_ADDRESS, { from: user2, value: ETH(2.5) }), `STAKE_LIMIT`) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, bn(ETH(1)).add(limitIncreasePerBlock), false, true) + + // expect to grow for another 1.5 ETH since last submit + // every revert produces new block, so we need to account that block + await mineNBlocks(blocksToReachMaxStakeLimit / 2 - 1) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, ETH(2.5), false, true) + await assertRevert(app.submit(ZERO_ADDRESS, { from: user2, value: ETH(2.6) }), `STAKE_LIMIT`) + receipt = await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(2.5) }) + assertEvent(receipt, 'Submitted', { expectedArgs: { sender: user2, amount: ETH(2.5), referral: ZERO_ADDRESS } }) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, limitIncreasePerBlock.muln(2), false, true) + + await assertRevert(app.submit(ZERO_ADDRESS, { from: user2, value: ETH(0.1) }), `STAKE_LIMIT`) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, limitIncreasePerBlock.muln(3), false, true) + // once again, we are subtracting blocks number induced by revert checks + await mineNBlocks(blocksToReachMaxStakeLimit / 3 - 4) + + receipt = await app.submit(ZERO_ADDRESS, { from: user1, value: ETH(1) }) + assertEvent(receipt, 'Submitted', { expectedArgs: { sender: user1, amount: ETH(1), referral: ZERO_ADDRESS } }) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, ETH(0), false, true) + + // check that limit is restored completely + await mineNBlocks(blocksToReachMaxStakeLimit) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, expectedMaxStakeLimit, false, true) + + // check that limit is capped by maxLimit value and doesn't grow infinitely + await mineNBlocks(10) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, expectedMaxStakeLimit, false, true) + + await assertRevert(app.setStakingLimit(ETH(0), ETH(0), { from: voting }), `ZERO_MAX_STAKE_LIMIT`) + await assertRevert(app.setStakingLimit(ETH(1), ETH(1.1), { from: voting }), `TOO_LARGE_LIMIT_INCREASE`) + await assertRevert(app.setStakingLimit(ETH(1), bn(10), { from: voting }), `TOO_SMALL_LIMIT_INCREASE`) + }) + + it('resume staking with an one-shot limit works', async () => { + let receipt + + const expectedMaxStakeLimit = ETH(7) + const limitIncreasePerBlock = 0 + + receipt = await app.resumeStaking({ from: voting }) + assertEvent(receipt, 'StakingResumed') + receipt = await app.setStakingLimit(expectedMaxStakeLimit, limitIncreasePerBlock, { from: voting }) + assertEvent(receipt, 'StakingLimitSet', { + expectedArgs: { + maxStakeLimit: expectedMaxStakeLimit, + stakeLimitIncreasePerBlock: limitIncreasePerBlock + } + }) + + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, expectedMaxStakeLimit, false, true) + receipt = await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(5) }) + assertEvent(receipt, 'Submitted', { expectedArgs: { sender: user2, amount: ETH(5), referral: ZERO_ADDRESS } }) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, ETH(2), false, true) + receipt = await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(2) }) + assertEvent(receipt, 'Submitted', { expectedArgs: { sender: user2, amount: ETH(2), referral: ZERO_ADDRESS } }) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, ETH(0), false, true) + await assertRevert(app.submit(ZERO_ADDRESS, { from: user2, value: ETH(0.1) }), `STAKE_LIMIT`) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, ETH(0), false, true) + await mineNBlocks(100) + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, ETH(0), false, true) + }) + + it('resume with various changing limits work', async () => { + let receipt + + const expectedMaxStakeLimit = ETH(9) + const limitIncreasePerBlock = bn(expectedMaxStakeLimit).divn(100) + + receipt = await app.resumeStaking({ from: voting }) + assertEvent(receipt, 'StakingResumed') + receipt = await app.setStakingLimit(expectedMaxStakeLimit, limitIncreasePerBlock, { from: voting }) + assertEvent(receipt, 'StakingLimitSet', { + expectedArgs: { + maxStakeLimit: expectedMaxStakeLimit, + stakeLimitIncreasePerBlock: limitIncreasePerBlock + } + }) + + await verifyStakeLimitState(expectedMaxStakeLimit, limitIncreasePerBlock, expectedMaxStakeLimit, false, true) + + const smallerExpectedMaxStakeLimit = ETH(5) + const smallerLimitIncreasePerBlock = bn(smallerExpectedMaxStakeLimit).divn(200) + + receipt = await app.setStakingLimit(smallerExpectedMaxStakeLimit, smallerLimitIncreasePerBlock, { from: voting }) + assertEvent(receipt, 'StakingLimitSet', { + expectedArgs: { + maxStakeLimit: smallerExpectedMaxStakeLimit, + stakeLimitIncreasePerBlock: smallerLimitIncreasePerBlock + } + }) + + await verifyStakeLimitState(smallerExpectedMaxStakeLimit, smallerLimitIncreasePerBlock, smallerExpectedMaxStakeLimit, false, true) + + const largerExpectedMaxStakeLimit = ETH(10) + const largerLimitIncreasePerBlock = bn(largerExpectedMaxStakeLimit).divn(1000) + + receipt = await app.setStakingLimit(largerExpectedMaxStakeLimit, largerLimitIncreasePerBlock, { from: voting }) + assertEvent(receipt, 'StakingLimitSet', { + expectedArgs: { + maxStakeLimit: largerExpectedMaxStakeLimit, + stakeLimitIncreasePerBlock: largerLimitIncreasePerBlock + } + }) + + await verifyStakeLimitState(largerExpectedMaxStakeLimit, largerLimitIncreasePerBlock, smallerExpectedMaxStakeLimit, false, true) + + receipt = await app.removeStakingLimit({ from: voting }) + assertEvent(receipt, 'StakingLimitRemoved') + + await verifyStakeLimitState(0, 0, bn(2).pow(bn(256)).sub(bn(1)), false, false) + }) + it('reverts when trying to call unknown function', async () => { const wrongMethodABI = '0x00' await assertRevert(web3.eth.sendTransaction({ to: app.address, from: user2, value: ETH(1), data: wrongMethodABI }), 'NON_EMPTY_DATA') @@ -474,42 +829,7 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) assertBn(await app.getBufferedEther(), ETH(5)) }) - it('withrawal method reverts', async () => { - await operators.addNodeOperator('1', ADDRESS_1, { from: voting }) - await operators.addNodeOperator('2', ADDRESS_2, { from: voting }) - - await operators.setNodeOperatorStakingLimit(0, UNLIMITED, { from: voting }) - await operators.setNodeOperatorStakingLimit(1, UNLIMITED, { from: voting }) - - await app.setWithdrawalCredentials(pad('0x0202', 32), { from: voting }) - await operators.addSigningKeys( - 0, - 6, - hexConcat( - pad('0x010203', 48), - pad('0x010204', 48), - pad('0x010205', 48), - pad('0x010206', 48), - pad('0x010207', 48), - pad('0x010208', 48) - ), - hexConcat(pad('0x01', 96), pad('0x01', 96), pad('0x01', 96), pad('0x01', 96), pad('0x01', 96), pad('0x01', 96)), - { from: voting } - ) - - await web3.eth.sendTransaction({ to: app.address, from: user1, value: ETH(1) }) - await app.methods['depositBufferedEther()']({ from: depositor }) - assertBn(await app.getTotalPooledEther(), ETH(1)) - assertBn(await app.totalSupply(), tokens(1)) - assertBn(await app.getBufferedEther(), ETH(1)) - - await checkStat({ depositedValidators: 0, beaconValidators: 0, beaconBalance: ETH(0) }) - - await assertRevert(app.withdraw(tokens(1), pad('0x1000', 32), { from: nobody }), 'NOT_IMPLEMENTED_YET') - await assertRevert(app.withdraw(tokens(1), pad('0x1000', 32), { from: user1 }), 'NOT_IMPLEMENTED_YET') - }) - - it('pushBeacon works', async () => { + it('handleOracleReport works', async () => { await operators.addNodeOperator('1', ADDRESS_1, { from: voting }) await operators.addNodeOperator('2', ADDRESS_2, { from: voting }) @@ -530,12 +850,12 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) await app.methods['depositBufferedEther()']({ from: depositor }) await checkStat({ depositedValidators: 1, beaconValidators: 0, beaconBalance: ETH(0) }) - await assertRevert(app.pushBeacon(1, ETH(30), { from: appManager }), 'APP_AUTH_FAILED') + await assertRevert(app.handleOracleReport(1, ETH(30), { from: appManager }), 'APP_AUTH_FAILED') await oracle.reportBeacon(100, 1, ETH(30)) await checkStat({ depositedValidators: 1, beaconValidators: 1, beaconBalance: ETH(30) }) - await assertRevert(app.pushBeacon(1, ETH(29), { from: nobody }), 'APP_AUTH_FAILED') + await assertRevert(app.handleOracleReport(1, ETH(29), { from: nobody }), 'APP_AUTH_FAILED') await oracle.reportBeacon(50, 1, ETH(100)) // stale data await checkStat({ depositedValidators: 1, beaconValidators: 1, beaconBalance: ETH(100) }) @@ -640,9 +960,9 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) await assertRevert(app.stop({ from: user2 }), 'APP_AUTH_FAILED') await app.stop({ from: voting }) - await assertRevert(web3.eth.sendTransaction({ to: app.address, from: user1, value: ETH(4) }), 'CONTRACT_IS_STOPPED') - await assertRevert(web3.eth.sendTransaction({ to: app.address, from: user1, value: ETH(4) }), 'CONTRACT_IS_STOPPED') - await assertRevert(app.submit('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', { from: user1, value: ETH(4) }), 'CONTRACT_IS_STOPPED') + await assertRevert(web3.eth.sendTransaction({ to: app.address, from: user1, value: ETH(4) }), 'STAKING_PAUSED') + await assertRevert(web3.eth.sendTransaction({ to: app.address, from: user1, value: ETH(4) }), 'STAKING_PAUSED') + await assertRevert(app.submit('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', { from: user1, value: ETH(4) }), 'STAKING_PAUSED') await assertRevert(app.resume({ from: user2 }), 'APP_AUTH_FAILED') await app.resume({ from: voting }) @@ -704,7 +1024,7 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) await web3.eth.sendTransaction({ to: app.address, from: user2, value: ETH(34) }) await app.methods['depositBufferedEther()']({ from: depositor }) - // some slashing occured + // some slashing occurred await oracle.reportBeacon(100, 1, ETH(30)) await checkStat({ depositedValidators: 1, beaconValidators: 1, beaconBalance: ETH(30) }) @@ -1057,14 +1377,38 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) it('burnShares works', async () => { await web3.eth.sendTransaction({ to: app.address, from: user1, value: ETH(1) }) - // not permited from arbitary address + // not permitted from arbitrary address await assertRevert(app.burnShares(user1, ETH(1), { from: nobody }), 'APP_AUTH_FAILED') // voting can burn shares of any user - await app.burnShares(user1, ETH(0.5), { from: voting }) - await app.burnShares(user1, ETH(0.5), { from: voting }) + const expectedPreTokenAmount = await app.getPooledEthByShares(ETH(0.5)) + let receipt = await app.burnShares(user1, ETH(0.5), { from: voting }) + const expectedPostTokenAmount = await app.getPooledEthByShares(ETH(0.5)) + assertEvent(receipt, 'SharesBurnt', { + expectedArgs: { + account: user1, + preRebaseTokenAmount: expectedPreTokenAmount, + postRebaseTokenAmount: expectedPostTokenAmount, + sharesAmount: ETH(0.5) + } + }) + + const expectedPreDoubledAmount = await app.getPooledEthByShares(ETH(0.5)) + receipt = await app.burnShares(user1, ETH(0.5), { from: voting }) + const expectedPostDoubledAmount = await app.getPooledEthByShares(ETH(0.5)) + assertEvent(receipt, 'SharesBurnt', { + expectedArgs: { + account: user1, + preRebaseTokenAmount: expectedPreDoubledAmount, + postRebaseTokenAmount: expectedPostDoubledAmount, + sharesAmount: ETH(0.5) + } + }) + + assertBn(expectedPreTokenAmount.mul(bn(2)), expectedPreDoubledAmount) + assertBn(tokens(0), await app.getPooledEthByShares(ETH(0.5))) - // user1 has zero shares afteralls + // user1 has zero shares after all assertBn(await app.sharesOf(user1), tokens(0)) // voting can't continue burning if user already has no shares @@ -1072,42 +1416,50 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) }) context('treasury', () => { - it('treasury adddress has been set after init', async () => { + it('treasury address has been set after init', async () => { assert.notEqual(await app.getTreasury(), ZERO_ADDRESS) }) - it(`treasury can't be set by an arbitary address`, async () => { - await assertRevert(app.setTreasury(user1, { from: nobody })) - await assertRevert(app.setTreasury(user1, { from: user1 })) + it(`treasury can't be set by an arbitrary address`, async () => { + await assertRevert(app.setProtocolContracts(await app.getOracle(), user1, await app.getInsuranceFund(), { from: nobody })) + await assertRevert(app.setProtocolContracts(await app.getOracle(), user1, await app.getInsuranceFund(), { from: user1 })) }) it('voting can set treasury', async () => { - await app.setTreasury(user1, { from: voting }) + const receipt = await app.setProtocolContracts(await app.getOracle(), user1, await app.getInsuranceFund(), { from: voting }) + assertEvent(receipt, 'ProtocolContactsSet', { expectedArgs: { treasury: user1 } }) assert.equal(await app.getTreasury(), user1) }) it('reverts when treasury is zero address', async () => { - await assertRevert(app.setTreasury(ZERO_ADDRESS, { from: voting }), 'SET_TREASURY_ZERO_ADDRESS') + await assertRevert( + app.setProtocolContracts(await app.getOracle(), ZERO_ADDRESS, await app.getInsuranceFund(), { from: voting }), + 'TREASURY_ZERO_ADDRESS' + ) }) }) context('insurance fund', () => { - it('insurance fund adddress has been set after init', async () => { + it('insurance fund address has been set after init', async () => { assert.notEqual(await app.getInsuranceFund(), ZERO_ADDRESS) }) - it(`insurance fund can't be set by an arbitary address`, async () => { - await assertRevert(app.setInsuranceFund(user1, { from: nobody })) - await assertRevert(app.setInsuranceFund(user1, { from: user1 })) + it(`insurance fund can't be set by an arbitrary address`, async () => { + await assertRevert(app.setProtocolContracts(await app.getOracle(), await app.getTreasury(), user1, { from: nobody })) + await assertRevert(app.setProtocolContracts(await app.getOracle(), await app.getTreasury(), user1, { from: user1 })) }) it('voting can set insurance fund', async () => { - await app.setInsuranceFund(user1, { from: voting }) + const receipt = await app.setProtocolContracts(await app.getOracle(), await app.getTreasury(), user1, { from: voting }) + assertEvent(receipt, 'ProtocolContactsSet', { expectedArgs: { insuranceFund: user1 } }) assert.equal(await app.getInsuranceFund(), user1) }) it('reverts when insurance fund is zero address', async () => { - await assertRevert(app.setInsuranceFund(ZERO_ADDRESS, { from: voting }), 'SET_INSURANCE_FUND_ZERO_ADDRESS') + await assertRevert( + app.setProtocolContracts(await app.getOracle(), await app.getTreasury(), ZERO_ADDRESS, { from: voting }), + 'INSURANCE_FUND_ZERO_ADDRESS' + ) }) }) @@ -1117,7 +1469,7 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor]) }) it('reverts when vault is not set', async () => { - await assertRevert(app.transferToVault(anyToken.address, { from: nobody }), 'RECOVER_VAULT_NOT_CONTRACT') + await assertRevert(app.transferToVault(anyToken.address, { from: nobody }), 'RECOVER_VAULT_ZERO') }) context('recovery works with vault mock deployed', () => { diff --git a/test/0.4.24/lidoPushBeacon.test.js b/test/0.4.24/lidoHandleOracleReport.test.js similarity index 90% rename from test/0.4.24/lidoPushBeacon.test.js rename to test/0.4.24/lidoHandleOracleReport.test.js index b42db18bd..747dd30c9 100644 --- a/test/0.4.24/lidoPushBeacon.test.js +++ b/test/0.4.24/lidoHandleOracleReport.test.js @@ -7,7 +7,7 @@ const OracleMock = artifacts.require('OracleMock.sol') const ETH = (value) => web3.utils.toWei(value + '', 'ether') -contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) => { +contract('Lido handleOracleReport', ([appManager, voting, user1, user2, user3, nobody]) => { let appBase, app, oracle before('deploy base app', async () => { @@ -53,7 +53,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 0, beaconValidators: 0, beaconBalance: ETH(0) }) assertBn(await app.getBufferedEther(), ETH(0)) assertBn(await app.getTotalPooledEther(), ETH(0)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) @@ -62,7 +62,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 0, beaconValidators: 0, beaconBalance: ETH(0) }) assertBn(await app.getBufferedEther(), ETH(0)) assertBn(await app.getTotalPooledEther(), ETH(0)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) }) @@ -80,7 +80,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 0, beaconValidators: 0, beaconBalance: ETH(0) }) assertBn(await app.getBufferedEther(), ETH(12)) assertBn(await app.getTotalPooledEther(), ETH(12)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) @@ -89,7 +89,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 0, beaconValidators: 0, beaconBalance: ETH(0) }) assertBn(await app.getBufferedEther(), ETH(12)) assertBn(await app.getTotalPooledEther(), ETH(12)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) }) @@ -113,7 +113,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 1, beaconValidators: 0, beaconBalance: ETH(0) }) assertBn(await app.getBufferedEther(), ETH(3)) assertBn(await app.getTotalPooledEther(), ETH(35)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) @@ -122,7 +122,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 1, beaconValidators: 0, beaconBalance: ETH(0) }) assertBn(await app.getBufferedEther(), ETH(3)) assertBn(await app.getTotalPooledEther(), ETH(35)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) @@ -131,7 +131,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 1, beaconValidators: 1, beaconBalance: ETH(31) }) assertBn(await app.getBufferedEther(), ETH(3)) assertBn(await app.getTotalPooledEther(), ETH(34)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) @@ -140,7 +140,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 1, beaconValidators: 1, beaconBalance: ETH(32) }) assertBn(await app.getBufferedEther(), ETH(3)) assertBn(await app.getTotalPooledEther(), ETH(35)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) }) @@ -164,7 +164,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 2, beaconValidators: 1, beaconBalance: ETH(0) }) assertBn(await app.getBufferedEther(), ETH(5)) assertBn(await app.getTotalPooledEther(), ETH(37)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) @@ -173,7 +173,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 2, beaconValidators: 1, beaconBalance: ETH(1) }) assertBn(await app.getBufferedEther(), ETH(5)) assertBn(await app.getTotalPooledEther(), ETH(38)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) @@ -182,7 +182,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 2, beaconValidators: 2, beaconBalance: ETH(62) }) assertBn(await app.getBufferedEther(), ETH(5)) assertBn(await app.getTotalPooledEther(), ETH(67)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) @@ -191,7 +191,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 2, beaconValidators: 1, beaconBalance: ETH(31) }) assertBn(await app.getBufferedEther(), ETH(5)) assertBn(await app.getTotalPooledEther(), ETH(68)) - assert.equal(await app.distributeRewardsCalled(), true) + assert.equal(await app.distributeFeeCalled(), true) assertBn(await app.totalRewards(), ETH(1)) }) @@ -200,7 +200,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 2, beaconValidators: 2, beaconBalance: ETH(63) }) assertBn(await app.getBufferedEther(), ETH(5)) assertBn(await app.getTotalPooledEther(), ETH(68)) - assert.equal(await app.distributeRewardsCalled(), true) + assert.equal(await app.distributeFeeCalled(), true) assertBn(await app.totalRewards(), ETH(1)) }) @@ -209,7 +209,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 2, beaconValidators: 1, beaconBalance: ETH(30) }) assertBn(await app.getBufferedEther(), ETH(5)) assertBn(await app.getTotalPooledEther(), ETH(67)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) }) @@ -232,7 +232,7 @@ contract('Lido pushBeacon', ([appManager, voting, user1, user2, user3, nobody]) checkStat({ depositedValidators: 5, beaconValidators: 4, beaconBalance: ETH(1) }) assertBn(await app.getBufferedEther(), ETH(0)) assertBn(await app.getTotalPooledEther(), ETH(33)) - assert.equal(await app.distributeRewardsCalled(), false) + assert.equal(await app.distributeFeeCalled(), false) assertBn(await app.totalRewards(), 0) }) }) diff --git a/test/0.4.24/lidooracle.test.js b/test/0.4.24/lidooracle.test.js index 6d1de7f55..06af6e61b 100644 --- a/test/0.4.24/lidooracle.test.js +++ b/test/0.4.24/lidooracle.test.js @@ -2,10 +2,12 @@ const { assert } = require('chai') const { newDao, newApp } = require('./helpers/dao') const { assertBn, assertRevert, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') const { toBN } = require('../helpers/utils') +const keccak256 = require('js-sha3').keccak_256 const LidoOracle = artifacts.require('LidoOracleMock.sol') const Lido = artifacts.require('LidoMockForOracle.sol') -const BeaconReportReceiver = artifacts.require('BeaconReportReceiverMock.sol') +const BeaconReportReceiver = artifacts.require('BeaconReportReceiverMock') +const BeaconReportReceiverWithoutERC165 = artifacts.require('BeaconReportReceiverMockWithoutERC165') const GENESIS_TIME = 1606824000 const EPOCH_LENGTH = 32 * 12 @@ -46,8 +48,9 @@ contract('LidoOracle', ([appManager, voting, user1, user2, user3, user4, user5, // Initialize the app's proxy. await app.setTime(GENESIS_TIME) - await app.initialize(appLido.address, 1, 32, 12, GENESIS_TIME) - await app.initialize_v2(1000, 500) // initialize the second version: 10% yearly increase, 5% moment decrease + + // 1000 and 500 stand for 10% yearly increase, 5% moment decrease + await app.initialize(appLido.address, 1, 32, 12, GENESIS_TIME, 1000, 500) }) it('beaconSpec is correct', async () => { @@ -80,6 +83,8 @@ contract('LidoOracle', ([appManager, voting, user1, user2, user3, user4, user5, assertBn(beaconSpec.genesisTime, 1) }) describe('Test utility functions:', function () { + this.timeout(60000) // addOracleMember edge-case is heavy on execution time + beforeEach(async () => { await app.setTime(GENESIS_TIME) }) @@ -99,6 +104,18 @@ contract('LidoOracle', ([appManager, voting, user1, user2, user3, user4, user5, await assertRevert(app.addOracleMember(user2, { from: voting }), 'MEMBER_EXISTS') }) + it('addOracleMember edge-case', async () => { + const promises = [] + const maxMembersCount = await app.MAX_MEMBERS() + for (let i = 0; i < maxMembersCount; ++i) { + const addr = '0x' + keccak256('member' + i).substring(0, 40) + promises.push(app.addOracleMember(addr, { from: voting })) + } + await Promise.all(promises) + + assertRevert(app.addOracleMember(user4, { from: voting }), 'TOO_MANY_MEMBERS') + }) + it('removeOracleMember works', async () => { await app.addOracleMember(user1, { from: voting }) @@ -481,6 +498,9 @@ contract('LidoOracle', ([appManager, voting, user1, user2, user3, user4, user5, }) it('quorum receiver called with same arguments as getLastCompletedReportDelta', async () => { + const badMock = await BeaconReportReceiverWithoutERC165.new() + await assertRevert(app.setBeaconReportReceiver(badMock.address, { from: voting }), 'BAD_BEACON_REPORT_RECEIVER') + const mock = await BeaconReportReceiver.new() let receipt = await app.setBeaconReportReceiver(mock.address, { from: voting }) assertEvent(receipt, 'BeaconReportReceiverSet', { expectedArgs: { callback: mock.address } }) diff --git a/test/0.4.24/node-operators-registry.test.js b/test/0.4.24/node-operators-registry.test.js index bb60450ba..3941afd88 100644 --- a/test/0.4.24/node-operators-registry.test.js +++ b/test/0.4.24/node-operators-registry.test.js @@ -1,8 +1,9 @@ const { assert } = require('chai') -const { hexSplit } = require('../helpers/utils') +const { hexSplit, toBN } = require('../helpers/utils') const { newDao, newApp } = require('./helpers/dao') const { ZERO_ADDRESS, getEventAt, getEventArgument } = require('@aragon/contract-helpers-test') const { assertBn, assertRevert, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') +const keccak256 = require('js-sha3').keccak_256 const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry.sol') const PoolMock = artifacts.require('PoolMock.sol') @@ -31,6 +32,11 @@ const hexConcat = (first, ...rest) => { return result } +const assertNoEvent = (receipt, eventName, msg) => { + const event = getEventAt(receipt, eventName) + assert.equal(event, undefined, msg) +} + const ETH = (value) => web3.utils.toWei(value + '', 'ether') const tokens = ETH @@ -82,6 +88,20 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob await assertRevert(app.addNodeOperator('1', ADDRESS_3, { from: nobody }), 'APP_AUTH_FAILED') }) + it('addNodeOperator limit works', async () => { + const maxOperatorsCount = await app.MAX_NODE_OPERATORS_COUNT() + const currentOperatorsCount = await app.getNodeOperatorsCount() + + for (let opIndex = currentOperatorsCount; opIndex < maxOperatorsCount; opIndex++) { + const name = keccak256('op' + opIndex) + const addr = '0x' + name.substr(0, 40) + + await app.addNodeOperator(name, addr, { from: voting }) + } + + await assertRevert(app.addNodeOperator('L', ADDRESS_4, { from: voting }), 'MAX_NODE_OPERATORS_COUNT_EXCEEDED') + }) + it('getNodeOperator works', async () => { await app.addNodeOperator('fo o', ADDRESS_1, { from: voting }) await app.addNodeOperator(' bar', ADDRESS_2, { from: voting }) @@ -138,7 +158,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assertBn(await app.getNodeOperatorsCount({ from: nobody }), 2) assertBn(await app.getActiveNodeOperatorsCount({ from: nobody }), 1) - await app.setNodeOperatorActive(0, false, { from: voting }) + await assertRevert(app.setNodeOperatorActive(0, false, { from: voting }), 'NODE_OPERATOR_ACTIVITY_ALREADY_SET') assert.equal((await app.getNodeOperator(0, false)).active, false) assertBn(await app.getActiveNodeOperatorsCount({ from: nobody }), 1) @@ -156,7 +176,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assertBn(await app.getNodeOperatorsCount({ from: nobody }), 2) assertBn(await app.getActiveNodeOperatorsCount({ from: nobody }), 1) - await app.setNodeOperatorActive(0, true, { from: voting }) + await assertRevert(app.setNodeOperatorActive(0, true, { from: voting }), 'NODE_OPERATOR_ACTIVITY_ALREADY_SET') assert.equal((await app.getNodeOperator(0, false)).active, true) assertBn(await app.getActiveNodeOperatorsCount({ from: nobody }), 1) @@ -174,6 +194,8 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.equal((await app.getNodeOperator(1, true)).name, ' bar') await app.setNodeOperatorName(0, 'zzz', { from: voting }) + await assertRevert(app.setNodeOperatorName(0, 'zzz', { from: voting }), 'NODE_OPERATOR_NAME_IS_THE_SAME') + assert.equal((await app.getNodeOperator(0, true)).name, 'zzz') assert.equal((await app.getNodeOperator(1, true)).name, ' bar') @@ -191,6 +213,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.equal((await app.getNodeOperator(1, false)).rewardAddress, ADDRESS_2) await app.setNodeOperatorRewardAddress(0, ADDRESS_4, { from: voting }) + await assertRevert(app.setNodeOperatorRewardAddress(0, ADDRESS_4, { from: voting }), 'NODE_OPERATOR_ADDRESS_IS_THE_SAME') assert.equal((await app.getNodeOperator(0, false)).rewardAddress, ADDRESS_4) assert.equal((await app.getNodeOperator(1, false)).rewardAddress, ADDRESS_2) @@ -209,6 +232,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assertBn((await app.getNodeOperator(1, false)).stakingLimit, 0) await app.setNodeOperatorStakingLimit(0, 40, { from: voting }) + await assertRevert(app.setNodeOperatorStakingLimit(0, 40, { from: voting }), 'NODE_OPERATOR_STAKING_LIMIT_IS_THE_SAME') assertBn((await app.getNodeOperator(0, false)).stakingLimit, 40) assertBn((await app.getNodeOperator(1, false)).stakingLimit, 0) @@ -217,11 +241,14 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('assignNextSigningKeys works', async () => { + let keysOpIndex = await app.getKeysOpIndex() let result = await pool.assignNextSigningKeys(10) let keysAssignedEvt = getEventAt(result, 'KeysAssigned').args assert.equal(keysAssignedEvt.pubkeys, null, 'empty cache, no singing keys added: pubkeys') assert.equal(keysAssignedEvt.signatures, null, 'empty cache, no singing keys added: signatures') + assertBn(await app.getKeysOpIndex(), keysOpIndex, 'keysOpIndex must not increase if no keys were assigned') + assertNoEvent(result, 'KeysOpIndexSet') await app.addNodeOperator('fo o', ADDRESS_1, { from: voting }) await app.addNodeOperator(' bar', ADDRESS_2, { from: voting }) @@ -229,11 +256,14 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob await app.setNodeOperatorStakingLimit(0, 10, { from: voting }) await app.setNodeOperatorStakingLimit(1, 10, { from: voting }) + keysOpIndex = await app.getKeysOpIndex() result = await pool.assignNextSigningKeys(10) keysAssignedEvt = getEventAt(result, 'KeysAssigned').args assert.equal(keysAssignedEvt.pubkeys, null, 'no singing keys added: pubkeys') assert.equal(keysAssignedEvt.signatures, null, 'no singing keys added: signatures') + assertBn(await app.getKeysOpIndex(), keysOpIndex, 'keysOpIndex must not increase if no keys were assigned') + assertNoEvent(result, 'KeysOpIndexSet') const op0 = { keys: [pad('0xaa0101', 48), pad('0xaa0202', 48), pad('0xaa0303', 48)], @@ -248,18 +278,25 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob await app.addSigningKeys(0, 3, hexConcat(...op0.keys), hexConcat(...op0.sigs), { from: voting }) await app.addSigningKeys(1, 3, hexConcat(...op1.keys), hexConcat(...op1.sigs), { from: voting }) + keysOpIndex = await app.getKeysOpIndex() result = await pool.assignNextSigningKeys(1) keysAssignedEvt = getEventAt(result, 'KeysAssigned').args assert.equal(keysAssignedEvt.pubkeys, op0.keys[0], 'assignment 1: pubkeys') assert.equal(keysAssignedEvt.signatures, op0.sigs[0], 'assignment 1: signatures') + assertBn(await app.getKeysOpIndex(), keysOpIndex.add(toBN(1)), 'keysOpIndex must increase if any keys were assigned') + assertEvent(result, 'KeysOpIndexSet') + keysOpIndex = await app.getKeysOpIndex() result = await pool.assignNextSigningKeys(2) keysAssignedEvt = getEventAt(result, 'KeysAssigned').args assert.sameMembers(hexSplit(keysAssignedEvt.pubkeys, PUBKEY_LENGTH_BYTES), [op0.keys[1], op1.keys[0]], 'assignment 2: pubkeys') assert.sameMembers(hexSplit(keysAssignedEvt.signatures, SIGNATURE_LENGTH_BYTES), [op0.sigs[1], op1.sigs[0]], 'assignment 2: signatures') + assertBn(await app.getKeysOpIndex(), keysOpIndex.add(toBN(1)), 'keysOpIndex must increase if any keys were assigned') + assertEvent(result, 'KeysOpIndexSet') + keysOpIndex = await app.getKeysOpIndex() result = await pool.assignNextSigningKeys(10) keysAssignedEvt = getEventAt(result, 'KeysAssigned').args @@ -268,18 +305,22 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob [op0.keys[2], op1.keys[1], op1.keys[2]], 'assignment 2: pubkeys' ) - assert.sameMembers( hexSplit(keysAssignedEvt.signatures, SIGNATURE_LENGTH_BYTES), [op0.sigs[2], op1.sigs[1], op1.sigs[2]], 'assignment 2: signatures' ) + assertBn(await app.getKeysOpIndex(), keysOpIndex.add(toBN(1)), 'keysOpIndex must increase if any keys were assigned') + assertEvent(result, 'KeysOpIndexSet') + keysOpIndex = await app.getKeysOpIndex() result = await pool.assignNextSigningKeys(10) keysAssignedEvt = getEventAt(result, 'KeysAssigned').args assert.equal(keysAssignedEvt.pubkeys, null, 'no singing keys left: pubkeys') assert.equal(keysAssignedEvt.signatures, null, 'no singing keys left: signatures') + assertBn(await app.getKeysOpIndex(), keysOpIndex, 'keysOpIndex must not increase if no keys were assigned') + assertNoEvent(result, 'KeysOpIndexSet') }) it('assignNextSigningKeys skips stopped operators', async () => { @@ -310,6 +351,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.sameMembers(hexSplit(keysAssignedEvt.signatures, SIGNATURE_LENGTH_BYTES), [op0.sigs[0], op1.sigs[0]], 'assignment 1: signatures') await app.setNodeOperatorActive(0, false, { from: voting }) + await assertRevert(app.setNodeOperatorActive(0, false, { from: voting }), 'NODE_OPERATOR_ACTIVITY_ALREADY_SET') result = await pool.assignNextSigningKeys(2) keysAssignedEvt = getEventAt(result, 'KeysAssigned').args diff --git a/test/0.4.24/staking-limit.test.js b/test/0.4.24/staking-limit.test.js new file mode 100644 index 000000000..6946ed06a --- /dev/null +++ b/test/0.4.24/staking-limit.test.js @@ -0,0 +1,210 @@ +const { assert } = require('chai') +const { assertBn, assertRevert } = require('@aragon/contract-helpers-test/src/asserts') +const { bn, MAX_UINT256 } = require('@aragon/contract-helpers-test') +const { toBN } = require('../helpers/utils') +const { waitBlocks } = require('../helpers/blockchain') + +const StakeLimitUtils = artifacts.require('StakeLimitUtilsMock.sol') + +const ETH = (value) => web3.utils.toWei(value + '', 'ether') + +// +// We need to pack four variables into the same 256bit-wide storage slot +// to lower the costs per each staking request. +// +// As a result, slot's memory aligned as follows: +// +// MSB ------------------------------------------------------------------------------> LSB +// 256____________160_________________________128_______________32_____________________ 0 +// |_______________|___________________________|________________|_______________________| +// | maxStakeLimit | maxStakeLimitGrowthBlocks | prevStakeLimit | prevStakeBlockNumber | +// |<-- 96 bits -->|<---------- 32 bits ------>|<-- 96 bits --->|<----- 32 bits ------->| +// +// +// NB: Internal representation conventions: +// +// - the `maxStakeLimitGrowthBlocks` field above represented as follows: +// `maxStakeLimitGrowthBlocks` = `maxStakeLimit` / `stakeLimitIncreasePerBlock` +// 32 bits 96 bits 96 bits +// +// +// - the "staking paused" state is encoded by `prevStakeBlockNumber` being zero, +// - the "staking unlimited" state is encoded by `maxStakeLimit` being zero and `prevStakeBlockNumber` being non-zero. +// + +contract('StakingLimits', ([account1]) => { + let limits + + before('deploy base app', async () => { + limits = await StakeLimitUtils.new() + }) + + it('encode zeros', async () => { + const slot = await limits.setStorageStakeLimitStruct(0, 0, 0, 0) + assertBn(slot, 0) + + const decodedSlot = await limits.getStorageStakeLimit(slot) + assertBn(decodedSlot.prevStakeBlockNumber, 0) + assertBn(decodedSlot.prevStakeLimit, 0) + assertBn(decodedSlot.maxStakeLimitGrowthBlocks, 0) + assertBn(decodedSlot.maxStakeLimit, 0) + }) + + it('check staking pause at start', async () => { + const slot = await limits.setStorageStakeLimitStruct(0, 0, 0, 0) + const paused = await limits.isStakingPaused(slot) + assert.equal(paused, true, 'staking not paused') + }) + + it('check staking pause with block number', async () => { + const prevStakeBlockNumber = 10 + const slot2 = await limits.setStorageStakeLimitStruct(prevStakeBlockNumber, 0, 0, 0) + const paused2 = await limits.isStakingPaused(slot2) + assert.equal(paused2, false, 'staking paused') + }) + + it('check staking pause/unpause', async () => { + let paused + const slot = await limits.setStorageStakeLimitStruct(1, 1, 1, 1) + paused = await limits.isStakingPaused(slot) + assert.equal(paused, false, 'staking paused') + + const slot2 = await limits.setStakeLimitPauseState(slot, true) + paused = await limits.isStakingPaused(slot2) + assert.equal(paused, true, 'staking not paused') + + const slot3 = await limits.setStakeLimitPauseState(slot, false) + paused = await limits.isStakingPaused(slot3) + assert.equal(paused, false, 'staking paused') + }) + + it('check staking rate limit', async () => { + let limited + const slot = await limits.setStorageStakeLimitStruct(0, 0, 0, 0) + limited = await limits.isStakingLimitSet(slot) + + assert.equal(limited, false, 'limits are limited') + + const maxStakeLimit = 10 + const slot2 = await limits.setStorageStakeLimitStruct(0, 0, 0, maxStakeLimit) + limited = await limits.isStakingLimitSet(slot2) + assert.equal(limited, true, 'limits are not limited') + + const slot3 = await limits.removeStakingLimit(slot2) + limited = await limits.isStakingLimitSet(slot3) + assert.equal(limited, false, 'limits are limited') + }) + + it('stake limit increase > max stake', async () => { + let maxStakeLimit = 5 + let maxStakeLimitIncreasePerBlock = 0 + const slot = await limits.setStorageStakeLimitStruct(0, 0, 0, 0) + await limits.setStakingLimit(slot, maxStakeLimit, maxStakeLimitIncreasePerBlock) + + maxStakeLimit = 5 + maxStakeLimitIncreasePerBlock = 5 + await limits.setStakingLimit(slot, maxStakeLimit, maxStakeLimitIncreasePerBlock) + + maxStakeLimit = 5 + maxStakeLimitGrowthBlocks = 6 + assertRevert(limits.setStakingLimit(slot, maxStakeLimit, maxStakeLimitGrowthBlocks), 'TOO_LARGE_LIMIT_INCREASE') + }) + + it('stake limit reverts on large values', async () => { + let maxStakeLimit = toBN(2).pow(toBN(96)) + let maxStakeLimitIncreasePerBlock = 1 + const slot = await limits.setStorageStakeLimitStruct(0, 0, 0, 0) + assertRevert(limits.setStakingLimit(slot, maxStakeLimit, maxStakeLimitIncreasePerBlock), 'TOO_LARGE_MAX_STAKE_LIMIT') + + maxStakeLimit = toBN(2).mul(toBN(10).pow(toBN(18))) + maxStakeLimitIncreasePerBlock = toBN(10) + assertRevert(limits.setStakingLimit(slot, maxStakeLimit, maxStakeLimitIncreasePerBlock), `TOO_SMALL_LIMIT_INCREASE`) + }) + + it('check update calculate stake limit with different blocks', async () => { + const block = await web3.eth.getBlock('latest') + + const maxStakeLimit = 100 + const increasePerBlock = 50 + const maxStakeLimitGrowthBlocks = maxStakeLimit / increasePerBlock + + const slot = await limits.setStorageStakeLimitStruct(block.number, 0, maxStakeLimitGrowthBlocks, maxStakeLimit) + + const currentStakeLimit2 = await limits.calculateCurrentStakeLimit(slot) + assertBn(currentStakeLimit2, 0) + + const block2 = await waitBlocks(1) + assert.equal(block2.number, block.number + 1) + const currentStakeLimit3 = await limits.calculateCurrentStakeLimit(slot) + assertBn(currentStakeLimit3, 50) + + const block3 = await waitBlocks(3) + assert.equal(block3.number, block.number + 1 + 3) + const currentStakeLimit4 = await limits.calculateCurrentStakeLimit(slot) + assertBn(currentStakeLimit4, 100) + }) + + it('check update stake limit', async () => { + const block = await web3.eth.getBlock('latest') + + const maxStakeLimit = 100 + const increasePerBlock = 50 + const maxStakeLimitGrowthBlocks = maxStakeLimit / increasePerBlock + + const slot = await limits.setStorageStakeLimitStruct(block.number, 0, maxStakeLimitGrowthBlocks, maxStakeLimit) + const decodedSlot = await limits.getStorageStakeLimit(slot) + assert.equal(decodedSlot.prevStakeBlockNumber, block.number) + assert.equal(decodedSlot.prevStakeLimit, 0) + + const block2 = await waitBlocks(3) + assert.equal(block2.number, block.number + 3) + + const currentStakeLimit2 = await limits.calculateCurrentStakeLimit(slot) + assertBn(currentStakeLimit2, maxStakeLimit) + + const deposit = 87 + const newSlot = await limits.updatePrevStakeLimit(slot, currentStakeLimit2 - deposit) + const decodedNewSlot = await limits.getStorageStakeLimit(newSlot) + assert.equal(decodedNewSlot.prevStakeBlockNumber, block2.number) + assert.equal(decodedNewSlot.prevStakeLimit, 13) + + // checking staking recovery + await waitBlocks(1) + const currentStakeLimit3 = await limits.calculateCurrentStakeLimit(newSlot) + assertBn(currentStakeLimit3, 13 + increasePerBlock) + + await waitBlocks(1) + const currentStakeLimit4 = await limits.calculateCurrentStakeLimit(newSlot) + assertBn(currentStakeLimit4, maxStakeLimit) + }) + + it('max values', async () => { + const block = await web3.eth.getBlock('latest') + + const max32 = toBN(2).pow(toBN(32)).sub(toBN(1)) // uint32 + const max96 = toBN(2).pow(toBN(96)).sub(toBN(1)) // uint96 + + const maxStakeLimit = max96 // uint96 + const maxStakeLimitGrowthBlocks = max32 + const maxPrevStakeLimit = max96 // uint96 + const maxBlock = max32 // uint32 + + // check that we CAN set max value + + const maxSlot = await limits.setStorageStakeLimitStruct(maxBlock, maxPrevStakeLimit, maxStakeLimitGrowthBlocks, maxStakeLimit) + const maxUint256 = toBN(2).pow(toBN(256)).sub(toBN(1)) + assertBn(maxSlot, maxUint256) + + const decodedRaw = await limits.getStorageStakeLimit(maxSlot) + + const decodedMaxLimit = decodedRaw.maxStakeLimit + const decodedMaxStakeLimitGrowthBlocks = decodedRaw.maxStakeLimitGrowthBlocks + const decodedPrevStakeLimit = decodedRaw.prevStakeLimit + const decodedPrevStakeBlockNumber = decodedRaw.prevStakeBlockNumber + + assertBn(decodedMaxLimit, max96) + assertBn(decodedMaxStakeLimitGrowthBlocks, max32) + assertBn(decodedPrevStakeLimit, max96) + assertBn(decodedPrevStakeBlockNumber, max32) + }) +}) diff --git a/test/0.4.24/steth.test.js b/test/0.4.24/steth.test.js index 7c36f6ac5..be35c12e0 100644 --- a/test/0.4.24/steth.test.js +++ b/test/0.4.24/steth.test.js @@ -1,5 +1,5 @@ const { assert } = require('chai') -const { assertBn, assertRevert, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') +const { assertBn, assertRevert, assertEvent, assertAmountOfEvents } = require('@aragon/contract-helpers-test/src/asserts') const { ZERO_ADDRESS, bn } = require('@aragon/contract-helpers-test') const StETH = artifacts.require('StETHMock') @@ -99,15 +99,23 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { it('transfer all balance works and emits event', async () => { const amount = await stEth.balanceOf(user1) const receipt = await stEth.transfer(user2, amount, { from: user1 }) + const sharesAmount = await stEth.getSharesByPooledEth(amount) + assertAmountOfEvents(receipt, 'Transfer', { expectedAmount: 1 }) + assertAmountOfEvents(receipt, 'TransferShares', { expectedAmount: 1 }) assertEvent(receipt, 'Transfer', { expectedArgs: { from: user1, to: user2, value: amount } }) + assertEvent(receipt, 'TransferShares', { expectedArgs: { from: user1, to: user2, sharesValue: sharesAmount } }) assertBn(await stEth.balanceOf(user1), tokens(0)) assertBn(await stEth.balanceOf(user2), tokens(100)) }) it('transfer zero tokens works and emits event', async () => { const amount = bn('0') + const sharesAmount = bn('0') const receipt = await stEth.transfer(user2, amount, { from: user1 }) + assertAmountOfEvents(receipt, 'Transfer', { expectedAmount: 1 }) + assertAmountOfEvents(receipt, 'TransferShares', { expectedAmount: 1 }) assertEvent(receipt, 'Transfer', { expectedArgs: { from: user1, to: user2, value: amount } }) + assertEvent(receipt, 'TransferShares', { expectedArgs: { from: user1, to: user2, sharesValue: sharesAmount } }) assertBn(await stEth.balanceOf(user1), tokens(100)) assertBn(await stEth.balanceOf(user2), tokens(0)) }) @@ -168,9 +176,14 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { it('transferFrom works and emits events', async () => { const amount = tokens(50) + const sharesAmount = await stEth.getSharesByPooledEth(amount) const receipt = await stEth.transferFrom(user1, user3, amount, { from: user2 }) + assertAmountOfEvents(receipt, 'Transfer', { expectedAmount: 1 }) + assertAmountOfEvents(receipt, 'TransferShares', { expectedAmount: 1 }) + assertAmountOfEvents(receipt, 'Approval', { expectedAmount: 1 }) assertEvent(receipt, 'Approval', { expectedArgs: { owner: user1, spender: user2, value: bn(0) } }) assertEvent(receipt, 'Transfer', { expectedArgs: { from: user1, to: user3, value: amount } }) + assertEvent(receipt, 'TransferShares', { expectedArgs: { from: user1, to: user3, sharesValue: sharesAmount } }) assertBn(await stEth.allowance(user2, user1), bn(0)) assertBn(await stEth.balanceOf(user1), tokens(50)) assertBn(await stEth.balanceOf(user3), tokens(50)) @@ -356,7 +369,16 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { }) it('burning zero value works', async () => { - await stEth.burnShares(user1, tokens(0)) + const receipt = await stEth.burnShares(user1, tokens(0)) + assertEvent(receipt, 'SharesBurnt', { + expectedArgs: { + account: user1, + preRebaseTokenAmount: tokens(0), + postRebaseTokenAmount: tokens(0), + sharesAmount: tokens(0) + } + }) + assertBn(await stEth.totalSupply(), tokens(300)) assertBn(await stEth.balanceOf(user1), tokens(100)) assertBn(await stEth.balanceOf(user2), tokens(100)) @@ -377,7 +399,18 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { totalSupply.mul(totalShares.sub(user1Shares)).div(totalSupply.sub(user1Balance).add(bn(tokens(10)))) ) - await stEth.burnShares(user1, sharesToBurn) + const expectedPreTokenAmount = await stEth.getPooledEthByShares(sharesToBurn) + const receipt = await stEth.burnShares(user1, sharesToBurn) + const expectedPostTokenAmount = await stEth.getPooledEthByShares(sharesToBurn) + assertEvent(receipt, 'SharesBurnt', { + expectedArgs: { + account: user1, + preRebaseTokenAmount: expectedPreTokenAmount, + postRebaseTokenAmount: expectedPostTokenAmount, + sharesAmount: sharesToBurn + } + }) + assertBn(await stEth.totalSupply(), tokens(300)) assertBn(await stEth.balanceOf(user1), bn(tokens(90)).subn(1)) // expected round error assertBn(await stEth.balanceOf(user2), tokens(105)) @@ -400,7 +433,18 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { totalSupply.mul(totalShares.sub(user1Shares)).div(totalSupply.sub(user1Balance).add(bn(tokens(50)))) ) - await stEth.burnShares(user1, sharesToBurn) + const expectedPreTokenAmount = await stEth.getPooledEthByShares(sharesToBurn) + const receipt = await stEth.burnShares(user1, sharesToBurn) + const expectedPostTokenAmount = await stEth.getPooledEthByShares(sharesToBurn) + assertEvent(receipt, 'SharesBurnt', { + expectedArgs: { + account: user1, + preRebaseTokenAmount: expectedPreTokenAmount, + postRebaseTokenAmount: expectedPostTokenAmount, + sharesAmount: sharesToBurn + } + }) + assertBn(await stEth.balanceOf(user1), tokens(50)) assertBn(await stEth.allowance(user1, user2), tokens(75)) @@ -412,7 +456,7 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { }) }) - context('share-related getters', async () => { + context('share-related getters and transfers', async () => { context('with zero totalPooledEther (supply)', async () => { it('getTotalSupply', async () => { assertBn(await stEth.totalSupply({ from: nobody }), tokens(0)) @@ -445,6 +489,16 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { assertBn(await stEth.getSharesByPooledEth(tokens(0)), tokens(0)) assertBn(await stEth.getSharesByPooledEth(tokens(100)), tokens(0)) }) + + it('transferShares', async () => { + assertBn(await stEth.balanceOf(nobody), tokens(0)) + + const receipt = await stEth.transferShares(user1, tokens(0), { from: nobody }) + assertEvent(receipt, 'Transfer', { expectedArgs: { from: nobody, to: user1, value: tokens(0) } }) + assertEvent(receipt, 'TransferShares', { expectedArgs: { from: nobody, to: user1, sharesValue: tokens(0) } }) + + assertBn(await stEth.balanceOf(nobody), tokens(0)) + }) }) context('with non-zero totalPooledEther (supply)', async () => { @@ -485,6 +539,42 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { assertBn(await stEth.getSharesByPooledEth(tokens(1)), tokens(1)) assertBn(await stEth.getSharesByPooledEth(tokens(100)), tokens(100)) }) + + it('transferShares', async () => { + assertBn(await stEth.balanceOf(user1), tokens(100)) + assertBn(await stEth.balanceOf(nobody), tokens(0)) + + let receipt = await stEth.transferShares(nobody, tokens(0), { from: user1 }) + assertAmountOfEvents(receipt, 'Transfer', { expectedAmount: 1 }) + assertAmountOfEvents(receipt, 'TransferShares', { expectedAmount: 1 }) + assertEvent(receipt, 'Transfer', { expectedArgs: { from: user1, to: nobody, value: tokens(0) } }) + assertEvent(receipt, 'TransferShares', { expectedArgs: { from: user1, to: nobody, sharesValue: tokens(0) } }) + + assertBn(await stEth.balanceOf(user1), tokens(100)) + assertBn(await stEth.balanceOf(nobody), tokens(0)) + + receipt = await stEth.transferShares(nobody, tokens(30), { from: user1 }) + assertAmountOfEvents(receipt, 'Transfer', { expectedAmount: 1 }) + assertAmountOfEvents(receipt, 'TransferShares', { expectedAmount: 1 }) + assertEvent(receipt, 'Transfer', { expectedArgs: { from: user1, to: nobody, value: tokens(30) } }) + assertEvent(receipt, 'TransferShares', { expectedArgs: { from: user1, to: nobody, sharesValue: tokens(30) } }) + + assertBn(await stEth.balanceOf(user1), tokens(70)) + assertBn(await stEth.balanceOf(nobody), tokens(30)) + + assertRevert(stEth.transferShares(nobody, tokens(75), { from: user1 }), 'TRANSFER_AMOUNT_EXCEEDS_BALANCE') + + await stEth.setTotalPooledEther(tokens(120)) + + receipt = await stEth.transferShares(nobody, tokens(70), { from: user1 }) + assertAmountOfEvents(receipt, 'Transfer', { expectedAmount: 1 }) + assertAmountOfEvents(receipt, 'TransferShares', { expectedAmount: 1 }) + assertEvent(receipt, 'Transfer', { expectedArgs: { from: user1, to: nobody, value: tokens(84) } }) + assertEvent(receipt, 'TransferShares', { expectedArgs: { from: user1, to: nobody, sharesValue: tokens(70) } }) + + assertBn(await stEth.balanceOf(user1), tokens(0)) + assertBn(await stEth.balanceOf(nobody), tokens(120)) + }) }) }) }) diff --git a/test/0.8.9/composite-post-rebase-beacon-receiver.test.js b/test/0.8.9/composite-post-rebase-beacon-receiver.test.js index 592b02e56..9481e8813 100644 --- a/test/0.8.9/composite-post-rebase-beacon-receiver.test.js +++ b/test/0.8.9/composite-post-rebase-beacon-receiver.test.js @@ -4,7 +4,8 @@ const { assertBn, assertRevert, assertEvent, assertAmountOfEvents } = require('@ const { ZERO_ADDRESS, bn } = require('@aragon/contract-helpers-test') const CompositePostRebaseBeaconReceiver = artifacts.require('CompositePostRebaseBeaconReceiver.sol') -const BeaconReceiverMock = artifacts.require('BeaconReceiverMock.sol') +const BeaconReceiverMock = artifacts.require('BeaconReceiverMock') +const BeaconReceiverMockWithoutERC165 = artifacts.require('BeaconReceiverMockWithoutERC165') const deployedCallbackCount = 8 @@ -32,6 +33,9 @@ contract('CompositePostRebaseBeaconReceiver', ([deployer, voting, oracle, anothe }) it(`add a single callback works`, async () => { + const invalidCallback = await BeaconReceiverMockWithoutERC165.new() + assertRevert(compositeReceiver.addCallback(invalidCallback.address, { from: voting }), `BAD_CALLBACK_INTERFACE`) + const receipt = await compositeReceiver.addCallback(callbackMocks[0], { from: voting }) assertBn(await compositeReceiver.callbacksLength(), bn(1)) diff --git a/test/0.8.9/deposit-security-module.test.js b/test/0.8.9/deposit-security-module.test.js index c1985318f..ace5bbbe8 100644 --- a/test/0.8.9/deposit-security-module.test.js +++ b/test/0.8.9/deposit-security-module.test.js @@ -573,6 +573,12 @@ contract('DepositSecurityModule', ([owner, stranger, guardian]) => { }) }) describe('levers', () => { + it('setLastDepositBlock', async () => { + assertRevert(depositSecurityModule.setLastDepositBlock(10, { from: stranger }), `not an owner`) + + await depositSecurityModule.setLastDepositBlock(10, { from: owner }) + assert.equal(await depositSecurityModule.getLastDepositBlock(), 10, 'wrong last deposit block') + }) it('setNodeOperatorsRegistry sets new value for nodeOperatorsRegistry if called by owner', async () => { const newNodeOperatorsRegistry = await NodeOperatorsRegistryMockForSecurityModule.new() assert.notEqual( diff --git a/test/0.8.9/lido-exec-layer-rewards-vault.js b/test/0.8.9/lido-exec-layer-rewards-vault.js new file mode 100644 index 000000000..18e44f82f --- /dev/null +++ b/test/0.8.9/lido-exec-layer-rewards-vault.js @@ -0,0 +1,204 @@ +const { assertBn, assertRevert, assertEvent, assertAmountOfEvents } = require('@aragon/contract-helpers-test/src/asserts') +const { ZERO_ADDRESS, bn } = require('@aragon/contract-helpers-test') +const { newDao, newApp } = require('../0.4.24/helpers/dao') + +const { assert } = require('chai') + +const LidoELRewardsVault = artifacts.require('LidoExecutionLayerRewardsVault.sol') + +const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') + +const LidoMock = artifacts.require('LidoMock.sol') +const LidoOracleMock = artifacts.require('OracleMock.sol') +const DepositContractMock = artifacts.require('DepositContractMock.sol') + +const ERC20OZMock = artifacts.require('ERC20OZMock.sol') +const ERC721OZMock = artifacts.require('ERC721OZMock.sol') + +const ETH = (value) => web3.utils.toWei(value + '', 'ether') +// semantic aliases +const stETH = ETH +const stETHShares = ETH + +contract('LidoExecutionLayerRewardsVault', ([appManager, voting, deployer, depositor, anotherAccount, ...otherAccounts]) => { + let oracle, lido, elRewardsVault + let treasuryAddr + let dao, acl, operators + + beforeEach('deploy lido with dao', async () => { + const lidoBase = await LidoMock.new({ from: deployer }) + oracle = await LidoOracleMock.new({ from: deployer }) + const depositContract = await DepositContractMock.new({ from: deployer }) + const nodeOperatorsRegistryBase = await NodeOperatorsRegistry.new({ from: deployer }) + + const daoAclObj = await newDao(appManager) + dao = daoAclObj.dao + acl = daoAclObj.acl + + // Instantiate a proxy for the app, using the base contract as its logic implementation. + let proxyAddress = await newApp(dao, 'lido', lidoBase.address, appManager) + lido = await LidoMock.at(proxyAddress) + + // NodeOperatorsRegistry + proxyAddress = await newApp(dao, 'node-operators-registry', nodeOperatorsRegistryBase.address, appManager) + operators = await NodeOperatorsRegistry.at(proxyAddress) + await operators.initialize(lido.address) + + // Init the BURN_ROLE role and assign in to voting + await acl.createPermission(voting, lido.address, await lido.BURN_ROLE(), appManager, { from: appManager }) + + // Initialize the app's proxy. + await lido.initialize(depositContract.address, oracle.address, operators.address) + treasuryAddr = await lido.getInsuranceFund() + + await oracle.setPool(lido.address) + await depositContract.reset() + + elRewardsVault = await LidoELRewardsVault.new(lido.address, treasuryAddr, { from: deployer }) + }) + + it('Addresses which are not Lido contract cannot withdraw from execution layer rewards vault', async () => { + await assertRevert(elRewardsVault.withdrawRewards(12345, { from: anotherAccount }), 'ONLY_LIDO_CAN_WITHDRAW') + await assertRevert(elRewardsVault.withdrawRewards(12345, { from: deployer }), 'ONLY_LIDO_CAN_WITHDRAW') + await assertRevert(elRewardsVault.withdrawRewards(12345, { from: appManager }), 'ONLY_LIDO_CAN_WITHDRAW') + }) + + it('Execution layer rewards vault can receive Ether by plain transfers (no call data)', async () => { + const before = +(await web3.eth.getBalance(elRewardsVault.address)).toString() + const amount = 0.02 + await web3.eth.sendTransaction({ to: elRewardsVault.address, from: anotherAccount, value: ETH(amount) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(before + amount)) + }) + + it('Execution layer rewards vault refuses to receive Ether by transfers with call data', async () => { + const before = +(await web3.eth.getBalance(elRewardsVault.address)).toString() + const amount = 0.02 + await assertRevert( + web3.eth.sendTransaction({ to: elRewardsVault.address, from: anotherAccount, value: ETH(amount), data: '0x12345678' }) + ) + }) + + describe('Recover ERC20 / ERC721', () => { + let mockERC20Token, mockNFT + let nft1, nft2 + let totalERC20Supply + + beforeEach(async () => { + // setup ERC20 token with total supply 100,000 units + // mint two NFTs + // the deployer solely holds newly created ERC20 and ERC721 items on setup + + nft1 = bn(666) + nft2 = bn(777) + totalERC20Supply = bn(1000000) + + mockERC20Token = await ERC20OZMock.new(totalERC20Supply, { from: deployer }) + + assertBn(await mockERC20Token.totalSupply(), totalERC20Supply) + assertBn(await mockERC20Token.balanceOf(deployer), totalERC20Supply) + + await mockERC20Token.balanceOf(deployer) + + mockNFT = await ERC721OZMock.new({ from: deployer }) + + await mockNFT.mintToken(nft1, { from: deployer }) + await mockNFT.mintToken(nft2, { from: deployer }) + + assertBn(await mockNFT.balanceOf(deployer), bn(2)) + assert.equal(await mockNFT.ownerOf(nft1), deployer) + assert.equal(await mockNFT.ownerOf(nft2), deployer) + }) + + it(`can't recover zero ERC20 amount`, async () => { + assertRevert(elRewardsVault.recoverERC20(mockERC20Token.address, bn(0)), `ZERO_RECOVERY_AMOUNT`) + }) + + it(`can't recover zero-address ERC20`, async () => { + assertRevert(elRewardsVault.recoverERC20(ZERO_ADDRESS, bn(10))) + }) + + it(`can't recover stETH by recoverERC20`, async () => { + // initial stETH balance is zero + assertBn(await lido.balanceOf(anotherAccount), stETH(0)) + // submit 10 ETH to mint 10 stETH + await web3.eth.sendTransaction({ from: anotherAccount, to: lido.address, value: ETH(10) }) + // check 10 stETH minted on balance + assertBn(await lido.balanceOf(anotherAccount), stETH(10)) + // transfer 5 stETH to the elRewardsVault account + await lido.transfer(elRewardsVault.address, stETH(5), { from: anotherAccount }) + + assertBn(await lido.balanceOf(anotherAccount), stETH(5)) + assertBn(await lido.balanceOf(elRewardsVault.address), stETH(5)) + }) + + it(`recover some accidentally sent ERC20`, async () => { + // distribute deployer's balance among anotherAccount and elRewardsVault + await mockERC20Token.transfer(anotherAccount, bn(400000), { from: deployer }) + await mockERC20Token.transfer(elRewardsVault.address, bn(600000), { from: deployer }) + + // check the resulted state + assertBn(await mockERC20Token.balanceOf(deployer), bn(0)) + assertBn(await mockERC20Token.balanceOf(anotherAccount), bn(400000)) + assertBn(await mockERC20Token.balanceOf(elRewardsVault.address), bn(600000)) + + // recover ERC20 + const firstReceipt = await elRewardsVault.recoverERC20(mockERC20Token.address, bn(100000), { from: deployer }) + assertEvent(firstReceipt, `ERC20Recovered`, { + expectedArgs: { requestedBy: deployer, token: mockERC20Token.address, amount: bn(100000) } + }) + + const secondReceipt = await elRewardsVault.recoverERC20(mockERC20Token.address, bn(400000), { from: anotherAccount }) + assertEvent(secondReceipt, `ERC20Recovered`, { + expectedArgs: { requestedBy: anotherAccount, token: mockERC20Token.address, amount: bn(400000) } + }) + + // check balances again + assertBn(await mockERC20Token.balanceOf(elRewardsVault.address), bn(100000)) + assertBn(await mockERC20Token.balanceOf(treasuryAddr), bn(500000)) + assertBn(await mockERC20Token.balanceOf(deployer), bn(0)) + assertBn(await mockERC20Token.balanceOf(anotherAccount), bn(400000)) + + // recover last portion + const lastReceipt = await elRewardsVault.recoverERC20(mockERC20Token.address, bn(100000), { from: anotherAccount }) + assertEvent(lastReceipt, `ERC20Recovered`, { + expectedArgs: { requestedBy: anotherAccount, token: mockERC20Token.address, amount: bn(100000) } + }) + + // balance is zero already, have to be reverted + assertRevert(elRewardsVault.recoverERC20(mockERC20Token.address, bn(1), { from: deployer }), `ERC20: transfer amount exceeds balance`) + }) + + it(`can't recover zero-address ERC721(NFT)`, async () => { + assertRevert(elRewardsVault.recoverERC721(ZERO_ADDRESS, 0)) + }) + + it(`recover some accidentally sent NFTs`, async () => { + // send nft1 to anotherAccount and nft2 to the elRewardsVault address + await mockNFT.transferFrom(deployer, anotherAccount, nft1, { from: deployer }) + await mockNFT.transferFrom(deployer, elRewardsVault.address, nft2, { from: deployer }) + + // check the new holders' rights + assertBn(await mockNFT.balanceOf(deployer), bn(0)) + assertBn(await mockNFT.balanceOf(anotherAccount), bn(1)) + assertBn(await mockNFT.balanceOf(elRewardsVault.address), bn(1)) + + // recover nft2 should work + const receiptNfc2 = await elRewardsVault.recoverERC721(mockNFT.address, nft2, { from: anotherAccount }) + assertEvent(receiptNfc2, `ERC721Recovered`, { expectedArgs: { requestedBy: anotherAccount, token: mockNFT.address, tokenId: nft2 } }) + + // but nft1 recovery should revert + assertRevert(elRewardsVault.recoverERC721(mockNFT.address, nft1), `ERC721: transfer caller is not owner nor approved`) + + // send nft1 to elRewardsVault and recover it + await mockNFT.transferFrom(anotherAccount, elRewardsVault.address, nft1, { from: anotherAccount }) + const receiptNft1 = await elRewardsVault.recoverERC721(mockNFT.address, nft1, { from: deployer }) + + assertEvent(receiptNft1, `ERC721Recovered`, { expectedArgs: { requestedBy: deployer, token: mockNFT.address, tokenId: nft1 } }) + + // check final NFT ownership state + assertBn(await mockNFT.balanceOf(treasuryAddr), bn(2)) + assertBn(await mockNFT.ownerOf(nft1), treasuryAddr) + assertBn(await mockNFT.ownerOf(nft2), treasuryAddr) + }) + }) +}) diff --git a/test/deposit.test.js b/test/deposit.test.js index bcdc22729..4e0f1f717 100644 --- a/test/deposit.test.js +++ b/test/deposit.test.js @@ -80,8 +80,15 @@ contract('Lido with official deposit contract', ([appManager, voting, user1, use // Set up the app's permissions. await acl.createPermission(voting, app.address, await app.PAUSE_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.RESUME_ROLE(), appManager, { from: appManager }) await acl.createPermission(voting, app.address, await app.MANAGE_FEE(), appManager, { from: appManager }) await acl.createPermission(voting, app.address, await app.MANAGE_WITHDRAWAL_KEY(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.SET_EL_REWARDS_VAULT_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE(), appManager, { + from: appManager + }) + await acl.createPermission(voting, app.address, await app.STAKING_PAUSE_ROLE(), appManager, { from: appManager }) + await acl.createPermission(voting, app.address, await app.STAKING_CONTROL_ROLE(), appManager, { from: appManager }) // await acl.createPermission(app.address, token.address, await token.MINT_ROLE(), appManager, { from: appManager }) // await acl.createPermission(app.address, token.address, await token.BURN_ROLE(), appManager, { from: appManager }) diff --git a/test/helpers/utils.js b/test/helpers/utils.js index a0b409009..bfb93399b 100644 --- a/test/helpers/utils.js +++ b/test/helpers/utils.js @@ -43,6 +43,22 @@ const div15 = (bn) => bn.div(new BN(1000000)).div(new BN(1000000)).div(new BN(10 const ETH = (value) => web3.utils.toWei(value + '', 'ether') const tokens = ETH +function formatWei(weiString) { + return ethers.utils.formatEther(ethers.utils.parseUnits(weiString, 'wei'), { commify: true }) + ' ETH' +} + +function formatBN(bn) { + return formatWei(bn.toString()) +} + +async function getEthBalance(address) { + return formatWei(await web3.eth.getBalance(address)) +} + +function formatStEth(bn) { + return ethers.utils.formatEther(ethers.utils.parseUnits(bn.toString(), 'wei'), { commify: true }) + ' stETH' +} + module.exports = { pad, hexConcat, @@ -50,5 +66,8 @@ module.exports = { toBN, div15, ETH, - tokens + tokens, + getEthBalance, + formatBN, + formatStEth: formatStEth } diff --git a/test/scenario/changing_oracles_during_epoch.js b/test/scenario/changing_oracles_during_epoch.js index dadde46fa..cbc02a9e1 100644 --- a/test/scenario/changing_oracles_during_epoch.js +++ b/test/scenario/changing_oracles_during_epoch.js @@ -35,10 +35,7 @@ contract('LidoOracle', ([appManager, voting, malicious1, malicious2, user1, user // Initialize the app's proxy. await app.setTime(GENESIS_TIME + 225 * EPOCH_LENGTH) - await app.initialize(appLido.address, 225, 32, 12, GENESIS_TIME) - - await app.setV1LastReportedEpochForTest(123) // pretend we had epoch 123 completed in v1 - await app.initialize_v2(1000, 500) // initialize the second version: 10% yearly increase, 5% moment decrease + await app.initialize(appLido.address, 225, 32, 12, GENESIS_TIME, 1000, 500) // Initialize the oracle time, quorum and basic oracles await app.setQuorum(4, { from: voting }) diff --git a/test/scenario/execution_layer_rewards_after_the_merge.js b/test/scenario/execution_layer_rewards_after_the_merge.js new file mode 100644 index 000000000..d48fe46f0 --- /dev/null +++ b/test/scenario/execution_layer_rewards_after_the_merge.js @@ -0,0 +1,813 @@ +const { assert } = require('chai') +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, hexConcat } = require('../helpers/utils') +const { deployDaoAndPool } = require('./helpers/deploy') + +const { signDepositData } = require('../0.8.9/helpers/signatures') +const { waitBlocks } = require('../helpers/blockchain') +const addresses = require('@aragon/contract-helpers-test/src/addresses') + +const LidoELRewardsVault = artifacts.require('LidoExecutionLayerRewardsVault.sol') +const RewardEmulatorMock = artifacts.require('RewardEmulatorMock.sol') + +const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') + +const TOTAL_BASIS_POINTS = 10000 + +contract('Lido: merge acceptance', (addresses) => { + const [ + // the root account which deployed the DAO + appManager, + // the address which we use to simulate the voting DAO application + voting, + // node operators + operator_1, + operator_2, + // users who deposit Ether to the pool + user1, + user2, + user3, + // unrelated address + nobody, + // Execution layer rewards source + userELRewards + ] = addresses + + let pool, nodeOperatorRegistry, token + let oracleMock, depositContractMock + let treasuryAddr, insuranceAddr, guardians + let depositSecurityModule, depositRoot + let rewarder, elRewardsVault + + // Total fee is 1% + const totalFeePoints = 0.01 * TOTAL_BASIS_POINTS + // Of this 1%, 30% goes to the treasury + const treasuryFeePoints = 0.3 * TOTAL_BASIS_POINTS + // 20% goes to the insurance fund + const insuranceFeePoints = 0.2 * TOTAL_BASIS_POINTS + // 50% goes to node operators + const nodeOperatorsFeePoints = 0.5 * TOTAL_BASIS_POINTS + + const withdrawalCredentials = pad('0x0202', 32) + + // Each node operator has its Ethereum 1 address, a name and a set of registered + // validators, each of them defined as a (public key, signature) pair + // NO with 1 validator + const nodeOperator1 = { + name: 'operator_1', + address: operator_1, + validators: [ + { + key: pad('0x010101', 48), + sig: pad('0x01', 96) + } + ] + } + + // NO with 1 validator + const nodeOperator2 = { + name: 'operator_2', + address: operator_2, + validators: [ + { + key: pad('0x020202', 48), + sig: pad('0x02', 96) + } + ] + } + + before('deploy base stuff', async () => { + const deployed = await deployDaoAndPool(appManager, voting) + + // contracts/StETH.sol + token = deployed.pool + + // contracts/Lido.sol + pool = deployed.pool + + // contracts/nos/NodeOperatorsRegistry.sol + nodeOperatorRegistry = deployed.nodeOperatorRegistry + + // mocks + oracleMock = deployed.oracleMock + depositContractMock = deployed.depositContractMock + + // addresses + treasuryAddr = deployed.treasuryAddr + insuranceAddr = deployed.insuranceAddr + depositSecurityModule = deployed.depositSecurityModule + guardians = deployed.guardians + + depositRoot = await depositContractMock.get_deposit_root() + + elRewardsVault = await LidoELRewardsVault.new(pool.address, treasuryAddr) + await pool.setELRewardsVault(elRewardsVault.address, { from: voting }) + + // At first go through tests assuming there is no withdrawal limit + await pool.setELRewardsWithdrawalLimit(TOTAL_BASIS_POINTS, { from: voting }) + + rewarder = await RewardEmulatorMock.new(elRewardsVault.address) + + assertBn(await web3.eth.getBalance(rewarder.address), ETH(0), 'rewarder balance') + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') + + // Fee and its distribution are in basis points, 10000 corresponding to 100% + + await pool.setFee(totalFeePoints, { from: voting }) + await pool.setFeeDistribution(treasuryFeePoints, insuranceFeePoints, nodeOperatorsFeePoints, { from: voting }) + + // Fee and distribution were set + + assertBn(await pool.getFee({ from: nobody }), totalFeePoints, 'total fee') + + const distribution = await pool.getFeeDistribution({ from: nobody }) + assertBn(distribution.treasuryFeeBasisPoints, treasuryFeePoints, 'treasury fee') + assertBn(distribution.insuranceFeeBasisPoints, insuranceFeePoints, 'insurance fee') + assertBn(distribution.operatorsFeeBasisPoints, nodeOperatorsFeePoints, 'node operators fee') + + await pool.setWithdrawalCredentials(withdrawalCredentials, { from: voting }) + + // Withdrawal credentials were set + assert.equal(await pool.getWithdrawalCredentials({ from: nobody }), withdrawalCredentials, 'withdrawal credentials') + + // How many validators can this node operator register + const validatorsLimit = 100000000 + let txn = await nodeOperatorRegistry.addNodeOperator(nodeOperator1.name, nodeOperator1.address, { from: voting }) + await nodeOperatorRegistry.setNodeOperatorStakingLimit(0, validatorsLimit, { from: voting }) + + // Some Truffle versions fail to decode logs here, so we're decoding them explicitly using a helper + nodeOperator1.id = getEventArgument(txn, 'NodeOperatorAdded', 'id', { decodeForAbi: NodeOperatorsRegistry._json.abi }) + assertBn(nodeOperator1.id, 0, 'operator id') + + assertBn(await nodeOperatorRegistry.getNodeOperatorsCount(), 1, 'total node operators') + + const numKeys = 1 + + await nodeOperatorRegistry.addSigningKeysOperatorBH( + nodeOperator1.id, + numKeys, + nodeOperator1.validators[0].key, + nodeOperator1.validators[0].sig, + { + from: nodeOperator1.address + } + ) + + // The key was added + + let totalKeys = await nodeOperatorRegistry.getTotalSigningKeyCount(nodeOperator1.id, { from: nobody }) + assertBn(totalKeys, 1, 'total signing keys') + + // The key was not used yet + + let unusedKeys = await nodeOperatorRegistry.getUnusedSigningKeyCount(nodeOperator1.id, { from: nobody }) + assertBn(unusedKeys, 1, 'unused signing keys') + + txn = await nodeOperatorRegistry.addNodeOperator(nodeOperator2.name, nodeOperator2.address, { from: voting }) + await nodeOperatorRegistry.setNodeOperatorStakingLimit(1, validatorsLimit, { from: voting }) + + // Some Truffle versions fail to decode logs here, so we're decoding them explicitly using a helper + nodeOperator2.id = getEventArgument(txn, 'NodeOperatorAdded', 'id', { decodeForAbi: NodeOperatorsRegistry._json.abi }) + assertBn(nodeOperator2.id, 1, 'operator id') + + assertBn(await nodeOperatorRegistry.getNodeOperatorsCount(), 2, 'total node operators') + + await nodeOperatorRegistry.addSigningKeysOperatorBH( + nodeOperator2.id, + numKeys, + nodeOperator2.validators[0].key, + nodeOperator2.validators[0].sig, + { + from: nodeOperator2.address + } + ) + + // The key was added + + totalKeys = await nodeOperatorRegistry.getTotalSigningKeyCount(nodeOperator2.id, { from: nobody }) + assertBn(totalKeys, 1, 'total signing keys') + + // The key was not used yet + unusedKeys = await nodeOperatorRegistry.getUnusedSigningKeyCount(nodeOperator2.id, { from: nobody }) + assertBn(unusedKeys, 1, 'unused signing keys') + }) + + it('the first user deposits 3 ETH to the pool', async () => { + await web3.eth.sendTransaction({ to: pool.address, from: user1, value: ETH(3) }) + 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(depositRoot, keysOpIndex, block.number, block.hash, signatures) + + // No Ether was deposited yet to the validator contract + + assertBn(await depositContractMock.totalCalls(), 0) + + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 0, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, 0, 'remote ether2') + + // All Ether was buffered within the pool contract atm + + assertBn(await pool.getBufferedEther(), ETH(3), 'buffered ether') + assertBn(await pool.getTotalPooledEther(), ETH(3), 'total pooled ether') + + // The amount of tokens corresponding to the deposited ETH value was minted to the user + + assertBn(await token.balanceOf(user1), tokens(3), 'user1 tokens') + + assertBn(await token.totalSupply(), tokens(3), 'token total supply') + }) + + it('the second user deposits 30 ETH to the pool', async () => { + await web3.eth.sendTransaction({ to: pool.address, from: user2, value: ETH(30) }) + 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(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 + + assertBn(await depositContractMock.totalCalls(), 1) + + const regCall = await depositContractMock.calls.call(0) + assert.equal(regCall.pubkey, nodeOperator1.validators[0].key) + assert.equal(regCall.withdrawal_credentials, withdrawalCredentials) + assert.equal(regCall.signature, nodeOperator1.validators[0].sig) + assertBn(regCall.value, ETH(32)) + + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 1, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, 0, 'remote ether2') + + // Some Ether remained buffered within the pool contract + + assertBn(await pool.getBufferedEther(), ETH(1), 'buffered ether') + assertBn(await pool.getTotalPooledEther(), ETH(1 + 32), 'total pooled ether') + + // The amount of tokens corresponding to the deposited ETH value was minted to the users + + assertBn(await token.balanceOf(user1), tokens(3), 'user1 tokens') + assertBn(await token.balanceOf(user2), tokens(30), 'user2 tokens') + + assertBn(await token.totalSupply(), tokens(3 + 30), 'token total supply') + }) + + it('the third user deposits 64 ETH to the pool', async () => { + await web3.eth.sendTransaction({ to: pool.address, from: user3, value: ETH(64) }) + + 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(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 + + assertBn(await depositContractMock.totalCalls(), 2) + + const regCall = await depositContractMock.calls.call(1) + assert.equal(regCall.pubkey, nodeOperator2.validators[0].key) + assert.equal(regCall.withdrawal_credentials, withdrawalCredentials) + assert.equal(regCall.signature, nodeOperator2.validators[0].sig) + assertBn(regCall.value, ETH(32)) + + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 2, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, 0, 'remote ether2') + + // The pool ran out of validator keys, so the remaining 32 ETH were added to the + // pool buffer + + assertBn(await pool.getBufferedEther(), ETH(1 + 32), 'buffered ether') + assertBn(await pool.getTotalPooledEther(), ETH(33 + 64), 'total pooled ether') + + // The amount of tokens corresponding to the deposited ETH value was minted to the users + + assertBn(await token.balanceOf(user1), tokens(3), 'user1 tokens') + assertBn(await token.balanceOf(user2), tokens(30), 'user2 tokens') + assertBn(await token.balanceOf(user3), tokens(64), 'user3 tokens') + + assertBn(await token.totalSupply(), tokens(3 + 30 + 64), 'token total supply') + }) + + it('collect 9 ETH execution layer rewards to the vault', async () => { + await rewarder.reward({ from: userELRewards, value: ETH(9) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(9), 'Execution layer rewards vault balance') + }) + + it('the oracle reports balance increase on Ethereum2 side (+32 ETH) and claims collected execution layer rewards (+9 ETH)', async () => { + const epoch = 100 + + // Total shares are equal to deposited eth before ratio change and fee mint + + const oldTotalShares = await token.getTotalShares() + assertBn(oldTotalShares, ETH(97), 'total shares') + + // Old total pooled Ether + + const oldTotalPooledEther = await pool.getTotalPooledEther() + assertBn(oldTotalPooledEther, ETH(33 + 64), 'total pooled ether') + + // Reporting 1.5-fold balance increase (64 => 96) + + await oracleMock.reportBeacon(epoch, 2, ETH(96)) + + // Execution layer rewards just claimed + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') + + // Total shares increased because fee minted (fee shares added) + // shares ~= oldTotalShares + reward * oldTotalShares / (newTotalPooledEther - reward) + + const newTotalShares = await token.getTotalShares() + + assertBn(newTotalShares, new BN('97289047169125663202'), 'total shares') + + const elRewards = 9 + + // Total pooled Ether increased + + const newTotalPooledEther = await pool.getTotalPooledEther() + assertBn(newTotalPooledEther, ETH(33 + 96 + elRewards), 'total pooled ether') + + // Ether2 stat reported by the pool changed correspondingly + + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 2, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, ETH(96), 'remote ether2') + + // Buffered Ether amount changed on execution layer rewards + assertBn(await pool.getBufferedEther(), ETH(33 + elRewards), 'buffered ether') + + // New tokens was minted to distribute fee + assertBn(await token.totalSupply(), tokens(129 + elRewards), 'token total supply') + + const reward = toBN(ETH(96 - 64 + elRewards)) + const mintedAmount = new BN(totalFeePoints).mul(reward).divn(TOTAL_BASIS_POINTS) + + // Token user balances increased + assertBn(await token.balanceOf(user1), new BN('4255360824742268041'), 'user1 tokens') + assertBn(await token.balanceOf(user2), new BN('42553608247422680412'), 'user2 tokens') + assertBn(await token.balanceOf(user3), new BN('90781030927835051546'), 'user3 tokens') + + // Fee, in the form of minted tokens, was distributed between treasury, insurance fund + // and node operators + // treasuryTokenBalance ~= mintedAmount * treasuryFeePoints / 10000 + // insuranceTokenBalance ~= mintedAmount * insuranceFeePoints / 10000 + assertBn(await token.balanceOf(treasuryAddr), new BN('123000000000000001'), 'treasury tokens') + assertBn(await token.balanceOf(insuranceAddr), new BN('81999999999999999'), 'insurance tokens') + + // The node operators' fee is distributed between all active node operators, + // proportional to their effective stake (the amount of Ether staked by the operator's + // used and non-stopped validators). + // + // In our case, both node operators received the same fee since they have the same + // effective stake (one signing key used from each operator, staking 32 ETH) + + assertBn(await token.balanceOf(nodeOperator1.address), new BN('102499999999999999'), 'operator_1 tokens') + assertBn(await token.balanceOf(nodeOperator2.address), new BN('102499999999999999'), 'operator_2 tokens') + + // Real minted amount should be a bit less than calculated caused by round errors on mint and transfer operations + assert( + mintedAmount + .sub( + new BN(0) + .add(await token.balanceOf(treasuryAddr)) + .add(await token.balanceOf(insuranceAddr)) + .add(await token.balanceOf(nodeOperator1.address)) + .add(await token.balanceOf(nodeOperator2.address)) + .add(await token.balanceOf(nodeOperatorRegistry.address)) + ) + .lt(mintedAmount.divn(100)) + ) + }) + + it('collect another 7 ETH execution layer rewards to the vault', async () => { + await rewarder.reward({ from: userELRewards, value: ETH(2) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(2), 'Execution layer rewards vault balance') + + await rewarder.reward({ from: userELRewards, value: ETH(5) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(7), 'Execution layer rewards vault balance') + }) + + it('the oracle reports same balance on Ethereum2 side (+0 ETH) and claims collected execution layer rewards (+7 ETH)', async () => { + const epoch = 101 + + // Total shares are equal to deposited eth before ratio change and fee mint + const oldTotalShares = await token.getTotalShares() + assertBn(oldTotalShares, new BN('97289047169125663202'), 'total shares') + + // Old total pooled Ether + + const oldTotalPooledEther = await pool.getTotalPooledEther() + assertBn(oldTotalPooledEther, ETH(138), 'total pooled ether') + + // Reporting the same balance as it was before (96ETH => 96ETH) + await oracleMock.reportBeacon(epoch, 2, ETH(96)) + + // Execution layer rewards just claimed + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') + + // Total shares preserved because fee shares NOT minted + // shares ~= oldTotalShares + reward * oldTotalShares / (newTotalPooledEther - reward) + + const newTotalShares = await token.getTotalShares() + assertBn(newTotalShares, oldTotalShares, 'total shares') + + // Total pooled Ether increased + + const newTotalPooledEther = await pool.getTotalPooledEther() + assertBn(newTotalPooledEther, ETH(138 + 7), 'total pooled ether') + + // Ether2 stat reported by the pool changed correspondingly + + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 2, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, ETH(96), 'remote ether2') + + // Buffered Ether amount changed on execution layer rewards + assertBn(await pool.getBufferedEther(), ETH(42 + 7), 'buffered ether') + + assertBn(await token.totalSupply(), tokens(145), 'token total supply') + + const reward = toBN(0) + const mintedAmount = new BN(0) + + // All of the balances should be increased with proportion of newTotalPooledEther/oldTotalPooledEther (which is >1) + // cause shares per user and overall shares number are preserved + + assertBn(await token.balanceOf(user1), new BN('4471212460779919318'), 'user1 tokens') + assertBn(await token.balanceOf(user2), new BN('44712124607799193187'), 'user2 tokens') + assertBn(await token.balanceOf(user3), new BN('95385865829971612132'), 'user3 tokens') + + assertBn(await token.balanceOf(treasuryAddr), new BN('129239130434782610'), 'treasury tokens') + assertBn(await token.balanceOf(insuranceAddr), new BN('86159420289855071'), 'insurance tokens') + assertBn(await token.balanceOf(nodeOperator1.address), new BN('107699275362318839'), 'operator_1 tokens') + assertBn(await token.balanceOf(nodeOperator2.address), new BN('107699275362318839'), 'operator_2 tokens') + }) + + it('collect another 5 ETH execution layer rewards to the vault', async () => { + await rewarder.reward({ from: userELRewards, value: ETH(5) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(5), 'Execution layer rewards vault balance') + }) + + it('the oracle reports loss on Ethereum2 side (-2 ETH) and claims collected execution layer rewards (+5 ETH)', async () => { + const epoch = 102 + + // Total shares are equal to deposited eth before ratio change and fee mint + const oldTotalShares = await token.getTotalShares() + assertBn(oldTotalShares, new BN('97289047169125663202'), 'total shares') + + // Old total pooled Ether + + const oldTotalPooledEther = await pool.getTotalPooledEther() + assertBn(oldTotalPooledEther, ETH(145), 'total pooled ether') + + // Reporting balance decrease (96ETH => 94ETH) + await oracleMock.reportBeacon(epoch, 2, ETH(94)) + + // Execution layer rewards just claimed + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') + + // Total shares preserved because fee shares NOT minted + // shares ~= oldTotalShares + reward * oldTotalShares / (newTotalPooledEther - reward) + const newTotalShares = await token.getTotalShares() + assertBn(newTotalShares, oldTotalShares, 'total shares') + + // Total pooled Ether increased by 5ETH - 2ETH + const newTotalPooledEther = await pool.getTotalPooledEther() + assertBn(newTotalPooledEther, ETH(145 + 3), 'total pooled ether') + + // Ether2 stat reported by the pool changed correspondingly + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 2, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, ETH(94), 'remote ether2') + + // Buffered Ether amount changed on execution layer rewards + assertBn(await pool.getBufferedEther(), ETH(49 + 5), 'buffered ether') + + assertBn(await token.totalSupply(), tokens(145 + 3), 'token total supply') + + const reward = toBN(0) + const mintedAmount = new BN(0) + + // All of the balances should be increased with proportion of newTotalPooledEther/oldTotalPooledEther (which is >1) + // cause shares per user and overall shares number are preserved + + assertBn(await token.balanceOf(user1), new BN('4563720304796055580'), 'user1 tokens') + assertBn(await token.balanceOf(user2), new BN('45637203047960555804'), 'user2 tokens') + assertBn(await token.balanceOf(user3), new BN('97359366502315852383'), 'user3 tokens') + + assertBn(await token.balanceOf(treasuryAddr), new BN('131913043478260871'), 'treasury tokens') + assertBn(await token.balanceOf(insuranceAddr), new BN('87942028985507245'), 'insurance tokens') + assertBn(await token.balanceOf(nodeOperator1.address), new BN('109927536231884057'), 'operator_1 tokens') + assertBn(await token.balanceOf(nodeOperator2.address), new BN('109927536231884057'), 'operator_2 tokens') + }) + + it('collect another 3 ETH execution layer rewards to the vault', async () => { + await rewarder.reward({ from: userELRewards, value: ETH(3) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(3), 'Execution layer rewards vault balance') + }) + + it('the oracle reports loss on Ethereum2 side (-3 ETH) and claims collected execution layer rewards (+3 ETH)', async () => { + const epoch = 103 + + // Total shares are equal to deposited eth before ratio change and fee mint + const oldTotalShares = await token.getTotalShares() + assertBn(oldTotalShares, new BN('97289047169125663202'), 'total shares') + + // Old total pooled Ether + + const oldTotalPooledEther = await pool.getTotalPooledEther() + assertBn(oldTotalPooledEther, ETH(148), 'total pooled ether') + + // Reporting balance decrease (94ETH => 91ETH) + await oracleMock.reportBeacon(epoch, 2, ETH(91)) + + // Execution layer rewards just claimed + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') + + // Total shares preserved because fee shares NOT minted + // shares ~= oldTotalShares + reward * oldTotalShares / (newTotalPooledEther - reward) + const newTotalShares = await token.getTotalShares() + assertBn(newTotalShares, oldTotalShares, 'total shares') + + // Total pooled Ether increased by 5ETH - 2ETH + const newTotalPooledEther = await pool.getTotalPooledEther() + assertBn(newTotalPooledEther, oldTotalPooledEther, 'total pooled ether') + + // Ether2 stat reported by the pool changed correspondingly + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 2, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, ETH(91), 'remote ether2') + + // Buffered Ether amount changed on execution layer rewards + assertBn(await pool.getBufferedEther(), ETH(54 + 3), 'buffered ether') + + assertBn(await token.totalSupply(), tokens(148), 'token total supply') + + const reward = toBN(0) + const mintedAmount = new BN(0) + + // All of the balances should be the same as before cause overall changes sums to zero + assertBn(await token.balanceOf(user1), new BN('4563720304796055580'), 'user1 tokens') + assertBn(await token.balanceOf(user2), new BN('45637203047960555804'), 'user2 tokens') + assertBn(await token.balanceOf(user3), new BN('97359366502315852383'), 'user3 tokens') + + assertBn(await token.balanceOf(treasuryAddr), new BN('131913043478260871'), 'treasury tokens') + assertBn(await token.balanceOf(insuranceAddr), new BN('87942028985507245'), 'insurance tokens') + assertBn(await token.balanceOf(nodeOperator1.address), new BN('109927536231884057'), 'operator_1 tokens') + assertBn(await token.balanceOf(nodeOperator2.address), new BN('109927536231884057'), 'operator_2 tokens') + }) + + it('collect another 2 ETH execution layer rewards to the vault', async () => { + await rewarder.reward({ from: userELRewards, value: ETH(2) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(2), 'Execution layer rewards vault balance') + }) + + it('the oracle reports loss on Ethereum2 side (-8 ETH) and claims collected execution layer rewards (+2 ETH)', async () => { + const epoch = 104 + + // Total shares are equal to deposited eth before ratio change and fee mint + const oldTotalShares = await token.getTotalShares() + assertBn(oldTotalShares, new BN('97289047169125663202'), 'total shares') + + // Old total pooled Ether + + const oldTotalPooledEther = await pool.getTotalPooledEther() + assertBn(oldTotalPooledEther, ETH(148), 'total pooled ether') + + // Reporting balance decrease (91ETH => 83ETH) + await oracleMock.reportBeacon(epoch, 2, ETH(83)) + + // Execution layer rewards just claimed + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') + + // Total shares preserved because fee shares NOT minted + // shares ~= oldTotalShares + reward * oldTotalShares / (newTotalPooledEther - reward) + const newTotalShares = await token.getTotalShares() + assertBn(newTotalShares, oldTotalShares, 'total shares') + + // Total pooled Ether decreased by 8ETH-2ETH + const newTotalPooledEther = await pool.getTotalPooledEther() + assertBn(newTotalPooledEther, ETH(142), 'total pooled ether') + + // Ether2 stat reported by the pool changed correspondingly + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 2, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, ETH(83), 'remote ether2') + + // Buffered Ether amount changed on execution layer rewards + assertBn(await pool.getBufferedEther(), ETH(57 + 2), 'buffered ether') + + assertBn(await token.totalSupply(), tokens(142), 'token total supply') + + // All of the balances should be decreased with proportion of newTotalPooledEther/oldTotalPooledEther (which is <1) + // cause shares per user and overall shares number are preserved + assertBn(await token.balanceOf(user1), new BN('4378704616763783056'), 'user1 tokens') + assertBn(await token.balanceOf(user2), new BN('43787046167637830569'), 'user2 tokens') + assertBn(await token.balanceOf(user3), new BN('93412365157627371881'), 'user3 tokens') + + assertBn(await token.balanceOf(treasuryAddr), new BN('126565217391304349'), 'treasury tokens') + assertBn(await token.balanceOf(insuranceAddr), new BN('84376811594202897'), 'insurance tokens') + assertBn(await token.balanceOf(nodeOperator1.address), new BN('105471014492753622'), 'operator_1 tokens') + assertBn(await token.balanceOf(nodeOperator2.address), new BN('105471014492753622'), 'operator_2 tokens') + }) + + it('collect another 3 ETH execution layer rewards to the vault', async () => { + await rewarder.reward({ from: userELRewards, value: ETH(3) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(3), 'Execution layer vault balance') + }) + + it('the oracle reports balance increase on Ethereum2 side (+2 ETH) and claims collected execution layer rewards (+3 ETH)', async () => { + const epoch = 105 + + // Total shares are equal to deposited eth before ratio change and fee mint + const oldTotalShares = await token.getTotalShares() + assertBn(oldTotalShares, new BN('97289047169125663202'), 'total shares') + + // Old total pooled Ether + + const oldTotalPooledEther = await pool.getTotalPooledEther() + assertBn(oldTotalPooledEther, ETH(142), 'total pooled ether') + + // Reporting balance increase (83ETH => 85ETH) + await oracleMock.reportBeacon(epoch, 2, ETH(85)) + + // Execution layer rewards just claimed + assertBn(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') + + // Total shares increased because fee minted (fee shares added) + // shares ~= oldTotalShares + reward * oldTotalShares / (newTotalPooledEther - reward) + + const newTotalShares = await token.getTotalShares() + assertBn(newTotalShares, new BN('97322149941214511675'), 'total shares') + + // Total pooled Ether increased by 2ETH+3ETH + const newTotalPooledEther = await pool.getTotalPooledEther() + assertBn(newTotalPooledEther, ETH(142 + 5), 'total pooled ether') + + // Ether2 stat reported by the pool changed correspondingly + const ether2Stat = await pool.getBeaconStat() + assertBn(ether2Stat.depositedValidators, 2, 'deposited ether2') + assertBn(ether2Stat.beaconBalance, ETH(85), 'remote ether2') + + // Buffered Ether amount changed on execution layer rewards + assertBn(await pool.getBufferedEther(), ETH(59 + 3), 'buffered ether') + + assertBn(await token.totalSupply(), tokens(142 + 5), 'token total supply') + + // Token user balances increased + assertBn(await token.balanceOf(user1), new BN('4531342559390407888'), 'user1 tokens') + assertBn(await token.balanceOf(user2), new BN('45313425593904078888'), 'user2 tokens') + assertBn(await token.balanceOf(user3), new BN('96668641266995368295'), 'user3 tokens') + + // Fee, in the form of minted tokens, was distributed between treasury, insurance fund + // and node operators + // treasuryTokenBalance = (oldTreasuryShares + mintedRewardShares * treasuryFeePoints / 10000) * sharePrice + assertBn((await token.balanceOf(treasuryAddr)).divn(10), new BN('14597717391304348'), 'treasury tokens') + // should preserver treasuryFeePoints/insuranceFeePoints ratio + assertBn((await token.balanceOf(insuranceAddr)).divn(10), new BN('9731811594202898'), 'insurance tokens') + + // The node operators' fee is distributed between all active node operators, + // proportional to their effective stake (the amount of Ether staked by the operator's + // used and non-stopped validators). + // + // In our case, both node operators received the same fee since they have the same + // effective stake (one signing key used from each operator, staking 32 ETH) + assertBn((await token.balanceOf(nodeOperator1.address)).divn(10), new BN('12164764492753623'), 'operator_1 tokens') + assertBn((await token.balanceOf(nodeOperator2.address)).divn(10), new BN('12164764492753623'), 'operator_2 tokens') + }) + + it('collect 0.1 ETH execution layer rewards to elRewardsVault and withdraw it entirely by means of multiple oracle reports (+1 ETH)', async () => { + const toNum = (bn) => { + return +bn.toString() + } + const toE18 = (x) => { + return x * 1e18 + } + const fromNum = (x) => { + return new BN(String(x)) + } + + // Specify different withdrawal limits for a few epochs to test different values + const getELRewardsWithdrawalLimitFromEpoch = (_epoch) => { + if (_epoch === 106) { + return 2 + } else if (_epoch === 107) { + return 0 + } else { + return 3 + } + } + + const elRewards = toE18(0.1) + await rewarder.reward({ from: userELRewards, value: fromNum(elRewards) }) + assertBn(await web3.eth.getBalance(elRewardsVault.address), fromNum(elRewards), 'Execution layer rewards vault balance') + + let epoch = 106 + let lastBeaconBalance = toE18(85) + await pool.setELRewardsWithdrawalLimit(getELRewardsWithdrawalLimitFromEpoch(epoch), { from: voting }) + + let elRewardsWithdrawalLimitPoints = toNum(await pool.getELRewardsWithdrawalLimit()) + let elRewardsVaultBalance = toNum(await web3.eth.getBalance(elRewardsVault.address)) + let totalPooledEther = toNum(await pool.getTotalPooledEther()) + let bufferedEther = toNum(await pool.getBufferedEther()) + let totalSupply = toNum(await pool.totalSupply()) + const beaconBalanceInc = toE18(1) + let elRewardsWithdrawn = 0 + + // Do multiple oracle reports to withdraw all ETH from execution layer rewards vault + while (elRewardsVaultBalance > 0) { + const elRewardsWithdrawalLimit = getELRewardsWithdrawalLimitFromEpoch(epoch) + await pool.setELRewardsWithdrawalLimit(elRewardsWithdrawalLimit, { from: voting }) + elRewardsWithdrawalLimitPoints = toNum(await pool.getELRewardsWithdrawalLimit()) + + const maxELRewardsAmountPerWithdrawal = Math.floor( + ((totalPooledEther + beaconBalanceInc) * elRewardsWithdrawalLimitPoints) / TOTAL_BASIS_POINTS + ) + const elRewardsToWithdraw = Math.min(maxELRewardsAmountPerWithdrawal, elRewardsVaultBalance) + + // Reporting balance increase + await oracleMock.reportBeacon(epoch, 2, fromNum(lastBeaconBalance + beaconBalanceInc)) + + assertBn( + await web3.eth.getBalance(elRewardsVault.address), + elRewardsVaultBalance - elRewardsToWithdraw, + 'Execution layer rewards vault balance' + ) + + assertBn(await pool.getTotalPooledEther(), totalPooledEther + beaconBalanceInc + elRewardsToWithdraw, 'total pooled ether') + + assertBn(await pool.totalSupply(), totalSupply + beaconBalanceInc + elRewardsToWithdraw, 'token total supply') + + assertBn(await pool.getBufferedEther(), bufferedEther + elRewardsToWithdraw, 'buffered ether') + + elRewardsVaultBalance = toNum(await web3.eth.getBalance(elRewardsVault.address)) + totalPooledEther = toNum(await pool.getTotalPooledEther()) + bufferedEther = toNum(await pool.getBufferedEther()) + totalSupply = toNum(await pool.totalSupply()) + + lastBeaconBalance += beaconBalanceInc + epoch += 1 + elRewardsWithdrawn += elRewardsToWithdraw + } + + assert.equal(elRewardsWithdrawn, elRewards) + }) +}) diff --git a/test/scenario/helpers/deploy.js b/test/scenario/helpers/deploy.js index 0a7d02baa..b6334e503 100644 --- a/test/scenario/helpers/deploy.js +++ b/test/scenario/helpers/deploy.js @@ -67,10 +67,15 @@ async function deployDaoAndPool(appManager, voting) { const [ POOL_PAUSE_ROLE, + POOL_RESUME_ROLE, POOL_MANAGE_FEE, POOL_MANAGE_WITHDRAWAL_KEY, POOL_BURN_ROLE, DEPOSIT_ROLE, + STAKING_PAUSE_ROLE, + STAKING_CONTROL_ROLE, + SET_EL_REWARDS_VAULT_ROLE, + SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE, NODE_OPERATOR_REGISTRY_MANAGE_SIGNING_KEYS, NODE_OPERATOR_REGISTRY_ADD_NODE_OPERATOR_ROLE, NODE_OPERATOR_REGISTRY_SET_NODE_OPERATOR_ACTIVE_ROLE, @@ -80,10 +85,15 @@ async function deployDaoAndPool(appManager, voting) { NODE_OPERATOR_REGISTRY_REPORT_STOPPED_VALIDATORS_ROLE ] = await Promise.all([ pool.PAUSE_ROLE(), + pool.RESUME_ROLE(), pool.MANAGE_FEE(), pool.MANAGE_WITHDRAWAL_KEY(), pool.BURN_ROLE(), pool.DEPOSIT_ROLE(), + pool.STAKING_PAUSE_ROLE(), + pool.STAKING_CONTROL_ROLE(), + pool.SET_EL_REWARDS_VAULT_ROLE(), + pool.SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE(), nodeOperatorRegistry.MANAGE_SIGNING_KEYS(), nodeOperatorRegistry.ADD_NODE_OPERATOR_ROLE(), nodeOperatorRegistry.SET_NODE_OPERATOR_ACTIVE_ROLE(), @@ -96,9 +106,14 @@ async function deployDaoAndPool(appManager, voting) { await Promise.all([ // Allow voting to manage the pool acl.createPermission(voting, pool.address, POOL_PAUSE_ROLE, appManager, { from: appManager }), + acl.createPermission(voting, pool.address, POOL_RESUME_ROLE, appManager, { from: appManager }), acl.createPermission(voting, pool.address, POOL_MANAGE_FEE, appManager, { from: appManager }), acl.createPermission(voting, pool.address, POOL_MANAGE_WITHDRAWAL_KEY, appManager, { from: appManager }), acl.createPermission(voting, pool.address, POOL_BURN_ROLE, appManager, { from: appManager }), + acl.createPermission(voting, pool.address, STAKING_PAUSE_ROLE, appManager, { from: appManager }), + acl.createPermission(voting, pool.address, STAKING_CONTROL_ROLE, appManager, { from: appManager }), + acl.createPermission(voting, pool.address, SET_EL_REWARDS_VAULT_ROLE, appManager, { from: appManager }), + acl.createPermission(voting, pool.address, SET_EL_REWARDS_WITHDRAWAL_LIMIT_ROLE, appManager, { from: appManager }), // Allow depositor to deposit buffered ether acl.createPermission(depositSecurityModule.address, pool.address, DEPOSIT_ROLE, appManager, { from: appManager }), diff --git a/test/scenario/lido_penalties_slashing.js b/test/scenario/lido_penalties_slashing.js index d21b2539c..79c651542 100644 --- a/test/scenario/lido_penalties_slashing.js +++ b/test/scenario/lido_penalties_slashing.js @@ -117,7 +117,10 @@ contract('Lido: penalties, slashing, operator stops', (addresses) => { const validatorsLimit = 0 const txn = await nodeOperatorRegistry.addNodeOperator(nodeOperator1.name, nodeOperator1.address, { from: voting }) - await nodeOperatorRegistry.setNodeOperatorStakingLimit(0, validatorsLimit, { from: voting }) + await assertRevert( + nodeOperatorRegistry.setNodeOperatorStakingLimit(0, validatorsLimit, { from: voting }), + 'NODE_OPERATOR_STAKING_LIMIT_IS_THE_SAME' + ) // Some Truffle versions fail to decode logs here, so we're decoding them explicitly using a helper nodeOperator1.id = getEventArgument(txn, 'NodeOperatorAdded', 'id', { decodeForAbi: NodeOperatorsRegistry._json.abi }) diff --git a/test/scenario/lido_rewards_distribution_math.js b/test/scenario/lido_rewards_distribution_math.js index 232cd6c60..a4160bdcd 100644 --- a/test/scenario/lido_rewards_distribution_math.js +++ b/test/scenario/lido_rewards_distribution_math.js @@ -1,6 +1,6 @@ const { assert } = require('chai') const { BN } = require('bn.js') -const { assertBn, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') +const { assertBn, assertEvent, assertRevert } = require('@aragon/contract-helpers-test/src/asserts') const { getEventArgument, ZERO_ADDRESS } = require('@aragon/contract-helpers-test') const { pad, ETH } = require('../helpers/utils') @@ -120,7 +120,10 @@ contract('Lido: rewards distribution math', (addresses) => { const validatorsLimit = 0 const txn = await nodeOperatorRegistry.addNodeOperator(nodeOperator1.name, nodeOperator1.address, { from: voting }) - await nodeOperatorRegistry.setNodeOperatorStakingLimit(0, validatorsLimit, { from: voting }) + await assertRevert( + nodeOperatorRegistry.setNodeOperatorStakingLimit(0, validatorsLimit, { from: voting }), + 'NODE_OPERATOR_STAKING_LIMIT_IS_THE_SAME' + ) // Some Truffle versions fail to decode logs here, so we're decoding them explicitly using a helper nodeOperator1.id = getEventArgument(txn, 'NodeOperatorAdded', 'id', { decodeForAbi: NodeOperatorsRegistry._json.abi }) @@ -554,7 +557,7 @@ contract('Lido: rewards distribution math', (addresses) => { } async function readLastPoolEventLog() { - const events = await pool.getPastEvents() + const events = await pool.getPastEvents('Transfer') let reportedMintAmount = new BN(0) const tos = [] const values = [] diff --git a/yarn.lock b/yarn.lock index d1db316f1..8553f3589 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1937,7 +1937,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/block@npm:^3.4.0, @ethereumjs/block@npm:^3.5.0, @ethereumjs/block@npm:^3.6.0": +"@ethereumjs/block@npm:^3.5.0, @ethereumjs/block@npm:^3.6.0": version: 3.6.0 resolution: "@ethereumjs/block@npm:3.6.0" dependencies: @@ -1949,7 +1949,19 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/blockchain@npm:^5.4.0, @ethereumjs/blockchain@npm:^5.5.0": +"@ethereumjs/block@npm:^3.6.1": + version: 3.6.1 + resolution: "@ethereumjs/block@npm:3.6.1" + dependencies: + "@ethereumjs/common": ^2.6.1 + "@ethereumjs/tx": ^3.5.0 + ethereumjs-util: ^7.1.4 + merkle-patricia-tree: ^4.2.3 + checksum: e61e50e6a550720b7ea614108d9e503aecef8beb2a56948db532b8b76c07a87ef1ea8ccc3232fc2cb3ba3f24f882ae1656a8be5192bf3318ef84c7d520b63650 + languageName: node + linkType: hard + +"@ethereumjs/blockchain@npm:^5.5.0, @ethereumjs/blockchain@npm:^5.5.1": version: 5.5.1 resolution: "@ethereumjs/blockchain@npm:5.5.1" dependencies: @@ -1965,7 +1977,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/common@npm:^2.4.0, @ethereumjs/common@npm:^2.6.0": +"@ethereumjs/common@npm:^2.6.0": version: 2.6.0 resolution: "@ethereumjs/common@npm:2.6.0" dependencies: @@ -1975,6 +1987,16 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/common@npm:^2.6.1, @ethereumjs/common@npm:^2.6.2": + version: 2.6.2 + resolution: "@ethereumjs/common@npm:2.6.2" + dependencies: + crc-32: ^1.2.0 + ethereumjs-util: ^7.1.4 + checksum: 5f9447ab56d8917ea5f1d7efe772f8315997568efa5f37ee174a9199c5ccb695b3405949ffec62df3168c3daa93f45358454f3601ac055540eadbfa147feb3e5 + languageName: node + linkType: hard + "@ethereumjs/ethash@npm:^1.1.0": version: 1.1.0 resolution: "@ethereumjs/ethash@npm:1.1.0" @@ -1988,7 +2010,7 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/tx@npm:^3.3.0, @ethereumjs/tx@npm:^3.4.0": +"@ethereumjs/tx@npm:^3.4.0": version: 3.4.0 resolution: "@ethereumjs/tx@npm:3.4.0" dependencies: @@ -1998,23 +2020,33 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/vm@npm:^5.5.2": - version: 5.6.0 - resolution: "@ethereumjs/vm@npm:5.6.0" +"@ethereumjs/tx@npm:^3.5.0": + version: 3.5.0 + resolution: "@ethereumjs/tx@npm:3.5.0" dependencies: - "@ethereumjs/block": ^3.6.0 - "@ethereumjs/blockchain": ^5.5.0 - "@ethereumjs/common": ^2.6.0 - "@ethereumjs/tx": ^3.4.0 + "@ethereumjs/common": ^2.6.1 + ethereumjs-util: ^7.1.4 + checksum: 97db5545803cecd721f2fe45c95f5892b7594afb4721566cc0a0516ddbcf385dbec7e850425106c624fa19289987fd67007c2fe14f1cc4c5325866c3356f7103 + languageName: node + linkType: hard + +"@ethereumjs/vm@npm:^5.6.0": + version: 5.7.1 + resolution: "@ethereumjs/vm@npm:5.7.1" + dependencies: + "@ethereumjs/block": ^3.6.1 + "@ethereumjs/blockchain": ^5.5.1 + "@ethereumjs/common": ^2.6.2 + "@ethereumjs/tx": ^3.5.0 async-eventemitter: ^0.2.4 core-js-pure: ^3.0.1 - debug: ^2.2.0 - ethereumjs-util: ^7.1.3 + debug: ^4.3.3 + ethereumjs-util: ^7.1.4 functional-red-black-tree: ^1.0.1 mcl-wasm: ^0.7.1 - merkle-patricia-tree: ^4.2.2 + merkle-patricia-tree: ^4.2.3 rustbn.js: ~0.2.0 - checksum: 275c1f8804e37e3df6d51c75c5c396dcf3ead0b2bb159970971d592d734e146c5284efcc02fcfbb8f4332e36cd8d126c1cb73ea6e4620bcfd329d544358f86ba + checksum: 890c145f788307d51ccf12832cac8c0c31a8048b4590edc16ecdbead0714045caa56ee6bc3c9ce236e39a57d2670e5be685057ae3c031c2003721680268fddf4 languageName: node linkType: hard @@ -3674,8 +3706,9 @@ __metadata: ethereumjs-testrpc-sc: ^6.5.1-sc.1 ethereumjs-util: ^7.0.8 ethers: ^5.0.19 - hardhat: ^2.7.0 - hardhat-gas-reporter: 1.0.7 + hardhat: ^2.8.0 + hardhat-contract-sizer: ^2.5.0 + hardhat-gas-reporter: 1.0.8 husky: ^4.3.0 ipfs-http-client: ^55.0.0 lerna: ^3.22.1 @@ -3707,6 +3740,19 @@ __metadata: languageName: unknown linkType: soft +"@metamask/eth-sig-util@npm:^4.0.0": + version: 4.0.0 + resolution: "@metamask/eth-sig-util@npm:4.0.0" + dependencies: + ethereumjs-abi: ^0.6.8 + ethereumjs-util: ^6.2.1 + ethjs-util: ^0.1.6 + tweetnacl: ^1.0.3 + tweetnacl-util: ^0.15.1 + checksum: ab2b1ddb3db3fdf64ba89f22d93a0c584d2870c83f01b2ad2c46a8b1fcd45398b24ed9b427dbdcc34f9afeb66341536dbc1e6094e15f5c91972baa2fed4a87bf + languageName: node + linkType: hard + "@mrmlnc/readdir-enhanced@npm:^2.2.1": version: 2.2.1 resolution: "@mrmlnc/readdir-enhanced@npm:2.2.1" @@ -8449,6 +8495,19 @@ __metadata: languageName: node linkType: hard +"cli-table3@npm:^0.6.0": + version: 0.6.1 + resolution: "cli-table3@npm:0.6.1" + dependencies: + colors: 1.4.0 + string-width: ^4.2.0 + dependenciesMeta: + colors: + optional: true + checksum: b8104fe8ed7608ce7da368ac7626c744ea695cdef81c757bb7e4f122e156cc6cac5c6ba50da6a8ef0ae3af90c64763cbf557f31cd8d2b1b3977ac8768a0fa0f9 + languageName: node + linkType: hard + "cli-truncate@npm:^2.1.0": version: 2.1.0 resolution: "cli-truncate@npm:2.1.0" @@ -9773,7 +9832,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.1": +"debug@npm:^4.3.1, debug@npm:^4.3.3": version: 4.3.3 resolution: "debug@npm:4.3.3" dependencies: @@ -11926,7 +11985,7 @@ __metadata: languageName: node linkType: hard -"ethereumjs-util@npm:6.2.1, ethereumjs-util@npm:^6.0.0, ethereumjs-util@npm:^6.1.0, ethereumjs-util@npm:^6.2.0": +"ethereumjs-util@npm:6.2.1, ethereumjs-util@npm:^6.0.0, ethereumjs-util@npm:^6.1.0, ethereumjs-util@npm:^6.2.0, ethereumjs-util@npm:^6.2.1": version: 6.2.1 resolution: "ethereumjs-util@npm:6.2.1" dependencies: @@ -12024,6 +12083,19 @@ __metadata: languageName: node linkType: hard +"ethereumjs-util@npm:^7.1.4": + version: 7.1.4 + resolution: "ethereumjs-util@npm:7.1.4" + dependencies: + "@types/bn.js": ^5.1.0 + bn.js: ^5.1.2 + create-hash: ^1.1.2 + ethereum-cryptography: ^0.1.3 + rlp: ^2.2.4 + checksum: 735c642f90cb4154b0d21fa495cc30a393c87d9424dafecb301f2b2d3618f3e9722322f8558bdd3be86baf52fa0ceb6db9cb348d4ee679e06991ef312240491b + languageName: node + linkType: hard + "ethereumjs-vm@npm:4.2.0": version: 4.2.0 resolution: "ethereumjs-vm@npm:4.2.0" @@ -12267,7 +12339,7 @@ __metadata: languageName: node linkType: hard -"ethjs-util@npm:0.1.6, ethjs-util@npm:^0.1.3": +"ethjs-util@npm:0.1.6, ethjs-util@npm:^0.1.3, ethjs-util@npm:^0.1.6": version: 0.1.6 resolution: "ethjs-util@npm:0.1.6" dependencies: @@ -14233,29 +14305,42 @@ __metadata: languageName: node linkType: hard -"hardhat-gas-reporter@npm:1.0.7": - version: 1.0.7 - resolution: "hardhat-gas-reporter@npm:1.0.7" +"hardhat-contract-sizer@npm:^2.5.0": + version: 2.5.0 + resolution: "hardhat-contract-sizer@npm:2.5.0" + dependencies: + chalk: ^4.0.0 + cli-table3: ^0.6.0 + peerDependencies: + hardhat: ^2.0.0 + checksum: 35c01af2cec385bbe1d51e5fdf5fb8e3fe2a6066f48c3b0800bf55710f89c01c52a0b43ce4e1f4218baf67a22b2b3f33132c20d994063a7aae0baa3697e35eab + languageName: node + linkType: hard + +"hardhat-gas-reporter@npm:1.0.8": + version: 1.0.8 + resolution: "hardhat-gas-reporter@npm:1.0.8" dependencies: array-uniq: 1.0.3 eth-gas-reporter: ^0.2.24 sha1: ^1.1.1 peerDependencies: hardhat: ^2.0.2 - checksum: 8751df1eb1ad56e851a07f4b72ecf4ca9f7c827d78d8ca65ce19e5266828bc64597c84055893258caaa45d32d006bad7d152c82df9de0cb56c072282082ee36d + checksum: cbee5088e797a149360a2e38ef40edb7234de272993c3ef426b4635991cb176964fd816caa8f40ec3931025b2164d46b7e3f6b24e9a14d110ac26d1f3ca65a25 languageName: node linkType: hard -"hardhat@npm:^2.7.0": - version: 2.7.0 - resolution: "hardhat@npm:2.7.0" +"hardhat@npm:^2.8.0": + version: 2.8.4 + resolution: "hardhat@npm:2.8.4" dependencies: - "@ethereumjs/block": ^3.4.0 - "@ethereumjs/blockchain": ^5.4.0 - "@ethereumjs/common": ^2.4.0 - "@ethereumjs/tx": ^3.3.0 - "@ethereumjs/vm": ^5.5.2 + "@ethereumjs/block": ^3.6.0 + "@ethereumjs/blockchain": ^5.5.0 + "@ethereumjs/common": ^2.6.0 + "@ethereumjs/tx": ^3.4.0 + "@ethereumjs/vm": ^5.6.0 "@ethersproject/abi": ^5.1.2 + "@metamask/eth-sig-util": ^4.0.0 "@sentry/node": ^5.18.1 "@solidity-parser/parser": ^0.14.0 "@types/bn.js": ^5.1.0 @@ -14269,10 +14354,9 @@ __metadata: debug: ^4.1.1 enquirer: ^2.3.0 env-paths: ^2.2.0 - eth-sig-util: ^2.5.2 ethereum-cryptography: ^0.1.2 ethereumjs-abi: ^0.6.8 - ethereumjs-util: ^7.1.0 + ethereumjs-util: ^7.1.3 find-up: ^2.1.0 fp-ts: 1.19.3 fs-extra: ^7.0.1 @@ -14281,9 +14365,9 @@ __metadata: immutable: ^4.0.0-rc.12 io-ts: 1.10.4 lodash: ^4.17.11 - merkle-patricia-tree: ^4.2.0 + merkle-patricia-tree: ^4.2.2 mnemonist: ^0.38.0 - mocha: ^7.1.2 + mocha: ^7.2.0 node-fetch: ^2.6.0 qs: ^6.7.0 raw-body: ^2.4.1 @@ -14299,7 +14383,7 @@ __metadata: ws: ^7.4.6 bin: hardhat: internal/cli/cli.js - checksum: 883032c7f6025c263166dc69bc9695c35723ce672895236455912886ecc9a186e3b218c402db87e7d514da1d45aa806a848cf2512d61a570629e91fd20ba9828 + checksum: 32319b9e97272778c2112e29c3a5c7231779c68fa5894a5474bfe3c27be6f4ddb0eef5f7973a1dd8e572e7b4c9fe7cd9865cfca2bcdf17ab5a8aea239a10c58b languageName: node linkType: hard @@ -18993,7 +19077,7 @@ __metadata: languageName: node linkType: hard -"merkle-patricia-tree@npm:^4.2.0, merkle-patricia-tree@npm:^4.2.2": +"merkle-patricia-tree@npm:^4.2.2": version: 4.2.2 resolution: "merkle-patricia-tree@npm:4.2.2" dependencies: @@ -19008,6 +19092,20 @@ __metadata: languageName: node linkType: hard +"merkle-patricia-tree@npm:^4.2.3": + version: 4.2.3 + resolution: "merkle-patricia-tree@npm:4.2.3" + dependencies: + "@types/levelup": ^4.3.0 + ethereumjs-util: ^7.1.4 + level-mem: ^5.0.1 + level-ws: ^2.0.0 + readable-stream: ^3.6.0 + semaphore-async-await: ^1.5.1 + checksum: cab8b2d4415be6ad513fd095d8b4b4b29c96bac6ff79092b21349cb52b2c03c00ad812f3e0b490a4818e5e71f10a997674731c8f30be30fd328636b79a4a8fc4 + languageName: node + linkType: hard + "methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -19445,7 +19543,7 @@ __metadata: languageName: node linkType: hard -"mocha@npm:^7.1.1, mocha@npm:^7.1.2": +"mocha@npm:^7.1.1, mocha@npm:^7.1.2, mocha@npm:^7.2.0": version: 7.2.0 resolution: "mocha@npm:7.2.0" dependencies: @@ -26729,7 +26827,7 @@ resolve@1.1.x: languageName: node linkType: hard -"tweetnacl-util@npm:^0.15.0": +"tweetnacl-util@npm:^0.15.0, tweetnacl-util@npm:^0.15.1": version: 0.15.1 resolution: "tweetnacl-util@npm:0.15.1" checksum: d4d40600eca66561c6cb73fdce5f2927876526e8cae4dea12b4b6f19a32f8e9e66f308d24cd131e850c83e00e1be5cdb0c65660806d9bf0d2d80ffb4c7cb0c55 @@ -26743,7 +26841,7 @@ resolve@1.1.x: languageName: node linkType: hard -"tweetnacl@npm:^1.0.0": +"tweetnacl@npm:^1.0.0, tweetnacl@npm:^1.0.3": version: 1.0.3 resolution: "tweetnacl@npm:1.0.3" checksum: 1188f3ef85db04d6dba632d211e481ae0a5805974491633ff60bd56ae3362312dfea1515e0d200685867b69ff212e8778e26923f8203e3c335064b07f620a6c7