diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol index 75f0ddfc0..c3df43a08 100644 --- a/src/test/integration/IntegrationBase.t.sol +++ b/src/test/integration/IntegrationBase.t.sol @@ -4,6 +4,7 @@ pragma solidity =0.8.12; import "forge-std/Test.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; import "src/test/integration/IntegrationDeployer.t.sol"; import "src/test/integration/TimeMachine.t.sol"; @@ -11,6 +12,11 @@ import "src/test/integration/User.t.sol"; abstract contract IntegrationBase is IntegrationDeployer { + using Strings for *; + + uint numStakers = 0; + uint numOperators = 0; + /** * Gen/Init methods: */ @@ -20,7 +26,10 @@ abstract contract IntegrationBase is IntegrationDeployer { * This user is ready to deposit into some strategies and has some underlying token balances */ function _newRandomStaker() internal returns (User, IStrategy[] memory, uint[] memory) { - (User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser(); + string memory stakerName = string.concat("- Staker", numStakers.toString()); + numStakers++; + + (User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser(stakerName); assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "_newRandomStaker: failed to award token balances"); @@ -28,7 +37,10 @@ abstract contract IntegrationBase is IntegrationDeployer { } function _newRandomOperator() internal returns (User, IStrategy[] memory, uint[] memory) { - (User operator, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser(); + string memory operatorName = string.concat("- Operator", numOperators.toString()); + numOperators++; + + (User operator, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser(operatorName); operator.registerAsOperator(); operator.depositIntoEigenlayer(strategies, tokenBalances); @@ -148,8 +160,6 @@ abstract contract IntegrationBase is IntegrationDeployer { bytes32[] memory withdrawalRoots, string memory err ) internal { - bytes32[] memory expectedRoots = _getWithdrawalHashes(withdrawals); - for (uint i = 0; i < withdrawals.length; i++) { assert_ValidWithdrawalHash(withdrawals[i], withdrawalRoots[i], err); } @@ -224,6 +234,28 @@ abstract contract IntegrationBase is IntegrationDeployer { } } + function assert_Snap_Delta_OperatorShares( + User operator, + IStrategy[] memory strategies, + int[] memory shareDeltas, + string memory err + ) internal { + uint[] memory curShares = _getOperatorShares(operator, strategies); + // Use timewarp to get previous operator shares + uint[] memory prevShares = _getPrevOperatorShares(operator, strategies); + + // For each strategy, check (prev + added == cur) + for (uint i = 0; i < strategies.length; i++) { + uint expectedShares; + if (shareDeltas[i] < 0) { + expectedShares = prevShares[i] - uint(-shareDeltas[i]); + } else { + expectedShares = prevShares[i] + uint(shareDeltas[i]); + } + assertEq(expectedShares, curShares[i], err); + } + } + /// Snapshot assertions for strategyMgr.stakerStrategyShares and eigenPodMgr.podOwnerShares: /// @dev Check that the staker has `addedShares` additional delegatable shares @@ -319,6 +351,22 @@ abstract contract IntegrationBase is IntegrationDeployer { } } + function assert_Snap_Delta_StakerShares( + User staker, + IStrategy[] memory strategies, + int[] memory shareDeltas, + string memory err + ) internal { + int[] memory curShares = _getStakerSharesInt(staker, strategies); + // Use timewarp to get previous staker shares + int[] memory prevShares = _getPrevStakerSharesInt(staker, strategies); + + // For each strategy, check (prev + added == cur) + for (uint i = 0; i < strategies.length; i++) { + assertEq(prevShares[i] + shareDeltas[i], curShares[i], err); + } + } + /// Snapshot assertions for underlying token balances: /// @dev Check that the staker has `addedTokens` additional underlying tokens @@ -396,7 +444,6 @@ abstract contract IntegrationBase is IntegrationDeployer { function assert_Snap_Added_QueuedWithdrawal( User staker, - IDelegationManager.Withdrawal memory withdrawal, string memory err ) internal { uint curQueuedWithdrawal = _getCumulativeWithdrawals(staker); @@ -442,9 +489,109 @@ abstract contract IntegrationBase is IntegrationDeployer { return (withdrawStrats, withdrawShares); } - /** - * Helpful getters: - */ + function _randBalanceUpdate( + User staker, + IStrategy[] memory strategies + ) internal returns (int[] memory, int[] memory, int[] memory) { + + int[] memory tokenDeltas = new int[](strategies.length); + int[] memory stakerShareDeltas = new int[](strategies.length); + int[] memory operatorShareDeltas = new int[](strategies.length); + + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + // TODO - could choose and set a "next updatable validator" at random here + uint40 validator = staker.getUpdatableValidator(); + uint64 beaconBalanceGwei = beaconChain.balanceOfGwei(validator); + + // For native eth, add or remove a random amount of Gwei - minimum 1 + // and max of the current beacon chain balance + int64 deltaGwei = int64(int(_randUint({ min: 1, max: beaconBalanceGwei }))); + bool addTokens = _randBool(); + deltaGwei = addTokens ? deltaGwei : -deltaGwei; + + tokenDeltas[i] = int(deltaGwei) * int(GWEI_TO_WEI); + + // stakerShareDeltas[i] = _calculateSharesDelta(newPodBalanceGwei, oldPodBalanceGwei); + stakerShareDeltas[i] = _calcNativeETHStakerShareDelta(staker, validator, beaconBalanceGwei, deltaGwei); + operatorShareDeltas[i] = _calcNativeETHOperatorShareDelta(staker, stakerShareDeltas[i]); + + emit log_named_uint("current beacon balance (gwei): ", beaconBalanceGwei); + // emit log_named_uint("current validator pod balance (gwei): ", oldPodBalanceGwei); + emit log_named_int("beacon balance delta (gwei): ", deltaGwei); + emit log_named_int("staker share delta (gwei): ", stakerShareDeltas[i] / int(GWEI_TO_WEI)); + emit log_named_int("operator share delta (gwei): ", operatorShareDeltas[i] / int(GWEI_TO_WEI)); + } else { + // For LSTs, mint a random token amount + uint portion = _randUint({ min: MIN_BALANCE, max: MAX_BALANCE }); + StdCheats.deal(address(strat.underlyingToken()), address(staker), portion); + + int delta = int(portion); + tokenDeltas[i] = delta; + stakerShareDeltas[i] = int(strat.underlyingToShares(uint(delta))); + operatorShareDeltas[i] = int(strat.underlyingToShares(uint(delta))); + } + } + return (tokenDeltas, stakerShareDeltas, operatorShareDeltas); + } + + function _calcNativeETHStakerShareDelta( + User staker, + uint40 validatorIndex, + uint64 beaconBalanceGwei, + int64 deltaGwei + ) internal view returns (int) { + uint64 oldPodBalanceGwei = + staker + .pod() + .validatorPubkeyHashToInfo(beaconChain.pubkeyHash(validatorIndex)) + .restakedBalanceGwei; + + uint64 newPodBalanceGwei = _calcPodBalance(beaconBalanceGwei, deltaGwei); + + return (int(uint(newPodBalanceGwei)) - int(uint(oldPodBalanceGwei))) * int(GWEI_TO_WEI); + } + + function _calcPodBalance(uint64 beaconBalanceGwei, int64 deltaGwei) internal pure returns (uint64) { + uint64 podBalanceGwei; + if (deltaGwei < 0) { + podBalanceGwei = beaconBalanceGwei - uint64(uint(int(-deltaGwei))); + } else { + podBalanceGwei = beaconBalanceGwei + uint64(uint(int(deltaGwei))); + } + + if (podBalanceGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { + podBalanceGwei = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; + } + + return podBalanceGwei; + } + + function _calcNativeETHOperatorShareDelta(User staker, int shareDelta) internal view returns (int) { + int curPodOwnerShares = eigenPodManager.podOwnerShares(address(staker)); + int newPodOwnerShares = curPodOwnerShares + shareDelta; + + if (curPodOwnerShares <= 0) { + // if the shares started negative and stayed negative, then there cannot have been an increase in delegateable shares + if (newPodOwnerShares <= 0) { + return 0; + // if the shares started negative and became positive, then the increase in delegateable shares is the ending share amount + } else { + return newPodOwnerShares; + } + } else { + // if the shares started positive and became negative, then the decrease in delegateable shares is the starting share amount + if (newPodOwnerShares <= 0) { + return (-curPodOwnerShares); + // if the shares started positive and stayed positive, then the change in delegateable shares + // is the difference between starting and ending amounts + } else { + return (newPodOwnerShares - curPodOwnerShares); + } + } + } /// @dev For some strategies/underlying token balances, calculate the expected shares received /// from depositing all tokens @@ -456,7 +603,7 @@ abstract contract IntegrationBase is IntegrationDeployer { uint tokenBalance = tokenBalances[i]; if (strat == BEACONCHAIN_ETH_STRAT) { - expectedShares[i] = tokenBalances[i]; + expectedShares[i] = tokenBalance; } else { expectedShares[i] = strat.underlyingToShares(tokenBalance); } @@ -570,6 +717,31 @@ abstract contract IntegrationBase is IntegrationDeployer { return curShares; } + /// @dev Uses timewarp modifier to get staker shares at the last snapshot + function _getPrevStakerSharesInt( + User staker, + IStrategy[] memory strategies + ) internal timewarp() returns (int[] memory) { + return _getStakerSharesInt(staker, strategies); + } + + /// @dev Looks up each strategy and returns a list of the staker's shares + function _getStakerSharesInt(User staker, IStrategy[] memory strategies) internal view returns (int[] memory) { + int[] memory curShares = new int[](strategies.length); + + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + curShares[i] = eigenPodManager.podOwnerShares(address(staker)); + } else { + curShares[i] = int(strategyManager.stakerStrategyShares(address(staker), strat)); + } + } + + return curShares; + } + function _getPrevCumulativeWithdrawals(User staker) internal timewarp() returns (uint) { return _getCumulativeWithdrawals(staker); } diff --git a/src/test/integration/IntegrationChecks.t.sol b/src/test/integration/IntegrationChecks.t.sol index edb6548e2..45c36e9d6 100644 --- a/src/test/integration/IntegrationChecks.t.sol +++ b/src/test/integration/IntegrationChecks.t.sol @@ -7,7 +7,11 @@ import "src/test/integration/User.t.sol"; /// @notice Contract that provides utility functions to reuse common test blocks & checks contract IntegrationCheckUtils is IntegrationBase { - function check_Deposit_State(User staker, IStrategy[] memory strategies, uint[] memory shares) internal { + function check_Deposit_State( + User staker, + IStrategy[] memory strategies, + uint[] memory shares + ) internal { /// Deposit into strategies: // For each of the assets held by the staker (either StrategyManager or EigenPodManager), // the staker calls the relevant deposit function, depositing all held assets. @@ -18,7 +22,12 @@ contract IntegrationCheckUtils is IntegrationBase { assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); } - function check_Delegation_State(User staker, User operator, IStrategy[] memory strategies, uint[] memory shares) internal { + function check_Delegation_State( + User staker, + User operator, + IStrategy[] memory strategies, + uint[] memory shares + ) internal { /// Delegate to an operator: // // ... check that the staker is now delegated to the operator, and that the operator @@ -26,10 +35,18 @@ contract IntegrationCheckUtils is IntegrationBase { assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); assert_HasExpectedShares(staker, strategies, shares, "staker should still have expected shares after delegating"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); } - function check_QueuedWithdrawal_State(User staker, User operator, IStrategy[] memory strategies, uint[] memory shares, IDelegationManager.Withdrawal[] memory withdrawals, bytes32[] memory withdrawalRoots) internal { + function check_QueuedWithdrawal_State( + User staker, + User operator, + IStrategy[] memory strategies, + uint[] memory shares, + IDelegationManager.Withdrawal[] memory withdrawals, + bytes32[] memory withdrawalRoots + ) internal { // The staker will queue one or more withdrawals for the selected strategies and shares // // ... check that each withdrawal was successfully enqueued, that the returned roots diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol index 0774df734..cd90b633b 100644 --- a/src/test/integration/IntegrationDeployer.t.sol +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -170,7 +170,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { delayedWithdrawalRouter, eigenPodManager, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, - GOERLI_GENESIS_TIME + 0 ); eigenPodBeacon = new UpgradeableBeacon(address(pod)); @@ -329,7 +329,7 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { * * Assets are pulled from `strategies` based on a random staker/operator `assetType` */ - function _randUser() internal returns (User, IStrategy[] memory, uint[] memory) { + function _randUser(string memory name) internal returns (User, IStrategy[] memory, uint[] memory) { // For the new user, select what type of assets they'll have and whether // they'll use `xWithSignature` methods. // @@ -340,11 +340,11 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { // Create User contract based on deposit type: User user; if (userType == DEFAULT) { - user = new User(); + user = new User(name); } else if (userType == ALT_METHODS) { // User will use nonstandard methods like: // `delegateToBySignature` and `depositIntoStrategyWithSignature` - user = User(new User_AltMethods()); + user = User(new User_AltMethods(name)); } else { revert("_randUser: unimplemented userType"); } @@ -462,6 +462,10 @@ abstract contract IntegrationDeployer is Test, IUserDeployer { return min + value; } + function _randBool() internal returns (bool) { + return _randUint({ min: 0, max: 1 }) == 0; + } + function _randAssetType() internal returns (uint) { uint idx = _randUint({ min: 0, max: assetTypes.length - 1 }); uint assetType = uint(uint8(assetTypes[idx])); diff --git a/src/test/integration/User.t.sol b/src/test/integration/User.t.sol index 50dae0e5a..bf1a177b3 100644 --- a/src/test/integration/User.t.sol +++ b/src/test/integration/User.t.sol @@ -36,14 +36,16 @@ contract User is Test { BeaconChainMock beaconChain; // User's EigenPod and each of their validator indices within that pod - EigenPod pod; + EigenPod public pod; uint40[] validators; IStrategy constant BEACONCHAIN_ETH_STRAT = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); IERC20 constant NATIVE_ETH = IERC20(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); uint constant GWEI_TO_WEI = 1e9; - constructor() { + string public NAME; + + constructor(string memory name) { IUserDeployer deployer = IUserDeployer(msg.sender); delegationManager = deployer.delegationManager(); @@ -53,6 +55,8 @@ contract User is Test { beaconChain = deployer.beaconChain(); pod = EigenPod(payable(eigenPodManager.createPod())); + + NAME = name; } modifier createSnapshot() virtual { @@ -67,6 +71,8 @@ contract User is Test { */ function registerAsOperator() public createSnapshot virtual { + emit log(_name(".registerAsOperator")); + IDelegationManager.OperatorDetails memory details = IDelegationManager.OperatorDetails({ earningsReceiver: address(this), delegationApprover: address(0), @@ -78,6 +84,7 @@ contract User is Test { /// @dev For each strategy/token balance, call the relevant deposit method function depositIntoEigenlayer(IStrategy[] memory strategies, uint[] memory tokenBalances) public createSnapshot virtual { + emit log(_name(".depositIntoEigenlayer")); for (uint i = 0; i < strategies.length; i++) { IStrategy strat = strategies[i]; @@ -117,14 +124,53 @@ contract User is Test { } } + function updateBalances(IStrategy[] memory strategies, int[] memory tokenDeltas) public createSnapshot virtual { + emit log(_name(".updateBalances")); + + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + int delta = tokenDeltas[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + // TODO - right now, we just grab the first validator + uint40 validator = getUpdatableValidator(); + BalanceUpdate memory update = beaconChain.updateBalance(validator, delta); + + int sharesBefore = eigenPodManager.podOwnerShares(address(this)); + + pod.verifyBalanceUpdates({ + oracleTimestamp: update.oracleTimestamp, + validatorIndices: update.validatorIndices, + stateRootProof: update.stateRootProof, + validatorFieldsProofs: update.validatorFieldsProofs, + validatorFields: update.validatorFields + }); + + int sharesAfter = eigenPodManager.podOwnerShares(address(this)); + + emit log_named_int("pod owner shares before: ", sharesBefore); + emit log_named_int("pod owner shares after: ", sharesAfter); + } else { + uint tokens = uint(delta); + IERC20 underlyingToken = strat.underlyingToken(); + underlyingToken.approve(address(strategyManager), tokens); + strategyManager.depositIntoStrategy(strat, underlyingToken, tokens); + } + } + } + /// @dev Delegate to the operator without a signature function delegateTo(User operator) public createSnapshot virtual { + emit log_named_string(_name(".delegateTo: "), operator.NAME()); + ISignatureUtils.SignatureWithExpiry memory emptySig; delegationManager.delegateTo(address(operator), emptySig, bytes32(0)); } /// @dev Undelegate from operator function undelegate() public createSnapshot virtual returns(IDelegationManager.Withdrawal[] memory){ + emit log(_name(".undelegate")); + IDelegationManager.Withdrawal[] memory withdrawal = new IDelegationManager.Withdrawal[](1); withdrawal[0] = _getExpectedWithdrawalStructForStaker(address(this)); delegationManager.undelegate(address(this)); @@ -133,6 +179,8 @@ contract User is Test { /// @dev Force undelegate staker function forceUndelegate(User staker) public createSnapshot virtual returns(IDelegationManager.Withdrawal[] memory){ + emit log_named_string(_name(".forceUndelegate: "), staker.NAME()); + IDelegationManager.Withdrawal[] memory withdrawal = new IDelegationManager.Withdrawal[](1); withdrawal[0] = _getExpectedWithdrawalStructForStaker(address(staker)); delegationManager.undelegate(address(staker)); @@ -144,6 +192,7 @@ contract User is Test { IStrategy[] memory strategies, uint[] memory shares ) public createSnapshot virtual returns (IDelegationManager.Withdrawal[] memory) { + emit log(_name(".queueWithdrawals")); address operator = delegationManager.delegatedTo(address(this)); address withdrawer = address(this); @@ -176,12 +225,40 @@ contract User is Test { return (withdrawals); } + + function completeWithdrawalsAsTokens(IDelegationManager.Withdrawal[] memory withdrawals) public createSnapshot virtual returns (IERC20[][] memory) { + emit log(_name(".completeWithdrawalsAsTokens")); + + IERC20[][] memory tokens = new IERC20[][](withdrawals.length); + + for (uint i = 0; i < withdrawals.length; i++) { + tokens[i] = _completeQueuedWithdrawal(withdrawals[i], true); + } + + return tokens; + } function completeWithdrawalAsTokens(IDelegationManager.Withdrawal memory withdrawal) public createSnapshot virtual returns (IERC20[] memory) { + emit log(_name(".completeWithdrawalAsTokens")); + return _completeQueuedWithdrawal(withdrawal, true); } + function completeWithdrawalsAsShares(IDelegationManager.Withdrawal[] memory withdrawals) public createSnapshot virtual returns (IERC20[][] memory) { + emit log(_name(".completeWithdrawalsAsShares")); + + IERC20[][] memory tokens = new IERC20[][](withdrawals.length); + + for (uint i = 0; i < withdrawals.length; i++) { + tokens[i] = _completeQueuedWithdrawal(withdrawals[i], false); + } + + return tokens; + } + function completeWithdrawalAsShares(IDelegationManager.Withdrawal memory withdrawal) public createSnapshot virtual returns (IERC20[] memory) { + emit log(_name(".completeWithdrawalAsShares")); + return _completeQueuedWithdrawal(withdrawal, false); } @@ -255,6 +332,14 @@ contract User is Test { shares: shares }); } + + function _name(string memory s) internal view returns (string memory) { + return string.concat(NAME, s); + } + + function getUpdatableValidator() public view returns (uint40) { + return validators[0]; + } } /// @notice A user contract that calls nonstandard methods (like xBySignature methods) @@ -262,9 +347,10 @@ contract User_AltMethods is User { mapping(bytes32 => bool) public signedHashes; - constructor() User() {} + constructor(string memory name) User(name) {} function delegateTo(User operator) public createSnapshot override { + emit log_named_string(_name(".delegateTo: "), operator.NAME()); // Create empty data ISignatureUtils.SignatureWithExpiry memory emptySig; uint256 expiry = type(uint256).max; @@ -286,6 +372,8 @@ contract User_AltMethods is User { } function depositIntoEigenlayer(IStrategy[] memory strategies, uint[] memory tokenBalances) public createSnapshot override { + emit log(_name(".depositIntoEigenlayer")); + uint256 expiry = type(uint256).max; for (uint i = 0; i < strategies.length; i++) { IStrategy strat = strategies[i]; diff --git a/src/test/integration/mocks/BeaconChainMock.t.sol b/src/test/integration/mocks/BeaconChainMock.t.sol index a1a5dc05f..b7a121a2f 100644 --- a/src/test/integration/mocks/BeaconChainMock.t.sol +++ b/src/test/integration/mocks/BeaconChainMock.t.sol @@ -26,6 +26,14 @@ struct BeaconWithdrawal { bytes32[][] withdrawalFields; } +struct BalanceUpdate { + uint64 oracleTimestamp; + BeaconChainProofs.StateRootProof stateRootProof; + uint40[] validatorIndices; + bytes[] validatorFieldsProofs; + bytes32[][] validatorFields; +} + contract BeaconChainMock is Test { Vm cheats = Vm(HEVM_ADDRESS); @@ -68,6 +76,8 @@ contract BeaconChainMock is Test { uint balanceWei, bytes memory withdrawalCreds ) public returns (uint40, CredentialsProofs memory) { + emit log_named_uint("- BeaconChain.newValidator with balance: ", balanceWei); + // These checks mimic the checks made in the beacon chain deposit contract // // We sanity-check them here because this contract sorta acts like the @@ -104,6 +114,8 @@ contract BeaconChainMock is Test { * destination. */ function exitValidator(uint40 validatorIndex) public returns (BeaconWithdrawal memory) { + emit log_named_uint("- BeaconChain.exitValidator: ", validatorIndex); + Validator memory validator = validators[validatorIndex]; // Get the withdrawal amount and destination @@ -120,6 +132,39 @@ contract BeaconChainMock is Test { return withdrawal; } + /** + * Note: `delta` is expected to be a raw token amount. This method will convert the delta to Gwei + */ + function updateBalance(uint40 validatorIndex, int delta) public returns (BalanceUpdate memory) { + delta /= int(GWEI_TO_WEI); + + emit log_named_uint("- BeaconChain.updateBalance for validator: ", validatorIndex); + emit log_named_int("- BeaconChain.updateBalance delta gwei: ", delta); + + // Apply delta and update validator balance in state + uint64 newBalance; + if (delta <= 0) { + newBalance = validators[validatorIndex].effectiveBalanceGwei - uint64(uint(-delta)); + } else { + newBalance = validators[validatorIndex].effectiveBalanceGwei + uint64(uint(delta)); + } + validators[validatorIndex].effectiveBalanceGwei = newBalance; + + // Generate balance update proof + Validator memory validator = validators[validatorIndex]; + BalanceUpdate memory update = _genBalanceUpdateProof(validator); + + return update; + } + + function balanceOfGwei(uint40 validatorIndex) public view returns (uint64) { + return validators[validatorIndex].effectiveBalanceGwei; + } + + function pubkeyHash(uint40 validatorIndex) public view returns (bytes32) { + return validators[validatorIndex].pubkeyHash; + } + /** * INTERNAL/HELPER METHODS: */ @@ -298,6 +343,54 @@ contract BeaconChainMock is Test { return withdrawal; } + function _genBalanceUpdateProof(Validator memory validator) internal returns (BalanceUpdate memory) { + BalanceUpdate memory update; + + update.validatorIndices = new uint40[](1); + update.validatorIndices[0] = validator.validatorIndex; + + // Create validatorFields showing the balance update + update.validatorFields = new bytes32[][](1); + update.validatorFields[0] = new bytes32[](2 ** BeaconChainProofs.VALIDATOR_FIELD_TREE_HEIGHT); + update.validatorFields[0][BeaconChainProofs.VALIDATOR_PUBKEY_INDEX] = validator.pubkeyHash; + update.validatorFields[0][BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] = + bytes32(validator.withdrawalCreds); + update.validatorFields[0][BeaconChainProofs.VALIDATOR_BALANCE_INDEX] = + _toLittleEndianUint64(validator.effectiveBalanceGwei); + + // Calculate beaconStateRoot using validator index and an empty proof: + update.validatorFieldsProofs = new bytes[](1); + update.validatorFieldsProofs[0] = new bytes(VAL_FIELDS_PROOF_LEN); + bytes32 validatorRoot = Merkle.merkleizeSha256(update.validatorFields[0]); + uint index = _calcValProofIndex(validator.validatorIndex); + + bytes32 beaconStateRoot = Merkle.processInclusionProofSha256({ + proof: update.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 + }); + + update.stateRootProof = BeaconChainProofs.StateRootProof({ + beaconStateRoot: beaconStateRoot, + proof: blockRootProof + }); + + // Send the block root to the oracle and increment timestamp: + update.oracleTimestamp = uint64(nextTimestamp); + oracle.setBlockRoot(nextTimestamp, blockRoot); + nextTimestamp++; + + return update; + } + /** * @dev Generates converging merkle proofs for timestampRoot and withdrawalRoot * under the executionPayloadRoot. diff --git a/src/test/integration/tests/Deposit_Delegate_UpdateBalance.t.sol b/src/test/integration/tests/Deposit_Delegate_UpdateBalance.t.sol new file mode 100644 index 000000000..a781ca6f4 --- /dev/null +++ b/src/test/integration/tests/Deposit_Delegate_UpdateBalance.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "src/test/integration/IntegrationChecks.t.sol"; +import "src/test/integration/User.t.sol"; + +contract Integration_Deposit_Delegate_UpdateBalance is IntegrationCheckUtils { + + /// Generates a random stake and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a ALL shares + /// 4. updates their balance randomly + /// 5. completes the queued withdrawal as tokens + function testFuzz_deposit_delegate_updateBalance_completeAsTokens(uint24 _random) public { + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create an operator and staker with some underlying assets + ( + User staker, + IStrategy[] memory strategies, + uint[] memory tokenBalances + ) = _newRandomStaker(); + (User operator, ,) = _newRandomOperator(); + uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances); + + assert_HasNoDelegatableShares(staker, "staker should not have delegatable shares before depositing"); + assertFalse(delegationManager.isDelegated(address(staker)), "staker should not be delegated"); + + /// 1. Deposit into strategies + staker.depositIntoEigenlayer(strategies, tokenBalances); + check_Deposit_State(staker, strategies, shares); + + /// 2. Delegate to an operator + staker.delegateTo(operator); + check_Delegation_State(staker, operator, strategies, shares); + + /// 3. Queue withdrawals for ALL shares + IDelegationManager.Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares); + bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals); + check_QueuedWithdrawal_State(staker, operator, strategies, shares, withdrawals, withdrawalRoots); + + // Generate a random balance update: + // - For LSTs, the tokenDelta is positive tokens minted to the staker + // - For ETH, the tokenDelta is a positive or negative change in beacon chain balance + ( + int[] memory tokenDeltas, + int[] memory stakerShareDeltas, + int[] memory operatorShareDeltas + ) = _randBalanceUpdate(staker, strategies); + + // 4. Update LST balance by depositing, and beacon balance by submitting a proof + staker.updateBalances(strategies, tokenDeltas); + assert_Snap_Delta_StakerShares(staker, strategies, stakerShareDeltas, "staker should have applied deltas correctly"); + assert_Snap_Delta_OperatorShares(operator, strategies, operatorShareDeltas, "operator should have applied deltas correctly"); + + // Fast forward to when we can complete the withdrawal + cheats.roll(block.number + delegationManager.withdrawalDelayBlocks()); + + // 5. Complete queued withdrawals as tokens + staker.completeWithdrawalsAsTokens(withdrawals); + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed"); + assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); + } +} \ No newline at end of file