diff --git a/src/contracts/libraries/BeaconChainProofs.sol b/src/contracts/libraries/BeaconChainProofs.sol index 8fa745f7a..46f1aa6ad 100644 --- a/src/contracts/libraries/BeaconChainProofs.sol +++ b/src/contracts/libraries/BeaconChainProofs.sol @@ -453,7 +453,7 @@ library BeaconChainProofs { /** * @dev Retrieves a validator's pubkey hash */ - function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { return validatorFields[VALIDATOR_PUBKEY_INDEX]; } @@ -466,7 +466,7 @@ library BeaconChainProofs { /** * @dev Retrieves a validator's effective balance (in gwei) */ - function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { return Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); } diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index f210e0166..b4574a328 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -22,7 +22,7 @@ abstract contract IntegrationBase is IntegrationDeployer { function _newRandomStaker() internal returns (User, IStrategy[] memory, uint[] memory) { (User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser(); - assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "_newStaker: failed to award token balances"); + assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "_newRandomStaker: failed to award token balances"); return (staker, strategies, tokenBalances); } @@ -33,9 +33,9 @@ abstract contract IntegrationBase is IntegrationDeployer { operator.registerAsOperator(); operator.depositIntoEigenlayer(strategies, tokenBalances); - assert_Snap_AddedStakerShares(operator, strategies, tokenBalances, "_newOperator: failed to add delegatable shares"); - assert_Snap_AddedOperatorShares(operator, strategies, tokenBalances, "_newOperator: failed to award shares to operator"); - assertTrue(delegationManager.isOperator(address(operator)), "_newOperator: operator should be registered"); + assert_Snap_AddedStakerShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to add delegatable shares"); + assert_Snap_AddedOperatorShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to award shares to operator"); + assertTrue(delegationManager.isOperator(address(operator)), "_newRandomOperator: operator should be registered"); return (operator, strategies, tokenBalances); } @@ -88,9 +88,15 @@ abstract contract IntegrationBase is IntegrationDeployer { uint actualShares; if (strat == BEACONCHAIN_ETH_STRAT) { - // TODO - // actualShares = eigenPodManager.podOwnerShares(address(user)); - revert("unimplemented"); + // This method should only be used for tests that handle positive + // balances. Negative balances are an edge case that require + // the own tests and helper methods. + int shares = eigenPodManager.podOwnerShares(address(user)); + if (shares < 0) { + revert("assert_HasExpectedShares: negative shares"); + } + + actualShares = uint(shares); } else { actualShares = strategyManager.stakerStrategyShares(address(user), strat); } @@ -251,9 +257,7 @@ abstract contract IntegrationBase is IntegrationDeployer { uint tokenBalance = tokenBalances[i]; if (strat == BEACONCHAIN_ETH_STRAT) { - // TODO - need to calculate this - // expectedShares[i] = eigenPodManager.underlyingToShares(tokenBalance); - revert("_calculateExpectedShares: unimplemented for native eth"); + expectedShares[i] = tokenBalances[i]; } else { expectedShares[i] = strat.underlyingToShares(tokenBalance); } @@ -271,9 +275,7 @@ abstract contract IntegrationBase is IntegrationDeployer { IStrategy strat = strategies[i]; if (strat == BEACONCHAIN_ETH_STRAT) { - // TODO - need to calculate this - // expectedTokens[i] = eigenPodManager.underlyingToShares(tokenBalance); - revert("_calculateExpectedShares: unimplemented for native eth"); + expectedTokens[i] = shares[i]; } else { expectedTokens[i] = strat.sharesToUnderlying(shares[i]); } @@ -323,8 +325,15 @@ abstract contract IntegrationBase is IntegrationDeployer { IStrategy strat = strategies[i]; if (strat == BEACONCHAIN_ETH_STRAT) { - // curShares[i] = eigenPodManager.podOwnerShares(address(staker)); - revert("TODO: unimplemented"); + // This method should only be used for tests that handle positive + // balances. Negative balances are an edge case that require + // the own tests and helper methods. + int shares = eigenPodManager.podOwnerShares(address(staker)); + if (shares < 0) { + revert("_getStakerShares: negative shares"); + } + + curShares[i] = uint(shares); } else { curShares[i] = strategyManager.stakerStrategyShares(address(staker), strat); } @@ -349,7 +358,11 @@ abstract contract IntegrationBase is IntegrationDeployer { uint[] memory balances = new uint[](tokens.length); for (uint i = 0; i < tokens.length; i++) { - balances[i] = tokens[i].balanceOf(address(staker)); + if (tokens[i] == NATIVE_ETH) { + balances[i] = address(staker).balance; + } else { + balances[i] = tokens[i].balanceOf(address(staker)); + } } return balances; diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index a18262b53..c867c6f27 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -20,7 +20,8 @@ import "src/contracts/permissions/PauserRegistry.sol"; import "src/test/mocks/EmptyContract.sol"; import "src/test/mocks/ETHDepositMock.sol"; -import "src/test/mocks/BeaconChainOracleMock.sol"; +import "src/test/integration/mocks/BeaconChainOracleMock.t.sol"; +import "src/test/integration/mocks/BeaconChainMock.t.sol"; import "src/test/integration/User.t.sol"; @@ -54,6 +55,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { // Mock Contracts to deploy ETHPOSDepositMock ethPOSDeposit; BeaconChainOracleMock beaconChainOracle; + BeaconChainMock public beaconChain; // ProxyAdmin ProxyAdmin eigenLayerProxyAdmin; @@ -72,7 +74,10 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { // Constants uint64 constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; uint64 constant GOERLI_GENESIS_TIME = 1616508000; + IStrategy constant BEACONCHAIN_ETH_STRAT = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); + IERC20 constant NATIVE_ETH = IERC20(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); + uint constant MIN_BALANCE = 1e6; uint constant MAX_BALANCE = 5e6; @@ -85,12 +90,12 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { uint constant NO_ASSETS = (FLAG << 0); // will have no assets uint constant HOLDS_LST = (FLAG << 1); // will hold some random amount of LSTs uint constant HOLDS_ETH = (FLAG << 2); // will hold some random amount of ETH - uint constant HOLDS_MIX = (FLAG << 3); // will hold a mix of LSTs and ETH + uint constant HOLDS_ALL = (FLAG << 3); // will hold every LST and ETH /// @dev User contract flags /// These are used with _configRand to determine what User contracts can be deployed uint constant DEFAULT = (FLAG << 0); - uint constant SIGNED_METHODS = (FLAG << 1); + uint constant ALT_METHODS = (FLAG << 1); // /// @dev Withdrawal flags // /// These are used with _configRand to determine how a user conducts a withdrawal @@ -117,10 +122,10 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { assetTypeToStr[NO_ASSETS] = "NO_ASSETS"; assetTypeToStr[HOLDS_LST] = "HOLDS_LST"; assetTypeToStr[HOLDS_ETH] = "HOLDS_ETH"; - assetTypeToStr[HOLDS_MIX] = "HOLDS_MIX"; + assetTypeToStr[HOLDS_ALL] = "HOLDS_ALL"; userTypeToStr[DEFAULT] = "DEFAULT"; - userTypeToStr[SIGNED_METHODS] = "SIGNED_METHODS"; + userTypeToStr[ALT_METHODS] = "ALT_METHODS"; } function setUp() public virtual { @@ -253,7 +258,12 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { ethStrats.push(BEACONCHAIN_ETH_STRAT); mixedStrats.push(BEACONCHAIN_ETH_STRAT); + // Create time machine and set block timestamp forward so we can create EigenPod proofs in the past timeMachine = new TimeMachine(); + timeMachine.setProofGenStartTime(2 hours); + + // Create mock beacon chain / proof gen interface + beaconChain = new BeaconChainMock(timeMachine, beaconChainOracle); } /// @dev Deploy a strategy and its underlying token, push to global lists of tokens/strategies, and whitelist @@ -291,8 +301,6 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { emit log_named_uint("_configRand: set random seed to: ", _randomSeed); random = keccak256(abi.encodePacked(_randomSeed)); - emit log_named_uint("_configRand: allowed asset types: ", _assetTypes); - // Convert flag bitmaps to bytes of set bits for easy use with _randUint assetTypes = _bitmapToBytes(_assetTypes); userTypes = _bitmapToBytes(_userTypes); @@ -328,11 +336,12 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { User user; if (userType == DEFAULT) { user = new User(); - } else if (userType == SIGNED_METHODS) { - // User will use `delegateToBySignature` and `depositIntoStrategyWithSignature` - user = User(new User_SignedMethods()); + } else if (userType == ALT_METHODS) { + // User will use nonstandard methods like: + // `delegateToBySignature` and `depositIntoStrategyWithSignature` + user = User(new User_AltMethods()); } else { - revert("_newUser: unimplemented userType"); + revert("_randUser: unimplemented userType"); } // For the specific asset selection we made, get a random assortment of @@ -348,9 +357,10 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { /// NO_ASSETS - return will be empty /// HOLDS_LST - `strategies` will be a random subset of initialized strategies /// `tokenBalances` will be the user's balances in each token - /// HOLDS_ETH - `strategies` will only contain BEACON_CHAIN_ETH_STRAT, and + /// HOLDS_ETH - `strategies` will only contain BEACONCHAIN_ETH_STRAT, and /// `tokenBalances` will contain the user's eth balance - /// HOLDS_MIX - random combination of `HOLDS_LST` and `HOLDS_ETH` + /// HOLDS_ALL - `strategies` will contain ALL initialized strategies AND BEACONCHAIN_ETH_STRAT, and + /// `tokenBalances` will contain random token/eth balances accordingly function _dealRandAssets(User user, uint assetType) internal returns (IStrategy[] memory, uint[] memory) { IStrategy[] memory strategies; @@ -378,14 +388,42 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { tokenBalances[i] = balance; strategies[i] = strat; } - - return (strategies, tokenBalances); } else if (assetType == HOLDS_ETH) { - revert("_getRandAssets: HOLDS_ETH unimplemented"); - } else if (assetType == HOLDS_MIX) { - revert("_getRandAssets: HOLDS_MIX unimplemented"); + strategies = new IStrategy[](1); + tokenBalances = new uint[](1); + + // Award the user with a random multiple of 32 ETH + uint amount = 32 ether * _randUint({ min: 1, max: 3 }); + cheats.deal(address(user), amount); + + strategies[0] = BEACONCHAIN_ETH_STRAT; + tokenBalances[0] = amount; + } else if (assetType == HOLDS_ALL) { + uint numLSTs = lstStrats.length; + strategies = new IStrategy[](numLSTs + 1); + tokenBalances = new uint[](numLSTs + 1); + + // For each LST, award the user a random balance of the underlying token + for (uint i = 0; i < numLSTs; i++) { + IStrategy strat = lstStrats[i]; + IERC20 underlyingToken = strat.underlyingToken(); + + uint balance = _randUint({ min: MIN_BALANCE, max: MAX_BALANCE }); + StdCheats.deal(address(underlyingToken), address(user), balance); + + tokenBalances[i] = balance; + strategies[i] = strat; + } + + // Award the user with a random multiple of 32 ETH + uint amount = 32 ether * _randUint({ min: 1, max: 3 }); + cheats.deal(address(user), amount); + + // Add BEACONCHAIN_ETH_STRAT and eth balance + strategies[numLSTs] = BEACONCHAIN_ETH_STRAT; + tokenBalances[numLSTs] = amount; } else { - revert("_getRandAssets: assetType unimplemented"); + revert("_dealRandAssets: assetType unimplemented"); } return (strategies, tokenBalances); @@ -467,10 +505,16 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { for (uint i = 0; i < strategies.length; i++) { IStrategy strat = strategies[i]; - IERC20 underlyingToken = strat.underlyingToken(); - emit log_named_string("token name: ", IERC20Metadata(address(underlyingToken)).name()); - emit log_named_uint("token balance: ", tokenBalances[i]); + if (strat == BEACONCHAIN_ETH_STRAT) { + emit log_named_string("token name: ", "Native ETH"); + emit log_named_uint("token balance: ", tokenBalances[i]); + } else { + IERC20 underlyingToken = strat.underlyingToken(); + + emit log_named_string("token name: ", IERC20Metadata(address(underlyingToken)).name()); + emit log_named_uint("token balance: ", tokenBalances[i]); + } } } } \ No newline at end of file diff --git a/src/test/integration/README.md b/src/test/integration/README.md index 1ff4e9efd..e1c2b75af 100644 --- a/src/test/integration/README.md +++ b/src/test/integration/README.md @@ -16,7 +16,7 @@ Looking at the current tests is a good place to start. During the test, the config passed into `_configRand` will randomly generate only the values you configure: * `assetTypes` affect the assets granted to Users when they are first created. You can use this to ensure your flows and assertions work when users are holding only LSTs, native ETH, or some combination. -* `userTypes` affect the actual `User` contract being deployed. The `DEFAULT` flag deploys the base `User` contract, while `SIGNED_METHODS` deploys a version that derives from the same contract, but overrides some methods to use "functionWithSignature" variants. +* `userTypes` affect the actual `User` contract being deployed. The `DEFAULT` flag deploys the base `User` contract, while `ALT_METHODS` deploys a version that derives from the same contract, but overrides some methods to use "functionWithSignature" and other variants. Here's an example: @@ -27,7 +27,7 @@ function testFuzz_deposit_delegate_EXAMPLE(uint24 _random) public { _configRand({ _randomSeed: _random, _assetTypes: HOLDS_LST, - _userTypes: DEFAULT | SIGNED_METHODS + _userTypes: DEFAULT | ALT_METHODS }); // Because of the `assetTypes` flags above, this will create two Users for our test, @@ -93,5 +93,4 @@ function testFuzz_deposit_delegate_EXAMPLE(uint24 _random) public { ### What needs to be done? * Suggest or PR cleanup if you have ideas. Currently, the `IntegrationDeployer` contract is pretty messy. -* Currently, the only supported assetTypes are `NO_ASSETS` and `HOLDS_LST`. There are flags for `HOLDS_ETH` and `HOLDS_MIXED`, but we need to implement `EigenPod` proof generation/usage before they can be used. * Coordinate in Slack to pick out some user flows to write tests for! \ No newline at end of file diff --git a/src/test/integration/TimeMachine.t.sol b/src/test/integration/TimeMachine.t.sol index 0c8d9ddff..86a7c8930 100644 --- a/src/test/integration/TimeMachine.t.sol +++ b/src/test/integration/TimeMachine.t.sol @@ -10,6 +10,8 @@ contract TimeMachine is Test { bool pastExists = false; uint lastSnapshot; + uint64 public proofGenStartTime; + function createSnapshot() public returns (uint) { uint snapshot = cheats.snapshot(); lastSnapshot = snapshot; @@ -30,4 +32,14 @@ contract TimeMachine is Test { function warpToPresent(uint curState) public { cheats.revertTo(curState); } + + /// @dev Sets the timestamp we use for proof gen to now, + /// then sets block timestamp to now + secondsAgo. + /// + /// This means we can create mock proofs using an oracle time + /// of `proofGenStartTime`. + function setProofGenStartTime(uint secondsAgo) public { + proofGenStartTime = uint64(block.timestamp); + cheats.warp(block.timestamp + secondsAgo); + } } \ No newline at end of file diff --git a/src/test/integration/User.t.sol b/src/test/integration/User.t.sol index f4ffd5689..806994d69 100644 --- a/src/test/integration/User.t.sol +++ b/src/test/integration/User.t.sol @@ -6,17 +6,20 @@ import "forge-std/Test.sol"; import "src/contracts/core/DelegationManager.sol"; import "src/contracts/core/StrategyManager.sol"; import "src/contracts/pods/EigenPodManager.sol"; +import "src/contracts/pods/EigenPod.sol"; import "src/contracts/interfaces/IDelegationManager.sol"; import "src/contracts/interfaces/IStrategy.sol"; import "src/test/integration/TimeMachine.t.sol"; +import "src/test/integration/mocks/BeaconChainMock.t.sol"; interface IUserDeployer { function delegationManager() external view returns (DelegationManager); function strategyManager() external view returns (StrategyManager); function eigenPodManager() external view returns (EigenPodManager); function timeMachine() external view returns (TimeMachine); + function beaconChain() external view returns (BeaconChainMock); } contract User is Test { @@ -29,7 +32,16 @@ contract User is Test { TimeMachine timeMachine; + /// @dev Native restaker state vars + + BeaconChainMock beaconChain; + // User's EigenPod and each of their validator indices within that pod + EigenPod pod; + uint40[] validators; + IStrategy constant BEACONCHAIN_ETH_STRAT = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); + IERC20 constant NATIVE_ETH = IERC20(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); + uint constant GWEI_TO_WEI = 1e9; constructor() { IUserDeployer deployer = IUserDeployer(msg.sender); @@ -38,6 +50,9 @@ contract User is Test { strategyManager = deployer.strategyManager(); eigenPodManager = deployer.eigenPodManager(); timeMachine = deployer.timeMachine(); + + beaconChain = deployer.beaconChain(); + pod = EigenPod(payable(eigenPodManager.createPod())); } modifier createSnapshot() virtual { @@ -45,6 +60,12 @@ contract User is Test { _; } + receive() external payable {} + + /** + * DelegationManager methods: + */ + function registerAsOperator() public createSnapshot virtual { IDelegationManager.OperatorDetails memory details = IDelegationManager.OperatorDetails({ earningsReceiver: address(this), @@ -63,8 +84,31 @@ contract User is Test { uint tokenBalance = tokenBalances[i]; if (strat == BEACONCHAIN_ETH_STRAT) { - // TODO handle this flow - need to deposit into EPM + prove credentials - revert("depositIntoEigenlayer: unimplemented"); + // We're depositing via `eigenPodManager.stake`, which only accepts + // deposits of exactly 32 ether. + require(tokenBalance % 32 ether == 0, "User.depositIntoEigenlayer: balance must be multiple of 32 eth"); + + // For each multiple of 32 ether, deploy a new validator to the same pod + uint numValidators = tokenBalance / 32 ether; + for (uint j = 0; j < numValidators; j++) { + eigenPodManager.stake{ value: 32 ether }("", "", bytes32(0)); + + (uint40 newValidatorIndex, CredentialsProofs memory proofs) = + beaconChain.newValidator({ + balanceWei: 32 ether, + withdrawalCreds: _podWithdrawalCredentials() + }); + + validators.push(newValidatorIndex); + + pod.verifyWithdrawalCredentials({ + oracleTimestamp: proofs.oracleTimestamp, + stateRootProof: proofs.stateRootProof, + validatorIndices: proofs.validatorIndices, + validatorFieldsProofs: proofs.validatorFieldsProofs, + validatorFields: proofs.validatorFields + }); + } } else { IERC20 underlyingToken = strat.underlyingToken(); underlyingToken.approve(address(strategyManager), tokenBalance); @@ -129,7 +173,37 @@ contract User is Test { IStrategy strat = withdrawal.strategies[i]; if (strat == BEACONCHAIN_ETH_STRAT) { - tokens[i] = IERC20(address(0)); + tokens[i] = NATIVE_ETH; + + // If we're withdrawing as tokens, we need to process a withdrawal proof first + if (receiveAsTokens) { + + emit log("exiting validators and processing withdrawals..."); + + uint numValidators = validators.length; + for (uint j = 0; j < numValidators; j++) { + emit log_named_uint("exiting validator ", j); + + uint40 validatorIndex = validators[j]; + BeaconWithdrawal memory proofs = beaconChain.exitValidator(validatorIndex); + + uint64 withdrawableBefore = pod.withdrawableRestakedExecutionLayerGwei(); + + pod.verifyAndProcessWithdrawals({ + oracleTimestamp: proofs.oracleTimestamp, + stateRootProof: proofs.stateRootProof, + withdrawalProofs: proofs.withdrawalProofs, + validatorFieldsProofs: proofs.validatorFieldsProofs, + validatorFields: proofs.validatorFields, + withdrawalFields: proofs.withdrawalFields + }); + + uint64 withdrawableAfter = pod.withdrawableRestakedExecutionLayerGwei(); + + emit log_named_uint("pod withdrawable before: ", withdrawableBefore); + emit log_named_uint("pod withdrawable after: ", withdrawableAfter); + } + } } else { tokens[i] = strat.underlyingToken(); } @@ -139,10 +213,14 @@ contract User is Test { return tokens; } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(pod)); + } } -/// @notice A user contract that implements 1271 signatures -contract User_SignedMethods is User { +/// @notice A user contract that calls nonstandard methods (like xBySignature methods) +contract User_AltMethods is User { mapping(bytes32 => bool) public signedHashes; @@ -176,8 +254,31 @@ contract User_SignedMethods is User { uint tokenBalance = tokenBalances[i]; if (strat == BEACONCHAIN_ETH_STRAT) { - // TODO handle this flow - need to deposit into EPM + prove credentials - revert("depositIntoEigenlayer: unimplemented"); + // We're depositing via `eigenPodManager.stake`, which only accepts + // deposits of exactly 32 ether. + require(tokenBalance % 32 ether == 0, "User.depositIntoEigenlayer: balance must be multiple of 32 eth"); + + // For each multiple of 32 ether, deploy a new validator to the same pod + uint numValidators = tokenBalance / 32 ether; + for (uint j = 0; j < numValidators; j++) { + eigenPodManager.stake{ value: 32 ether }("", "", bytes32(0)); + + (uint40 newValidatorIndex, CredentialsProofs memory proofs) = + beaconChain.newValidator({ + balanceWei: 32 ether, + withdrawalCreds: _podWithdrawalCredentials() + }); + + validators.push(newValidatorIndex); + + pod.verifyWithdrawalCredentials({ + oracleTimestamp: proofs.oracleTimestamp, + stateRootProof: proofs.stateRootProof, + validatorIndices: proofs.validatorIndices, + validatorFieldsProofs: proofs.validatorFieldsProofs, + validatorFields: proofs.validatorFields + }); + } } else { // Approve token IERC20 underlyingToken = strat.underlyingToken(); diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol new file mode 100644 index 000000000..a1a5dc05f --- /dev/null +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -0,0 +1,698 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "forge-std/Test.sol"; + +import "src/contracts/libraries/BeaconChainProofs.sol"; +import "src/contracts/libraries/Merkle.sol"; + +import "src/test/integration/TimeMachine.t.sol"; +import "src/test/integration/mocks/BeaconChainOracleMock.t.sol"; + +struct CredentialsProofs { + uint64 oracleTimestamp; + BeaconChainProofs.StateRootProof stateRootProof; + uint40[] validatorIndices; + bytes[] validatorFieldsProofs; + bytes32[][] validatorFields; +} + +struct BeaconWithdrawal { + uint64 oracleTimestamp; + BeaconChainProofs.StateRootProof stateRootProof; + BeaconChainProofs.WithdrawalProof[] withdrawalProofs; + bytes[] validatorFieldsProofs; + bytes32[][] validatorFields; + bytes32[][] withdrawalFields; +} + +contract BeaconChainMock is Test { + + Vm cheats = Vm(HEVM_ADDRESS); + + struct Validator { + bytes32 pubkeyHash; + uint40 validatorIndex; + bytes withdrawalCreds; + uint64 effectiveBalanceGwei; + } + + uint40 nextValidatorIndex = 0; + uint64 nextTimestamp; + + // Sequential list of created Validators + // Validator[] validators; + // mapping(uint40 => Validator) validators; + + mapping(uint40 => Validator) validators; + + BeaconChainOracleMock oracle; + + /// @dev All withdrawals are processed with index == 0 + uint64 constant WITHDRAWAL_INDEX = 0; + uint constant GWEI_TO_WEI = 1e9; + + constructor(TimeMachine timeMachine, BeaconChainOracleMock beaconChainOracle) { + nextTimestamp = timeMachine.proofGenStartTime(); + oracle = beaconChainOracle; + } + + /** + * @dev Processes a deposit for a new validator and returns the + * information needed to prove withdrawal credentials. + * + * For now, this returns empty proofs that will pass in the oracle, + * but in the future this should use FFI to return a valid proof. + */ + function newValidator( + uint balanceWei, + bytes memory withdrawalCreds + ) public returns (uint40, CredentialsProofs memory) { + // These checks mimic the checks made in the beacon chain deposit contract + // + // We sanity-check them here because this contract sorta acts like the + // deposit contract and this ensures we only create validators that could + // exist IRL + require(balanceWei >= 1 ether, "BeaconChainMock.newValidator: deposit value too low"); + require(balanceWei % 1 gwei == 0, "BeaconChainMock.newValidator: value not multiple of gwei"); + uint depositAmount = balanceWei / GWEI_TO_WEI; + require(depositAmount <= type(uint64).max, "BeaconChainMock.newValidator: deposit value too high"); + + // Create unique index for new validator + uint40 validatorIndex = nextValidatorIndex; + nextValidatorIndex++; + + // Create new validator and record in state + Validator memory validator = Validator({ + pubkeyHash: keccak256(abi.encodePacked(validatorIndex)), + validatorIndex: validatorIndex, + withdrawalCreds: withdrawalCreds, + effectiveBalanceGwei: uint64(depositAmount) + }); + validators[validatorIndex] = validator; + + return (validator.validatorIndex, _genCredentialsProof(validator)); + } + + /** + * @dev Exit a validator from the beacon chain, given its validatorIndex + * The passed-in validatorIndex should correspond to a validator created + * via `newValidator` above. + * + * This method will return the exit proofs needed to process eigenpod withdrawals. + * Additionally, it will send the withdrawal amount to the validator's withdrawal + * destination. + */ + function exitValidator(uint40 validatorIndex) public returns (BeaconWithdrawal memory) { + Validator memory validator = validators[validatorIndex]; + + // Get the withdrawal amount and destination + uint amountToWithdraw = validator.effectiveBalanceGwei * GWEI_TO_WEI; + address destination = _toAddress(validator.withdrawalCreds); + + // Generate exit proofs for a full exit + BeaconWithdrawal memory withdrawal = _genExitProof(validator); + + // Update state - set validator balance to zero and send balance to withdrawal destination + validators[validatorIndex].effectiveBalanceGwei = 0; + cheats.deal(destination, destination.balance + amountToWithdraw); + + return withdrawal; + } + + /** + * INTERNAL/HELPER METHODS: + */ + + /** + * @dev For a new validator, generate the beacon chain block root and merkle proof + * needed to prove withdrawal credentials to an EigenPod. + * + * The generated block root is sent to the `BeaconChainOracleMock`, and can be + * queried using `proof.oracleTimestamp` to validate the generated proof. + */ + function _genCredentialsProof(Validator memory validator) internal returns (CredentialsProofs memory) { + CredentialsProofs memory proof; + + proof.validatorIndices = new uint40[](1); + proof.validatorIndices[0] = validator.validatorIndex; + + // Create validatorFields for the new validator + proof.validatorFields = new bytes32[][](1); + proof.validatorFields[0] = new bytes32[](2 ** BeaconChainProofs.VALIDATOR_FIELD_TREE_HEIGHT); + proof.validatorFields[0][BeaconChainProofs.VALIDATOR_PUBKEY_INDEX] = validator.pubkeyHash; + proof.validatorFields[0][BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = + bytes32(validator.withdrawalCreds); + proof.validatorFields[0][BeaconChainProofs.VALIDATOR_BALANCE_INDEX] = + _toLittleEndianUint64(validator.effectiveBalanceGwei); + + // Calculate beaconStateRoot using validator index and an empty proof: + proof.validatorFieldsProofs = new bytes[](1); + proof.validatorFieldsProofs[0] = new bytes(VAL_FIELDS_PROOF_LEN); + bytes32 validatorRoot = Merkle.merkleizeSha256(proof.validatorFields[0]); + uint index = _calcValProofIndex(validator.validatorIndex); + + bytes32 beaconStateRoot = Merkle.processInclusionProofSha256({ + proof: proof.validatorFieldsProofs[0], + leaf: validatorRoot, + index: index + }); + + // Calculate blockRoot using beaconStateRoot and an empty proof: + bytes memory blockRootProof = new bytes(BLOCKROOT_PROOF_LEN); + bytes32 blockRoot = Merkle.processInclusionProofSha256({ + proof: blockRootProof, + leaf: beaconStateRoot, + index: BeaconChainProofs.STATE_ROOT_INDEX + }); + + proof.stateRootProof = BeaconChainProofs.StateRootProof({ + beaconStateRoot: beaconStateRoot, + proof: blockRootProof + }); + + // Send the block root to the oracle and increment timestamp: + proof.oracleTimestamp = uint64(nextTimestamp); + oracle.setBlockRoot(nextTimestamp, blockRoot); + nextTimestamp++; + + return proof; + } + + /** + * @dev Generates the proofs and roots needed to prove a validator's exit from + * the beacon chain. + * + * The generated beacon block root is sent to `BeaconChainOracleMock`, and can + * be queried using `withdrawal.oracleTimestamp` to validate the generated proof. + * + * Since a withdrawal proof requires proving multiple leaves in the same tree, this + * method uses `_genConvergentProofs` to calculate proofs and roots for intermediate + * subtrees, while retaining the information needed to supply an eigenpod with a proof. + * + * The overall merkle tree being proven looks like this: + * + * - beaconBlockRoot (submitted to oracle at end) + * -- beaconStateRoot + * ---- validatorFieldsRoot + * ---- blockRoot (from historical summaries) + * -------- slotRoot + * -------- executionPayloadRoot + * ---------------- timestampRoot + * ---------------- withdrawalFieldsRoot + * + * This method first generates proofs for the lowest leaves, and uses the resulting + * intermediate hashes to generate proofs for higher leaves. Eventually, all of these + * roots are calculated and the final beaconBlockRoot can be calculated and sent to the + * oracle. + */ + function _genExitProof(Validator memory validator) internal returns (BeaconWithdrawal memory) { + BeaconWithdrawal memory withdrawal; + uint64 withdrawalEpoch = uint64(block.timestamp); + + // Get a new, unique timestamp for queries to the oracle + withdrawal.oracleTimestamp = uint64(nextTimestamp); + nextTimestamp++; + + // Initialize proof arrays + BeaconChainProofs.WithdrawalProof memory withdrawalProof = _initWithdrawalProof({ + withdrawalEpoch: withdrawalEpoch, + withdrawalIndex: WITHDRAWAL_INDEX, + oracleTimestamp: withdrawal.oracleTimestamp + }); + + // Calculate withdrawalFields and record the validator's index and withdrawal amount + withdrawal.withdrawalFields = new bytes32[][](1); + withdrawal.withdrawalFields[0] = new bytes32[](2 ** BeaconChainProofs.WITHDRAWAL_FIELD_TREE_HEIGHT); + withdrawal.withdrawalFields[0][BeaconChainProofs.WITHDRAWAL_VALIDATOR_INDEX_INDEX] = + _toLittleEndianUint64(validator.validatorIndex); + withdrawal.withdrawalFields[0][BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX] = + _toLittleEndianUint64(validator.effectiveBalanceGwei); + + { + /** + * Generate proofs then root for subtree: + * + * executionPayloadRoot + * - timestampRoot (withdrawalProof.timestampProof) + * - withdrawalFieldsRoot (withdrawalProof.withdrawalProof) + */ + withdrawalProof.executionPayloadRoot = _genExecPayloadProofs({ + withdrawalProof: withdrawalProof, + withdrawalRoot: Merkle.merkleizeSha256(withdrawal.withdrawalFields[0]) + }); + } + + { + /** + * Generate proofs then root for subtree: + * + * blockRoot (historical summaries) + * - slotRoot (withdrawalProof.slotProof) + * - executionPayloadRoot (withdrawalProof.executionPayloadProof) + */ + withdrawalProof.blockRoot = _genBlockRootProofs({ + withdrawalProof: withdrawalProof + }); + } + + // validatorFields + withdrawal.validatorFields = new bytes32[][](1); + withdrawal.validatorFields[0] = new bytes32[](2 ** BeaconChainProofs.VALIDATOR_FIELD_TREE_HEIGHT); + withdrawal.validatorFields[0][BeaconChainProofs.VALIDATOR_PUBKEY_INDEX] = validator.pubkeyHash; + withdrawal.validatorFields[0][BeaconChainProofs.VALIDATOR_WITHDRAWABLE_EPOCH_INDEX] = + _toLittleEndianUint64(withdrawalEpoch); + + withdrawal.validatorFieldsProofs = new bytes[](1); + withdrawal.validatorFieldsProofs[0] = new bytes(VAL_FIELDS_PROOF_LEN); + + { + /** + * Generate proofs then root for subtree: + * + * beaconStateRoot + * - validatorFieldsRoot (withdrawal.validatorFieldsProofs[0]) + * - blockRoot (historical summaries) (withdrawalProof.historicalSummaryBlockRootProof) + */ + withdrawal.stateRootProof.beaconStateRoot = _genBeaconStateRootProofs({ + withdrawalProof: withdrawalProof, + validatorFieldsProof: withdrawal.validatorFieldsProofs[0], + validatorIndex: validator.validatorIndex, + validatorRoot: Merkle.merkleizeSha256(withdrawal.validatorFields[0]) + }); + } + + withdrawal.withdrawalProofs = new BeaconChainProofs.WithdrawalProof[](1); + withdrawal.withdrawalProofs[0] = withdrawalProof; + + // Calculate beaconBlockRoot using beaconStateRoot and an empty proof: + withdrawal.stateRootProof.proof = new bytes(BLOCKROOT_PROOF_LEN); + bytes32 beaconBlockRoot = Merkle.processInclusionProofSha256({ + proof: withdrawal.stateRootProof.proof, + leaf: withdrawal.stateRootProof.beaconStateRoot, + index: BeaconChainProofs.STATE_ROOT_INDEX + }); + + // Send the block root to the oracle + oracle.setBlockRoot(withdrawal.oracleTimestamp, beaconBlockRoot); + return withdrawal; + } + + /** + * @dev Generates converging merkle proofs for timestampRoot and withdrawalRoot + * under the executionPayloadRoot. + * + * `withdrawalProof.timestampProof` and `withdrawalProof.withdrawalProof` are + * directly updated here. + * + * @return executionPayloadRoot + */ + function _genExecPayloadProofs( + BeaconChainProofs.WithdrawalProof memory withdrawalProof, + bytes32 withdrawalRoot + ) internal view returns (bytes32) { + + uint withdrawalProofIndex = + (BeaconChainProofs.WITHDRAWALS_INDEX << (BeaconChainProofs.WITHDRAWALS_TREE_HEIGHT + 1)) | + uint(withdrawalProof.withdrawalIndex); + + /** + * Generate merkle proofs for timestampRoot and withdrawalRoot + * that converge at or before executionPayloadRoot. + * + * timestampProof length: 4 + * withdrawalProof length: 9 + */ + _genConvergentProofs({ + shortProof: withdrawalProof.timestampProof, + shortIndex: BeaconChainProofs.TIMESTAMP_INDEX, + shortLeaf: withdrawalProof.timestampRoot, + longProof: withdrawalProof.withdrawalProof, + longIndex: withdrawalProofIndex, + longLeaf: withdrawalRoot + }); + + // Use generated proofs to calculate tree root and verify both proofs + // result in the same root: + bytes32 execPayloadRoot = Merkle.processInclusionProofSha256({ + proof: withdrawalProof.timestampProof, + leaf: withdrawalProof.timestampRoot, + index: BeaconChainProofs.TIMESTAMP_INDEX + }); + + bytes32 expectedRoot = Merkle.processInclusionProofSha256({ + proof: withdrawalProof.withdrawalProof, + leaf: withdrawalRoot, + index: withdrawalProofIndex + }); + + require(execPayloadRoot == expectedRoot, "_genExecPayloadProofs: mismatched roots"); + + return execPayloadRoot; + } + + /** + * @dev Generates converging merkle proofs for slotRoot and executionPayloadRoot + * under the block root (historical summaries). + * + * `withdrawalProof.slotProof` and `withdrawalProof.executionPayloadProof` are + * directly updated here. + * + * @return historical summary block root + */ + function _genBlockRootProofs( + BeaconChainProofs.WithdrawalProof memory withdrawalProof + ) internal view returns (bytes32) { + + uint slotRootIndex = BeaconChainProofs.SLOT_INDEX; + uint execPayloadIndex = + (BeaconChainProofs.BODY_ROOT_INDEX << BeaconChainProofs.BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT) | + BeaconChainProofs.EXECUTION_PAYLOAD_INDEX; + + /** + * Generate merkle proofs for slotRoot and executionPayloadRoot + * that converge at or before block root. + * + * slotProof length: 3 + * executionPayloadProof length: 7 + */ + _genConvergentProofs({ + shortProof: withdrawalProof.slotProof, + shortIndex: slotRootIndex, + shortLeaf: withdrawalProof.slotRoot, + longProof: withdrawalProof.executionPayloadProof, + longIndex: execPayloadIndex, + longLeaf: withdrawalProof.executionPayloadRoot + }); + + // Use generated proofs to calculate tree root and verify both proofs + // result in the same root: + bytes32 blockRoot = Merkle.processInclusionProofSha256({ + proof: withdrawalProof.slotProof, + leaf: withdrawalProof.slotRoot, + index: slotRootIndex + }); + + bytes32 expectedRoot = Merkle.processInclusionProofSha256({ + proof: withdrawalProof.executionPayloadProof, + leaf: withdrawalProof.executionPayloadRoot, + index: execPayloadIndex + }); + + require(blockRoot == expectedRoot, "_genBlockRootProofs: mismatched roots"); + + return blockRoot; + } + + /** + * @dev Generates converging merkle proofs for block root and validatorRoot + * under the beaconStateRoot. + * + * `withdrawalProof.historicalSummaryBlockRootProof` and `validatorFieldsProof` are + * directly updated here. + * + * @return beaconStateRoot + */ + function _genBeaconStateRootProofs( + BeaconChainProofs.WithdrawalProof memory withdrawalProof, + bytes memory validatorFieldsProof, + uint40 validatorIndex, + bytes32 validatorRoot + ) internal view returns (bytes32) { + uint blockHeaderIndex = _calcBlockHeaderIndex(withdrawalProof); + uint validatorProofIndex = _calcValProofIndex(validatorIndex); + + /** + * Generate merkle proofs for validatorRoot and blockRoot + * that converge at or before beaconStateRoot. + * + * historicalSummaryBlockRootProof length: 44 + * validatorFieldsProof length: 46 + */ + _genConvergentProofs({ + shortProof: withdrawalProof.historicalSummaryBlockRootProof, + shortIndex: blockHeaderIndex, + shortLeaf: withdrawalProof.blockRoot, + longProof: validatorFieldsProof, + longIndex: validatorProofIndex, + longLeaf: validatorRoot + }); + + // Use generated proofs to calculate tree root and verify both proofs + // result in the same root: + bytes32 beaconStateRoot = Merkle.processInclusionProofSha256({ + proof: withdrawalProof.historicalSummaryBlockRootProof, + leaf: withdrawalProof.blockRoot, + index: blockHeaderIndex + }); + + bytes32 expectedRoot = Merkle.processInclusionProofSha256({ + proof: validatorFieldsProof, + leaf: validatorRoot, + index: validatorProofIndex + }); + + require(beaconStateRoot == expectedRoot, "_genBeaconStateRootProofs: mismatched roots"); + + return beaconStateRoot; + } + + /** + * @dev Generates converging merkle proofs given two leaves and empty proofs. + * Basics: + * - `shortProof` and `longProof` start as empty proofs initialized to the correct + * length for their respective paths. + * - At the end of the method, `shortProof` and `longProof` are still entirely empty + * EXCEPT at the point where the proofs would normally converge under the root hash. + * - At this point, `shortProof` will be assigned the current hash for the `longLeaf` proof + * ... and `longProof` will be assigned the current hash for the `shortLeaf` proof + * + * Steps: + * 1. Because the beacon chain has trees and leaves at varying heights, this method + * first calculates the root of the longer proof's subtree so that the remaining + * proof length is the same for both leaves. + * 2. This method simultaneously computes each leaf's remaining proof step-by-step, + * performing effectively the same steps as `Merkle.processInclusionProof256`. + * 3. At each step, we check to see if the current indices represent sibling leaves. + * 4. If `shortIndex` and `longIndex` are siblings: + * - longProof[longProof_i] = curShortHash + * - shortProof[shortProof_i] = curLongHash + * + * ... Once we've found this convergence and placed each sibling's current hash in + * its opposing sibling's proof, we're done! + * @param shortProof An empty proof initialized to the correct length for the shorter proof path + * @param shortIndex The index of the + */ + function _genConvergentProofs( + bytes memory shortProof, + uint shortIndex, + bytes32 shortLeaf, + bytes memory longProof, + uint longIndex, + bytes32 longLeaf + ) internal view { + require(longProof.length >= shortProof.length, "_genConvergentProofs: invalid input"); + + bytes32[1] memory curShortHash = [shortLeaf]; + bytes32[1] memory curLongHash = [longLeaf]; + + // Calculate root of long subtree + uint longProofOffset = longProof.length - shortProof.length; + for (uint i = 32; i <= longProofOffset; i += 32) { + if (longIndex % 2 == 0) { + assembly { + mstore(0x00, mload(curLongHash)) + mstore(0x20, mload(add(longProof, i))) + } + } else { + assembly { + mstore(0x00, mload(add(longProof, i))) + mstore(0x20, mload(curLongHash)) + } + } + + // Compute hash and divide index + assembly { + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, curLongHash, 0x20)) { + revert(0, 0) + } + longIndex := div(longIndex, 2) + } + } + + { + // Now that we've calculated the longest sub-tree, continue merklizing both trees simultaneously. + // When we reach two leaf indices s.t. A is even and B == A + 1, or vice versa, we know we have + // found the point where the two sub-trees converge. + uint longProof_i = 32 + longProofOffset; + uint shortProof_i = 32; + bool foundConvergence; + for (; longProof_i <= longProof.length; ) { + if (_areSiblings(longIndex, shortIndex)) { + foundConvergence = true; + assembly { + mstore(add(longProof, longProof_i), mload(curShortHash)) + mstore(add(shortProof, shortProof_i), mload(curLongHash)) + } + + break; + } + + // Compute next hash for longProof + { + if (longIndex % 2 == 0) { + assembly { + mstore(0x00, mload(curLongHash)) + mstore(0x20, mload(add(longProof, longProof_i))) + } + } else { + assembly { + mstore(0x00, mload(add(longProof, longProof_i))) + mstore(0x20, mload(curLongHash)) + } + } + + // Compute hash and divide index + assembly { + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, curLongHash, 0x20)) { + revert(0, 0) + } + longIndex := div(longIndex, 2) + } + } + + // Compute next hash for shortProof + { + if (shortIndex % 2 == 0) { + assembly { + mstore(0x00, mload(curShortHash)) + mstore(0x20, mload(add(shortProof, shortProof_i))) + } + } else { + assembly { + mstore(0x00, mload(add(shortProof, shortProof_i))) + mstore(0x20, mload(curShortHash)) + } + } + + // Compute hash and divide index + assembly { + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, curShortHash, 0x20)) { + revert(0, 0) + } + shortIndex := div(shortIndex, 2) + } + } + + longProof_i += 32; + shortProof_i += 32; + } + + require(foundConvergence, "proofs did not converge!"); + } + } + + /** + * PROOF LENGTHS, MISC CONSTANTS, AND OTHER HELPERS: + */ + + uint immutable BLOCKROOT_PROOF_LEN = 32 * BeaconChainProofs.BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT; + uint immutable VAL_FIELDS_PROOF_LEN = 32 * ( + (BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1) + BeaconChainProofs.BEACON_STATE_FIELD_TREE_HEIGHT + ); + + uint immutable WITHDRAWAL_PROOF_LEN = 32 * ( + BeaconChainProofs.EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT + + BeaconChainProofs.WITHDRAWALS_TREE_HEIGHT + 1 + ); + uint immutable EXECPAYLOAD_PROOF_LEN = 32 * ( + BeaconChainProofs.BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + + BeaconChainProofs.BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT + ); + uint immutable SLOT_PROOF_LEN = 32 * ( + BeaconChainProofs.BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + ); + uint immutable TIMESTAMP_PROOF_LEN = 32 * ( + BeaconChainProofs.EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT + ); + uint immutable HISTSUMMARY_PROOF_LEN = 32 * ( + BeaconChainProofs.BEACON_STATE_FIELD_TREE_HEIGHT + + BeaconChainProofs.HISTORICAL_SUMMARIES_TREE_HEIGHT + + BeaconChainProofs.BLOCK_ROOTS_TREE_HEIGHT + 2 + ); + + uint immutable HIST_SUMMARIES_PROOF_INDEX = BeaconChainProofs.HISTORICAL_SUMMARIES_INDEX << ( + BeaconChainProofs.HISTORICAL_SUMMARIES_TREE_HEIGHT + 1 + + BeaconChainProofs.BLOCK_ROOTS_TREE_HEIGHT + 1 + ); + + function _initWithdrawalProof( + uint64 withdrawalEpoch, + uint64 withdrawalIndex, + uint64 oracleTimestamp + ) internal view returns (BeaconChainProofs.WithdrawalProof memory) { + return BeaconChainProofs.WithdrawalProof({ + withdrawalProof: new bytes(WITHDRAWAL_PROOF_LEN), + slotProof: new bytes(SLOT_PROOF_LEN), + executionPayloadProof: new bytes(EXECPAYLOAD_PROOF_LEN), + timestampProof: new bytes(TIMESTAMP_PROOF_LEN), + historicalSummaryBlockRootProof: new bytes(HISTSUMMARY_PROOF_LEN), + blockRootIndex: 0, + historicalSummaryIndex: 0, + withdrawalIndex: withdrawalIndex, + blockRoot: bytes32(0), + slotRoot: _toLittleEndianUint64(withdrawalEpoch * BeaconChainProofs.SLOTS_PER_EPOCH), + timestampRoot: _toLittleEndianUint64(oracleTimestamp), + executionPayloadRoot: bytes32(0) + }); + } + + function _calcBlockHeaderIndex(BeaconChainProofs.WithdrawalProof memory withdrawalProof) internal view returns (uint) { + return + HIST_SUMMARIES_PROOF_INDEX | + (uint(withdrawalProof.historicalSummaryIndex) << (BeaconChainProofs.BLOCK_ROOTS_TREE_HEIGHT + 1)) | + (BeaconChainProofs.BLOCK_SUMMARY_ROOT_INDEX << BeaconChainProofs.BLOCK_ROOTS_TREE_HEIGHT) | + uint(withdrawalProof.blockRootIndex); + } + + function _calcValProofIndex(uint40 validatorIndex) internal pure returns (uint) { + return + (BeaconChainProofs.VALIDATOR_TREE_ROOT_INDEX << (BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1)) | + uint(validatorIndex); + } + + /// @dev Returns true if a and b are sibling indices in the same sub-tree. + /// + /// i.e. the indices belong two child nodes that share a parent: + /// [A, B] or [B, A] + function _areSiblings(uint a, uint b) internal pure returns (bool) { + return + (a % 2 == 0 && b == a + 1) || (b % 2 == 0 && a == b + 1); + } + + /// @dev Opposite of Endian.fromLittleEndianUint64 + function _toLittleEndianUint64(uint64 num) internal 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); + } + + /// @dev Helper to convert 32-byte withdrawal credentials to an address + function _toAddress(bytes memory withdrawalCreds) internal pure returns (address a) { + bytes32 creds = bytes32(withdrawalCreds); + uint160 mask = type(uint160).max; + + assembly { a := and(creds, mask) } + } +} \ No newline at end of file diff --git a/src/test/integration/mocks/BeaconChainOracleMock.t.sol b/src/test/integration/mocks/BeaconChainOracleMock.t.sol new file mode 100644 index 000000000..74390123e --- /dev/null +++ b/src/test/integration/mocks/BeaconChainOracleMock.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "src/contracts/interfaces/IBeaconChainOracle.sol"; + +contract BeaconChainOracleMock is IBeaconChainOracle { + + mapping(uint64 => bytes32) blockRoots; + + function timestampToBlockRoot(uint timestamp) public view returns (bytes32) { + return blockRoots[uint64(timestamp)]; + } + + function setBlockRoot(uint64 timestamp, bytes32 blockRoot) public { + blockRoots[timestamp] = blockRoot; + } +} \ No newline at end of file diff --git a/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol b/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol index 4d25dfd9e..79f69dccc 100644 --- a/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol +++ b/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol @@ -15,8 +15,8 @@ contract Deposit_Delegate_Queue_Complete is IntegrationBase { // When new Users are created, they will choose a random configuration from these params: _configRand({ _randomSeed: _random, - _assetTypes: HOLDS_LST, - _userTypes: DEFAULT | SIGNED_METHODS + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS }); /// 0. Create an operator and a staker with: @@ -104,8 +104,8 @@ contract Deposit_Delegate_Queue_Complete is IntegrationBase { // When new Users are created, they will choose a random configuration from these params: _configRand({ _randomSeed: _random, - _assetTypes: HOLDS_LST, - _userTypes: DEFAULT | SIGNED_METHODS + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS }); /// 0. Create an operator and a staker with: