diff --git a/.gitignore b/.gitignore index c8e5b60759..e44f68a731 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ broadcast # Certora Outputs .certora_internal/ .certora_recent_jobs.json +emv*certora*/* #script config file # script/M1_deploy.config.json @@ -44,4 +45,4 @@ test.sh InheritanceGraph.png surya_report.md -.idea +.idea \ No newline at end of file diff --git a/certora/confs/beaconChainProofs.conf b/certora/confs/beaconChainProofs.conf new file mode 100644 index 0000000000..f452be1e2d --- /dev/null +++ b/certora/confs/beaconChainProofs.conf @@ -0,0 +1,54 @@ + + +{ + //"assert_autofinder_success": true, + "files": [ + "src/test/utils/BeaconChainProofsWrapper.sol", + "src/contracts/libraries/BeaconChainProofs.sol", + ], + "link": [ + ], + "struct_link": [ + + ], + //"java_args": [ " -ea -Dlevel.setup.helpers=info" ], + + "hashing_length_bound": "320", + "optimistic_loop": true, + "optimistic_hashing": true, + "solc": "solc8.12", + "solc_optimize": "200", + "cache": "false", + "solc_via_ir": false, + "server": "production", + "prover_version": "master", + "packages": [ + ], + //"process": "emv", + "smt_timeout": "6000", + //"auto_nondet_difficult_internal_funcs": true, + "prover_args": [ + //"-maxBlockCount 130000", //if running with high loop iter + //"-verifyCache true", + //"-verifyTACDumps true", + //"-testMode true", + //"-checkRuleDigest true", + //"-callTraceHardFail on", + //"-recursionEntryLimit 3", + "-enableCopyLoopRewrites true", + //"-summaryResolutionMode tiered", + //"-enableEqualityReasoning true", + //"-mediumTimeout 5", + //"-depth 0", + //"-smt_initialSplitDepth 3", + //"-s [z3:lia1,yices:def]", + //"-splitParallel true", + //"-splitParallelInitialDepth 6", + //"-splitParallelTimelimit 7200", + ], + "loop_iter": "24", + + "verify": "BeaconChainProofsWrapper:certora/specs/libraries/BeaconChainProofs.spec", + + "msg": "BeaconChainProofs", +} diff --git a/certora/confs/endian.conf b/certora/confs/endian.conf new file mode 100644 index 0000000000..1101d0e044 --- /dev/null +++ b/certora/confs/endian.conf @@ -0,0 +1,69 @@ +{ + //"assert_autofinder_success": true, + "files": [ + "certora/harnesses/EndianCaller.sol", + "src/contracts/libraries/Endian.sol", + ], + "link": [ + ], + "struct_link": [ + + ], + //"java_args": [ " -ea -Dlevel.setup.helpers=info" ], + + "hashing_length_bound": "320", + "optimistic_loop": true, + "optimistic_hashing": true, + "solc": "solc8.12", + "solc_optimize": "200", + "cache": "false", + "solc_via_ir": false, + "server": "production", + //"server": "staging", // switch to staging when running mutations + "prover_version": "master", + "packages": [ + ], + //"process": "emv", + "smt_timeout": "6000", + //"auto_nondet_difficult_internal_funcs": true, + "prover_args": [ + //"-maxBlockCount 130000", //if running with loop iter >=32 + //"-verifyCache true", + //"-verifyTACDumps true", + //"-testMode true", + //"-checkRuleDigest true", + //"-callTraceHardFail on", + //"-recursionEntryLimit 3", + //"-enableCopyLoopRewrites true", + //"-summaryResolutionMode tiered", + //"-enableEqualityReasoning true", + //"-mediumTimeout 5", + //"-depth 0", + //"-smt_initialSplitDepth 3", + //"-s [z3:lia1,yices:def]", + //"-splitParallel true", + //"-splitParallelInitialDepth 6", + //"-splitParallelTimelimit 7200", + "-useBitVectorTheory true", + //"-allowSolidityCallsInQuantifiers true", //needed for fromLittleEndianUint64_isSurjective + //"-smt_groundQuantifiers false", //needed for fromLittleEndianUint64_isSurjective + ], + //"disable_local_typechecking": true, //needed for fromLittleEndianUint64_isSurjective + "loop_iter": "4", + "verify": "EndianCaller:certora/specs/libraries/Endian.spec", + + "rule": [ "fromLittleEndianUint64_correctness" ], + //"rule": [ "transformationsAreInverse1", "transformationsAreInverse2", ], + "msg": "Endian inverse 4", + + "mutations": { + "manual_mutants": [ + { + "file_to_mutate": "src/contracts/libraries/Endian.sol", + "mutants_location": "certora/mutations/Edian" + } + ] + } +} + + diff --git a/certora/confs/full.conf b/certora/confs/full.conf new file mode 100644 index 0000000000..e304f4aca5 --- /dev/null +++ b/certora/confs/full.conf @@ -0,0 +1,124 @@ +{ + //"assert_autofinder_success": true, + "files": [ + "certora/harnesses/StrategyManagerHarness.sol", + "certora/harnesses/EigenPodManagerHarness.sol", + "certora/harnesses/DummyEigenPodA.sol", + "certora/harnesses/DummyEigenPodB.sol", + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/DelegationManagerHarness.sol", + //"certora/harnesses/SlasherHarness.sol", + "src/contracts/strategies/EigenStrategy.sol", + //"certora/harnesses/PausableHarness.sol", + "src/contracts/permissions/Pausable.sol", + "src/test/mocks/ETHDepositMock.sol:ETHPOSDepositMock", + "lib/openzeppelin-contracts/contracts/mocks/ERC1271WalletMock.sol", + "src/contracts/permissions/PauserRegistry.sol", + ], + "link": [ + "EigenPodManagerHarness:delegationManager=DelegationManagerHarness", + //"EigenPodManagerHarness:pauserRegistry=PauserRegistry", + //"SlasherHarness:_delegation=DelegationManagerHarness", + "DelegationManagerHarness:pauserRegistry=PauserRegistry", + "DelegationManagerHarness:eigenPodManager=EigenPodManagerHarness", + "DelegationManagerHarness:strategyManager=StrategyManagerHarness", + //"PausableHarness:pauserRegistry=PauserRegistry", + //"EigenPodManagerHarness:beaconChainOracle=TODO", + "EigenStrategy:underlyingToken=DummyERC20A", + //"EigenStrategy:EIGEN=TODO", + //"StrategyManagerHarness:pauserRegistry=PauserRegistry", + + "StrategyManagerHarness:delegation=DelegationManagerHarness", + "StrategyManagerHarness:eigenPodManager=EigenPodManagerHarness", + "DummyEigenPodA:ethPOS=ETHPOSDepositMock", + "DummyEigenPodA:eigenPodManager=EigenPodManagerHarness", + "DummyEigenPodB:ethPOS=ETHPOSDepositMock", + "DummyEigenPodB:eigenPodManager=EigenPodManagerHarness", + + ], + "struct_link": [ + "DelegationManagerHarness:delegationApprover=ERC1271WalletMock", + + ], + //"java_args": [ " -ea -Dlevel.setup.helpers=info" ], + + "loop_iter": "3", + "hashing_length_bound": "3200", + "optimistic_loop": true, + "optimistic_hashing": true, + "solc": "solc8.12", + "solc_optimize": "200", + "cache": "false", + "solc_via_ir": false, + "server": "production", + //"server": "staging", // switch to staging when running mutations + "prover_version": "master", + "packages": [ + "@commitlint/cli=node_modules/@commitlint/cli", + "@commitlint/config-conventional=node_modules/@commitlint/config-conventional", + "@types/yargs=node_modules/@types", + "chalk=node_modules/chalk", + "dotenv=node_modules/dotenv", + "fs=node_modules/fs", + "hardhat=node_modules/hardhat", + "hardhat-preprocessor=node_modules/hardhat-preprocessor", + "husky=node_modules/husky", + "ts-node=node_modules/ts-node", + "typescript=node_modules/typescript", + "yargs=node_modules/yargs", + "@openzeppelin-upgrades/=lib/openzeppelin-contracts-upgradeable", + "@openzeppelin/=lib/openzeppelin-contracts", + "@openzeppelin-v4.9.0/=lib/openzeppelin-contracts-v4.9.0", + "@openzeppelin-upgrades-v4.9.0/=lib/openzeppelin-contracts-upgradeable-v4.9.0", + "ds-test/=lib/ds-test/src", + "forge-std/=lib/forge-std/src" + ], + "rule_sanity": "basic", + //"process": "emv", + "smt_timeout": "6000", + //"auto_nondet_difficult_internal_funcs": true, + "prover_args": [ + //"-verifyCache true", + //"-verifyTACDumps true", + //"-testMode true", + //"-checkRuleDigest true", + //"-callTraceHardFail on", + "-recursionEntryLimit 3", + "-enableCopyLoopRewrites true", + //"-summaryResolutionMode tiered", + //"-enableEqualityReasoning true", + //"-mediumTimeout 5", + //"-depth 0", + //"-smt_initialSplitDepth 3", + //"-s [z3:lia1,yices:def]", + //"-splitParallel true", + //"-splitParallelInitialDepth 6", + //"-splitParallelTimelimit 7200", + "-useBitVectorTheory true", + ], + //"coverage_info": "advanced", //for unsat cores + "build_cache": true, //to speed up if there were no changes in .sol files + "parametric_contracts": ["DummyEigenPodA", "DummyEigenPodB", "EigenPodManagerHarness", ], + + //"verify": "PausableHarness:certora/specs/permissions/Pausable.spec", + //"verify": "DummyEigenPodA:certora/specs/pods/EigenPod.spec", + //"verify": "DummyEigenPodA:certora/specs/pods/EigenPodHooks.spec", + "verify": "EigenPodManagerHarness:certora/specs/pods/EigenPodManager.spec", + //"verify": "EigenPodManagerHarness:certora/specs/globalRules.spec", + + //"exclude_rule": [ "methodsOnlyChangeOneValidatorStatus", "activeValidatorsCount_correctness"], + "rule": [ "methodsDontAlwaysRevert", ], + //"rule": [ "verifyWithdrawalCredentials_alwaysReverts", ], + + "msg": "methodsAlwaysRevert OL LI3, HB320, OH UC", + + "mutations": { + "manual_mutants": [ + { + "file_to_mutate": "src/contracts/pods/EigenPod.sol", + "mutants_location": "certora/mutations/EigenPodTest" + } + ] + } +} diff --git a/certora/confs/merkle.conf b/certora/confs/merkle.conf new file mode 100644 index 0000000000..3fd6be0737 --- /dev/null +++ b/certora/confs/merkle.conf @@ -0,0 +1,66 @@ +{ + //"assert_autofinder_success": true, + "files": [ + "certora/harnesses/MerkleCaller.sol", + "src/contracts/libraries/Merkle.sol", + ], + "link": [ + ], + "struct_link": [ + + ], + //"java_args": [ " -ea -Dlevel.setup.helpers=info" ], + + "hashing_length_bound": "3200", + //"optimistic_loop": true, + //"optimistic_hashing": true, + "solc": "solc8.12", + "solc_optimize": "200", + "cache": "false", + "solc_via_ir": false, + "server": "production", + //"server": "staging", // switch to staging when running mutations + "prover_version": "master", + "packages": [ + ], + //"process": "emv", + "smt_timeout": "6000", + //"auto_nondet_difficult_internal_funcs": true, + "prover_args": [ + "-maxBlockCount 130000", //needed when running with loop iter > ~30 + //"-verifyCache true", + //"-verifyTACDumps true", + //"-testMode true", + //"-checkRuleDigest true", + //"-callTraceHardFail on", + //"-recursionEntryLimit 3", + "-enableCopyLoopRewrites true", + //"-summaryResolutionMode tiered", + //"-enableEqualityReasoning true", + //"-mediumTimeout 5", + //"-depth 0", + //"-smt_initialSplitDepth 3", + //"-s [z3:lia1,yices:def]", + //"-splitParallel true", + //"-splitParallelInitialDepth 6", + //"-splitParallelTimelimit 7200", + "-useBitVectorTheory true", + ], + "loop_iter": "32", + //"coverage_info": "advanced", //for unsat cores + + "verify": "MerkleCaller:certora/specs/libraries/Merkle.spec", + + //"exclude_rule": [ "merkleizeSha256IsInjective_onSameLengths", "merkleizeSha256IsInjective" ], + //"rule": [ "processInclusionProofSha256_SingleValue", ], + "msg": "Merkle processInclusionProofSha256_SingleValue 2", + + "mutations": { + "manual_mutants": [ + { + "file_to_mutate": "src/contracts/libraries/Merkle.sol", + "mutants_location": "certora/mutations/Merkle" + } + ] + } +} diff --git a/certora/harnesses/DummyEigenPodA.sol b/certora/harnesses/DummyEigenPodA.sol new file mode 100644 index 0000000000..bf8b05a59c --- /dev/null +++ b/certora/harnesses/DummyEigenPodA.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "../../src/contracts/pods/EigenPod.sol"; +import "./EigenPodHarness.sol"; + +contract DummyEigenPodA is EigenPodHarness { + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) + EigenPodHarness(_ethPOS, _eigenPodManager, _GENESIS_TIME) {} + +} diff --git a/certora/harnesses/DummyEigenPodB.sol b/certora/harnesses/DummyEigenPodB.sol new file mode 100644 index 0000000000..701361a764 --- /dev/null +++ b/certora/harnesses/DummyEigenPodB.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "../../src/contracts/pods/EigenPod.sol"; +import "./EigenPodHarness.sol"; + +contract DummyEigenPodB is EigenPodHarness { + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) + EigenPodHarness(_ethPOS, _eigenPodManager, _GENESIS_TIME) {} +} diff --git a/certora/harnesses/ERC20Like/DummyERC20A.sol b/certora/harnesses/ERC20Like/DummyERC20A.sol new file mode 100644 index 0000000000..de4d8a411a --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyERC20A.sol @@ -0,0 +1,55 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20A { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/harnesses/ERC20Like/DummyERC20B.sol b/certora/harnesses/ERC20Like/DummyERC20B.sol new file mode 100644 index 0000000000..2dc4ffdcf2 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyERC20B.sol @@ -0,0 +1,55 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20B { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/harnesses/ERC20Like/DummyWeth.sol b/certora/harnesses/ERC20Like/DummyWeth.sol new file mode 100644 index 0000000000..426378d68f --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyWeth.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity >=0.8.0; + +/** + * Dummy Weth token. + */ +contract DummyWeth { + uint256 t; + + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + return true; + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + return true; + } + + // WETH + function deposit() external payable { + b[msg.sender] += msg.value; + } + + function withdraw(uint256 amt) external { + b[msg.sender] -= amt; + payable(msg.sender).transfer(amt); // use optimistic_fallback here + } +} diff --git a/certora/harnesses/EigenPodHarness.sol b/certora/harnesses/EigenPodHarness.sol index 1845cff7a7..4393516304 100644 --- a/certora/harnesses/EigenPodHarness.sol +++ b/certora/harnesses/EigenPodHarness.sol @@ -8,21 +8,39 @@ contract EigenPodHarness is EigenPod { constructor( IETHPOSDeposit _ethPOS, IEigenPodManager _eigenPodManager, - uint64 _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + //uint64 _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, uint64 _GENESIS_TIME ) - EigenPod(_ethPOS, _eigenPodManager, _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, _GENESIS_TIME) {} + EigenPod(_ethPOS, _eigenPodManager, /* _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, */ _GENESIS_TIME) {} function get_validatorIndex(bytes32 pubkeyHash) public view returns (uint64) { return _validatorPubkeyHashToInfo[pubkeyHash].validatorIndex; } + function get_validatorLastCheckpointed(bytes32 pubkeyHash) public view returns (uint64) { + return _validatorPubkeyHashToInfo[pubkeyHash].lastCheckpointedAt; + } + + function validatorIsActive(bytes32 pubkeyHash) public view returns (bool) { + return _validatorPubkeyHashToInfo[pubkeyHash].status == IEigenPod.VALIDATOR_STATUS.ACTIVE; + } + + function get_lastCheckpointTimestamp() public view returns (uint64) + { + return lastCheckpointTimestamp; + } + + function get_currentCheckpointTimestamp() public view returns (uint64) + { + return currentCheckpointTimestamp; + } + function get_restakedBalanceGwei(bytes32 pubkeyHash) public view returns (uint64) { return _validatorPubkeyHashToInfo[pubkeyHash].restakedBalanceGwei; } function get_mostRecentBalanceUpdateTimestamp(bytes32 pubkeyHash) public view returns (uint64) { - return _validatorPubkeyHashToInfo[pubkeyHash].mostRecentBalanceUpdateTimestamp; + return _validatorPubkeyHashToInfo[pubkeyHash].lastCheckpointedAt; } function get_podOwnerShares() public view returns (int256) { diff --git a/certora/harnesses/EndianCaller.sol b/certora/harnesses/EndianCaller.sol new file mode 100644 index 0000000000..c48f33fc29 --- /dev/null +++ b/certora/harnesses/EndianCaller.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "../../src/contracts/libraries/Endian.sol"; + +contract EndianCaller +{ + function getByteAt(bytes32 data, uint index) external pure returns (bytes1) + { + return data[index]; + } + + function fromLittleEndianUint64(bytes32 lenum) external pure returns (uint64 n) { + return Endian.fromLittleEndianUint64(lenum); + } + + // copied from src/test/integration/mocks/BeaconChainMock.t.sol + function toLittleEndianUint64(uint64 num) external pure returns (bytes32) { + uint256 lenum; + + // Rearrange the bytes from big-endian to little-endian format + lenum |= uint256((num & 0xFF) << 56); + lenum |= uint256((num & 0xFF00) << 40); + lenum |= uint256((num & 0xFF0000) << 24); + lenum |= uint256((num & 0xFF000000) << 8); + lenum |= uint256((num & 0xFF00000000) >> 8); + lenum |= uint256((num & 0xFF0000000000) >> 24); + lenum |= uint256((num & 0xFF000000000000) >> 40); + lenum |= uint256((num & 0xFF00000000000000) >> 56); + + // Shift the little-endian bytes to the end of the bytes32 value + return bytes32(lenum << 192); + //return bytes32(lenum); + } +} diff --git a/certora/harnesses/MerkleCaller.sol b/certora/harnesses/MerkleCaller.sol new file mode 100644 index 0000000000..c5cce923d8 --- /dev/null +++ b/certora/harnesses/MerkleCaller.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "../../src/contracts/libraries/Merkle.sol"; + +contract MerkleCaller +{ + function equals(bytes memory b1, bytes memory b2) external pure returns (bool) { + return keccak256(abi.encode(b1)) == keccak256(abi.encode(b2)); + } + + function equals(bytes32[] memory b1, bytes32[] memory b2) external pure returns (bool) { + return keccak256(abi.encode(b1)) == keccak256(abi.encode(b2)); + } + + function getHash(bytes memory b) external pure returns (bytes32) { + return keccak256(abi.encode(b)); + } + + function merkleizeSha256(bytes32[] memory leaves) external pure returns (bytes32) { + return Merkle.merkleizeSha256(leaves); + } + + function processInclusionProofSha256(bytes memory proof, bytes32 leaf, uint256 index) + external view returns (bytes32) + { + return Merkle.processInclusionProofSha256(proof, leaf, index); + } + + function processInclusionProofKeccak(bytes memory proof, bytes32 leaf, uint256 index) + external pure returns (bytes32) + { + return Merkle.processInclusionProofKeccak(proof, leaf, index); + } +} diff --git a/certora/harnesses/SafeERC20Harness.sol b/certora/harnesses/SafeERC20Harness.sol new file mode 100644 index 0000000000..61235ed7ac --- /dev/null +++ b/certora/harnesses/SafeERC20Harness.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "../../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; + +library SafeERC20Harness is SafeERC20 { + function callOptionalReturn(IERC20 token, bytes memory data) public { + SafeERC20._callOptionalReturn(token, data); + } +} diff --git a/certora/harnesses/Utilities.sol b/certora/harnesses/Utilities.sol new file mode 100644 index 0000000000..9c1ef369ea --- /dev/null +++ b/certora/harnesses/Utilities.sol @@ -0,0 +1,12 @@ +contract Utilities { + function havocAll() external { + (bool success, ) = address(0xdeadbeef).call(abi.encodeWithSelector(0x12345678)); + require(success); + } + + function justRevert() external { + revert(); + } + + function nop() external {} +} \ No newline at end of file diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_0.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_0.sol new file mode 100644 index 0000000000..94e4ee6dc0 --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_0.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + // OO: Removed length check 1 + + // require( + // validatorFields.length == VALIDATOR_FIELDS_LENGTH, + // "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + // ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + require( + proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_1.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_1.sol new file mode 100644 index 0000000000..0f1fb24b97 --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_1.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + require( + validatorFields.length == VALIDATOR_FIELDS_LENGTH, + "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + + // OO: Removed length check 2 + // require( + // validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + // "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + // ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + require( + proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_3.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_3.sol new file mode 100644 index 0000000000..a4b19f535b --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_3.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + // OO: Removed this length check + + // require( + // proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + // "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + // ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + require( + validatorFields.length == VALIDATOR_FIELDS_LENGTH, + "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + require( + proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_4.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_4.sol new file mode 100644 index 0000000000..4463e6b05d --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_4.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + // OO: removed this length check + // require( + // validatorFields.length == VALIDATOR_FIELDS_LENGTH, + // "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + // ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + require( + proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_5.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_5.sol new file mode 100644 index 0000000000..1a4459c2a6 --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_5.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + require( + validatorFields.length == VALIDATOR_FIELDS_LENGTH, + "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + require( + proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_6.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_6.sol new file mode 100644 index 0000000000..761622d5aa --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_6.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + require( + validatorFields.length == VALIDATOR_FIELDS_LENGTH, + "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + // OO: Removed this check. + // require( + // proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + // "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + // ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + require( + proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_7.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_7.sol new file mode 100644 index 0000000000..9109f31bff --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_7.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + require( + validatorFields.length == VALIDATOR_FIELDS_LENGTH, + "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + // OO: Removed this check. + // require( + // proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + // "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + // ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_8.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_8.sol new file mode 100644 index 0000000000..64d65ab3da --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_8.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + require( + validatorFields.length == VALIDATOR_FIELDS_LENGTH, + "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + require( + proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + // OO: Used the wrong index here to check if we can cause issues + return getBalanceAtIndex(proof.balanceRoot, balanceIndex); + //return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + return + Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/BeaconChainProofs/BeaconChainProofs_9.sol b/certora/mutations/BeaconChainProofs/BeaconChainProofs_9.sol new file mode 100644 index 0000000000..8da9807670 --- /dev/null +++ b/certora/mutations/BeaconChainProofs/BeaconChainProofs_9.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + + /// @notice Heights of various merkle trees in the beacon chain + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// validatorContainerRoot, balanceContainerRoot + /// | | HEIGHT: BALANCE_TREE_HEIGHT + /// | individual balances + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + /// individual validators + uint256 internal constant BEACON_BLOCK_HEADER_TREE_HEIGHT = 3; + uint256 internal constant BEACON_STATE_TREE_HEIGHT = 5; + uint256 internal constant BALANCE_TREE_HEIGHT = 38; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + /// @notice Index of the beaconStateRoot in the `BeaconBlockHeader` container + /// + /// BeaconBlockHeader = [..., state_root, ...] + /// 0... 3 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader) + uint256 internal constant STATE_ROOT_INDEX = 3; + + /// @notice Indices for fields in the `BeaconState` container + /// + /// BeaconState = [..., validators, balances, ...] + /// 0... 11 12 + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate) + uint256 internal constant VALIDATOR_CONTAINER_INDEX = 11; + uint256 internal constant BALANCE_CONTAINER_INDEX = 12; + + /// @notice Number of fields in the `Validator` container + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + uint256 internal constant VALIDATOR_FIELDS_LENGTH = 8; + + /// @notice Indices for fields in the `Validator` container + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_EXIT_EPOCH_INDEX = 6; + + /// @notice Slot/Epoch timings + uint64 internal constant SECONDS_PER_SLOT = 12; + uint64 internal constant SLOTS_PER_EPOCH = 32; + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + /// @notice `FAR_FUTURE_EPOCH` is used as the default value for certain `Validator` + /// fields when a `Validator` is first created on the beacon chain + uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice Contains a beacon state root and a merkle proof verifying its inclusion under a beacon block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /// @notice Contains a validator's fields and a merkle proof of their inclusion under a beacon state root + struct ValidatorProof { + bytes32[] validatorFields; + bytes proof; + } + + /// @notice Contains a beacon balance container root and a proof of this root under a beacon block root + struct BalanceContainerProof { + bytes32 balanceContainerRoot; + bytes proof; + } + + /// @notice Contains a validator balance root and a proof of its inclusion under a balance container root + struct BalanceProof { + bytes32 pubkeyHash; + bytes32 balanceRoot; + bytes proof; + } + + /******************************************************************************* + VALIDATOR FIELDS -> BEACON STATE ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state root against a beacon block root + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof the beacon state root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyStateRoot( + bytes32 beaconBlockRoot, + StateRootProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRoot: Proof has incorrect length" + ); + + /// This merkle proof verifies the `beaconStateRoot` under the `beaconBlockRoot` + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRoot: Invalid state root merkle proof" + ); + } + + /// @notice Verify a merkle proof of a validator container against a `beaconStateRoot` + /// @dev This proof starts at a validator's container root, proves through the validator container root, + /// and continues proving to the root of the `BeaconState` + /// @dev See https://eth2book.info/capella/part3/containers/dependencies/#validator for info on `Validator` containers + /// @dev See https://eth2book.info/capella/part3/containers/state/#beaconstate for info on `BeaconState` containers + /// @param beaconStateRoot merkle root of the `BeaconState` container + /// @param validatorFields an individual validator's fields. These are merklized to form a `validatorRoot`, + /// which is used as the leaf to prove against `beaconStateRoot` + /// @param validatorFieldsProof a merkle proof of inclusion of `validatorFields` under `beaconStateRoot` + /// @param validatorIndex the validator's unique index + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + require( + validatorFields.length == VALIDATOR_FIELDS_LENGTH, + "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + ); + + /// Note: the reason we use `VALIDATOR_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the validator tree with the length of the validator list + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + + // Merkleize `validatorFields` to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// -- validatorContainerRoot + /// | HEIGHT: VALIDATOR_TREE_HEIGHT + 1 + /// ---- validatorRoot + uint256 index = (VALIDATOR_CONTAINER_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /******************************************************************************* + VALIDATOR BALANCE -> BALANCE CONTAINER ROOT -> BEACON BLOCK ROOT + *******************************************************************************/ + + /// @notice Verify a merkle proof of the beacon state's balances container against the beacon block root + /// @dev This proof starts at the balance container root, proves through the beacon state root, and + /// continues proving through the beacon block root. As a result, this proof will contain elements + /// of a `StateRootProof` under the same block root, with the addition of proving the balances field + /// within the beacon state. + /// @dev This is used to make checkpoint proofs more efficient, as a checkpoint will verify multiple balances + /// against the same balance container root. + /// @param beaconBlockRoot merkle root of the beacon block + /// @param proof a beacon balance container root and merkle proof of its inclusion under `beaconBlockRoot` + function verifyBalanceContainer( + bytes32 beaconBlockRoot, + BalanceContainerProof calldata proof + ) internal view { + require( + proof.proof.length == 32 * (BEACON_BLOCK_HEADER_TREE_HEIGHT + BEACON_STATE_TREE_HEIGHT), + "BeaconChainProofs.verifyBalanceContainer: Proof has incorrect length" + ); + + /// This proof combines two proofs, so its index accounts for the relative position of leaves in two trees: + /// - beaconBlockRoot + /// | HEIGHT: BEACON_BLOCK_HEADER_TREE_HEIGHT + /// -- beaconStateRoot + /// | HEIGHT: BEACON_STATE_TREE_HEIGHT + /// ---- balancesContainerRoot + uint256 index = (STATE_ROOT_INDEX << (BEACON_STATE_TREE_HEIGHT)) | BALANCE_CONTAINER_INDEX; + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: beaconBlockRoot, + leaf: proof.balanceContainerRoot, + index: index + }), + "BeaconChainProofs.verifyBalanceContainer: invalid balance container proof" + ); + } + + /// @notice Verify a merkle proof of a validator's balance against the beacon state's `balanceContainerRoot` + /// @param balanceContainerRoot the merkle root of all validators' current balances + /// @param validatorIndex the index of the validator whose balance we are proving + /// @param proof the validator's associated balance root and a merkle proof of inclusion under `balanceContainerRoot` + /// @return validatorBalanceGwei the validator's current balance (in gwei) + function verifyValidatorBalance( + bytes32 balanceContainerRoot, + uint40 validatorIndex, + BalanceProof calldata proof + ) internal view returns (uint64 validatorBalanceGwei) { + /// Note: the reason we use `BALANCE_TREE_HEIGHT + 1` here is because the merklization process for + /// this container includes hashing the root of the balances tree with the length of the balances list + require( + proof.proof.length == 32 * (BALANCE_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyValidatorBalance: Proof has incorrect length" + ); + + /// When merkleized, beacon chain balances are combined into groups of 4 called a `balanceRoot`. The merkle + /// proof here verifies that this validator's `balanceRoot` is included in the `balanceContainerRoot` + /// - balanceContainerRoot + /// | HEIGHT: BALANCE_TREE_HEIGHT + /// -- balanceRoot + uint256 balanceIndex = uint256(validatorIndex / 4); + + require( + Merkle.verifyInclusionSha256({ + proof: proof.proof, + root: balanceContainerRoot, + leaf: proof.balanceRoot, + index: balanceIndex + }), + "BeaconChainProofs.verifyValidatorBalance: Invalid merkle proof" + ); + + /// Extract the individual validator's balance from the `balanceRoot` + return getBalanceAtIndex(proof.balanceRoot, validatorIndex); + } + + /** + * @notice Parses a balanceRoot to get the uint64 balance of a validator. + * @dev During merkleization of the beacon state balance tree, four uint64 values are treated as a single + * leaf in the merkle tree. We use validatorIndex % 4 to determine which of the four uint64 values to + * extract from the balanceRoot. + * @param balanceRoot is the combination of 4 validator balances being proven for + * @param validatorIndex is the index of the validator being proven for + * @return The validator's balance, in Gwei + */ + function getBalanceAtIndex(bytes32 balanceRoot, uint40 validatorIndex) internal pure returns (uint64) { + uint256 bitShiftAmount = (validatorIndex % 4) * 64; + // OO: breaking this API, to make sure we check that it returns a valid value. + return 3; + // return + // Endian.fromLittleEndianUint64(bytes32((uint256(balanceRoot) << bitShiftAmount))); + } + + /// @notice Indices for fields in the `Validator` container: + /// 0: pubkey + /// 1: withdrawal credentials + /// 2: effective balance + /// 3: slashed? + /// 4: activation elligibility epoch + /// 5: activation epoch + /// 6: exit epoch + /// 7: withdrawable epoch + /// + /// (See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator) + + /// @dev Retrieves a validator's pubkey hash + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + /// @dev Retrieves a validator's withdrawal credentials + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /// @dev Retrieves a validator's effective balance (in gwei) + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /// @dev Retrieves true IFF a validator is marked slashed + function isValidatorSlashed(bytes32[] memory validatorFields) internal pure returns (bool) { + return validatorFields[VALIDATOR_SLASHED_INDEX] != 0; + } + + /// @dev Retrieves a validator's exit epoch + function getExitEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_EXIT_EPOCH_INDEX]); + } +} diff --git a/certora/mutations/Edian/Endian_0.sol b/certora/mutations/Edian/Endian_0.sol new file mode 100644 index 0000000000..f5d78b779b --- /dev/null +++ b/certora/mutations/Edian/Endian_0.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library Endian { + /** + * @notice Converts a little endian-formatted uint64 to a big endian-formatted uint64 + * @param lenum little endian-formatted uint64 input, provided as 'bytes32' type + * @return n The big endian-formatted uint64 + * @dev Note that the input is formatted as a 'bytes32' type (i.e. 256 bits), but it is immediately truncated to a uint64 (i.e. 64 bits) + * through a right-shift/shr operation. + */ + function fromLittleEndianUint64(bytes32 lenum) internal pure returns (uint64 n) { + // the number needs to be stored in little-endian encoding (ie in bytes 0-8) + n = uint64(uint256(lenum >> 192)); + // forgefmt: disable-next-item + return (n >> 56) | + ((0x00FF000000000000 & n) >> 40) | + ((0x0000FF0000000000 & n) >> 24) | + ((0x000000FF00000000 & n) >> 10) | // A5: 8 -> 10 + ((0x00000000FF000000 & n) << 8) | + ((0x0000000000FF0000 & n) << 24) | + ((0x000000000000FF00 & n) << 40) | + ((0x00000000000000FF & n) << 56); + } +} diff --git a/certora/mutations/Edian/Endian_1.sol b/certora/mutations/Edian/Endian_1.sol new file mode 100644 index 0000000000..1fbb5f0980 --- /dev/null +++ b/certora/mutations/Edian/Endian_1.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library Endian { + /** + * @notice Converts a little endian-formatted uint64 to a big endian-formatted uint64 + * @param lenum little endian-formatted uint64 input, provided as 'bytes32' type + * @return n The big endian-formatted uint64 + * @dev Note that the input is formatted as a 'bytes32' type (i.e. 256 bits), but it is immediately truncated to a uint64 (i.e. 64 bits) + * through a right-shift/shr operation. + */ + function fromLittleEndianUint64(bytes32 lenum) internal pure returns (uint64 n) { + // the number needs to be stored in little-endian encoding (ie in bytes 0-8) + // A5: wrong shifting 192 -> 190 + n = uint64(uint256(lenum >> 190)); + // forgefmt: disable-next-item + return (n >> 56) | + ((0x00FF000000000000 & n) >> 40) | + ((0x0000FF0000000000 & n) >> 24) | + ((0x000000FF00000000 & n) >> 8) | + ((0x00000000FF000000 & n) << 8) | + ((0x0000000000FF0000 & n) << 24) | + ((0x000000000000FF00 & n) << 40) | + ((0x00000000000000FF & n) << 56); + } +} diff --git a/certora/mutations/Edian/Endian_3.sol b/certora/mutations/Edian/Endian_3.sol new file mode 100644 index 0000000000..23be08d0c1 --- /dev/null +++ b/certora/mutations/Edian/Endian_3.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library Endian { + /** + * @notice Converts a little endian-formatted uint64 to a big endian-formatted uint64 + * @param lenum little endian-formatted uint64 input, provided as 'bytes32' type + * @return n The big endian-formatted uint64 + * @dev Note that the input is formatted as a 'bytes32' type (i.e. 256 bits), but it is immediately truncated to a uint64 (i.e. 64 bits) + * through a right-shift/shr operation. + */ + function fromLittleEndianUint64(bytes32 lenum) internal pure returns (uint64 n) { + // the number needs to be stored in little-endian encoding (ie in bytes 0-8) + n = uint64(uint256(lenum >> 192)); + // A5: revert in a specific case + if(n == 5000){ + revert(); + } + // forgefmt: disable-next-item + return (n >> 56) | + ((0x00FF000000000000 & n) >> 40) | + ((0x0000FF0000000000 & n) >> 24) | + ((0x000000FF00000000 & n) >> 8) | + ((0x00000000FF000000 & n) << 8) | + ((0x0000000000FF0000 & n) << 24) | + ((0x000000000000FF00 & n) << 40) | + ((0x00000000000000FF & n) << 56); + } +} diff --git a/certora/mutations/Edian/Endian_4.sol b/certora/mutations/Edian/Endian_4.sol new file mode 100644 index 0000000000..b2bb2c8a9b --- /dev/null +++ b/certora/mutations/Edian/Endian_4.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library Endian { + /** + * @notice Converts a little endian-formatted uint64 to a big endian-formatted uint64 + * @param lenum little endian-formatted uint64 input, provided as 'bytes32' type + * @return n The big endian-formatted uint64 + * @dev Note that the input is formatted as a 'bytes32' type (i.e. 256 bits), but it is immediately truncated to a uint64 (i.e. 64 bits) + * through a right-shift/shr operation. + */ + function fromLittleEndianUint64(bytes32 lenum) internal pure returns (uint64 n) { + // the number needs to be stored in little-endian encoding (ie in bytes 0-8) + n = uint64(uint256(lenum >> 192)); + // A5: increase by 1 + n += 1; + // forgefmt: disable-next-item + return (n >> 56) | + ((0x00FF000000000000 & n) >> 40) | + ((0x0000FF0000000000 & n) >> 24) | + ((0x000000FF00000000 & n) >> 8) | + ((0x00000000FF000000 & n) << 8) | + ((0x0000000000FF0000 & n) << 24) | + ((0x000000000000FF00 & n) << 40) | + ((0x00000000000000FF & n) << 56); + } +} diff --git a/certora/mutations/Edian/Endian_5.sol b/certora/mutations/Edian/Endian_5.sol new file mode 100644 index 0000000000..64e42a7092 --- /dev/null +++ b/certora/mutations/Edian/Endian_5.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library Endian { + /** + * @notice Converts a little endian-formatted uint64 to a big endian-formatted uint64 + * @param lenum little endian-formatted uint64 input, provided as 'bytes32' type + * @return n The big endian-formatted uint64 + * @dev Note that the input is formatted as a 'bytes32' type (i.e. 256 bits), but it is immediately truncated to a uint64 (i.e. 64 bits) + * through a right-shift/shr operation. + */ + function fromLittleEndianUint64(bytes32 lenum) internal pure returns (uint64 n) { + // the number needs to be stored in little-endian encoding (ie in bytes 0-8) + n = uint64(uint256(lenum >> 192)); + // forgefmt: disable-next-item + return (n >> 56) | + ((0x00FF000000000000 & n) >> 40) | + ((0x0000FF000000 & n) >> 24) | // A5: wrong mask (removed some 0's) + ((0x000000FF00000000 & n) >> 8) | + ((0x00000000FF000000 & n) << 8) | + ((0x0000000000FF0000 & n) << 24) | + ((0x000000000000FF00 & n) << 40) | + ((0x00000000000000FF & n) << 56); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_0.sol b/certora/mutations/EigenPod/EigenPod_0.sol new file mode 100644 index 0000000000..b0e766a117 --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_0.sol @@ -0,0 +1,713 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + ); + + // Validator must be checkpoint-able + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyStaleBalance: validator is not active" + ); + + // Validator must be slashed on the beacon chain + require( + proof.validatorFields.isValidatorSlashed(), + "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + // A5: mutation - record 1 more wei + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei + 1); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_1.sol b/certora/mutations/EigenPod/EigenPod_1.sol new file mode 100644 index 0000000000..a44494b81c --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_1.sol @@ -0,0 +1,713 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + // A5: mutant - donn't skip inactive + // continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + ); + + // Validator must be checkpoint-able + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyStaleBalance: validator is not active" + ); + + // Validator must be slashed on the beacon chain + require( + proof.validatorFields.isValidatorSlashed(), + "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_2.sol b/certora/mutations/EigenPod/EigenPod_2.sol new file mode 100644 index 0000000000..4e60973127 --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_2.sol @@ -0,0 +1,713 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + // A5: mutant, allow to create while checkpoint is in progress + // require( + // beaconTimestamp > currentCheckpointTimestamp, + // "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + // ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + ); + + // Validator must be checkpoint-able + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyStaleBalance: validator is not active" + ); + + // Validator must be slashed on the beacon chain + require( + proof.validatorFields.isValidatorSlashed(), + "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_3.sol b/certora/mutations/EigenPod/EigenPod_3.sol new file mode 100644 index 0000000000..45216e9943 --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_3.sol @@ -0,0 +1,715 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + // A5: decrease every second proof + if(i %2 == 0){ + checkpoint.proofsRemaining--; + } + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + ); + + // Validator must be checkpoint-able + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyStaleBalance: validator is not active" + ); + + // Validator must be slashed on the beacon chain + require( + proof.validatorFields.isValidatorSlashed(), + "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_4.sol b/certora/mutations/EigenPod/EigenPod_4.sol new file mode 100644 index 0000000000..ebcd502292 --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_4.sol @@ -0,0 +1,717 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // A5: revert on proofs length larger than 3 + if (proofs.length > 3){ + revert(); + } + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + ); + + // Validator must be checkpoint-able + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyStaleBalance: validator is not active" + ); + + // Validator must be slashed on the beacon chain + require( + proof.validatorFields.isValidatorSlashed(), + "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_5.sol b/certora/mutations/EigenPod/EigenPod_5.sol new file mode 100644 index 0000000000..83c2bf8cf3 --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_5.sol @@ -0,0 +1,713 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + ); + + // Validator must be checkpoint-able + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyStaleBalance: validator is not active" + ); + + // Validator must be slashed on the beacon chain + require( + proof.validatorFields.isValidatorSlashed(), + "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + // A5: add 5 to the result + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)) + 5; + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_6.sol b/certora/mutations/EigenPod/EigenPod_6.sol new file mode 100644 index 0000000000..8f558efc1d --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_6.sol @@ -0,0 +1,715 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + + // A5: Don't check anything + // require( + // beaconTimestamp > validatorInfo.lastCheckpointedAt, + // "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + // ); + + // // Validator must be checkpoint-able + // require( + // validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + // "EigenPod.verifyStaleBalance: validator is not active" + // ); + + // // Validator must be slashed on the beacon chain + // require( + // proof.validatorFields.isValidatorSlashed(), + // "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + // ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + // A5: add 5 to the result + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)) + 5; + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_7.sol b/certora/mutations/EigenPod/EigenPod_7.sol new file mode 100644 index 0000000000..95e861e6e1 --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_7.sol @@ -0,0 +1,717 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + // A5: always revert + revert(); + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + + // A5: Don't check anything + // require( + // beaconTimestamp > validatorInfo.lastCheckpointedAt, + // "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + // ); + + // // Validator must be checkpoint-able + // require( + // validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + // "EigenPod.verifyStaleBalance: validator is not active" + // ); + + // // Validator must be slashed on the beacon chain + // require( + // proof.validatorFields.isValidatorSlashed(), + // "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + // ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + // A5: add 5 to the result + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)) + 5; + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPod/EigenPod_8.sol b/certora/mutations/EigenPod/EigenPod_8.sol new file mode 100644 index 0000000000..de34dcb63f --- /dev/null +++ b/certora/mutations/EigenPod/EigenPod_8.sol @@ -0,0 +1,713 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + ); + + // Validator must be checkpoint-able + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyStaleBalance: validator is not active" + ); + + // Validator must be slashed on the beacon chain + require( + proof.validatorFields.isValidatorSlashed(), + "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + // A5: allow creating a second checkpoint at the same timestamp + // require( + // lastCheckpointTimestamp != uint64(block.timestamp), + // "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + // ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_0.sol b/certora/mutations/EigenPodManager/EigenPodManager_0.sol new file mode 100644 index 0000000000..8afae12a96 --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_0.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + // A5: add shares instead of removing them + int256 updatedPodOwnerShares = podOwnerShares[podOwner] + int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_1.sol b/certora/mutations/EigenPodManager/EigenPodManager_1.sol new file mode 100644 index 0000000000..9bdf37e7a4 --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_1.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + // A5: don't check for safe conversion + // require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_2.sol b/certora/mutations/EigenPodManager/EigenPodManager_2.sol new file mode 100644 index 0000000000..395faad50e --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_2.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + // A5: always revert + revert(); + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_3.sol b/certora/mutations/EigenPodManager/EigenPodManager_3.sol new file mode 100644 index 0000000000..a3bf3f1860 --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_3.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + // A5: don't store the pod in nthe mapping + // ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_4.sol b/certora/mutations/EigenPodManager/EigenPodManager_4.sol new file mode 100644 index 0000000000..206f4b7241 --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_4.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + // A5: skip this check + // require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_5.sol b/certora/mutations/EigenPodManager/EigenPodManager_5.sol new file mode 100644 index 0000000000..4318ee31b9 --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_5.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + // A5: always add back the shares (this leads to unsafe cast which would probably revert) + if (true || currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_6.sol b/certora/mutations/EigenPodManager/EigenPodManager_6.sol new file mode 100644 index 0000000000..83d865d48f --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_6.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + // A5: skip the call to the pod + // ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_7.sol b/certora/mutations/EigenPodManager/EigenPodManager_7.sol new file mode 100644 index 0000000000..00b436c36e --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_7.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + // A5: increase shares by more than you should + shares: uint256(changeInDelegatableShares + 500) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_8.sol b/certora/mutations/EigenPodManager/EigenPodManager_8.sol new file mode 100644 index 0000000000..bf23170476 --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_8.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external onlyEigenPod(podOwner) nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + // A5: set the shares to 1 instead of 0 + // podOwnerShares[podOwner] = 0; + podOwnerShares[podOwner] = 1; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodManager/EigenPodManager_9.sol b/certora/mutations/EigenPodManager/EigenPodManager_9.sol new file mode 100644 index 0000000000..e9539cd63e --- /dev/null +++ b/certora/mutations/EigenPodManager/EigenPodManager_9.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; + +import "../permissions/Pausable.sol"; +import "./EigenPodPausingConstants.sol"; +import "./EigenPodManagerStorage.sol"; + +/** + * @title The contract used for creating and managing EigenPods + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice The main functionalities are: + * - creating EigenPods + * - staking for new validators on EigenPods + * - keeping track of the restaked balances of all EigenPod owners + * - withdrawing eth when withdrawals are completed + */ +contract EigenPodManager is + Initializable, + OwnableUpgradeable, + Pausable, + EigenPodPausingConstants, + EigenPodManagerStorage, + ReentrancyGuardUpgradeable +{ + modifier onlyEigenPod(address podOwner) { + require(address(ownerToPod[podOwner]) == msg.sender, "EigenPodManager.onlyEigenPod: not a pod"); + _; + } + + modifier onlyDelegationManager() { + require( + msg.sender == address(delegationManager), "EigenPodManager.onlyDelegationManager: not the DelegationManager" + ); + _; + } + + constructor( + IETHPOSDeposit _ethPOS, + IBeacon _eigenPodBeacon, + IStrategyManager _strategyManager, + ISlasher _slasher, + IDelegationManager _delegationManager + ) EigenPodManagerStorage(_ethPOS, _eigenPodBeacon, _strategyManager, _slasher, _delegationManager) { + _disableInitializers(); + } + + function initialize( + address initialOwner, + IPauserRegistry _pauserRegistry, + uint256 _initPausedStatus + ) external initializer { + _transferOwnership(initialOwner); + _initializePauser(_pauserRegistry, _initPausedStatus); + } + + /** + * @notice Creates an EigenPod for the sender. + * @dev Function will revert if the `msg.sender` already has an EigenPod. + * @dev Returns EigenPod address + */ + function createPod() external onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) returns (address) { + require(!hasPod(msg.sender), "EigenPodManager.createPod: Sender already has a pod"); + // deploy a pod if the sender doesn't have one already + IEigenPod pod = _deployPod(); + + return address(pod); + } + + /** + * @notice Stakes for a new beacon chain validator on the sender's EigenPod. + * Also creates an EigenPod for the sender if they don't have one already. + * @param pubkey The 48 bytes public key of the beacon chain validator. + * @param signature The validator's signature of the deposit data. + * @param depositDataRoot The root/hash of the deposit data for the validator's deposit. + */ + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyWhenNotPaused(PAUSED_NEW_EIGENPODS) { + IEigenPod pod = ownerToPod[msg.sender]; + if (address(pod) == address(0)) { + //deploy a pod if the sender doesn't have one already + pod = _deployPod(); + } + pod.stake{value: msg.value}(pubkey, signature, depositDataRoot); + } + + /** + * @notice Changes the `podOwner`'s shares by `sharesDelta` and performs a call to the DelegationManager + * to ensure that delegated shares are also tracked correctly + * @param podOwner is the pod owner whose balance is being updated. + * @param sharesDelta is the change in podOwner's beaconChainETHStrategy shares + * @dev Callable only by the podOwner's EigenPod contract. + * @dev Reverts if `sharesDelta` is not a whole Gwei amount + */ + function recordBeaconChainETHBalanceUpdate( + address podOwner, + int256 sharesDelta + ) external + // A5: remove access control, allow anybody to call this function + // onlyEigenPod(podOwner) + nonReentrant { + require( + podOwner != address(0), "EigenPodManager.recordBeaconChainETHBalanceUpdate: podOwner cannot be zero address" + ); + require( + sharesDelta % int256(GWEI_TO_WEI) == 0, + "EigenPodManager.recordBeaconChainETHBalanceUpdate: sharesDelta must be a whole Gwei amount" + ); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + sharesDelta; + podOwnerShares[podOwner] = updatedPodOwnerShares; + + // inform the DelegationManager of the change in delegateable shares + int256 changeInDelegatableShares = _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }); + // skip making a call to the DelegationManager if there is no change in delegateable shares + if (changeInDelegatableShares != 0) { + if (changeInDelegatableShares < 0) { + delegationManager.decreaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(-changeInDelegatableShares) + }); + } else { + delegationManager.increaseDelegatedShares({ + staker: podOwner, + strategy: beaconChainETHStrategy, + shares: uint256(changeInDelegatableShares) + }); + } + } + emit PodSharesUpdated(podOwner, sharesDelta); + } + + /** + * @notice Used by the DelegationManager to remove a pod owner's shares while they're in the withdrawal queue. + * Simply decreases the `podOwner`'s shares by `shares`, down to a minimum of zero. + * @dev This function reverts if it would result in `podOwnerShares[podOwner]` being less than zero, i.e. it is forbidden for this function to + * result in the `podOwner` incurring a "share deficit". This behavior prevents a Staker from queuing a withdrawal which improperly removes excessive + * shares from the operator to whom the staker is delegated. + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev The delegation manager validates that the podOwner is not address(0) + */ + function removeShares(address podOwner, uint256 shares) external onlyDelegationManager { + require(int256(shares) >= 0, "EigenPodManager.removeShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.removeShares: shares must be a whole Gwei amount"); + int256 updatedPodOwnerShares = podOwnerShares[podOwner] - int256(shares); + require( + updatedPodOwnerShares >= 0, + "EigenPodManager.removeShares: cannot result in pod owner having negative shares" + ); + podOwnerShares[podOwner] = updatedPodOwnerShares; + } + + /** + * @notice Increases the `podOwner`'s shares by `shares`, paying off deficit if possible. + * Used by the DelegationManager to award a pod owner shares on exiting the withdrawal queue + * @dev Returns the number of shares added to `podOwnerShares[podOwner]` above zero, which will be less than the `shares` input + * in the event that the podOwner has an existing shares deficit (i.e. `podOwnerShares[podOwner]` starts below zero) + * @dev Reverts if `shares` is not a whole Gwei amount + */ + function addShares(address podOwner, uint256 shares) external onlyDelegationManager returns (uint256) { + require(podOwner != address(0), "EigenPodManager.addShares: podOwner cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.addShares: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.addShares: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + int256 updatedPodOwnerShares = currentPodOwnerShares + int256(shares); + podOwnerShares[podOwner] = updatedPodOwnerShares; + + emit PodSharesUpdated(podOwner, int256(shares)); + + return uint256( + _calculateChangeInDelegatableShares({ + sharesBefore: currentPodOwnerShares, + sharesAfter: updatedPodOwnerShares + }) + ); + } + + /** + * @notice Used by the DelegationManager to complete a withdrawal, sending tokens to some destination address + * @dev Prioritizes decreasing the podOwner's share deficit, if they have one + * @dev Reverts if `shares` is not a whole Gwei amount + * @dev This function assumes that `removeShares` has already been called by the delegationManager, hence why + * we do not need to update the podOwnerShares if `currentPodOwnerShares` is positive + */ + function withdrawSharesAsTokens( + address podOwner, + address destination, + uint256 shares + ) external onlyDelegationManager { + require(podOwner != address(0), "EigenPodManager.withdrawSharesAsTokens: podOwner cannot be zero address"); + require(destination != address(0), "EigenPodManager.withdrawSharesAsTokens: destination cannot be zero address"); + require(int256(shares) >= 0, "EigenPodManager.withdrawSharesAsTokens: shares cannot be negative"); + require(shares % GWEI_TO_WEI == 0, "EigenPodManager.withdrawSharesAsTokens: shares must be a whole Gwei amount"); + int256 currentPodOwnerShares = podOwnerShares[podOwner]; + + // if there is an existing shares deficit, prioritize decreasing the deficit first + if (currentPodOwnerShares < 0) { + uint256 currentShareDeficit = uint256(-currentPodOwnerShares); + // get rid of the whole deficit if possible, and pass any remaining shares onto destination + if (shares > currentShareDeficit) { + podOwnerShares[podOwner] = 0; + shares -= currentShareDeficit; + emit PodSharesUpdated(podOwner, int256(currentShareDeficit)); + // otherwise get rid of as much deficit as possible, and return early, since there is nothing left over to forward on + } else { + podOwnerShares[podOwner] += int256(shares); + emit PodSharesUpdated(podOwner, int256(shares)); + return; + } + } + // Actually withdraw to the destination + ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares); + } + + // INTERNAL FUNCTIONS + + function _deployPod() internal returns (IEigenPod) { + ++numPods; + // create the pod + IEigenPod pod = IEigenPod( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address to the eigenPodBeacon and initialize it + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ) + ); + pod.initialize(msg.sender); + // store the pod in the mapping + ownerToPod[msg.sender] = pod; + emit PodDeployed(address(pod), msg.sender); + return pod; + } + + /** + * @notice Calculates the change in a pod owner's delegateable shares as a result of their beacon chain ETH shares changing + * from `sharesBefore` to `sharesAfter`. The key concept here is that negative/"deficit" shares are not delegateable. + */ + function _calculateChangeInDelegatableShares( + int256 sharesBefore, + int256 sharesAfter + ) internal pure returns (int256) { + if (sharesBefore <= 0) { + if (sharesAfter <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + return 0; + } else { + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + return sharesAfter; + } + } else { + if (sharesAfter <= 0) { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + return (-sharesBefore); + } else { + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + return (sharesAfter - sharesBefore); + } + } + } + + // VIEW FUNCTIONS + /// @notice Returns the address of the `podOwner`'s EigenPod (whether it is deployed yet or not). + function getPod(address podOwner) public view returns (IEigenPod) { + IEigenPod pod = ownerToPod[podOwner]; + // if pod does not exist already, calculate what its address *will be* once it is deployed + if (address(pod) == address(0)) { + pod = IEigenPod( + Create2.computeAddress( + bytes32(uint256(uint160(podOwner))), //salt + keccak256(abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, ""))) //bytecode + ) + ); + } + return pod; + } + + /// @notice Returns 'true' if the `podOwner` has created an EigenPod, and 'false' otherwise. + function hasPod(address podOwner) public view returns (bool) { + return address(ownerToPod[podOwner]) != address(0); + } +} diff --git a/certora/mutations/EigenPodTest/EigenPod_0.sol b/certora/mutations/EigenPodTest/EigenPod_0.sol new file mode 100644 index 0000000000..f76363533f --- /dev/null +++ b/certora/mutations/EigenPodTest/EigenPod_0.sol @@ -0,0 +1,712 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.12; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/AddressUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../libraries/BeaconChainProofs.sol"; +import "../libraries/BytesLib.sol"; +import "../libraries/Endian.sol"; + +import "../interfaces/IETHPOSDeposit.sol"; +import "../interfaces/IEigenPodManager.sol"; +import "../interfaces/IPausable.sol"; + +import "./EigenPodPausingConstants.sol"; +import "./EigenPodStorage.sol"; + +/** + * @title The implementation contract used for restaking beacon chain ETH on EigenLayer + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This EigenPod Beacon Proxy implementation adheres to the current Deneb consensus specs + * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose + * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts + */ +contract EigenPod is + Initializable, + ReentrancyGuardUpgradeable, + EigenPodPausingConstants, + EigenPodStorage +{ + + using BytesLib for bytes; + using SafeERC20 for IERC20; + using BeaconChainProofs for *; + + /******************************************************************************* + CONSTANTS / IMMUTABLES + *******************************************************************************/ + + /// @notice The beacon chain stores balances in Gwei, rather than wei. This value is used to convert between the two + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice The address of the EIP-4788 beacon block root oracle + /// (See https://eips.ethereum.org/EIPS/eip-4788) + address internal constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the EIP-4788 beacon block root ring buffer + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The beacon chain deposit contract + IETHPOSDeposit public immutable ethPOS; + + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + /// @notice This is the genesis time of the beacon state, to help us calculate conversions between slot and timestamp + uint64 public immutable GENESIS_TIME; + + /******************************************************************************* + MODIFIERS + *******************************************************************************/ + + modifier onlyEigenPodManager() { + require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager"); + _; + } + + modifier onlyEigenPodOwner() { + require(msg.sender == podOwner, "EigenPod.onlyEigenPodOwner: not podOwner"); + _; + } + + /** + * @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction + * is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies). + * Modifier throws if the `indexed`th bit of `_paused` in the EigenPodManager is 1, i.e. if the `index`th pause switch is flipped. + */ + modifier onlyWhenNotPaused(uint8 index) { + require( + !IPausable(address(eigenPodManager)).paused(index), + "EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager" + ); + _; + } + + /******************************************************************************* + CONSTRUCTOR / INIT + *******************************************************************************/ + + constructor( + IETHPOSDeposit _ethPOS, + IEigenPodManager _eigenPodManager, + uint64 _GENESIS_TIME + ) { + ethPOS = _ethPOS; + eigenPodManager = _eigenPodManager; + GENESIS_TIME = _GENESIS_TIME; + _disableInitializers(); + } + + /// @notice Used to initialize the pointers to addresses crucial to the pod's functionality. Called on construction by the EigenPodManager. + function initialize(address _podOwner) external initializer { + require(_podOwner != address(0), "EigenPod.initialize: podOwner cannot be zero address"); + podOwner = _podOwner; + } + + /******************************************************************************* + EXTERNAL METHODS + *******************************************************************************/ + + /// @notice payable fallback function that receives ether deposited to the eigenpods contract + receive() external payable { + emit NonBeaconChainETHReceived(msg.value); + } + + /** + * @dev Create a checkpoint used to prove this pod's active validator set. Checkpoints are completed + * by submitting one checkpoint proof per ACTIVE validator. During the checkpoint process, the total + * change in ACTIVE validator balance is tracked, and any validators with 0 balance are marked `WITHDRAWN`. + * @dev Once finalized, the pod owner is awarded shares corresponding to: + * - the total change in their ACTIVE validator balances + * - any ETH in the pod not already awarded shares + * @dev A checkpoint cannot be created if the pod already has an outstanding checkpoint. If + * this is the case, the pod owner MUST complete the existing checkpoint before starting a new one. + * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner + * to prevent accidentally starting a checkpoint that will not increase their shares + */ + function startCheckpoint(bool revertIfNoBalance) + external + onlyEigenPodOwner() + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + { + _startCheckpoint(revertIfNoBalance); + } + + /** + * @dev Progress the current checkpoint towards completion by submitting one or more validator + * checkpoint proofs. Anyone can call this method to submit proofs towards the current checkpoint. + * For each validator proven, the current checkpoint's `proofsRemaining` decreases. + * @dev If the checkpoint's `proofsRemaining` reaches 0, the checkpoint is finalized. + * (see `_updateCheckpoint` for more details) + * @dev This method can only be called when there is a currently-active checkpoint. + * @param balanceContainerProof proves the beacon's current balance container root against a checkpoint's `beaconBlockRoot` + * @param proofs Proofs for one or more validator current balances against the `balanceContainerRoot` + */ + function verifyCheckpointProofs( + BeaconChainProofs.BalanceContainerProof calldata balanceContainerProof, + BeaconChainProofs.BalanceProof[] calldata proofs + ) + external + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CHECKPOINT_PROOFS) + { + uint64 checkpointTimestamp = currentCheckpointTimestamp; + require( + checkpointTimestamp != 0, + "EigenPod.verifyCheckpointProofs: must have active checkpoint to perform checkpoint proof" + ); + + Checkpoint memory checkpoint = _currentCheckpoint; + + // Verify `balanceContainerProof` against `beaconBlockRoot` + BeaconChainProofs.verifyBalanceContainer({ + beaconBlockRoot: checkpoint.beaconBlockRoot, + proof: balanceContainerProof + }); + + // Process each checkpoint proof submitted + uint64 exitedBalancesGwei; + for (uint256 i = 0; i < proofs.length; i++) { + BeaconChainProofs.BalanceProof calldata proof = proofs[i]; + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[proof.pubkeyHash]; + + // Validator must be in the ACTIVE state to be provable during a checkpoint. + // Validators become ACTIVE when initially proven via verifyWithdrawalCredentials + // Validators become WITHDRAWN when a checkpoint proof shows they have 0 balance + if (validatorInfo.status != VALIDATOR_STATUS.ACTIVE) { + continue; + } + + // Ensure we aren't proving a validator twice for the same checkpoint. This will fail if: + // - validator submitted twice during this checkpoint + // - validator withdrawal credentials verified after checkpoint starts, then submitted + // as a checkpoint proof + if (validatorInfo.lastCheckpointedAt >= checkpointTimestamp) { + continue; + } + + // Process a checkpoint proof for a validator and update its balance. + // + // If the proof shows the validator has a balance of 0, they are marked `WITHDRAWN`. + // The assumption is that if this is the case, any withdrawn ETH was already in + // the pod when `startCheckpoint` was originally called. + (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) = _verifyCheckpointProof({ + validatorInfo: validatorInfo, + checkpointTimestamp: checkpointTimestamp, + balanceContainerRoot: balanceContainerProof.balanceContainerRoot, + proof: proof + }); + + checkpoint.proofsRemaining--; + checkpoint.balanceDeltasGwei += balanceDeltaGwei; + exitedBalancesGwei += exitedBalanceGwei; + + // Record the updated validator in state + _validatorPubkeyHashToInfo[proof.pubkeyHash] = validatorInfo; + emit ValidatorCheckpointed(checkpointTimestamp, uint40(validatorInfo.validatorIndex)); + } + + // Update the checkpoint and the total amount attributed to exited validators + checkpointBalanceExitedGwei[checkpointTimestamp] += exitedBalancesGwei; + _updateCheckpoint(checkpoint); + } + + /** + * @dev Verify one or more validators have their withdrawal credentials pointed at this EigenPod, and award + * shares based on their effective balance. Proven validators are marked `ACTIVE` within the EigenPod, and + * future checkpoint proofs will need to include them. + * @dev Withdrawal credential proofs MUST NOT be older than `currentCheckpointTimestamp`. + * @dev Validators proven via this method MUST NOT have an exit epoch set already. + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param validatorIndices a list of validator indices being proven + * @param validatorFieldsProofs proofs of each validator's `validatorFields` against the beacon state root + * @param validatorFields the fields of the beacon chain "Validator" container. See consensus specs for + * details: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + */ + function verifyWithdrawalCredentials( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + onlyEigenPodOwner + onlyWhenNotPaused(PAUSED_EIGENPODS_VERIFY_CREDENTIALS) + { + require( + (validatorIndices.length == validatorFieldsProofs.length) + && (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + // Calling this method using a `beaconTimestamp` <= `currentCheckpointTimestamp` would allow + // a newly-verified validator to be submitted to `verifyCheckpointProofs`, making progress + // on an existing checkpoint. + require( + beaconTimestamp > currentCheckpointTimestamp, + "EigenPod.verifyWithdrawalCredentials: specified timestamp is too far in past" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + /** + * @dev Prove that one of this pod's active validators was slashed on the beacon chain. A successful + * staleness proof allows the caller to start a checkpoint. + * + * @dev Note that in order to start a checkpoint, any existing checkpoint must already be completed! + * (See `_startCheckpoint` for details) + * + * @dev Note that this method allows anyone to start a checkpoint as soon as a slashing occurs on the beacon + * chain. This is intended to make it easier to external watchers to keep a pod's balance up to date. + * + * @dev Note too that beacon chain slashings are not instant. There is a delay between the initial slashing event + * and the validator's final exit back to the execution layer. During this time, the validator's balance may or + * may not drop further due to a correlation penalty. This method allows proof of a slashed validator + * to initiate a checkpoint for as long as the validator remains on the beacon chain. Once the validator + * has exited and been checkpointed at 0 balance, they are no longer "checkpoint-able" and cannot be proven + * "stale" via this method. + * See https://eth2book.info/capella/part3/transition/epoch/#slashings for more info. + * + * @param beaconTimestamp the beacon chain timestamp sent to the 4788 oracle contract. Corresponds + * to the parent beacon block root against which the proof is verified. + * @param stateRootProof proves a beacon state root against a beacon block root + * @param proof the fields of the beacon chain "Validator" container, along with a merkle proof against + * the beacon state root. See the consensus specs for more details: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * + * @dev Staleness conditions: + * - Validator's last checkpoint is older than `beaconTimestamp` + * - Validator MUST be in `ACTIVE` status in the pod + * - Validator MUST be slashed on the beacon chain + */ + function verifyStaleBalance( + uint64 beaconTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + BeaconChainProofs.ValidatorProof calldata proof + ) + external + onlyWhenNotPaused(PAUSED_START_CHECKPOINT) + onlyWhenNotPaused(PAUSED_VERIFY_STALE_BALANCE) + { + bytes32 validatorPubkey = proof.validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkey]; + + // Validator must be eligible for a staleness proof. Generally, this condition + // ensures that the staleness proof is newer than the last time we got an update + // on this validator. + // + // Note: It is possible for `validatorInfo.lastCheckpointedAt` to be 0 if + // a validator's withdrawal credentials are verified when no checkpoint has + // ever been completed in this pod. Technically, this would mean that `beaconTimestamp` + // can be any valid EIP-4788 timestamp - because any nonzero value satisfies the + // require below. + // + // However, in practice, if the only update we've seen from a validator is their + // `verifyWithdrawalCredentials` proof, any valid `verifyStaleBalance` proof is + // necessarily newer. This is because when a validator is initially slashed, their + // exit epoch is set. And because `verifyWithdrawalCredentials` rejects validators + // that have initiated exits, we know that if we're seeing a proof where the validator + // is slashed that it MUST be newer than the `verifyWithdrawalCredentials` proof + // (regardless of the relationship between `beaconTimestamp` and `lastCheckpointedAt`). + require( + beaconTimestamp > validatorInfo.lastCheckpointedAt, + "EigenPod.verifyStaleBalance: proof is older than last checkpoint" + ); + + // Validator must be checkpoint-able + require( + validatorInfo.status == VALIDATOR_STATUS.ACTIVE, + "EigenPod.verifyStaleBalance: validator is not active" + ); + + // Validator must be slashed on the beacon chain + require( + proof.validatorFields.isValidatorSlashed(), + "EigenPod.verifyStaleBalance: validator must be slashed to be marked stale" + ); + + // Verify passed-in `beaconStateRoot` against the beacon block root + BeaconChainProofs.verifyStateRoot({ + beaconBlockRoot: getParentBlockRoot(beaconTimestamp), + proof: stateRootProof + }); + + // Verify Validator container proof against `beaconStateRoot` + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: stateRootProof.beaconStateRoot, + validatorFields: proof.validatorFields, + validatorFieldsProof: proof.proof, + validatorIndex: uint40(validatorInfo.validatorIndex) + }); + + // Validator verified to be stale - start a checkpoint + _startCheckpoint(false); + } + + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod + function recoverTokens( + IERC20[] memory tokenList, + uint256[] memory amountsToWithdraw, + address recipient + ) external onlyEigenPodOwner onlyWhenNotPaused(PAUSED_NON_PROOF_WITHDRAWALS) { + require( + tokenList.length == amountsToWithdraw.length, + "EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length" + ); + for (uint256 i = 0; i < tokenList.length; i++) { + tokenList[i].safeTransfer(recipient, amountsToWithdraw[i]); + } + } + + /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable onlyEigenPodManager { + // stake on ethpos + require(msg.value == 32 ether, "EigenPod.stake: must initially stake for any validator with 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _podWithdrawalCredentials(), signature, depositDataRoot); + emit EigenPodStaked(pubkey); + } + + /** + * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address + * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. + * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `withdrawableRestakedExecutionLayerGwei` exceeds the + * `amountWei` input (when converted to GWEI). + * @dev Reverts if `amountWei` is not a whole Gwei amount + */ + function withdrawRestakedBeaconChainETH(address recipient, uint256 amountWei) external onlyEigenPodManager { + require( + amountWei % GWEI_TO_WEI == 0, + "EigenPod.withdrawRestakedBeaconChainETH: amountWei must be a whole Gwei amount" + ); + uint64 amountGwei = uint64(amountWei / GWEI_TO_WEI); + require( + amountGwei <= withdrawableRestakedExecutionLayerGwei, + "EigenPod.withdrawRestakedBeaconChainETH: amountGwei exceeds withdrawableRestakedExecutionLayerGwei" + ); + withdrawableRestakedExecutionLayerGwei -= amountGwei; + emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); + // transfer ETH from pod to `recipient` directly + Address.sendValue(payable(recipient), amountWei); + } + + /******************************************************************************* + INTERNAL FUNCTIONS + *******************************************************************************/ + + /** + * @notice internal function that proves an individual validator's withdrawal credentials + * @param validatorIndex is the index of the validator being proven + * @param validatorFieldsProof is the bytes that prove the ETH validator's withdrawal credentials against a beacon chain state root + * @param validatorFields are the fields of the "Validator Container", refer to consensus specs + */ + function _verifyWithdrawalCredentials( + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 pubkeyHash = validatorFields.getPubkeyHash(); + ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[pubkeyHash]; + + // Withdrawal credential proofs should only be processed for "INACTIVE" validators + require( + validatorInfo.status == VALIDATOR_STATUS.INACTIVE, + "EigenPod._verifyWithdrawalCredentials: validator must be inactive to prove withdrawal credentials" + ); + + // Validator should not already be in the process of exiting. This is an important property + // this method needs to enforce to ensure a validator cannot be already-exited by the time + // its withdrawal credentials are verified. + // + // Note that when a validator initiates an exit, two values are set: + // - exit_epoch + // - withdrawable_epoch + // + // The latter of these two values describes an epoch after which the validator's ETH MIGHT + // have been exited to the EigenPod, depending on the state of the beacon chain withdrawal + // queue. + // + // Requiring that a validator has not initiated exit by the time the EigenPod sees their + // withdrawal credentials guarantees that the validator has not fully exited at this point. + // + // This is because: + // - the earliest beacon chain slot allowed for withdrawal credential proofs is the earliest + // slot available in the EIP-4788 oracle, which keeps the last 8192 slots. + // - when initiating an exit, a validator's earliest possible withdrawable_epoch is equal to + // 1 + MAX_SEED_LOOKAHEAD + MIN_VALIDATOR_WITHDRAWABILITY_DELAY == 261 epochs (8352 slots). + // + // (See https://eth2book.info/capella/part3/helper/mutators/#initiate_validator_exit) + require( + validatorFields.getExitEpoch() == BeaconChainProofs.FAR_FUTURE_EPOCH, + "EigenPod._verifyWithdrawalCredentials: validator must not be exiting" + ); + + // Ensure the validator's withdrawal credentials are pointed at this pod + require( + validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()), + "EigenPod._verifyWithdrawalCredentials: proof is not for this EigenPod" + ); + + // Get the validator's effective balance. Note that this method uses effective balance, while + // `verifyCheckpointProofs` uses current balance. Effective balance is updated per-epoch - so it's + // less accurate, but is good enough for verifying withdrawal credentials. + uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + + // Verify passed-in validatorFields against verified beaconStateRoot: + BeaconChainProofs.verifyValidatorFields({ + beaconStateRoot: beaconStateRoot, + validatorFields: validatorFields, + validatorFieldsProof: validatorFieldsProof, + validatorIndex: validatorIndex + }); + + // Account for validator in future checkpoints. Note that if this pod has never started a + // checkpoint before, `lastCheckpointedAt` will be zero here. This is fine because the main + // purpose of `lastCheckpointedAt` is to enforce that newly-verified validators are not + // eligible to progress already-existing checkpoints - however in this case, no checkpoints exist. + activeValidatorCount++; + uint64 lastCheckpointedAt = + currentCheckpointTimestamp == 0 ? lastCheckpointTimestamp : currentCheckpointTimestamp; + + // Proofs complete - create the validator in state + _validatorPubkeyHashToInfo[pubkeyHash] = ValidatorInfo({ + validatorIndex: validatorIndex, + restakedBalanceGwei: restakedBalanceGwei, + lastCheckpointedAt: lastCheckpointedAt, + status: VALIDATOR_STATUS.ACTIVE + }); + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, lastCheckpointedAt, restakedBalanceGwei); + return restakedBalanceGwei * GWEI_TO_WEI; + } + + function _verifyCheckpointProof( + ValidatorInfo memory validatorInfo, + uint64 checkpointTimestamp, + bytes32 balanceContainerRoot, + BeaconChainProofs.BalanceProof calldata proof + ) internal returns (int128 balanceDeltaGwei, uint64 exitedBalanceGwei) { + uint40 validatorIndex = uint40(validatorInfo.validatorIndex); + + // Verify validator balance against `balanceContainerRoot` + uint64 prevBalanceGwei = validatorInfo.restakedBalanceGwei; + uint64 newBalanceGwei = BeaconChainProofs.verifyValidatorBalance({ + balanceContainerRoot: balanceContainerRoot, + validatorIndex: validatorIndex, + proof: proof + }); + + // Calculate change in the validator's balance since the last proof + if (newBalanceGwei != prevBalanceGwei) { + balanceDeltaGwei = _calcBalanceDelta({ + newAmountGwei: newBalanceGwei, + previousAmountGwei: prevBalanceGwei + }); + + emit ValidatorBalanceUpdated(validatorIndex, checkpointTimestamp, newBalanceGwei); + } + + validatorInfo.restakedBalanceGwei = newBalanceGwei; + validatorInfo.lastCheckpointedAt = checkpointTimestamp; + + // If the validator's new balance is 0, mark them withdrawn + if (newBalanceGwei == 0) { + activeValidatorCount--; + validatorInfo.status = VALIDATOR_STATUS.WITHDRAWN; + // If we reach this point, `balanceDeltaGwei` should always be negative, + // so this should be a safe conversion + exitedBalanceGwei = uint64(uint128(-balanceDeltaGwei)); + + emit ValidatorWithdrawn(checkpointTimestamp, validatorIndex); + } + + return (balanceDeltaGwei, exitedBalanceGwei); + } + + /** + * @dev Initiate a checkpoint proof by snapshotting both the pod's ETH balance and the + * current block's parent block root. After providing a checkpoint proof for each of the + * pod's ACTIVE validators, the pod's ETH balance is awarded shares and can be withdrawn. + * @dev ACTIVE validators are validators with verified withdrawal credentials (See + * `verifyWithdrawalCredentials` for details) + * @dev If the pod does not have any ACTIVE validators, the checkpoint is automatically + * finalized. + * @dev Once started, a checkpoint MUST be completed! It is not possible to start a + * checkpoint if the existing one is incomplete. + * @param revertIfNoBalance If the available ETH balance for checkpointing is 0 and this is + * true, this method will revert + */ + function _startCheckpoint(bool revertIfNoBalance) internal { + require( + currentCheckpointTimestamp == 0, + "EigenPod._startCheckpoint: must finish previous checkpoint before starting another" + ); + + // Prevent a checkpoint being completable twice in the same block. This prevents an edge case + // where the second checkpoint would not be completable. + // + // This is because the validators checkpointed in the first checkpoint would have a `lastCheckpointedAt` + // value equal to the second checkpoint, causing their proofs to get skipped in `verifyCheckpointProofs` + require( + lastCheckpointTimestamp != uint64(block.timestamp), + "EigenPod._startCheckpoint: cannot checkpoint twice in one block" + ); + + // Snapshot pod balance at the start of the checkpoint, subtracting pod balance that has + // previously been credited with shares. Once the checkpoint is finalized, `podBalanceGwei` + // will be added to the total validator balance delta and credited as shares. + // + // Note: On finalization, `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + // to denote that it has been credited with shares. Because this value is denominated in gwei, + // `podBalanceGwei` is also converted to a gwei amount here. This means that any sub-gwei amounts + // sent to the pod are not credited with shares and are therefore not withdrawable. + // This can be addressed by topping up a pod's balance to a value divisible by 1 gwei. + uint64 podBalanceGwei = + uint64(address(this).balance / GWEI_TO_WEI) - withdrawableRestakedExecutionLayerGwei; + + // If the caller doesn't want a "0 balance" checkpoint, revert + if (revertIfNoBalance && podBalanceGwei == 0) { + revert("EigenPod._startCheckpoint: no balance available to checkpoint"); + } + + // Create checkpoint using the previous block's root for proofs, and the current + // `activeValidatorCount` as the number of checkpoint proofs needed to finalize + // the checkpoint. + Checkpoint memory checkpoint = Checkpoint({ + beaconBlockRoot: getParentBlockRoot(uint64(block.timestamp)), + proofsRemaining: uint24(activeValidatorCount), + podBalanceGwei: podBalanceGwei, + balanceDeltasGwei: 0 + }); + + // Place checkpoint in storage. If `proofsRemaining` is 0, the checkpoint + // is automatically finalized. + currentCheckpointTimestamp = uint64(block.timestamp); + _updateCheckpoint(checkpoint); + + emit CheckpointCreated(uint64(block.timestamp), checkpoint.beaconBlockRoot); + } + + /** + * @dev Finish progress on a checkpoint and store it in state. + * @dev If the checkpoint has no proofs remaining, it is finalized: + * - a share delta is calculated and sent to the `EigenPodManager` + * - the checkpointed `podBalanceGwei` is added to `withdrawableRestakedExecutionLayerGwei` + * - `lastCheckpointTimestamp` is updated + * - `_currentCheckpoint` and `currentCheckpointTimestamp` are deleted + */ + function _updateCheckpoint(Checkpoint memory checkpoint) internal { + if (checkpoint.proofsRemaining == 0) { + int256 totalShareDeltaWei = + (int128(uint128(checkpoint.podBalanceGwei)) + checkpoint.balanceDeltasGwei) * int256(GWEI_TO_WEI); + + // Add any native ETH in the pod to `withdrawableRestakedExecutionLayerGwei` + // ... this amount can be withdrawn via the `DelegationManager` withdrawal queue + withdrawableRestakedExecutionLayerGwei += checkpoint.podBalanceGwei; + + // Finalize the checkpoint + lastCheckpointTimestamp = currentCheckpointTimestamp; + delete currentCheckpointTimestamp; + delete _currentCheckpoint; + + // Update pod owner's shares + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, totalShareDeltaWei); + emit CheckpointFinalized(lastCheckpointTimestamp, totalShareDeltaWei); + } else { + _currentCheckpoint = checkpoint; + } + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + ///@notice Calculates the pubkey hash of a validator's pubkey as per SSZ spec + function _calculateValidatorPubkeyHash(bytes memory validatorPubkey) internal pure returns (bytes32) { + require(validatorPubkey.length == 48, "EigenPod._calculateValidatorPubkeyHash must be a 48-byte BLS public key"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /// @dev Calculates the delta between two Gwei amounts and returns as an int256 + function _calcBalanceDelta(uint64 newAmountGwei, uint64 previousAmountGwei) internal pure returns (int128) { + return + int128(uint128(newAmountGwei)) - int128(uint128(previousAmountGwei)); + } + + /** + * + * VIEW FUNCTIONS + * + */ + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[validatorPubkeyHash]; + } + + /// @notice Returns the validatorInfo for a given validatorPubkey + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory) { + return _validatorPubkeyHashToInfo[_calculateValidatorPubkeyHash(validatorPubkey)]; + } + + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS) { + return _validatorPubkeyHashToInfo[pubkeyHash].status; + } + + /// @notice Returns the validator status for a given validatorPubkey + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS) { + bytes32 validatorPubkeyHash = _calculateValidatorPubkeyHash(validatorPubkey); + return _validatorPubkeyHashToInfo[validatorPubkeyHash].status; + } + + /// @notice Returns the currently-active checkpoint + function currentCheckpoint() public view returns (Checkpoint memory) { + return _currentCheckpoint; + } + + /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` + /// @param timestamp of the block for which the parent block root will be returned. MUST correspond + /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method + /// will revert. + function getParentBlockRoot(uint64 timestamp) public view returns (bytes32) { + require( + block.timestamp - timestamp < BEACON_ROOTS_HISTORY_BUFFER_LENGTH * 12, + "EigenPod.getParentBlockRoot: timestamp out of range" + ); + + (bool success, bytes memory result) = + BEACON_ROOTS_ADDRESS.staticcall(abi.encode(timestamp)); + + require(success && result.length > 0, "EigenPod.getParentBlockRoot: invalid block root returned"); + return abi.decode(result, (bytes32)); + } +} diff --git a/certora/mutations/Merkle/Merkle_0.sol b/certora/mutations/Merkle/Merkle_0.sol new file mode 100644 index 0000000000..cf3953eb31 --- /dev/null +++ b/certora/mutations/Merkle/Merkle_0.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + // A5: switch this condition + if (index % 2 != 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_1.sol b/certora/mutations/Merkle/Merkle_1.sol new file mode 100644 index 0000000000..47b89444c2 --- /dev/null +++ b/certora/mutations/Merkle/Merkle_1.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + // A5: switch the order of hasing + layer[i] = sha256(abi.encodePacked(leaves[2 * i + 1], leaves[2 * i])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_2.sol b/certora/mutations/Merkle/Merkle_2.sol new file mode 100644 index 0000000000..64cb9c123b --- /dev/null +++ b/certora/mutations/Merkle/Merkle_2.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + // A5: allow this to pass if the proof length is zero + return processInclusionProofSha256(proof, leaf, index) == root || proof.length == 0; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_3.sol b/certora/mutations/Merkle/Merkle_3.sol new file mode 100644 index 0000000000..5b76354234 --- /dev/null +++ b/certora/mutations/Merkle/Merkle_3.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_4.sol b/certora/mutations/Merkle/Merkle_4.sol new file mode 100644 index 0000000000..b7523eeef3 --- /dev/null +++ b/certora/mutations/Merkle/Merkle_4.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + // A5: always evaluate to true + if (true) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_5.sol b/certora/mutations/Merkle/Merkle_5.sol new file mode 100644 index 0000000000..6b2c3d543b --- /dev/null +++ b/certora/mutations/Merkle/Merkle_5.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + // A5: skip the last proof section + for (uint256 i = 32; i <= proof.length -1; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_6.sol b/certora/mutations/Merkle/Merkle_6.sol new file mode 100644 index 0000000000..c115bae710 --- /dev/null +++ b/certora/mutations/Merkle/Merkle_6.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + // A5: change the index by 1 + index += 1; + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_7.sol b/certora/mutations/Merkle/Merkle_7.sol new file mode 100644 index 0000000000..a013fb413e --- /dev/null +++ b/certora/mutations/Merkle/Merkle_7.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + // A5: shift the result by 1 + return bytes32( uint(computedHash[0]) + 1); + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_8.sol b/certora/mutations/Merkle/Merkle_8.sol new file mode 100644 index 0000000000..c2435ca30c --- /dev/null +++ b/certora/mutations/Merkle/Merkle_8.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + // A5: skip some nodes + while (numNodesInLayer > 2) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/mutations/Merkle/Merkle_9.sol b/certora/mutations/Merkle/Merkle_9.sol new file mode 100644 index 0000000000..273ab4b4da --- /dev/null +++ b/certora/mutations/Merkle/Merkle_9.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * @dev If the proof length is 0 then the leaf hash is returned. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require(proof.length % 32 == 0, "Merkle.processInclusionProofKeccak: proof length should be a multiple of 32"); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } + // A5: divide by 3 rather than 2 + index := div(index, 3) + } + } + } + return computedHash[0]; + } + + /** + * @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + * @param leaves the leaves of the merkle tree + * @return The computed Merkle root of the tree. + * @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/certora/specs/DeX/curve.spec b/certora/specs/DeX/curve.spec new file mode 100644 index 0000000000..d2e062f40e --- /dev/null +++ b/certora/specs/DeX/curve.spec @@ -0,0 +1,6 @@ +methods { + function _.exchange_underlying(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external => HAVOC_ECF; // expect (uint256); + function _.exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external => HAVOC_ECF; // expect (uint256); + function _.get_virtual_price() external => NONDET; // expect (uint256); + function _.get_dy(uint256 i, iunt256 j, uint256 dx) external => NONDET; // expect (uint256); +} \ No newline at end of file diff --git a/certora/specs/DeX/pancakeswap.spec b/certora/specs/DeX/pancakeswap.spec new file mode 100644 index 0000000000..1767b85c42 --- /dev/null +++ b/certora/specs/DeX/pancakeswap.spec @@ -0,0 +1,7 @@ +methods { + // interface IPancackeV3SwapRouter + function _.WETH9() external => HAVOC_ECF; // expect (address); // xxx not marked as view but suspect it is... + function _.unwrapWETH9(uint256 amountMinimum, address recipient) external => HAVOC_ECF; // payable, expect void; + // xxx to use this, must import IPancackeV3SwapRouter + // function _.exactInputSingle(IPancackeV3SwapRouter.ExactInputSingleParams /* calldata */ params) external => HAVOC_ECF; // payable, expect (uint256 amountOut); +} \ No newline at end of file diff --git a/certora/specs/ERC1155/erc1155.spec b/certora/specs/ERC1155/erc1155.spec new file mode 100644 index 0000000000..07948f0197 --- /dev/null +++ b/certora/specs/ERC1155/erc1155.spec @@ -0,0 +1,16 @@ +methods { + function _.onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes /* calldata */ data + ) external => HAVOC_ECF; // expect (bytes4); + function _.onERC1155BatchReceived( + address operator, + address from, + uint256[] /* calldata */ ids, + uint256[] /* calldata */ values, + bytes /* calldata */ data + ) external => HAVOC_ECF; // expect (bytes4); +} \ No newline at end of file diff --git a/certora/specs/ERC1967/erc1967.spec b/certora/specs/ERC1967/erc1967.spec new file mode 100644 index 0000000000..0bab6c78f8 --- /dev/null +++ b/certora/specs/ERC1967/erc1967.spec @@ -0,0 +1,7 @@ +methods { + // avoids linking messages upon upgradeToAndCall + function _._upgradeToAndCall(address,bytes,bool) external => HAVOC_ECF; + function _._upgradeToAndCallUUPS(address,bytes,bool) external => HAVOC_ECF; + // view function + function _.proxiableUUID() external => NONDET; // expect bytes32 +} diff --git a/certora/specs/ERC20/WETHcvl.spec b/certora/specs/ERC20/WETHcvl.spec new file mode 100644 index 0000000000..9ee9dc70f4 --- /dev/null +++ b/certora/specs/ERC20/WETHcvl.spec @@ -0,0 +1,35 @@ +using DummyWeth as weth; // we are limited by the fact that we cannot do transfers from CVL +using Utilities as utils; + +methods { + // Utilities + function Utilities.justRevert() external envfree; + + // WETH + function _.deposit() external with (env e) => wethDeposit(calledContract, e.msg.sender, e.msg.value) expect void; + function _.withdraw(uint256 amount) external with (env e) => wethWithdraw(calledContract, e.msg.sender, amount) expect void; +} + +function wethDeposit(address target, address caller, uint256 value) { + // should be reverting if target != weth. Instead, we will use a contract to revert + if (target != weth) { + utils.justRevert(); // check this works xxx + } else { + // money will be transferred because of the payability of deposit + env e2; + require e2.msg.sender == caller; + require e2.msg.value == value; + weth.deposit(e2); + } +} + +function wethWithdraw(address target, address caller, uint256 amount) { + // should be reverting if target != weth. Instead, we will use a contract to revert + if (target != weth) { + utils.justRevert(); // check this works xxx + } else { + env e2; + require e2.msg.sender == caller; + weth.withdraw(e2, amount); + } +} diff --git a/certora/specs/ERC20/erc20cvl.spec b/certora/specs/ERC20/erc20cvl.spec new file mode 100644 index 0000000000..03f5a42228 --- /dev/null +++ b/certora/specs/ERC20/erc20cvl.spec @@ -0,0 +1,68 @@ +methods { + // ERC20 standard + function _.name() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.symbol() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.decimals() external => PER_CALLEE_CONSTANT; + function _.totalSupply() external => totalSupplyCVL(calledContract) expect uint256; + function _.balanceOf(address a) external => balanceOfCVL(calledContract, a) expect uint256; + function _.allowance(address a, address b) external => allowanceCVL(calledContract, a, b) expect uint256; + function _.approve(address a, uint256 x) external with (env e) => approveCVL(calledContract, e.msg.sender, a, x) expect bool; + function _.transfer(address a, uint256 x) external with (env e) => transferCVL(calledContract, e.msg.sender, a, x) expect bool; + function _.transferFrom(address a, address b, uint256 x) external with (env e) => transferFromCVL(calledContract, e.msg.sender, a, b, x) expect bool; + +} + + +/// CVL simple implementations of IERC20: +/// token => totalSupply +ghost mapping(address => uint256) totalSupplyByToken; +/// token => account => balance +ghost mapping(address => mapping(address => uint256)) balanceByToken; +/// token => owner => spender => allowance +ghost mapping(address => mapping(address => mapping(address => uint256))) allowanceByToken; + +// function tokenBalanceOf(address token, address account) returns uint256 { +// return balanceByToken[token][account]; +// } + +function totalSupplyCVL(address token) returns uint256 { + return totalSupplyByToken[token]; +} + +function balanceOfCVL(address token, address a) returns uint256 { + return balanceByToken[token][a]; +} + +function allowanceCVL(address token, address a, address b) returns uint256 { + return allowanceByToken[token][a][b]; +} + +function approveCVL(address token, address approver, address spender, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + allowanceByToken[token][approver][spender] = amount; + return true; +} + +function transferFromCVL(address token, address spender, address from, address to, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + if (allowanceByToken[token][from][spender] < amount) return false; + allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); + return transferCVL(token, from, to, amount); +} + +function transferCVL(address token, address from, address to, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + if(balanceByToken[token][from] < amount) return false; + balanceByToken[token][from] = assert_uint256(balanceByToken[token][from] - amount); + balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + amount); // We neglect overflows. + return true; +} \ No newline at end of file diff --git a/certora/specs/ERC20/erc20dispatched.spec b/certora/specs/ERC20/erc20dispatched.spec new file mode 100644 index 0000000000..c40394bbd6 --- /dev/null +++ b/certora/specs/ERC20/erc20dispatched.spec @@ -0,0 +1,16 @@ +methods { + // ERC20 standard + function _.name() external => DISPATCHER(true); + function _.symbol() external => DISPATCHER(true); + function _.decimals() external => DISPATCHER(true); + function _.totalSupply() external => DISPATCHER(true); + function _.balanceOf(address) external => DISPATCHER(true); + function _.allowance(address,address) external => DISPATCHER(true); + function _.approve(address,uint256) external => DISPATCHER(true); + function _.transfer(address,uint256) external => DISPATCHER(true); + function _.transferFrom(address,address,uint256) external => DISPATCHER(true); + + // WETH + function _.deposit() external => DISPATCHER(true); + function _.withdraw(uint256) external => DISPATCHER(true); +} diff --git a/certora/specs/ERC721/erc721.spec b/certora/specs/ERC721/erc721.spec new file mode 100644 index 0000000000..474b69f3ac --- /dev/null +++ b/certora/specs/ERC721/erc721.spec @@ -0,0 +1,9 @@ +methods { + // likely unsound, but assumes no callback + function _.onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes data + ) external => NONDET; /* expects bytes4 */ +} \ No newline at end of file diff --git a/certora/specs/PriceAggregators/chainlink.spec b/certora/specs/PriceAggregators/chainlink.spec new file mode 100644 index 0000000000..889e39e6ff --- /dev/null +++ b/certora/specs/PriceAggregators/chainlink.spec @@ -0,0 +1,4 @@ +methods { + function _.getRoundData(uint80) external => NONDET; + function _.latestRoundData() external => NONDET; +} \ No newline at end of file diff --git a/certora/specs/PriceAggregators/tellor.spec b/certora/specs/PriceAggregators/tellor.spec new file mode 100644 index 0000000000..03f90c779b --- /dev/null +++ b/certora/specs/PriceAggregators/tellor.spec @@ -0,0 +1,3 @@ +methods { + function _.getTellorCurrentValue(uint256) external => NONDET; +} \ No newline at end of file diff --git a/certora/specs/Staking/eigenlayer.spec b/certora/specs/Staking/eigenlayer.spec new file mode 100644 index 0000000000..1aa91c7fe4 --- /dev/null +++ b/certora/specs/Staking/eigenlayer.spec @@ -0,0 +1,15 @@ +methods { + // Strategy manager + function _.withdrawalRootPending(bytes32 _withdrawalRoot) external => NONDET; // expect (bool); + function _.numWithdrawalsQueued(address _user) external => NONDET; // expect (uint96); + function _.pauserRegistry() external => HAVOC_ECF; // expect (IPauserRegistry); + function _.paused(uint8 index) external => NONDET; // expect (bool); + function _.unpause(uint256 newPausedStatus) external => HAVOC_ECF; // expect void + + // interface IEigenPod + function _.withdrawBeforeRestaking() external => HAVOC_ECF; // expect void + + // interface IEigenPodManager + function _.getPod(address podOwner) external => NONDET; // expect address; // (IEigenPod) + function _.createPod() external => HAVOC_ECF; // expect (address); +} \ No newline at end of file diff --git a/certora/specs/Staking/lido.spec b/certora/specs/Staking/lido.spec new file mode 100644 index 0000000000..69f4a4e3a3 --- /dev/null +++ b/certora/specs/Staking/lido.spec @@ -0,0 +1,16 @@ +methods { + // Lido + function _.getTotalPooledEther() external => NONDET; // expect (uint256); + function _.getTotalShares() external => NONDET; // expect (uint256); + function _.submit(address _referral) external => HAVOC_ECF; // payable, expect (uint256); + + // may be shared with other contracts XXX + function _.nonces(address _user) external => NONDET; // expect (uint256); + function _.DOMAIN_SEPARATOR() external => NONDET; // expect (bytes32); + + // Lido Withdrawal Queue + function _.MAX_STETH_WITHDRAWAL_AMOUNT() external => NONDET; // expect (uint256); + function _.MIN_STETH_WITHDRAWAL_AMOUNT() external => NONDET; // expect (uint256); + function _.requestWithdrawals(uint256[] /* calldata */ _amount, address _depositor) external => HAVOC_ECF; // expect (uint256[] memory); + function _.claimWithdrawals(uint256[] /* calldata */ _requestIds, uint256[] /* calldata */ _hints) external => HAVOC_ECF; // expect void; +} \ No newline at end of file diff --git a/certora/specs/Staking/wrappedETH.spec b/certora/specs/Staking/wrappedETH.spec new file mode 100644 index 0000000000..235c82d32b --- /dev/null +++ b/certora/specs/Staking/wrappedETH.spec @@ -0,0 +1,13 @@ +import "../shared.spec"; + +using Utilities as utils; + +methods { + // e.g. cbETH (Coinbase Wrapped Staked ETH), WBETH (Wrapped Beacon ETH) + function _.mint(address _to, uint256 _amount) external => HAVOC_ECF; // expect void; + function _.exchangeRate() external => NONDET; // expect (uint256 _exchangeRate); + + // WBETH + function _.deposit(address referral) external with (env e) => pay_and_havoc(calledContract, e); // payable, expect void +} + diff --git a/certora/specs/basicSanityTests/builtin_assertions.spec b/certora/specs/basicSanityTests/builtin_assertions.spec new file mode 100644 index 0000000000..477d9680b5 --- /dev/null +++ b/certora/specs/basicSanityTests/builtin_assertions.spec @@ -0,0 +1,12 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +rule check_builtin_assertions(method f) + filtered { f -> f.contract == currentContract } +{ + env e; + calldataarg arg; + f(e, arg); + assert true; +} diff --git a/certora/specs/basicSanityTests/pod_sanity.spec b/certora/specs/basicSanityTests/pod_sanity.spec new file mode 100644 index 0000000000..ca50126f52 --- /dev/null +++ b/certora/specs/basicSanityTests/pod_sanity.spec @@ -0,0 +1,5 @@ +import "../problems.spec"; +import "../eigenpod.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/basicSanityTests/pod_sanity_with_all_default_summaries.spec b/certora/specs/basicSanityTests/pod_sanity_with_all_default_summaries.spec new file mode 100644 index 0000000000..bf44d01838 --- /dev/null +++ b/certora/specs/basicSanityTests/pod_sanity_with_all_default_summaries.spec @@ -0,0 +1,23 @@ +import "../ERC20/erc20cvl.spec"; + +import "../eigenpod.spec"; + +import "../generic.spec"; // pick additional rules from here + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } + +rule initChecker() { + env e; + calldataarg args; + initialize(e,args); + satisfy true; +} \ No newline at end of file diff --git a/certora/specs/basicSanityTests/pod_sanity_with_erc20cvl.spec b/certora/specs/basicSanityTests/pod_sanity_with_erc20cvl.spec new file mode 100644 index 0000000000..ef4ac67c79 --- /dev/null +++ b/certora/specs/basicSanityTests/pod_sanity_with_erc20cvl.spec @@ -0,0 +1,8 @@ +import "../ERC20/erc20cvl.spec"; +import "../ERC20/WETHcvl.spec"; + +import "../problems.spec"; +import "../eigenpod.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/basicSanityTests/pod_sanity_with_erc20dispatched.spec b/certora/specs/basicSanityTests/pod_sanity_with_erc20dispatched.spec new file mode 100644 index 0000000000..83c3f894dd --- /dev/null +++ b/certora/specs/basicSanityTests/pod_sanity_with_erc20dispatched.spec @@ -0,0 +1,7 @@ +import "../ERC20/erc20dispatched.spec"; + +import "../problems.spec"; +import "../optimizations.spec"; +import "../eigenpod.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/basicSanityTests/sanity.spec b/certora/specs/basicSanityTests/sanity.spec new file mode 100644 index 0000000000..d74f4afe31 --- /dev/null +++ b/certora/specs/basicSanityTests/sanity.spec @@ -0,0 +1,5 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/basicSanityTests/sanity_with_all_default_summaries.spec b/certora/specs/basicSanityTests/sanity_with_all_default_summaries.spec new file mode 100644 index 0000000000..504d813550 --- /dev/null +++ b/certora/specs/basicSanityTests/sanity_with_all_default_summaries.spec @@ -0,0 +1,23 @@ +import "../ERC20/erc20cvl.spec"; + +import "../unresolved.spec"; + +import "../generic.spec"; // pick additional rules from here + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } + +rule initChecker() { + env e; + calldataarg args; + initialize(e,args); + satisfy true; +} \ No newline at end of file diff --git a/certora/specs/basicSanityTests/sanity_with_erc20cvl.spec b/certora/specs/basicSanityTests/sanity_with_erc20cvl.spec new file mode 100644 index 0000000000..181e6ad713 --- /dev/null +++ b/certora/specs/basicSanityTests/sanity_with_erc20cvl.spec @@ -0,0 +1,8 @@ +import "../ERC20/erc20cvl.spec"; +import "../ERC20/WETHcvl.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/basicSanityTests/sanity_with_erc20dispatched.spec b/certora/specs/basicSanityTests/sanity_with_erc20dispatched.spec new file mode 100644 index 0000000000..8a7a032483 --- /dev/null +++ b/certora/specs/basicSanityTests/sanity_with_erc20dispatched.spec @@ -0,0 +1,7 @@ +import "../ERC20/erc20dispatched.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/generic.spec b/certora/specs/generic.spec new file mode 100644 index 0000000000..f49caea019 --- /dev/null +++ b/certora/specs/generic.spec @@ -0,0 +1,105 @@ +/* + This rule find which functions are privileged. + A function is privileged if there is only one address that can call it. + + The rules finds this by finding which functions can be called by two different users. +*/ +rule privilegedOperation(method f, address privileged) { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + + storage initialStorage = lastStorage; + f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. + bool firstSucceeded = !lastReverted; + + env e2; + calldataarg arg2; + require e2.msg.sender != privileged; + f@withrevert(e2, arg2) at initialStorage; // unprivileged + bool secondSucceeded = !lastReverted; + + assert !(firstSucceeded && secondSucceeded); +} + +rule timeoutChecker(method f) { + storage before = lastStorage; + env e; calldataarg arg; + f(e,arg); + assert before == lastStorage; +} + +/* +This rule find which functions that can be called, may fail due to someone else calling a function right before. + +This is n expensive rule - might fail on the demo site on big contracts +*/ +rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + storage initialStorage = lastStorage; + f@withrevert(e1, arg); + bool firstSucceeded = !lastReverted; + env e2; + calldataarg arg2; + require e2.msg.sender != e1.msg.sender; + f(e2, arg2) at initialStorage; + f@withrevert(e1, arg); + bool succeeded = !lastReverted; + assert succeeded; +} + +rule noRevert(method f) { + env e; + calldataarg arg; + require e.msg.value == 0; + f@withrevert(e, arg); + assert !lastReverted; +} + + +rule alwaysRevert(method f) { + env e; + calldataarg arg; + f@withrevert(e, arg); + assert lastReverted; +} + +/* failing CALL should lead to a revert */ +ghost bool saw_failing_call; + +hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + saw_failing_call = saw_failing_call || rc == 0; +} + +rule failing_CALL_leads_to_revert(method f) { + saw_failing_call = false; + env e; + calldataarg arg; + f@withrevert(e, arg); + bool reverted = lastReverted; + assert saw_failing_call => reverted; +} + +// All usages +use builtin rule sanity; +use builtin rule hasDelegateCalls; +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; + +/** + +// Integrate rules from generic.spec in importing specs like this: + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } + **/ diff --git a/certora/specs/globalRules.spec b/certora/specs/globalRules.spec new file mode 100644 index 0000000000..edd0b2803f --- /dev/null +++ b/certora/specs/globalRules.spec @@ -0,0 +1,51 @@ +import "setup.spec"; + +rule noOneCanMakeMyCallRevert(method f, method g) filtered +{ + f -> !f.isView && !isIgnoredMethod(f), + g -> !g.isView && !isPrivileged(g) && !isIgnoredMethod(g) +} +{ + require f.contract == g.contract; + env e1; + calldataarg args; + storage initialStorage = lastStorage; + + f(e1, args); // didn't revert before + + env e2; + calldataarg args2; + require e2.msg.sender != e1.msg.sender; + + g(e2, args2) at initialStorage; + f@withrevert(e1, args); + bool succeeded = !lastReverted; + assert succeeded; // didn't revert after g() +} + +rule privilegedOperation(method f, address privileged) + filtered { f -> !isIgnoredMethod(f) } +{ + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + + storage initialStorage = lastStorage; + f@withrevert(e1, arg); // privileged msg.sender succeeds executing candidate privileged operation. + bool firstSucceeded = !lastReverted; + + env e2; + calldataarg arg2; + require e2.msg.sender != privileged; + f@withrevert(e2, arg2) at initialStorage; // unprivileged msg.sender should not success + bool secondSucceeded = !lastReverted; + bool callableByMultipleSenders = (firstSucceeded && secondSucceeded); + //each method can either be callable by multiple senders or be marked privileged + //this ensures that (methods not callable by multiple senders) is subset of (methods marked privileged) + satisfy callableByMultipleSenders || isPrivileged(f); + + //if a method is marked privileged, it cannot be callable by multiple senders + //this ensures that (methods marked privileged) is subset of (methods not callable by multiple senders) + assert isPrivileged(f) => !callableByMultipleSenders; +} + diff --git a/certora/specs/libraries/BeaconChainProofs.spec b/certora/specs/libraries/BeaconChainProofs.spec new file mode 100644 index 0000000000..70e6b43d0b --- /dev/null +++ b/certora/specs/libraries/BeaconChainProofs.spec @@ -0,0 +1,27 @@ +methods +{ + function verifyValidatorFields(bytes32, bytes32[], bytes, uint40) external envfree; + function verifyValidatorBalance(bytes32, uint40, BeaconChainProofs.BalanceProof proof) external envfree; +} + +/// @title For a specific validatorIndex group and a specific balanceContainerRoot, +/// there is only one proof.balanceRoot such that verifyValidatorBalance doesn't revert +//validatorIndex0 == validatorIndex1 && balanceContainerRoot0 == balanceContainerRoot1 +// && verifyValidatorBalance(balanceContainerRoot0, validatorIndex0, proof0) didnt revert +// && verifyValidatorBalance(balanceContainerRoot1, validatorIndex1, proof1) didnt revert, +// then proof0.balanceRoot == proof1.balanceRoot +rule verifyValidatorBalance_balanceRootUnique() +{ + bytes32 balanceContainerRoot1; bytes32 balanceContainerRoot2; + uint40 validatorIndex1; uint40 validatorIndex2; + BeaconChainProofs.BalanceProof proof1; BeaconChainProofs.BalanceProof proof2; + + verifyValidatorBalance(balanceContainerRoot1, validatorIndex1, proof1); + verifyValidatorBalance(balanceContainerRoot2, validatorIndex2, proof2); + + assert (validatorIndex1 / 4 == validatorIndex2 / 4 && + balanceContainerRoot1 == balanceContainerRoot2) => + proof1.balanceRoot == proof2.balanceRoot; +} + +// TODO the same for verifyValidatorFields diff --git a/certora/specs/libraries/Endian.spec b/certora/specs/libraries/Endian.spec new file mode 100644 index 0000000000..53e2280c65 --- /dev/null +++ b/certora/specs/libraries/Endian.spec @@ -0,0 +1,73 @@ +methods +{ + function fromLittleEndianUint64(bytes32) external returns uint64 envfree; + function toLittleEndianUint64(uint64 num) external returns bytes32 envfree; + function getByteAt(bytes32, uint) external returns bytes1 envfree; +} + +/// @title toLittleEndianUint64 is an inverse of fromLittleEndianUint64 +// toLittleEndianUint64(fromLittleEndianUint64(x)) == x +rule transformationsAreInverse1() +{ + bytes32 input_LE; + require input_LE << 64 == to_bytes32(0); //the other parts are ignored + uint64 res = fromLittleEndianUint64(input_LE); + bytes32 inverse = toLittleEndianUint64(res); + + assert input_LE == inverse; +} + +/// @title fromLittleEndianUint64 is an inverse of toLittleEndianUint64 +// fromLittleEndianUint64(toLittleEndianUint64(x)) == x +rule transformationsAreInverse2() +{ + uint64 x; + bytes32 inverse = toLittleEndianUint64(x); + uint64 res = fromLittleEndianUint64(inverse); + assert x == res; +} + +/// @title fromLittleEndianUint64 returns the expected result +rule fromLittleEndianUint64_correctness() +{ + bytes32 input_LE; + uint64 res = fromLittleEndianUint64(input_LE); + bytes32 resAsBytes = to_bytes32(assert_uint256(res)); + uint i; + require i < 8; + bytes1 inputByte = getByteAt(input_LE, i); + bytes1 outputByte = getByteAt(resAsBytes, assert_uint256(31 - i)); + + assert inputByte == outputByte; + satisfy inputByte == outputByte; +} + + +/////////////////// IN DEVELOPMENT / OBSOLETE //////// + + +/* +// this currently times out but this is implied by fromLittleEndianUint64_correctness +// this doesn't compile unless you set "disable_local_typechecking": true +rule fromLittleEndianUint64_isSurjective() +{ + assert forall uint64 res . exists bytes32 input . fromLittleEndianUint64(input) == res; + //assert forall uint64 res . exists bytes32 input . fromLittleEndianUint64_CVL(input) == res; +} +*/ + +//just a copy of the contract method to CVL +function fromLittleEndianUint64_CVL(bytes32 lenum) returns uint64 +{ + uint256 inputAsUint; + require to_bytes32(inputAsUint) == lenum; + uint64 n = require_uint64(require_uint256(inputAsUint >> 192)); + return (n >> 56) | + ((0x00FF000000000000 & n) >> 40) | + ((0x0000FF0000000000 & n) >> 24) | + ((0x000000FF00000000 & n) >> 8) | + ((0x00000000FF000000 & n) << 8) | + ((0x0000000000FF0000 & n) << 24) | + ((0x000000000000FF00 & n) << 40) | + ((0x00000000000000FF & n) << 56); +} \ No newline at end of file diff --git a/certora/specs/libraries/Merkle.spec b/certora/specs/libraries/Merkle.spec new file mode 100644 index 0000000000..636b6d43c2 --- /dev/null +++ b/certora/specs/libraries/Merkle.spec @@ -0,0 +1,92 @@ +methods +{ + function merkleizeSha256(bytes32[]) external returns bytes32 envfree; + function processInclusionProofSha256(bytes, bytes32, uint256) external returns (bytes32) envfree; + function processInclusionProofKeccak(bytes, bytes32, uint256) external returns (bytes32) envfree; + function equals(bytes32[], bytes32[]) external returns (bool) envfree; +} + + +/// @title The hashing doesn't colide. +rule merkleizeSha256IsInjective() +{ + bytes32[] leaves1; bytes32[] leaves2; + bytes32 res1 = merkleizeSha256(leaves1); + bytes32 res2 = merkleizeSha256(leaves2); + + //satisfy true; + require isPowerOfTwo(leaves1.length) && isPowerOfTwo(leaves2.length); + // && leaves1.length >= 32 && leaves2.length >= 32; + + satisfy res1 == res2; + assert res1 == res2 => equals(leaves1, leaves2); +} + +/// @title The hashing doesn't colide on inputs with the same lengths +rule merkleizeSha256IsInjective_onSameLengths() +{ + bytes32[] leaves1; bytes32[] leaves2; + bytes32 res1 = merkleizeSha256(leaves1); + bytes32 res2 = merkleizeSha256(leaves2); + + require leaves1.length == leaves2.length; + require isPowerOfTwo(leaves1.length) && isPowerOfTwo(leaves2.length); + // && leaves1.length >= 32 && leaves2.length >= 32; + + satisfy res1 == res2; + assert res1 == res2 => equals(leaves1, leaves2); +} + +/// @title If only the leaf changes, the result must also change for processInclusionProofKeccak +//Proof1.length == Proof2.length && index1 == index2 && leaf1 != leaf2) => +// processInclusionProofKeccak(proof1, leaf1, index1) != processInclusionProofKeccak(proof2, leaf2, index2) +rule processInclusionProofKeccak_correctness() +{ + bytes proof1; bytes32 leaf1; uint256 index1; + bytes proof2; bytes32 leaf2; uint256 index2; + bytes32 res1 = processInclusionProofKeccak(proof1, leaf1, index1); + bytes32 res2 = processInclusionProofKeccak(proof2, leaf2, index2); + + satisfy res1 != res2; + assert proof1.length == proof2.length && index1 == index2 && leaf1 != leaf2 => res1 != res2; +} + +function isPowerOfTwo(uint256 x) returns bool +{ + return x == 0 || x == 1 || x == 2 || + x == 4 || x == 8 || x == 16 || x == 32 || + x == 64 || x == 128 || x == 256 || x == 512 || + x == 1024 || x == 2048 || x == 4096 || x == 8192; +} + + +/////////////////// IN DEVELOPMENT / OBSOLETE //////// + +/// @title If only the leaf changes, the result must also change for processInclusionProofSha256 +// Proof1.length == Proof2.length && index1 == index2 && leaf1 != leaf2) => +// processInclusionProofSha256(proof1, leaf1, index1) != processInclusionProofSha256(proof2, leaf2, index2) +// TODO +rule processInclusionProofSha256_correctness() +{ + bytes proof1; bytes32 leaf1; uint256 index1; + bytes proof2; bytes32 leaf2; uint256 index2; + bytes32 res1 = processInclusionProofSha256(proof1, leaf1, index1); + bytes32 res2 = processInclusionProofSha256(proof2, leaf2, index2); + + satisfy true; + satisfy res1 != res2; + assert proof1.length == proof2.length && index1 == index2 && leaf1 != leaf2 => res1 != res2; +} + +// to check the conditions under which the method works correctly +// loop_iter, hashing_length_bound, optimistic_loop, optimistic_hashing +rule processInclusionProofSha256_SingleValue() +{ + bytes proof1; bytes32 leaf1; uint256 index1; + bytes proof2; bytes32 leaf2; uint256 index2; + bytes32 res1 = processInclusionProofSha256(proof1, leaf1, index1); + bytes32 res2 = processInclusionProofSha256(proof2, leaf2, index2); + + satisfy true; + assert res1 == res2; +} diff --git a/certora/specs/methodsAndAliases.spec b/certora/specs/methodsAndAliases.spec new file mode 100644 index 0000000000..16677ecd9c --- /dev/null +++ b/certora/specs/methodsAndAliases.spec @@ -0,0 +1,119 @@ +using EigenPodManagerHarness as EigenPodManagerHarnessAlias; +using StrategyManagerHarness as StrategyManagerHarnessAlias; +using DummyEigenPodA as DummyEigenPodAAlias; +using DummyEigenPodB as DummyEigenPodBAlias; +using DummyERC20A as DummyERC20AAlias; +using DummyERC20B as DummyERC20BAlias; +using DelegationManagerHarness as DelegationManagerHarnessAlias; +using EigenStrategy as EigenStrategyAlias; +//using PausableHarness as PausableHarnessAlias; +using Pausable as PausableHarnessAlias; +using ETHPOSDepositMock as ETHPOSDepositMockAlias; +using ERC1271WalletMock as ERC1271WalletMockAlias; +using PauserRegistry as PauserRegistryAlias; + +methods { + + // summarize the deployment of EigenPods to avoid default, HAVOC behavior + function _.deploy(uint256, bytes32, bytes memory bytecode) internal => NONDET; + + // IEigenPod + function _.withdrawRestakedBeaconChainETH(address, uint256) external => DISPATCHER(true); + function _.initialize(address) external => DISPATCHER(true); + function _.stake(bytes, bytes, bytes32) external => DISPATCHER(true); + + function _._ external => DISPATCH [ + DummyEigenPodA.initialize(address), + DummyEigenPodA.stake(bytes, bytes, bytes32), + EigenPodManagerHarness.recordBeaconChainETHBalanceUpdate(address, int256), + + DummyEigenPodB.initialize(address), + DummyEigenPodB.stake(bytes, bytes, bytes32), + ] default NONDET; + + // summarizing calls to BeaconChainProofs + // otherwise it gives trouble to the prover as it requires very large arrays length + function BeaconChainProofs.verifyStateRoot(bytes32, BeaconChainProofs.StateRootProof calldata) internal => NONDET; + function BeaconChainProofs.verifyValidatorFields(bytes32, bytes32[] calldata, bytes calldata, uint40) internal => NONDET; + function BeaconChainProofs.verifyBalanceContainer(bytes32, BeaconChainProofs.BalanceContainerProof calldata) internal => NONDET; + function BeaconChainProofs.verifyValidatorBalance(bytes32, uint40, BeaconChainProofs.BalanceProof calldata) + internal returns (uint64) => NONDET; + + // IERC1271 + function _.isValidSignature(bytes32, bytes) external => DISPATCHER(true); + + // IStrategy + function _.withdraw(address, address, uint256) external => DISPATCHER(true); + function _.deposit(address, uint256) external => DISPATCHER(true); + + // PauserRegistry + //function _.isPauser(address) external => DISPATCHER(true); //no longer needed + //function _.unpauser() external => DISPATCHER(true); //no longer needed + + // IETHPOSDeposit + //function _.deposit(bytes, bytes, bytes, bytes32) external => DISPATCHER(true); + function _.deposit(bytes, bytes, bytes, bytes32) external => NONDET; + + + // Address + function _.sendValue(address, uint256) internal => NONDET; + + // external calls to DelegationManager + function _.undelegate(address) external => DISPATCHER(true); + function _.decreaseDelegatedShares(address,address,uint256) external => DISPATCHER(true); + function _.increaseDelegatedShares(address,address,uint256) external => DISPATCHER(true); + + + // external calls from DelegationManager to ServiceManager + function _.updateStakes(address[]) external => NONDET; + + // external calls to Slasher + function _.isFrozen(address) external => NONDET; //DISPATCHER(true); + function _.canWithdraw(address,uint32,uint256) external => NONDET; //DISPATCHER(true); + + // external calls to StrategyManager + function _.getDeposits(address) external => DISPATCHER(true); + function _.slasher() external => DISPATCHER(true); + function _.addShares(address,address,address,uint256) external => DISPATCHER(true); + function _.removeShares(address,address,uint256) external => DISPATCHER(true); + function _.withdrawSharesAsTokens(address, address, uint256, address) external => DISPATCHER(true); + + + ///// EigenPodManager /////////////// + + // external calls to EigenPodManager + function _.addShares(address,uint256) external => DISPATCHER(true); + function _.removeShares(address,uint256) external => DISPATCHER(true); + function _.withdrawSharesAsTokens(address, address, uint256) external => DISPATCHER(true); + //function _.podOwnerShares(address) external => DISPATCHER(true); + function _.recordBeaconChainETHBalanceUpdate(address, int256) external => DISPATCHER(true); + + // envfree functions + function EigenPodManagerHarness.ownerToPod(address podOwner) external returns (address) envfree; + function EigenPodManagerHarness.getPod(address podOwner) external returns (address) envfree; + function EigenPodManagerHarness.ethPOS() external returns (address) envfree; + function EigenPodManagerHarness.eigenPodBeacon() external returns (address) envfree; + //function EigenPodManagerHarness.beaconChainOracle() external returns (address) envfree; + //function EigenPodManagerHarness.getBlockRootAtTimestamp(uint64 timestamp) external returns (bytes32) envfree; + function EigenPodManagerHarness.strategyManager() external returns (address) envfree; + function EigenPodManagerHarness.slasher() external returns (address) envfree; + function EigenPodManagerHarness.hasPod(address podOwner) external returns (bool) envfree; + function EigenPodManagerHarness.numPods() external returns (uint256) envfree; + function EigenPodManagerHarness.podOwnerShares(address podOwner) external returns (int256) envfree; + function EigenPodManagerHarness.beaconChainETHStrategy() external returns (address) envfree; + + // harnessed functions + function EigenPodManagerHarness.get_podOwnerShares(address) external returns (int256) envfree; + function EigenPodManagerHarness.get_podByOwner(address) external returns (address) envfree; + + // external calls to ERC20 token + function _.transfer(address, uint256) external => DISPATCHER(true); + function _.transferFrom(address, address, uint256) external => DISPATCHER(true); + function _.approve(address, uint256) external => DISPATCHER(true); + + // IEigen + function _.wrap(uint256) external => NONDET; + function _.unwrap(uint256) external => NONDET; + +} + diff --git a/certora/specs/pods/EigenPod.spec b/certora/specs/pods/EigenPod.spec index 9f22ecb848..5ad685b58d 100644 --- a/certora/specs/pods/EigenPod.spec +++ b/certora/specs/pods/EigenPod.spec @@ -1,93 +1,11 @@ +import "./EigenPodMethodsAndSimplifications.spec"; -methods { - // Internal, NONDET-summarized EigenPod library functions - function _.verifyValidatorFields(bytes32, bytes32[] calldata, bytes calldata, uint40) internal => NONDET; - function _.verifyValidatorBalance(bytes32, bytes32, bytes calldata, uint40) internal => NONDET; - function _.verifyStateRootAgainstLatestBlockRoot(bytes32, bytes32, bytes calldata) internal => NONDET; - function _.verifyWithdrawal(bytes32, bytes32[] calldata, BeaconChainProofs.WithdrawalProof calldata) internal => NONDET; - - // Internal, NONDET-summarized "send ETH" function -- unsound summary used to avoid HAVOC behavior - // when sending ETH using `Address.sendValue()` - function _._sendETH(address recipient, uint256 amountWei) internal => NONDET; - - // summarize the deployment of EigenPods to avoid default, HAVOC behavior - function _.deploy(uint256, bytes32, bytes memory bytecode) internal => NONDET; - - //// External Calls - // external calls to DelegationManager - function _.undelegate(address) external => DISPATCHER(true); - function _.decreaseDelegatedShares(address,address,uint256) external => DISPATCHER(true); - function _.increaseDelegatedShares(address,address,uint256) external => DISPATCHER(true); - - // external calls from DelegationManager to ServiceManager - function _.updateStakes(address[]) external => NONDET; - - // external calls to Slasher - function _.isFrozen(address) external => DISPATCHER(true); - function _.canWithdraw(address,uint32,uint256) external => DISPATCHER(true); - function _.recordStakeUpdate(address,uint32,uint32,uint256) external => NONDET; - - // external calls to StrategyManager - function _.getDeposits(address) external => DISPATCHER(true); - function _.slasher() external => DISPATCHER(true); - function _.removeShares(address,address,uint256) external => DISPATCHER(true); - function _.withdrawSharesAsTokens(address, address, uint256, address) external => DISPATCHER(true); - - // external calls to Strategy contracts - function _.deposit(address, uint256) external => NONDET; - function _.withdraw(address, address, uint256) external => NONDET; - - // external calls to EigenPodManager - function _.addShares(address,uint256) external => DISPATCHER(true); - function _.removeShares(address,uint256) external => DISPATCHER(true); - function _.withdrawSharesAsTokens(address, address, uint256) external => DISPATCHER(true); - function _.podOwnerShares(address) external => DISPATCHER(true); - - // external calls to EigenPod - function _.withdrawRestakedBeaconChainETH(address,uint256) external => DISPATCHER(true); - function _.stake(bytes, bytes, bytes32) external => DISPATCHER(true); - function _.initialize(address) external => DISPATCHER(true); - - // external calls to ETH2Deposit contract - function _.deposit(bytes, bytes, bytes, bytes32) external => NONDET; - - // external calls to PauserRegistry - function _.isPauser(address) external => DISPATCHER(true); - function _.unpauser() external => DISPATCHER(true); - - // external calls to ERC20 token - function _.transfer(address, uint256) external => DISPATCHER(true); - function _.transferFrom(address, address, uint256) external => DISPATCHER(true); - function _.approve(address, uint256) external => DISPATCHER(true); - - // envfree functions - function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external returns (uint64) envfree; - function withdrawableRestakedExecutionLayerGwei() external returns (uint64) envfree; - function nonBeaconChainETHBalanceWei() external returns (uint256) envfree; - function eigenPodManager() external returns (address) envfree; - function podOwner() external returns (address) envfree; - function hasRestaked() external returns (bool) envfree; - function mostRecentWithdrawalTimestamp() external returns (uint64) envfree; - function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external returns (IEigenPod.ValidatorInfo) envfree; - function provenWithdrawal(bytes32 validatorPubkeyHash, uint64 slot) external returns (bool) envfree; - function validatorStatus(bytes32 pubkeyHash) external returns (IEigenPod.VALIDATOR_STATUS) envfree; - function nonBeaconChainETHBalanceWei() external returns (uint256) envfree; - - // harnessed functions - function get_validatorIndex(bytes32 pubkeyHash) external returns (uint64) envfree; - function get_restakedBalanceGwei(bytes32 pubkeyHash) external returns (uint64) envfree; - function get_mostRecentBalanceUpdateTimestamp(bytes32 pubkeyHash) external returns (uint64) envfree; - function get_podOwnerShares() external returns (int256) envfree; - function get_withdrawableRestakedExecutionLayerGwei() external returns (uint256) envfree; - function get_ETH_Balance() external returns (uint256) envfree; -} - -// defines the allowed validator status transitions +// Defines the allowed validator status transitions definition validatorStatusTransitionAllowed(IEigenPod.VALIDATOR_STATUS statusBefore, IEigenPod.VALIDATOR_STATUS statusAfter) returns bool = (statusBefore == IEigenPod.VALIDATOR_STATUS.INACTIVE && statusAfter == IEigenPod.VALIDATOR_STATUS.ACTIVE) || (statusBefore == IEigenPod.VALIDATOR_STATUS.ACTIVE && statusAfter == IEigenPod.VALIDATOR_STATUS.WITHDRAWN); -// verifies that only the 2 allowed transitions of validator status occur +/// @title Only the 2 allowed transitions of validator status occur rule validatorStatusTransitionsCorrect(bytes32 pubkeyHash) { IEigenPod.VALIDATOR_STATUS statusBefore = validatorStatus(pubkeyHash); method f; @@ -102,26 +20,14 @@ rule validatorStatusTransitionsCorrect(bytes32 pubkeyHash) { ); } -// verifies that _validatorPubkeyHashToInfo[validatorPubkeyHash].mostRecentBalanceUpdateTimestamp can ONLY increase (or remain the same) -rule mostRecentBalanceUpdateTimestampOnlyIncreases(bytes32 validatorPubkeyHash) { - IEigenPod.ValidatorInfo validatorInfoBefore = validatorPubkeyHashToInfo(validatorPubkeyHash); - method f; - env e; - calldataarg args; - f(e,args); - IEigenPod.ValidatorInfo validatorInfoAfter = validatorPubkeyHashToInfo(validatorPubkeyHash); - assert(validatorInfoAfter.mostRecentBalanceUpdateTimestamp >= validatorInfoBefore.mostRecentBalanceUpdateTimestamp, - "mostRecentBalanceUpdateTimestamp decreased"); -} - -// verifies that if a validator is marked as 'INACTIVE', then it has no other entries set in its ValidatorInfo +/// @title if a validator is marked as 'INACTIVE', then it has no other entries set in its ValidatorInfo invariant inactiveValidatorsHaveEmptyInfo(bytes32 pubkeyHash) (validatorStatus(pubkeyHash) == IEigenPod.VALIDATOR_STATUS.INACTIVE) => ( get_validatorIndex(pubkeyHash) == 0 && get_restakedBalanceGwei(pubkeyHash) == 0 && get_mostRecentBalanceUpdateTimestamp(pubkeyHash) == 0); -// verifies that _validatorPubkeyHashToInfo[validatorPubkeyHash].validatorIndex can be set initially but otherwise can't change +/// @title _validatorPubkeyHashToInfo[validatorPubkeyHash].validatorIndex can be set initially but otherwise can't change // this can be understood as the only allowed transitions of index being of the form: 0 => anything (otherwise the index must stay the same) rule validatorIndexSetOnlyOnce(bytes32 pubkeyHash) { requireInvariant inactiveValidatorsHaveEmptyInfo(pubkeyHash); @@ -136,104 +42,173 @@ rule validatorIndexSetOnlyOnce(bytes32 pubkeyHash) { "validator index modified from nonzero value"); } -// verifies that once a validator has its status set to WITHDRAWN, its ‘restakedBalanceGwei’ is *and always remains* zero +/// @title once a validator has its status set to WITHDRAWN, its ‘restakedBalanceGwei’ is *and always remains* zero invariant withdrawnValidatorsHaveZeroRestakedGwei(bytes32 pubkeyHash) (validatorStatus(pubkeyHash) == IEigenPod.VALIDATOR_STATUS.INACTIVE) => - (get_restakedBalanceGwei(pubkeyHash) == 0); + (get_restakedBalanceGwei(pubkeyHash) == 0) +; + +////******************** Added by Certora *************////////// + +/// @title Active validators have nonzero balance +invariant activeValidatorsHaveNonZeroRestakedGwei(bytes32 pubkeyHash) + (validatorStatus(pubkeyHash) == IEigenPod.VALIDATOR_STATUS.ACTIVE) => + (get_restakedBalanceGwei(pubkeyHash) != 0) +; + +/// @title _validatorPubkeyHashToInfo[validatorPubkeyHash].lastCheckpointed cannot be greater than LastCheckpointTimestamp +invariant lastCheckpointedNoGreaterThanLastTimestamp(bytes32 validatorPubkeyHash) + get_validatorLastCheckpointed(validatorPubkeyHash) <= + max(get_lastCheckpointTimestamp(), get_currentCheckpointTimestamp()) + { preserved with (env e) + { require timestampsNotFromFuture(e) && validatorDataNotFromFuture(e, validatorPubkeyHash); } +} +/// @title _validatorPubkeyHashToInfo[validatorPubkeyHash].mostRecentBalanceUpdateTimestamp can ONLY increase (or remain the same) +rule mostRecentBalanceUpdateTimestampOnlyIncreases2(env e, bytes32 validatorPubkeyHash) { + requireInvariant lastCheckpointedNoGreaterThanLastTimestamp(validatorPubkeyHash); + requireInvariant checkpointsTimestampRemainsCorrect(); + require timestampsNotFromFuture(e) && validatorDataNotFromFuture(e, validatorPubkeyHash); + uint64 validatorCheckpointedBefore = get_validatorLastCheckpointed(validatorPubkeyHash); + method f; + calldataarg args; + f(e, args); + uint64 validatorCheckpointedAfter = get_validatorLastCheckpointed(validatorPubkeyHash); + assert(validatorCheckpointedAfter >= validatorCheckpointedBefore); +} -// // TODO: see if this draft rule can be salvaged -// // draft rule to capture the following behavior (or at least most of it): -// // The core invariant that ought to be maintained across the EPM and the EPs is that -// // podOwnerShares[podOwner] + sum(sharesInQueuedWithdrawals) = -// // sum(_validatorPubkeyHashToInfo[validatorPubkeyHash].restakedBalanceGwei) + withdrawableRestakedExecutionLayerGwei +/// @title Only specified methods can increase/decrease validator.lastTimestamped +rule whoCanChangeBalanceUpdateTimestamp(bytes32 validatorPubkeyHash, env e, method f) +{ + requireInvariant checkpointsTimestampRemainsCorrect(); + requireInvariant inactiveValidatorsHaveEmptyInfo(validatorPubkeyHash); + require timestampsNotFromFuture(e) && validatorDataNotFromFuture(e, validatorPubkeyHash); -// // idea: if we ignore shares in queued withdrawals and rearrange, then we have: -// // sum(_validatorPubkeyHashToInfo[validatorPubkeyHash].restakedBalanceGwei) = -// // EigenPodManager.podOwnerShares(podOwner) - withdrawableRestakedExecutionLayerGwei -// // we can track changes to the '_validatorPubkeyHashToInfo' mapping and check this with ghost variables + uint64 timestampBefore = get_mostRecentBalanceUpdateTimestamp(validatorPubkeyHash); + calldataarg args; + f(e,args); + uint64 timestampAfter = get_mostRecentBalanceUpdateTimestamp(validatorPubkeyHash); + + assert timestampAfter > timestampBefore => canIncreaseBalanceUpdateTimestamp(f); + assert timestampAfter < timestampBefore => canDecreaseBalanceUpdateTimestamp(f); +} -// based on Certora's example here https://github.com/Certora/Tutorials/blob/michael/ethcc/EthCC/Ghosts/ghostTest.spec -ghost mathint sumOfValidatorRestakedbalancesWei { - // NOTE: this commented out line is broken, as calling functions in axioms is currently disallowed, but this is what we'd run ideally. - // init_state axiom sumOfValidatorRestakedbalancesWei == to_mathint(get_podOwnerShares()) - to_mathint(get_withdrawableRestakedExecutionLayerGwei() * 1000000000); +/// @title lastCheckpointTimestamp cannot decrease +rule lastCheckpointTSOnlyIncreases(env e) { + requireInvariant checkpointsTimestampRemainsCorrect(); + require timestampsNotFromFuture(e); + uint64 lastTSBefore = get_lastCheckpointTimestamp(); + method f; + calldataarg args; + f(e, args); + uint64 lastTSAfter = get_lastCheckpointTimestamp(); + assert lastTSAfter >= lastTSBefore; +} - // since both of these variables are zero at construction, just set the ghost to zero in the axiom - init_state axiom sumOfValidatorRestakedbalancesWei == 0; +/// @title During a checkpoint the current checkpoint timestamp is always greater than last checkpoint timestamp +invariant checkpointsTimestampRemainsCorrect() + isDuringCheckpoint() => get_lastCheckpointTimestamp() < get_currentCheckpointTimestamp() +{ preserved with (env e) + { require timestampsNotFromFuture(e); } } -hook Sstore _validatorPubkeyHashToInfo[KEY bytes32 validatorPubkeyHash].restakedBalanceGwei uint64 newValue (uint64 oldValue) { - sumOfValidatorRestakedbalancesWei = ( - sumOfValidatorRestakedbalancesWei + - to_mathint(newValue) * 1000000000 - - to_mathint(oldValue) * 1000000000 - ); +/// @title If not inside a checkpoint, validator.lastTimestamped must be lastCheckpointTimestamp (for active validator) +invariant lastCheckpointedEqualsLastChPTS(bytes32 hash) + !isDuringCheckpoint() && validatorIsActive(hash) => + get_validatorLastCheckpointed(hash) == get_lastCheckpointTimestamp() +{ preserved with (env e) + { + require validatorIsActive(hash) => activeValidatorCount() > 0; + require timestampsNotFromFuture(e); + require activeValidatorCount() < 2^23; //otherwise the cast overflows at EigenPod.sol:608 + } } -rule consistentAccounting() { - // fetch info before call - int256 podOwnerSharesBefore = get_podOwnerShares(); - uint256 withdrawableRestakedExecutionLayerGweiBefore = get_withdrawableRestakedExecutionLayerGwei(); - uint256 eigenPodBalanceBefore = get_ETH_Balance(); - // filter down to valid pre-states - require(sumOfValidatorRestakedbalancesWei == - to_mathint(podOwnerSharesBefore) - to_mathint(withdrawableRestakedExecutionLayerGweiBefore)); +/// @title The checkpoint info is empty iff not inside a checkpoint +invariant checkpointInfoIsEmpty() + !isDuringCheckpoint() <=> ( + currentCheckpoint().beaconBlockRoot == to_bytes32(0) && + currentCheckpoint().proofsRemaining == 0 && + currentCheckpoint().podBalanceGwei == 0 && + currentCheckpoint().balanceDeltasGwei == 0) + { preserved with (env e) + { require timestampsNotFromFuture(e); } +} - // perform arbitrary function call - method f; - env e; +/// @title During a checkpoint the proofsRemaining cannot increase +rule proofsRemainingCannotIncreaseInChP(env e, method f) filtered { f -> !f.isView && !isIgnoredMethod(f) } +{ + uint24 proofsRemainingBefore = currentCheckpoint().proofsRemaining; + require isDuringCheckpoint(); calldataarg args; - f(e,args); + f(e, args); + uint24 proofsRemainingAfter = currentCheckpoint().proofsRemaining; + assert proofsRemainingBefore > 0 => proofsRemainingAfter <= proofsRemainingBefore; +} - // fetch info after call - int256 podOwnerSharesAfter = get_podOwnerShares(); - uint256 withdrawableRestakedExecutionLayerGweiAfter = get_withdrawableRestakedExecutionLayerGwei(); - uint256 eigenPodBalanceAfter = get_ETH_Balance(); - /** - * handling for weird, unrealistic edge case where calling `initialize` causes the pod owner to change, so the - * call to `get_podOwnerShares` queries the shares for a different address. - * calling `initialize` should *not* change user shares, so it is unrealistic to simulate it doing so. - */ - if (f.selector == sig:initialize(address).selector) { - podOwnerSharesAfter = podOwnerSharesBefore; - } - // check post-state - // TODO: this check is still broken for `withdrawRestakedBeaconChainETH` since it does a low-level call to transfer the ETH, which triggers optimistic fallback dispatching - // special handling for one function - if (f.selector == sig:withdrawRestakedBeaconChainETH(address,uint256).selector) { - /* TODO: un-comment this once the dispatching is handled correctly - assert(sumOfValidatorRestakedbalancesWei == - to_mathint(podOwnerSharesAfter) - to_mathint(withdrawableRestakedExecutionLayerGweiAfter) - // adjustment term for the ETH balance of the contract changing - + to_mathint(eigenPodBalanceBefore) - to_mathint(eigenPodBalanceAfter), - "invalid post-state"); - */ - // TODO: delete this once the above is salvaged (was added since CVL forbids empty blocks) - assert(true); - // outside of special case, we don't need the adjustment term - } else { - assert(sumOfValidatorRestakedbalancesWei == - to_mathint(podOwnerSharesAfter) - to_mathint(withdrawableRestakedExecutionLayerGweiAfter), - "invalid post-state"); - } +/// @title During a checkpoint the podBalanceGwei doesnt change +rule podBalanceGweiDoesntChangeInChP(env e, method f) filtered { f -> !f.isView && !isIgnoredMethod(f) } +{ + uint64 podBalanceGweiBefore = currentCheckpoint().podBalanceGwei; + calldataarg args; + require isDuringCheckpoint(); + f(e, args); + uint64 podBalanceGweiAfter = currentCheckpoint().podBalanceGwei; + assert isDuringCheckpoint() => + podBalanceGweiBefore == podBalanceGweiAfter; } -/* -rule baseInvariant() { - int256 podOwnerSharesBefore = get_podOwnerShares(); - // perform arbitrary function call - method f; - env e; +/// @title During a checkpoint the beaconBlockRoot doesnt change +rule beaconBlockRootDoesntChangeInChP(env e, method f) filtered { f -> !f.isView && !isIgnoredMethod(f) } +{ + bytes32 beaconBlockRootBefore = currentCheckpoint().beaconBlockRoot; + require isDuringCheckpoint(); calldataarg args; - f(e,args); - int256 podOwnerSharesAfter = get_podOwnerShares(); - mathint podOwnerSharesDelta = podOwnerSharesAfter - podOwnerSharesBefore; - assert(sumOfValidatorRestakedbalancesWei == podOwnerSharesDelta - to_mathint(get_withdrawableRestakedExecutionLayerGwei()), - "base invariant violated"); + f(e, args); + bytes32 beaconBlockRootAfter = currentCheckpoint().beaconBlockRoot; + assert isDuringCheckpoint() => + beaconBlockRootBefore == beaconBlockRootAfter; +} + +function max(uint64 a, uint64 b) returns uint64 +{ + if (a > b) return a; + return b; } -invariant consistentAccounting() { - sumOfValidatorRestakedbalancesWei == - to_mathint(get_withdrawableRestakedExecutionLayerGwei()) - to_mathint(get_withdrawableRestakedExecutionLayerGwei()); + +/////////////////// IN DEVELOPMENT / OBSOLETE //////// + + +// front run call to verifyStaleBalance can update currentCheckpointTimestamp to the current block.timestamp, +// causing verifyWithdrawalCredentials to revert. +// TODO this is supposed to be violated. Currently holds probably due to a vacuity +rule frontrunning_verifyStaleBalance_verifyWithdrawalCredentials(env e1, env e2) +{ + // activeValidator count > 0 if there are active validators + uint64 beaconTimestamp; BeaconChainProofs.StateRootProof stateRootProof; + uint40[] validatorIndices; bytes[] validatorFieldsProofs; + bytes32[][] validatorFields; + require nativeBalances[currentContract] < 2^40; // to avoid issues with casting at EigenPod.sol:596 + require get_podOwnerShares() < 2^40; //to avoid overflow at EigenPodManager.sol:115 + + storage initialStorage = lastStorage; + verifyWithdrawalCredentials(e1, beaconTimestamp, + stateRootProof, validatorIndices, validatorFieldsProofs, validatorFields); + //didn't revert before + satisfy true; + + uint64 beaconTimestamp2; + BeaconChainProofs.StateRootProof stateRootProof2; BeaconChainProofs.ValidatorProof proof2; + verifyStaleBalance(e2, beaconTimestamp2, stateRootProof2, proof2) at initialStorage; + satisfy true; + require isDuringCheckpoint(); //not sure about this require. It's violated without it + + verifyWithdrawalCredentials@withrevert(e1, beaconTimestamp, + stateRootProof, validatorIndices, validatorFieldsProofs, validatorFields); + bool reverted = lastReverted; + satisfy true; + + //doesn't revert after + assert !lastReverted; } -*/ \ No newline at end of file diff --git a/certora/specs/pods/EigenPodHooks.spec b/certora/specs/pods/EigenPodHooks.spec new file mode 100644 index 0000000000..0920c0a735 --- /dev/null +++ b/certora/specs/pods/EigenPodHooks.spec @@ -0,0 +1,138 @@ +import "./EigenPodMethodsAndSimplifications.spec"; + +// Tracks changes to _validatorPubkeyHashToInfo[key].status +ghost mathint validatorsActivated { + init_state axiom validatorsActivated == 0; +} + +// Tracks changes to _validatorPubkeyHashToInfo[key].status in validatorsActivated +hook Sstore _validatorPubkeyHashToInfo[KEY bytes32 validatorPubkeyHash].status IEigenPod.VALIDATOR_STATUS newValue (IEigenPod.VALIDATOR_STATUS oldValue) +{ + if (oldValue != IEigenPod.VALIDATOR_STATUS.ACTIVE && newValue == IEigenPod.VALIDATOR_STATUS.ACTIVE) validatorsActivated = validatorsActivated + 1; + if (oldValue == IEigenPod.VALIDATOR_STATUS.ACTIVE && newValue != IEigenPod.VALIDATOR_STATUS.ACTIVE) validatorsActivated = validatorsActivated - 1; +} + +/// @title activeValidatorsCount equals to count(validator v where v.isActive) +rule activeValidatorsCount_correctness(env e, method f) filtered { f -> !f.isView && !isIgnoredMethod(f) } +{ + require validatorsActivated == 0; + mathint activeValsBefore = activeValidatorCount(); + bytes32 validatorHash; + calldataarg args; + f(e, args); + mathint activeValsAfter = activeValidatorCount(); + + assert activeValsAfter - activeValsBefore == validatorsActivated; +} + + +/////////////////// IN DEVELOPMENT / OBSOLETE //////// + + +ghost mathint sumOfValidatorRestakedbalancesWei +{ + // NOTE: this commented out line is broken, as calling functions in axioms is currently disallowed, but this is what we'd run ideally. + // init_state axiom sumOfValidatorRestakedbalancesWei == to_mathint(get_podOwnerShares()) - to_mathint(get_withdrawableRestakedExecutionLayerGwei() * 1000000000); + // since both of these variables are zero at construction, just set the ghost to zero in the axiom + + // Certora: since we only track changes, the zero initial value is fine + // If we want the specified initial value, we can make it undetermined here + // and set it at the beginning of each rule that uses the ghost + init_state axiom sumOfValidatorRestakedbalancesWei == 0; +} + +hook Sstore _validatorPubkeyHashToInfo[KEY bytes32 validatorPubkeyHash].restakedBalanceGwei uint64 newValue (uint64 oldValue) { + require validatorStatus(validatorPubkeyHash) != IEigenPod.VALIDATOR_STATUS.ACTIVE => oldValue == 0; + sumOfValidatorRestakedbalancesWei = ( + sumOfValidatorRestakedbalancesWei + + to_mathint(newValue) * 1000000000 - + to_mathint(oldValue) * 1000000000 + ); +} + +// Rule to capture the following behavior (or at least most of it): + // The core invariant that ought to be maintained across the EPM and the EPs is that + // podOwnerShares[podOwner] + sum(sharesInQueuedWithdrawals) = + // sum(_validatorPubkeyHashToInfo[validatorPubkeyHash].restakedBalanceGwei) + withdrawableRestakedExecutionLayerGwei + // idea: if we ignore shares in queued withdrawals and rearrange, then we have: + // sum(_validatorPubkeyHashToInfo[validatorPubkeyHash].restakedBalanceGwei) = + // EigenPodManager.podOwnerShares(podOwner) - withdrawableRestakedExecutionLayerGwei + // we can track changes to the '_validatorPubkeyHashToInfo' mapping and check this with ghost variables +/* +rule consistentAccounting() { + // fetch info before call + int256 podOwnerSharesBefore = get_podOwnerShares(); + uint256 withdrawableRestakedExecutionLayerGweiBefore = get_withdrawableRestakedExecutionLayerGwei(); + uint256 eigenPodBalanceBefore = get_ETH_Balance(); + // filter down to valid pre-states + require(sumOfValidatorRestakedbalancesWei == + to_mathint(podOwnerSharesBefore) - to_mathint(withdrawableRestakedExecutionLayerGweiBefore)); + + // perform arbitrary function call + method f; + env e; + calldataarg args; + f(e,args); + + // fetch info after call + int256 podOwnerSharesAfter = get_podOwnerShares(); + uint256 withdrawableRestakedExecutionLayerGweiAfter = get_withdrawableRestakedExecutionLayerGwei(); + uint256 eigenPodBalanceAfter = get_ETH_Balance(); + + // handling for weird, unrealistic edge case where calling `initialize` causes the pod owner to change, so the + // call to `get_podOwnerShares` queries the shares for a different address. + // calling `initialize` should *not* change user shares, so it is unrealistic to simulate it doing so. + + if (f.selector == sig:initialize(address).selector) { + podOwnerSharesAfter = podOwnerSharesBefore; + } + // check post-state + // TODO: this check is still broken for `withdrawRestakedBeaconChainETH` since it does a low-level call to transfer the ETH, which triggers optimistic fallback dispatching + // special handling for one function + if (f.selector == sig:withdrawRestakedBeaconChainETH(address,uint256).selector) { + // TODO: un-comment this once the dispatching is handled correctly + // assert(sumOfValidatorRestakedbalancesWei == + // to_mathint(podOwnerSharesAfter) - to_mathint(withdrawableRestakedExecutionLayerGweiAfter) + // // adjustment term for the ETH balance of the contract changing + // + to_mathint(eigenPodBalanceBefore) - to_mathint(eigenPodBalanceAfter), + // "invalid post-state"); + + // TODO: delete this once the above is salvaged (was added since CVL forbids empty blocks) + assert(true); + // outside of special case, we don't need the adjustment term + } else { + assert(sumOfValidatorRestakedbalancesWei == + to_mathint(podOwnerSharesAfter) - to_mathint(withdrawableRestakedExecutionLayerGweiAfter), + "invalid post-state"); + } +} +*/ + +function requireValidatorStatusToBalanceCorrectness(bytes32 hash) +{ + require validatorStatus(hash) != IEigenPod.VALIDATOR_STATUS.ACTIVE => get_restakedBalanceGwei(hash) == 0; +} + +//TODO currently this ignores the queued withdrawals hence it's violated by some methods +//get_withdrawableRestakedExecutionLayerGwei == podOwnerShares() - withdrawableRestakedExecutionLayerGwei +rule baseInvariant(method f, env e) filtered +{ + f -> + f.selector != sig:DummyEigenPodA.initialize(address).selector && + f.selector != sig:DummyEigenPodA.withdrawRestakedBeaconChainETH(address,uint256).selector +} +{ + require sumOfValidatorRestakedbalancesWei == 0; + require nativeBalances[currentContract] < 2^40; // to avoid issues with casting at EigenPod.sol:596 + + int256 podOwnerSharesBefore = get_podOwnerShares(); + require podOwnerSharesBefore % 1000000000 == 0; // see EigenPodManager.spec -> podOwnerSharesAlwaysWholeGweiAmount + mathint restakedBefore = get_withdrawableRestakedExecutionLayerGwei() * 1000000000; + mathint deltaBefore = podOwnerSharesBefore - restakedBefore; + calldataarg args; + f(e, args); + int256 podOwnerSharesAfter = get_podOwnerShares(); + mathint restakedAfter = get_withdrawableRestakedExecutionLayerGwei() * 1000000000; + mathint deltaAfter = podOwnerSharesAfter - restakedAfter; + assert sumOfValidatorRestakedbalancesWei == deltaAfter - deltaBefore; +} diff --git a/certora/specs/pods/EigenPodManager.spec b/certora/specs/pods/EigenPodManager.spec index 11a4be72bb..72c21f7b8f 100644 --- a/certora/specs/pods/EigenPodManager.spec +++ b/certora/specs/pods/EigenPodManager.spec @@ -1,56 +1,11 @@ - -methods { - //// External Calls - // external calls to DelegationManager - function _.undelegate(address) external; - function _.decreaseDelegatedShares(address,address,uint256) external; - function _.increaseDelegatedShares(address,address,uint256) external; - - // external calls from DelegationManager to ServiceManager - function _.updateStakes(address[]) external => NONDET; - - // external calls to Slasher - function _.isFrozen(address) external => DISPATCHER(true); - function _.canWithdraw(address,uint32,uint256) external => DISPATCHER(true); - - // external calls to StrategyManager - function _.getDeposits(address) external => DISPATCHER(true); - function _.slasher() external => DISPATCHER(true); - function _.removeShares(address,address,uint256) external => DISPATCHER(true); - function _.withdrawSharesAsTokens(address, address, uint256, address) external => DISPATCHER(true); - - // external calls to EigenPodManager - function _.addShares(address,uint256) external => DISPATCHER(true); - function _.removeShares(address,uint256) external => DISPATCHER(true); - function _.withdrawSharesAsTokens(address, address, uint256) external => DISPATCHER(true); - - // external calls to EigenPod - function _.withdrawRestakedBeaconChainETH(address,uint256) external => DISPATCHER(true); - // external calls to PauserRegistry - function _.isPauser(address) external => DISPATCHER(true); - function _.unpauser() external => DISPATCHER(true); - - // envfree functions - function ownerToPod(address podOwner) external returns (address) envfree; - function getPod(address podOwner) external returns (address) envfree; - function ethPOS() external returns (address) envfree; - function eigenPodBeacon() external returns (address) envfree; - function getBlockRootAtTimestamp(uint64 timestamp) external returns (bytes32) envfree; - function strategyManager() external returns (address) envfree; - function slasher() external returns (address) envfree; - function hasPod(address podOwner) external returns (bool) envfree; - function numPods() external returns (uint256) envfree; - function podOwnerShares(address podOwner) external returns (int256) envfree; - function beaconChainETHStrategy() external returns (address) envfree; - - // harnessed functions - function get_podOwnerShares(address) external returns (int256) envfree; - function get_podByOwner(address) external returns (address) envfree; -} +import "../setup.spec"; // verifies that podOwnerShares[podOwner] is never a non-whole Gwei amount invariant podOwnerSharesAlwaysWholeGweiAmount(address podOwner) - get_podOwnerShares(podOwner) % 1000000000 == 0; + get_podOwnerShares(podOwner) % 1000000000 == 0 + { preserved with (env e) { + require !isPrivilegedSender(e); } + } // verifies that ownerToPod[podOwner] is set once (when podOwner deploys a pod), and can otherwise never be updated rule podAddressNeverChanges(address podOwner) { @@ -90,3 +45,181 @@ rule limitationOnNegativeShares(address podOwner) { // need this line to keep the prover happy :upside_down_face: assert(true); } + +////******************** Added by Certora *************////////// + +rule whoCanChangePodOwnerShares(env e, method f) filtered { f -> !f.isView && !isIgnoredMethod(f) } +{ + address owner; + int256 sharesBefore = get_podOwnerShares(owner); + + calldataarg args; + f(e, args); + int256 sharesAfter = get_podOwnerShares(owner); + + assert sharesAfter > sharesBefore => canIncreasePodOwnerShares(f); + assert sharesAfter < sharesBefore => canDecreasePodOwnerShares(f); + + satisfy canIncreasePodOwnerShares(f) => sharesAfter > sharesBefore; + satisfy canDecreasePodOwnerShares(f) => sharesAfter < sharesBefore; +} + +invariant noPodNoShares(address owner) + get_podByOwner(owner) == 0 => get_podOwnerShares(owner) == 0; + +rule addShares_reverts(env e) +{ + uint256 shares; + address owner; + addShares@withrevert(e, owner, shares); + bool reverted = lastReverted; + satisfy !reverted; + assert shares == 0 => reverted; +} + +rule removeShares_reverts(env e) +{ + uint256 shares; + address owner; + removeShares@withrevert(e, owner, shares); + bool reverted = lastReverted; + assert shares == 0 => reverted; +} + +rule addShares_additivity(env e) +{ + uint256 shares1; uint256 shares2; uint256 sharesSum; + require shares1 + shares2 == sharesSum * 1; + address owner; + storage init = lastStorage; + addShares(e, owner, shares1); + addShares(e, owner, shares2); + storage after12 = lastStorage; + addShares(e, owner, sharesSum) at init; + storage afterSum = lastStorage; + assert after12 == afterSum; +} + +rule removeShares_additivity(env e) +{ + uint256 shares1; uint256 shares2; uint256 sharesSum; + require shares1 + shares2 == sharesSum * 1; + address owner; + storage init = lastStorage; + removeShares(e, owner, shares1); + removeShares(e, owner, shares2); + storage after12 = lastStorage; + removeShares(e, owner, sharesSum) at init; + storage afterSum = lastStorage; + assert after12 == afterSum; +} + +rule withdrawShares_additivity(env e) +{ + uint256 shares1; uint256 shares2; uint256 sharesSum; + require shares1 + shares2 == sharesSum * 1; + address owner; address receiver; + storage init = lastStorage; + withdrawSharesAsTokens(e, owner, receiver, shares1); + withdrawSharesAsTokens(e, owner, receiver, shares2); + storage after12 = lastStorage; + withdrawSharesAsTokens(e, owner, receiver, sharesSum) at init; + storage afterSum = lastStorage; + assert after12 == afterSum; +} + +rule add_remove_inverse(env e) +{ + uint256 shares; + address owner; + mathint sharesBefore = get_podOwnerShares(owner); + addShares(e, owner, shares); + removeShares(e, owner, shares); + mathint sharesAfter = get_podOwnerShares(owner); + assert sharesBefore == sharesAfter; +} + +rule addShares_integrity(env e) +{ + uint256 shares; + address owner; + mathint sharesBefore = get_podOwnerShares(owner); + addShares(e, owner, shares); + mathint sharesAfter = get_podOwnerShares(owner); + assert sharesBefore + shares == sharesAfter; +} + +rule removeShares_integrity(env e) +{ + uint256 shares; + address owner; + mathint sharesBefore = get_podOwnerShares(owner); + removeShares(e, owner, shares); + mathint sharesAfter = get_podOwnerShares(owner); + assert sharesBefore - shares == sharesAfter; +} + +rule addShares_independence(env e) +{ + uint256 shares1; uint256 shares2; + address owner; + storage init = lastStorage; + addShares(e, owner, shares1); + addShares(e, owner, shares2); + storage storageAfter12 = lastStorage; + + addShares(e, owner, shares2) at init; + addShares(e, owner, shares1); + storage storageAfter21 = lastStorage; + assert storageAfter12 == storageAfter21; +} + +rule add_remove_independence(env e) +{ + uint256 shares1; uint256 shares2; + address owner; + storage init = lastStorage; + addShares(e, owner, shares1); + removeShares(e, owner, shares2); + storage storageAfter12 = lastStorage; + + removeShares(e, owner, shares2) at init; + addShares(e, owner, shares1); + storage storageAfter21 = lastStorage; + assert storageAfter12 == storageAfter21; +} + +rule addShares_revertsWhenNoPod(env e) +{ + uint256 shares; + address owner; + bool hasPod = get_podByOwner(owner) != 0; + satisfy true; + addShares@withrevert(e, owner, shares); + bool reverted = lastReverted; + + satisfy !hasPod && reverted; + assert !hasPod => reverted; +} + +rule removeShares_revertsWhenNoPod(env e) +{ + uint256 shares; + address owner; + bool hasPod = get_podByOwner(owner) != 0; + removeShares@withrevert(e, owner, shares); + bool reverted = lastReverted; + assert !hasPod => reverted; +} + +//////////////////// IN DEVELOPMENT / OBSOLETE ////////////// + + +// to check that methods work correctly +rule methodsDontAlwaysRevert(env e, method f) +{ + calldataarg args; + f@withrevert(e, args); + bool reverted = lastReverted; + satisfy !reverted; +} \ No newline at end of file diff --git a/certora/specs/pods/EigenPodMethodsAndSimplifications.spec b/certora/specs/pods/EigenPodMethodsAndSimplifications.spec new file mode 100644 index 0000000000..00b4807ff8 --- /dev/null +++ b/certora/specs/pods/EigenPodMethodsAndSimplifications.spec @@ -0,0 +1,66 @@ +import "./../setup.spec"; + +methods { + // Internal, NONDET-summarized EigenPod library functions + function _.verifyValidatorFields(bytes32, bytes32[] calldata, bytes calldata, uint40) internal => NONDET; + function _.verifyValidatorBalance(bytes32, uint40, BeaconChainProofs.BalanceProof calldata) internal => NONDET; + function _.verifyStateRoot(bytes32, BeaconChainProofs.StateRootProof calldata) internal => NONDET; + + // Internal, NONDET-summarized "send ETH" function -- unsound summary used to avoid HAVOC behavior + // when sending ETH using `Address.sendValue()` + function _._sendETH(address recipient, uint256 amountWei) internal => NONDET; + + //// External Calls + + // external calls to Slasher + function _.recordStakeUpdate(address,uint32,uint32,uint256) external => NONDET; + + // external calls to Strategy contracts + //function _.deposit(address, uint256) external => NONDET; + //function _.withdraw(address, address, uint256) external => NONDET; + + + // envfree functions + //function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external returns (uint64) envfree; + function withdrawableRestakedExecutionLayerGwei() external returns (uint64) envfree; + //function nonBeaconChainETHBalanceWei() external returns (uint256) envfree; + function eigenPodManager() external returns (address) envfree; + function podOwner() external returns (address) envfree; + //function hasRestaked() external returns (bool) envfree; + //function mostRecentWithdrawalTimestamp() external returns (uint64) envfree; + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external returns (IEigenPod.ValidatorInfo) envfree; + //function provenWithdrawal(bytes32 validatorPubkeyHash, uint64 slot) external returns (bool) envfree; + function validatorStatus(bytes32 pubkeyHash) external returns (IEigenPod.VALIDATOR_STATUS) envfree; + //function nonBeaconChainETHBalanceWei() external returns (uint256) envfree; + + // harnessed functions + function get_validatorIndex(bytes32 pubkeyHash) external returns (uint64) envfree; + function get_restakedBalanceGwei(bytes32 pubkeyHash) external returns (uint64) envfree; + function get_mostRecentBalanceUpdateTimestamp(bytes32 pubkeyHash) external returns (uint64) envfree; + function get_podOwnerShares() external returns (int256) envfree; + function get_withdrawableRestakedExecutionLayerGwei() external returns (uint256) envfree; + function get_ETH_Balance() external returns (uint256) envfree; + function get_currentCheckpointTimestamp() external returns (uint64) envfree; + function get_lastCheckpointTimestamp() external returns (uint64) envfree; + function validatorIsActive(bytes32) external returns (bool) envfree; + function get_validatorLastCheckpointed(bytes32) external returns (uint64) envfree; + function activeValidatorCount() external returns (uint256) envfree; + function currentCheckpoint() external returns (IEigenPod.Checkpoint) envfree; +} + +function isDuringCheckpoint() returns bool +{ + return get_currentCheckpointTimestamp() > 0; +} + +function timestampsNotFromFuture(env e) returns bool +{ + return e.block.timestamp > 0 && + e.block.timestamp >= require_uint256(get_currentCheckpointTimestamp()) && + e.block.timestamp >= require_uint256(get_lastCheckpointTimestamp()); +} + +function validatorDataNotFromFuture(env e, bytes32 validatorHash) returns bool +{ + return e.block.timestamp >= require_uint256(get_validatorLastCheckpointed(validatorHash)); +} \ No newline at end of file diff --git a/certora/specs/setup.spec b/certora/specs/setup.spec new file mode 100644 index 0000000000..6ee9e982e9 --- /dev/null +++ b/certora/specs/setup.spec @@ -0,0 +1,79 @@ +import "methodsAndAliases.spec"; + +definition isPrivileged(method f) returns bool = + f.selector == sig:DelegationManagerHarnessAlias.transferOwnership(address).selector || + f.selector == sig:DelegationManagerHarnessAlias.renounceOwnership().selector || + + f.selector == sig:DelegationManagerHarness.setMinWithdrawalDelayBlocks(uint256).selector || + f.selector == sig:DelegationManagerHarness.setStrategyWithdrawalDelayBlocks(address[],uint256[]).selector || + + ((f.contract == DummyEigenPodAAlias || f.contract == DummyEigenPodBAlias) && //there are methods with the same selector on other contracts so we have to distinguish + ( + f.selector == sig:DummyEigenPodA.recoverTokens(address[],uint256[],address).selector || + f.selector == sig:DummyEigenPodA.withdrawRestakedBeaconChainETH(address,uint256).selector || + //f.selector == sig:DummyEigenPodA.withdrawNonBeaconChainETHBalanceWei(address,uint256).selector || + //f.selector == sig:DummyEigenPodA.activateRestaking().selector || + f.selector == sig:DummyEigenPodA.verifyWithdrawalCredentials(uint64,BeaconChainProofs.StateRootProof,uint40[],bytes[],bytes32[][]).selector || + f.selector == sig:DummyEigenPodA.stake(bytes,bytes,bytes32).selector + )) || + + (f.contract == EigenPodManagerHarnessAlias && //there are methods with the same selector on other contracts so we have to distinguish + ( + f.selector == sig:EigenPodManagerHarness.addShares(address,uint256).selector || + f.selector == sig:EigenPodManagerHarness.removeShares(address,uint256).selector || + //f.selector == sig:EigenPodManagerHarness.setDenebForkTimestamp(uint64).selector || + f.selector == sig:EigenPodManagerHarness.initialize(address, address, uint256).selector || + f.selector == sig:EigenPodManagerHarness.withdrawSharesAsTokens(address,address,uint256).selector + )) || + + f.selector == sig:EigenStrategy.withdraw(address,address,uint256).selector || + f.selector == sig:EigenStrategy.deposit(address,uint256).selector || + + f.selector == sig:StrategyManagerHarness.removeStrategiesFromDepositWhitelist(address[]).selector || + f.selector == sig:StrategyManagerHarness.addShares(address,address,address,uint256).selector || + f.selector == sig:StrategyManagerHarness.withdrawSharesAsTokens(address,address,uint256,address).selector || + f.selector == sig:StrategyManagerHarness.setThirdPartyTransfersForbidden(address,bool).selector || + f.selector == sig:StrategyManagerHarness.setStrategyWhitelister(address).selector || + f.selector == sig:StrategyManagerHarness.addStrategiesToDepositWhitelist(address[],bool[]).selector || + f.selector == sig:StrategyManagerHarness.removeShares(address,address,uint256).selector || + + f.selector == sig:PauserRegistry.setIsPauser(address,bool).selector || + f.selector == sig:PauserRegistry.setUnpauser(address).selector; + +definition isIgnoredMethod(method f) returns bool = + f.selector == sig:certorafallback_0().selector; + +function isPrivilegedSender(env e) returns bool +{ + address sender = e.msg.sender; + if ( + sender == StrategyManagerHarnessAlias || + sender == StrategyManagerHarnessAlias || + sender == DummyEigenPodAAlias || + sender == DummyEigenPodBAlias || + sender == DummyERC20AAlias || + sender == DummyERC20BAlias || + sender == DelegationManagerHarnessAlias || + sender == EigenStrategyAlias || + sender == PausableHarnessAlias || + sender == ETHPOSDepositMockAlias || + sender == ERC1271WalletMockAlias || + sender == PauserRegistryAlias) + return true; + return false; +} + +definition canIncreasePodOwnerShares(method f) returns bool = + f.selector == sig:EigenPodManagerHarness.addShares(address,uint256).selector || + f.selector == sig:EigenPodManagerHarness.recordBeaconChainETHBalanceUpdate(address,int256).selector || + f.selector == sig:EigenPodManagerHarness.withdrawSharesAsTokens(address,address,uint256).selector || + f.selector == sig:DummyEigenPodA.startCheckpoint(bool).selector; + +definition canDecreasePodOwnerShares(method f) returns bool = + f.selector == sig:EigenPodManagerHarness.recordBeaconChainETHBalanceUpdate(address,int256).selector || + f.selector == sig:EigenPodManagerHarness.removeShares(address,uint256).selector; + +definition canIncreaseBalanceUpdateTimestamp(method f) returns bool = + f.selector == sig:DummyEigenPodA.verifyWithdrawalCredentials(uint64, BeaconChainProofs.StateRootProof, uint40[], bytes[], bytes32[][]).selector; + +definition canDecreaseBalanceUpdateTimestamp(method f) returns bool = false; diff --git a/certora/specs/shared.spec b/certora/specs/shared.spec new file mode 100644 index 0000000000..325fff72ac --- /dev/null +++ b/certora/specs/shared.spec @@ -0,0 +1,12 @@ +function pay_and_havoc(address receiver, env e) { + if (e.msg.sender == receiver) { + utils.havocAll(); + return; + } + + uint oldBalanceSender = nativeBalances[e.msg.sender]; + uint oldBalanceRecipient = nativeBalances[receiver]; + utils.havocAll(); + require nativeBalances[e.msg.sender] == oldBalanceSender - e.msg.value; + require nativeBalances[receiver] == oldBalanceRecipient + e.msg.value; +} \ No newline at end of file