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