diff --git a/.github/workflows/certora-prover.yml b/.github/workflows/certora-prover.yml index a464608ed..e1b3747ee 100644 --- a/.github/workflows/certora-prover.yml +++ b/.github/workflows/certora-prover.yml @@ -43,7 +43,7 @@ jobs: distribution: temurin java-version: '17' - name: Install certora - run: pip install certora-cli + run: pip install certora-cli==4.13.1 - name: Install solc run: | wget https://github.com/ethereum/solidity/releases/download/v0.8.12/solc-static-linux diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 000000000..352413f05 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,23 @@ +name: CI + +on: [push, pull_request] + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Install node dependencies + run: | + npm install conventional-changelog-conventionalcommits + npm install commitlint@latest + + - name: Validate current commit (last commit) with commitlint + if: github.event_name == 'push' + run: npx commitlint --from HEAD~1 --to HEAD --verbose + + - name: Validate PR commits with commitlint + if: github.event_name == 'pull_request' + run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose diff --git a/.github/workflows/testinparallel.yml b/.github/workflows/testinparallel.yml index b3103c825..97679d277 100644 --- a/.github/workflows/testinparallel.yml +++ b/.github/workflows/testinparallel.yml @@ -27,6 +27,7 @@ jobs: strategy: matrix: file: ${{fromJson(needs.prepare.outputs.matrix)}} + fail-fast: false steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 000000000..c160a7712 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit ${1} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..8717c11ab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ + +## Requirements + +Foundry +Git +Node.js + +## Setup Repo + +```bash +git clone git@github.com:Layr-Labs/eigenlayer-contracts.git +``` + +### Install Dependencies + +```bash +npm install + +npx husky install + +forge install +``` + +### Pull Requests + +All tests must pass + +Commits must be linted + +A descriptive summary of the PR has been provided. + +### Environment Variables + +Some of the tests and features of this repository require environment variables to be set up. + +Copy the .env.example file to create a .env and populate it with the appropriate environment values that you have control over diff --git a/CVL2.py b/CVL2.py deleted file mode 100644 index 35df1a535..000000000 --- a/CVL2.py +++ /dev/null @@ -1,287 +0,0 @@ -#!python3 - -import argparse -import os -import re -from functools import reduce -from itertools import chain -from pathlib import Path -from sys import stderr -from typing import Any, Dict, Optional -from typing import Tuple - -METHOD_NAME_RE = r'(?P[a-zA-Z_][a-zA-Z_0-9.]*)' -sinvoke_re = re.compile(rf'(\bsinvoke\s+{METHOD_NAME_RE})\s*\(') -invoke_re = re.compile(rf'(\binvoke\s+{METHOD_NAME_RE})\s*\(') -# Some funny stuff here in order to properly parse 'if(...foo().selector)' -selector_re = re.compile(rf'[^:]\b{METHOD_NAME_RE}(?=(\s*\([^)]*\)\.selector))') -# Find the start of the methods block. Ignore methods block that may be all on one -# line - this confuses the rest of the script and is not worth it. -methods_block_re = re.compile(r'methods\s*{\s*(?://.*)?$', re.MULTILINE) -method_line_re = re.compile(rf'\s*(?:/\*.*\*/)?\s*(?Pfunction)?\s*(?P{METHOD_NAME_RE}\s*\()') -rule_prefix_re = re.compile(rf'^{METHOD_NAME_RE}+\s*(\([^)]*\))?\s*{{?\s*$') -double_sig = re.compile(rf'sig:{METHOD_NAME_RE}.sig:') - -def fixup_sinvoke(line: str) -> Tuple[str, int]: - matches = sinvoke_re.findall(line) - # stat[] += len(matches) - return (reduce(lambda l, m: l.replace(m[0], f'{m[1]}'), - matches, - line), - len(matches)) - - -def fixup_invoke(line: str) -> Tuple[str, int]: - matches = invoke_re.findall(line) - return (reduce(lambda l, m: l.replace(m[0], f'{m[1]}@withrevert'), - matches, - line), - len(matches)) - - -def fixup_selector_sig(line: str) -> Tuple[str, int]: - matches = selector_re.findall(line) - matches = list(filter(lambda m: m[0] not in ('if', 'require', 'assert'), matches)) - l, n = (reduce(lambda l, m: l.replace(m[0] + m[1], f'sig:{m[0] + m[1]}'), - matches, - line), - len(matches)) - # fix double sig - double_sig_matches = double_sig.findall(l) - for match in double_sig_matches: - l = l.replace(f'sig:{match}.sig:', f'sig:{match}.') - return l, n - - -def fixup_rule_prefix(line: str) -> Tuple[str, int]: - matches = rule_prefix_re.match(line) - if matches and matches['name'] != "methods": - return 'rule ' + line, 1 - return line, 0 - - -def fixup_static_assert(line: str) -> Tuple[str, int]: - if line.lstrip().startswith('static_assert '): - return (line.replace('static_assert', 'assert', 1), 1) - return (line, 0) - - -def fixup_static_require(line: str) -> Tuple[str, int]: - if line.lstrip().startswith('static_require '): - return (line.replace('static_require', 'require', 1), 1) - return (line, 0) - - -def find_invoke_whole(line: str) -> bool: - return 'invoke_whole(' in line - - -def methods_block_add_semicolon(line: str, next_line: Optional[str]) -> Tuple[str, int]: - l, *comment = line.split('//', 1) - l = l.rstrip() - do_nothing = (line, 0) - if not l.lstrip(): - # an empty line - return do_nothing - - if any(l.endswith(s) for s in (';', '=>', '(', '{', '/*', '/**', '*/', ',')): - # this line doesn't need a semicolon - return do_nothing - - if any(w in l.split() for w in ('if', 'else')): - # this is a branching line, skip it - return do_nothing - - if next_line is not None and (next_line.lstrip().startswith("=>") or next_line.lstrip().startswith(')')): - # the method's summary is defined in the next line, don't append a ; - # also if we have a ) in the next line it means we broke lines for the parameters - return do_nothing - - return l + ';' + (f' //{comment[0]}' if comment else ''), 1 - - -def methods_block_prepend_function(line: str) -> Tuple[str, int]: - m = method_line_re.match(line) - if m is not None and m['func'] is None: - return line.replace(m['name_w_paren'], f'function {m["name_w_paren"]}', 1), 1 - return line, 0 - - -def methods_block_add_external_visibility_no_summary(line: str) -> Tuple[str, int]: - m = method_line_re.match(line) - if m is not None and ('=>' not in line or '=> DISPATCHER' in line): - replacables = [' returns ', ' returns(', ' envfree', ' =>', ';'] - for r in replacables: - if r in line: - if all(f' {vis}{r2}' not in line for vis in ('internal', 'external') for r2 in replacables): - line = line.replace(r, f' external{r}') - return line, 1 - return line, 0 - else: - print(f"Unable to add 'external' modifier to {line}") - return line, 0 - - -def methods_block_summary_should_have_wildcard(line: str) -> Tuple[str, int]: - m = method_line_re.match(line) - if m is not None and '=>' in line and '.' not in line and ' internal ' not in line: - line = line.replace("function ", "function _.") - return line, 1 - - return line, 0 - - -def append_semicolons_to_directives(line: str) -> Tuple[str, int]: - if line.lstrip().startswith(('pragma', 'import', 'using', 'use ')) and not line.rstrip().endswith((';', '{')): - line = line.rstrip() + ';' + os.linesep - return line, 1 - return line, 0 - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument('-f', '--files', metavar='FILE', nargs='+', help='list of files to change', type=Path, - default=list()) - parser.add_argument('-d', '--dirs', metavar='DIR', nargs='+', help='the dir to search', type=Path, default=list()) - parser.add_argument('-r', '--recursive', action='store_true', help='if set dirs will be searched recursively') - args = parser.parse_args() - - if not args.dirs and not args.files: - print('No files/dirs specified', file=stderr) - return 1 - if non_existent := list(filter(lambda f: not (f.exists() and f.is_file()), args.files)): - print(f'Cannot find files {non_existent}', file=stderr) - return 1 - if non_existent := list(filter(lambda d: not (d.exists() and d.is_dir()), args.dirs)): - print(f'Cannot find dirs {non_existent}', file=stderr) - return 1 - - spec_files: chain = chain() - for d in args.dirs: - dir_files_unfiltered = (f for f in d.glob(f'{"**/" if args.recursive else ""}*') - if f.suffix in ('.cvl', '.spec')) - dir_files = filter( - lambda fname: not any(p.startswith('.certora_config') for p in fname.parts), - dir_files_unfiltered) - spec_files = chain(spec_files, dir_files) - spec_files = chain(spec_files, args.files) - - sinvoke_str = 'sinvoke foo(...) -> foo(...)' - invoke_str = 'invoke foo(...) -> foo@withrevert(...)' - static_assert_str = 'static_assert -> assert' - static_require_str = 'static_require -> require' - invoke_whole_str = "lines with 'invoke_whole'" - sig_selector_str = "selectors prepended with 'sig:'" - rule_prefix_str = "rule declarations prepended with 'rule'" - semicolon_in_directives_str = "'pragma', 'import', 'using' or 'use' directives appended with ';'" - semicolon_in_methods_str = "lines in 'methods' block appended with ';'" - prepend_function_for_methods_str = "lines in 'methods' block prepended with 'function'" - add_external_visibility_for_non_summary_str = "declarations in 'methods' block with 'external' visibility added" - summary_should_have_wildcard_str = "declarations in 'methods' block with wildcard added" - - stats: Dict[Any, Any] = {} - for fname in spec_files: - print(f"processing {fname}") - stats[fname] = {} - stats[fname][sinvoke_str] = 0 - stats[fname][invoke_str] = 0 - stats[fname][static_assert_str] = 0 - stats[fname][static_require_str] = 0 - stats[fname][sig_selector_str] = 0 - stats[fname][rule_prefix_str] = 0 - stats[fname][invoke_whole_str] = [] - stats[fname][semicolon_in_directives_str] = 0 - stats[fname][semicolon_in_methods_str] = 0 - stats[fname][prepend_function_for_methods_str] = 0 - stats[fname][add_external_visibility_for_non_summary_str] = 0 - stats[fname][summary_should_have_wildcard_str] = 0 - - flines = open(fname).readlines() - for i, l in enumerate(flines): - l, num = fixup_sinvoke(l) - stats[fname][sinvoke_str] += num - - l, num = fixup_invoke(l) - stats[fname][invoke_str] += num - - l, num = fixup_static_assert(l) - stats[fname][static_assert_str] += num - - l, num = fixup_static_require(l) - stats[fname][static_require_str] += num - - l, num = append_semicolons_to_directives(l) - stats[fname][semicolon_in_directives_str] += num - - l, num = fixup_selector_sig(l) - stats[fname][sig_selector_str] += num - flines[i] = l - - l, num = fixup_rule_prefix(l) - stats[fname][rule_prefix_str] += num - flines[i] = l - - with open(fname, 'w') as f: - f.writelines(flines) - - # methods block - contents = open(fname).read() - methods_block_declaration = methods_block_re.search(contents) - if not methods_block_declaration: - print(f"{fname} has no methods block, skipping...") - continue - try: - prev, methods_block = methods_block_re.split(contents) - except ValueError: - print(f"{fname}: Failed to find methods block - more than one 'methods' block found") - continue - - try: - methods_block, rest = re.split(r'^}', methods_block, maxsplit=1, flags=re.MULTILINE) - except ValueError: - print(f"{fname}: Failed to find methods block - couldn't find block end") - continue - - if methods_block.count("{") != methods_block.count("}"): - print(f"{fname}: Failed to find methods block - something went wrong with finding the end of the block") - continue - - # add semicolons in the methods block where it's easy to do so - methods_block = methods_block.split("\n") - for i, l in enumerate(methods_block): - next_line = methods_block[i + 1] if i + 1 < len(methods_block) else None - l, n = methods_block_add_semicolon(l, next_line) - stats[fname][semicolon_in_methods_str] += n - l, n = methods_block_prepend_function(l) - stats[fname][prepend_function_for_methods_str] += n - l, n = methods_block_add_external_visibility_no_summary(l) - stats[fname][add_external_visibility_for_non_summary_str] += n - l, n = methods_block_summary_should_have_wildcard(l) - stats[fname][summary_should_have_wildcard_str] += n - methods_block[i] = l - methods_block = "\n".join(methods_block) - - with open(fname, 'w') as f: - f.write(prev + methods_block_declaration.group(0) + methods_block + '}' + rest) - - print('Change Statistics\n-----------------') - for fname, stat in stats.items(): - if all(not n for n in stat.values()): - continue - print(f'{fname}:') - for s, n in stat.items(): - if not n: - continue - print(f'\t{s}: {n}') - - print("The following files have instances of 'invoke_whole' which need to be removed manually") - for fname in stats: - inv_whole_list = stats[fname][invoke_whole_str] - if not inv_whole_list: - continue - print(f"\t{fname} (line{'s' if len(inv_whole_list) > 1 else ''} {', '.join(str(n) for n in inv_whole_list)})") - return 0 - - -if __name__ == "__main__": - exit(main()) \ No newline at end of file diff --git a/README.md b/README.md index fb31e1c85..e7dfc0880 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,18 @@

🚧 The Slasher contract is under active development and its interface expected to change. We recommend writing slashing logic without integrating with the Slasher at this point in time. 🚧

-EigenLayer (formerly 'EigenLayr') is a set of smart contracts deployed on Ethereum that enable restaking of assets to secure new services. -At present, this repository contains *both* the contracts for EigenLayer *and* a set of general "middleware" contracts, designed to be reusable across different applications built on top of EigenLayer. -Note that the interactions between middleware and EigenLayer are not yet "set in stone", and may change somewhat prior to the platform being fully live on mainnet; in particular, payment architecture is likely to evolve. As such, the "middleware" contracts should not be treated as definitive, but merely as a helpful reference, at least until the architecture is more settled. +EigenLayer is a set of smart contracts deployed on Ethereum that enable restaking of assets to secure new services. -Click the links in the Table of Contents below to access more specific documentation. We recommend starting with the [EigenLayer Technical Specification](docs/EigenLayer-tech-spec.md) to get a better overview before diving into any of the other docs. For contracts addresses deployed on Goerli, click [here](https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/script/output/M1_deployment_goerli_2023_3_23.json). -## Table of Contents -* [Introduction](#introduction) -* [Installation and Running Tests / Analyzers](#installation) -* [EigenLayer Technical Specification](docs/EigenLayer-tech-spec.md) +We recommend starting with the [technical documentation](docs/README.md) to get an overview of the contracts before diving into the code. + +For deployment addresses on both mainnet and Goerli, see [Deployments](#deployments) below. -Design Docs -* [Withdrawals Design Doc](docs/Guaranteed-stake-updates.md) -* [EigenPods Design Doc](docs/EigenPods.md) +## Table of Contents -Flow Docs -* [EigenLayer Withdrawal Flow](docs/EigenLayer-withdrawal-flow.md) -* [EigenLayer Deposit Flow](docs/EigenLayer-deposit-flow.md) -* [EigenLayer Delegation Flow](docs/EigenLayer-delegation-flow.md) -* [Middleware Registration Flow for Operators](docs/Middleware-registration-operator-flow.md) +* [Installation and Running Tests / Analyzers](#installation-and-running-tests--analyzers) +* [Technical Documentation](docs/README.md) +* [Deployments](#deployments) ## Installation and Running Tests / Analyzers @@ -44,7 +36,7 @@ See the [Foundry Docs](https://book.getfoundry.sh/) for more info on installatio You will notice that we also have hardhat installed in this repo. This is only used to generate natspec [docgen](https://github.com/OpenZeppelin/solidity-docgen). This is our workaround until foundry [finishes implementing](https://github.com/foundry-rs/foundry/issues/1675) the `forge doc` command. -To generate the docs, run `npx hardhat docgen` (you may need to run `npm install` first). The output is located in `docs/docgen` +To generate the docs, run `npx hardhat docgen` (you may need to run `npm install` first). ### Run Tests @@ -55,11 +47,12 @@ The main command to run tests is: `forge test -vv` ### Run Tests on a Fork -Environment config is contained in config.yml. Before running the following commands, [install yq](https://mikefarah.gitbook.io/yq/v/v3.x/). Then set up the environment with this script: + +Environment config is contained in config.yml. Before running the following commands, [install yq](https://mikefarah.gitbook.io/yq/v/v3.x/). Then set up the environment with this script: `source source-env.sh [CHAIN]` -for example, on goerli: `source source-env.sh goerli`. Currently options for `[CHAIN]` are `goerli`, `local`. Then to run the actual tests: +For example, on goerli: `source source-env.sh goerli`. Currently options for `[CHAIN]` are `goerli`, `local`. Then to run the actual tests: `forge test --fork-url [RPC_URL]` @@ -71,7 +64,7 @@ for example, on goerli: `source source-env.sh goerli`. Currently options for `[ ### Generate Inheritance and Control-Flow Graphs -first [install surya](https://github.com/ConsenSys/surya/) +First [install surya](https://github.com/ConsenSys/surya/) then run @@ -114,13 +107,13 @@ and/or | Name | Solidity | Proxy | Implementation | Notes | | -------- | -------- | -------- | -------- | -------- | -| StrategyManager | [`StrategyManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/core/StrategyManager.sol) | [`0x779...8E907`](https://goerli.etherscan.io/address/0x779d1b5315df083e3F9E94cB495983500bA8E907) | [`0x3FFa...3dE6`](https://goerli.etherscan.io/address/0x3FFa9daE46d15f15c925d4694c19c49e624b3dE6) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | +| StrategyManager | [`StrategyManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/core/StrategyManager.sol) | [`0x779...8E907`](https://goerli.etherscan.io/address/0x779d1b5315df083e3F9E94cB495983500bA8E907) | [`0x8676...0055`](https://goerli.etherscan.io/address/0x8676bb5f792ED407a237234Fe422aC6ed3540055) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | | Strategy: stETH | [`StrategyBaseTVLLimits`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/strategies/StrategyBaseTVLLimits.sol) | [`0xB6...d14da`](https://goerli.etherscan.io/address/0xb613e78e2068d7489bb66419fb1cfa11275d14da) | [`0x81E9...F8ebA`](https://goerli.etherscan.io/address/0x81E94e16949AC397d508B5C2557a272faD2F8ebA) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | | Strategy: rETH | [`StrategyBaseTVLLimits`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/strategies/StrategyBaseTVLLimits.sol) | [`0x8799...70b5`](https://goerli.etherscan.io/address/0x879944A8cB437a5f8061361f82A6d4EED59070b5) | [`0x81E9...F8ebA`](https://goerli.etherscan.io/address/0x81E94e16949AC397d508B5C2557a272faD2F8ebA) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | -| EigenPodManager | [`EigenPodManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/pods/EigenPodManager.sol) | [`0xa286...df41`](https://goerli.etherscan.io/address/0xa286b84C96aF280a49Fe1F40B9627C2A2827df41) | [`0x8563...582`](https://goerli.etherscan.io/address/0x856329254D0049093F6Dfa7dbF9dCC914c951582) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | -| EigenPod (beacon) | [`EigenPod`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/pods/EigenPod.sol) | [`0x309...8C9a5`](https://goerli.etherscan.io/address/0x3093F3B560352F896F0e9567019902C9Aff8C9a5) | [`0x0062...7F371`](https://goerli.etherscan.io/address/0x0062645382af44593ba2e453f51604833277f371) | - Beacon: [OpenZeppelin BeaconProxy@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/beacon/BeaconProxy.sol)
- Deployed pods use [UpgradableBeacon@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/beacon/UpgradeableBeacon.sol) | +| EigenPodManager | [`EigenPodManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/pods/EigenPodManager.sol) | [`0xa286b...df41`](https://goerli.etherscan.io/address/0xa286b84C96aF280a49Fe1F40B9627C2A2827df41) | [`0xdD09...901b`](https://goerli.etherscan.io/address/0xdD09d95bD25299EDBF4f33d76F84dBc77b0B901b) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | +| EigenPod (beacon) | [`EigenPod`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/pods/EigenPod.sol) | [`0x3093...C9a5`](https://goerli.etherscan.io/address/0x3093F3B560352F896F0e9567019902C9Aff8C9a5) | [`0x86bf...6CcA`](https://goerli.etherscan.io/address/0x86bf376E0C0c9c6D332E13422f35Aca75C106CcA) | - Beacon: [OpenZeppelin BeaconProxy@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/beacon/BeaconProxy.sol)
- Deployed pods use [UpgradableBeacon@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/beacon/UpgradeableBeacon.sol) | | DelayedWithdrawalRouter | [`DelayedWithdrawalRouter`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/pods/DelayedWithdrawalRouter.sol) | [`0x895...388f`](https://goerli.etherscan.io/address/0x89581561f1F98584F88b0d57c2180fb89225388f) | [`0x607...7fe`](https://goerli.etherscan.io/address/0x60700ade3Bf48C437a5D01b6Da8d7483cffa27fe) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | -| DelegationManager | [`DelegationManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/core/DelegationManager.sol) | [`0x1b7...Eb0a8`](https://goerli.etherscan.io/address/0x1b7b8F6b258f95Cf9596EabB9aa18B62940Eb0a8) | [`0x992...13A0`](https://goerli.etherscan.io/address/0x9928A49fD7c8580220fE8E6009a8ad87B44713A0) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | +| DelegationManager | [`DelegationManager`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/core/DelegationManager.sol) | [`0x1b7...Eb0a8`](https://goerli.etherscan.io/address/0x1b7b8F6b258f95Cf9596EabB9aa18B62940Eb0a8) | [`0x9b79...A99d`](https://goerli.etherscan.io/address/0x9b7980a32ceCe2Aa936DD2E43AF74af62581A99d) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | | Slasher | [`Slasher`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/core/Slasher.sol) | [`0xD1...0C22`](https://goerli.etherscan.io/address/0xD11d60b669Ecf7bE10329726043B3ac07B380C22) | [`0x3865...8Be6`](https://goerli.etherscan.io/address/0x3865B5F5297f86c5295c7f818BAD1fA5286b8Be6) | Proxy: [OpenZeppelin TUP@4.7.1](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol) | | PauserRegistry | [`PauserRegistry`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/permissions/PauserRegistry.sol) | - | [`0x81E9...F8ebA`](https://goerli.etherscan.io/address/0x81E94e16949AC397d508B5C2557a272faD2F8ebA) | | | Timelock | [Compound: `Timelock.sol`](https://github.com/compound-finance/compound-protocol/blob/a3214f67b73310d547e00fc578e8355911c9d376/contracts/Timelock.sol) | - | [`0xa7e7...796e`](https://goerli.etherscan.io/address/0xa7e72a0564ebf25fa082fc27020225edeaf1796e) | | diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 000000000..422b19445 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +module.exports = { extends: ['@commitlint/config-conventional'] }; diff --git a/docs/core/DelegationManager.md b/docs/core/DelegationManager.md index dd4adbc51..7f5ee2f07 100644 --- a/docs/core/DelegationManager.md +++ b/docs/core/DelegationManager.md @@ -63,7 +63,7 @@ Registers the caller as an Operator in EigenLayer. The new Operator provides the * `address delegationApprover`: if set, this address must sign and approve new delegation from Stakers to this Operator *(optional)* * `uint32 stakerOptOutWindowBlocks`: the minimum delay (in blocks) between beginning and completing registration for an AVS. *(currently unused)* -`registerAsOperator` cements the Operator's `OperatorDetails`, and self-delegates the Operator to themselves - permanently marking the caller as an Operator. They cannot "deregister" as an Operator - however, they can exit the system by withdrawing their funds via `queueWithdrawal`. +`registerAsOperator` cements the Operator's `OperatorDetails`, and self-delegates the Operator to themselves - permanently marking the caller as an Operator. They cannot "deregister" as an Operator - however, they can exit the system by withdrawing their funds via `queueWithdrawals`. *Effects*: * Sets `OperatorDetails` for the Operator in question @@ -165,7 +165,7 @@ Allows a Staker to delegate to an Operator by way of signature. This function ca These methods can be called by both Stakers AND Operators, and are used to (i) undelegate a Staker from an Operator, (ii) queue a withdrawal of a Staker/Operator's shares, or (iii) complete a queued withdrawal: * [`DelegationManager.undelegate`](#undelegate) -* [`DelegationManager.queueWithdrawal`](#queuewithdrawal) +* [`DelegationManager.queueWithdrawals`](#queuewithdrawals) * [`DelegationManager.completeQueuedWithdrawal`](#completequeuedwithdrawal) * [`DelegationManager.completeQueuedWithdrawals`](#completequeuedwithdrawals) @@ -207,44 +207,44 @@ Note that becoming an Operator is irreversible! Although Operators can withdraw, * See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares) * See [`StrategyManager.removeShares`](./StrategyManager.md#removeshares) -#### `queueWithdrawal` +#### `queueWithdrawals` ```solidity -function queueWithdrawal( - IStrategy[] calldata strategies, - uint[] calldata shares, - address withdrawer +function queueWithdrawals( + QueuedWithdrawalParams[] calldata queuedWithdrawalParams ) external onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE) - returns (bytes32) + returns (bytes32[] memory) ``` -Allows the caller to queue a withdrawal of their held shares across any strategy (in either/both the `EigenPodManager` or `StrategyManager`). If the caller is delegated to an Operator, the `shares` and `strategies` being withdrawn are immediately removed from that Operator's delegated share balances. Note that if the caller is an Operator, this still applies, as Operators are essentially delegated to themselves. +Allows the caller to queue one or more withdrawals of their held shares across any strategy (in either/both the `EigenPodManager` or `StrategyManager`). If the caller is delegated to an Operator, the `shares` and `strategies` being withdrawn are immediately removed from that Operator's delegated share balances. Note that if the caller is an Operator, this still applies, as Operators are essentially delegated to themselves. -`queueWithdrawal` works very similarly to `undelegate`, except that the caller is not undelegated, and also may: +`queueWithdrawals` works very similarly to `undelegate`, except that the caller is not undelegated, and also may: * Choose which strategies and how many shares to withdraw (as opposed to ALL shares/strategies) * Specify a `withdrawer` to receive withdrawn funds once the withdrawal is completed -All shares being withdrawn (whether via the `EigenPodManager` or `StrategyManager`) are removed while the withdrawal is in the queue. +All shares being withdrawn (whether via the `EigenPodManager` or `StrategyManager`) are removed while the withdrawals are in the queue. -The withdrawal can be completed by the `withdrawer` after `withdrawalDelayBlocks`, and does not require the `withdrawer` to "fully exit" from the system -- they may choose to receive their shares back in full once the withdrawal is completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details). +Withdrawals can be completed by the `withdrawer` after `withdrawalDelayBlocks`, and does not require the `withdrawer` to "fully exit" from the system -- they may choose to receive their shares back in full once the withdrawal is completed (see [`completeQueuedWithdrawal`](#completequeuedwithdrawal) for details). *Effects*: -* If the caller is delegated to an Operator, that Operator's delegated balances are decreased according to the `strategies` and `shares` being withdrawn. -* A `Withdrawal` is queued for the `withdrawer`, tracking the strategies and shares being withdrawn - * The caller's withdrawal nonce is increased - * The hash of the `Withdrawal` is marked as "pending" -* See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares) -* See [`StrategyManager.removeShares`](./StrategyManager.md#removeshares) +* For each withdrawal: + * If the caller is delegated to an Operator, that Operator's delegated balances are decreased according to the `strategies` and `shares` being withdrawn. + * A `Withdrawal` is queued for the `withdrawer`, tracking the strategies and shares being withdrawn + * The caller's withdrawal nonce is increased + * The hash of the `Withdrawal` is marked as "pending" + * See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares) + * See [`StrategyManager.removeShares`](./StrategyManager.md#removeshares) *Requirements*: * Pause status MUST NOT be set: `PAUSED_ENTER_WITHDRAWAL_QUEUE` -* `strategies.length` MUST equal `shares.length` -* `strategies.length` MUST NOT be equal to 0 -* The `withdrawer` MUST NOT be 0 -* See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares) -* See [`StrategyManager.removeShares`](./StrategyManager.md#removeshares) +* For each withdrawal: + * `strategies.length` MUST equal `shares.length` + * `strategies.length` MUST NOT be equal to 0 + * The `withdrawer` MUST NOT be 0 + * See [`EigenPodManager.removeShares`](./EigenPodManager.md#eigenpodmanagerremoveshares) + * See [`StrategyManager.removeShares`](./StrategyManager.md#removeshares) #### `completeQueuedWithdrawal` @@ -390,7 +390,7 @@ Called by the `EigenPodManager` when a Staker's shares decrease. This method is ```solidity function setWithdrawalDelayBlocks( - uint256 _newWithdrawalDelayBlocks + uint256 newWithdrawalDelayBlocks ) external onlyOwner @@ -403,4 +403,4 @@ Allows the `owner` to update the number of blocks that must pass before a withdr *Requirements*: * Caller MUST be the `owner` -* `_withdrawalDelayBlocks` MUST NOT be greater than `MAX_WITHDRAWAL_DELAY_BLOCKS` (50400) +* `newWithdrawalDelayBlocks` MUST NOT be greater than `MAX_WITHDRAWAL_DELAY_BLOCKS` (50400) diff --git a/docs/core/EigenPodManager.md b/docs/core/EigenPodManager.md index e255fd66c..1fe109fbf 100644 --- a/docs/core/EigenPodManager.md +++ b/docs/core/EigenPodManager.md @@ -306,7 +306,7 @@ This method is not allowed to cause the `Staker's` balance to go negative. This *Entry Points*: * `DelegationManager.undelegate` -* `DelegationManager.queueWithdrawal` +* `DelegationManager.queueWithdrawals` *Effects*: * Removes `shares` from `podOwner's` share balance @@ -331,7 +331,7 @@ function addShares( The `DelegationManager` calls this method when a queued withdrawal is completed and the withdrawer specifies that they want to receive the withdrawal as "shares" (rather than as the underlying tokens). A Pod Owner might want to do this in order to change their delegated Operator without needing to fully exit their validators. -Note that typically, shares from completed withdrawals are awarded to a `withdrawer` specified when the withdrawal is initiated in `DelegationManager.queueWithdrawal`. However, because beacon chain ETH shares are linked to proofs provided to a Pod Owner's `EigenPod`, this method is used to award shares to the original Pod Owner. +Note that typically, shares from completed withdrawals are awarded to a `withdrawer` specified when the withdrawal is initiated in `DelegationManager.queueWithdrawals`. However, because beacon chain ETH shares are linked to proofs provided to a Pod Owner's `EigenPod`, this method is used to award shares to the original Pod Owner. If the Pod Owner has a share deficit (negative shares), the deficit is repaid out of the added `shares`. If the Pod Owner's positive share count increases, this change is returned to the `DelegationManager` to be delegated to the Pod Owner's Operator (if they have one). @@ -432,7 +432,9 @@ Whether each withdrawal is a full or partial withdrawal is determined by the val * Full withdrawals are performed when a Pod Owner decides to fully exit a validator from the beacon chain. To do this, the Pod Owner should follow these steps: 1. Undelegate or queue a withdrawal (via the `DelegationManager`: ["Undelegating and Withdrawing"](./DelegationManager.md#undelegating-and-withdrawing)) 2. Exit their validator from the beacon chain and provide a proof to this method - 3. Complete their withdrawal (via [`DelegationManager.completeQueuedWithdrawal`](./DelegationManager.md#completequeuedwithdrawal)) + 3. Complete their withdrawal (via [`DelegationManager.completeQueuedWithdrawal`](./DelegationManager.md#completequeuedwithdrawal)). + +If the Pod Owner only exits their validator, the ETH of the pod owner is still staked through EigenLayer and can be used to service AVSs, even though their ETH has been withdrawn from the beacon chain. The protocol allows for this edge case. *Beacon chain proofs used*: * [`verifyStateRootAgainstLatestBlockRoot`](./proofs/BeaconChainProofs.md#beaconchainproofsverifystaterootagainstlatestblockroot) diff --git a/docs/core/StrategyManager.md b/docs/core/StrategyManager.md index bb18ddef8..26b39171d 100644 --- a/docs/core/StrategyManager.md +++ b/docs/core/StrategyManager.md @@ -127,7 +127,7 @@ The Staker's share balance for the `strategy` is decreased by the removed `share *Entry Points*: * `DelegationManager.undelegate` -* `DelegationManager.queueWithdrawal` +* `DelegationManager.queueWithdrawals` *Effects*: * The Staker's share balance for the given `strategy` is decreased by the given `shares` diff --git a/package-lock.json b/package-lock.json index c332c0bed..b8c159c9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,17 +12,523 @@ "solidity-docgen": "^0.6.0-beta.32" }, "devDependencies": { + "@commitlint/cli": "^18.2.0", + "@commitlint/config-conventional": "^18.1.0", "@types/yargs": "^17.0.28", "chalk": "^4.1.0", "dotenv": "^16.3.1", "fs": "^0.0.1-security", "hardhat": "^2.12.4", "hardhat-preprocessor": "^0.1.5", + "husky": "^8.0.3", "ts-node": "^10.9.1", "typescript": "^4.9.4", "yargs": "^17.7.2" } }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@commitlint/cli": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-18.2.0.tgz", + "integrity": "sha512-F/DCG791kMFmWg5eIdogakuGeg4OiI2kD430ed1a1Hh3epvrJdeIAgcGADAMIOmF+m0S1+VlIYUKG2dvQQ1Izw==", + "dev": true, + "dependencies": { + "@commitlint/format": "^18.1.0", + "@commitlint/lint": "^18.1.0", + "@commitlint/load": "^18.2.0", + "@commitlint/read": "^18.1.0", + "@commitlint/types": "^18.1.0", + "execa": "^5.0.0", + "lodash.isfunction": "^3.0.9", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-18.1.0.tgz", + "integrity": "sha512-8vvvtV3GOLEMHeKc8PjRL1lfP1Y4B6BG0WroFd9PJeRiOc3nFX1J0wlJenLURzl9Qus6YXVGWf+a/ZlbCKT3AA==", + "dev": true, + "dependencies": { + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-18.1.0.tgz", + "integrity": "sha512-kbHkIuItXn93o2NmTdwi5Mk1ujyuSIysRE/XHtrcps/27GuUKEIqBJp6TdJ4Sq+ze59RlzYSHMKuDKZbfg9+uQ==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.1.0", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-18.1.0.tgz", + "integrity": "sha512-CkPzJ9UBumIo54VDcpmBlaVX81J++wzEhN3DJH9+6PaLeiIG+gkSx8t7C2gfwG7PaiW4HzQtdQlBN5ab+c4vFQ==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.1.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-18.1.0.tgz", + "integrity": "sha512-w3Vt4K+O7+nSr9/gFSEfZ1exKUOPSlJaRpnk7Y+XowEhvwT7AIk1HNANH+gETf0zGZ020+hfiMW/Ome+SNCUsg==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-18.1.0.tgz", + "integrity": "sha512-So/w217tGWMZZb1yXcUFNF2qFLyYtSVqbnGoMbX8a+JKcG4oB11Gc1adS0ssUOMivtiNpaLtkSHFynyiwtJtiQ==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.1.0", + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-18.1.0.tgz", + "integrity": "sha512-fa1fY93J/Nx2GH6r6WOLdBOiL7x9Uc1N7wcpmaJ1C5Qs6P+rPSUTkofe2IOhSJIJoboHfAH6W0ru4xtK689t0Q==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.1.0", + "semver": "7.5.4" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@commitlint/is-ignored/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@commitlint/is-ignored/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@commitlint/lint": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-18.1.0.tgz", + "integrity": "sha512-LGB3eI5UYu5LLayibNrRM4bSbowr1z9uyqvp0c7+0KaSJi+xHxy/QEhb6fy4bMAtbXEvygY0sUu9HxSWg41rVQ==", + "dev": true, + "dependencies": { + "@commitlint/is-ignored": "^18.1.0", + "@commitlint/parse": "^18.1.0", + "@commitlint/rules": "^18.1.0", + "@commitlint/types": "^18.1.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-18.2.0.tgz", + "integrity": "sha512-xjX3d3CRlOALwImhOsmLYZh14/+gW/KxsY7+bPKrzmGuFailf9K7ckhB071oYZVJdACnpY4hDYiosFyOC+MpAA==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^18.1.0", + "@commitlint/execute-rule": "^18.1.0", + "@commitlint/resolve-extends": "^18.1.0", + "@commitlint/types": "^18.1.0", + "@types/node": "^18.11.9", + "chalk": "^4.1.0", + "cosmiconfig": "^8.0.0", + "cosmiconfig-typescript-loader": "^5.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@commitlint/load/node_modules/cosmiconfig-typescript-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", + "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", + "dev": true, + "dependencies": { + "jiti": "^1.19.1" + }, + "engines": { + "node": ">=v16" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=8.2", + "typescript": ">=4" + } + }, + "node_modules/@commitlint/load/node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@commitlint/message": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-18.1.0.tgz", + "integrity": "sha512-8dT/jJg73wf3o2Mut/fqEDTpBYSIEVtX5PWyuY/0uviEYeheZAczFo/VMIkeGzhJJn1IrcvAwWsvJ1lVGY2I/w==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-18.1.0.tgz", + "integrity": "sha512-23yv8uBweXWYn8bXk4PjHIsmVA+RkbqPh2h7irupBo2LthVlzMRc4LM6UStasScJ4OlXYYaWOmuP7jcExUF50Q==", + "dev": true, + "dependencies": { + "@commitlint/types": "^18.1.0", + "conventional-changelog-angular": "^6.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-18.1.0.tgz", + "integrity": "sha512-rzfzoKUwxmvYO81tI5o1371Nwt3vhcQR36oTNfupPdU1jgSL3nzBIS3B93LcZh3IYKbCIMyMPN5WZ10BXdeoUg==", + "dev": true, + "dependencies": { + "@commitlint/top-level": "^18.1.0", + "@commitlint/types": "^18.1.0", + "fs-extra": "^11.0.0", + "git-raw-commits": "^2.0.11", + "minimist": "^1.2.6" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read/node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@commitlint/read/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@commitlint/read/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-18.1.0.tgz", + "integrity": "sha512-3mZpzOEJkELt7BbaZp6+bofJyxViyObebagFn0A7IHaLARhPkWTivXdjvZHS12nAORftv88Yhbh8eCPKfSvB7g==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^18.1.0", + "@commitlint/types": "^18.1.0", + "import-fresh": "^3.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-18.1.0.tgz", + "integrity": "sha512-VJNQ674CRv4znI0DbsjZLVnn647J+BTxHGcrDIsYv7c99gW7TUGeIe5kL80G7l8+5+N0se8v9yn+Prr8xEy6Yw==", + "dev": true, + "dependencies": { + "@commitlint/ensure": "^18.1.0", + "@commitlint/message": "^18.1.0", + "@commitlint/to-lines": "^18.1.0", + "@commitlint/types": "^18.1.0", + "execa": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-18.1.0.tgz", + "integrity": "sha512-aHIoSDjG0ckxPLYDpODUeSLbEKmF6Jrs1B5JIssbbE9eemBtXtjm9yzdiAx9ZXcwoHlhbTp2fbndDb3YjlvJag==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-18.1.0.tgz", + "integrity": "sha512-1/USHlolIxJlsfLKecSXH+6PDojIvnzaJGPYwF7MtnTuuXCNQ4izkeqDsRuNMe9nU2VIKpK9OT8Q412kGNmgGw==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@commitlint/types": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-18.1.0.tgz", + "integrity": "sha512-65vGxZmbs+2OVwEItxhp3Ul7X2m2LyLfifYI/NdPwRqblmuES2w2aIRhIjb7cwUIBHHSTT8WXj4ixVHQibmvLQ==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">=v18" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1190,11 +1696,23 @@ "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==" }, + "node_modules/@types/minimist": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.4.tgz", + "integrity": "sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==", + "dev": true + }, "node_modules/@types/node": { "version": "18.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz", "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==" }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz", + "integrity": "sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==", + "dev": true + }, "node_modules/@types/pbkdf2": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", @@ -1306,6 +1824,22 @@ "node": ">=8" } }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -1370,6 +1904,21 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/async": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", @@ -1591,6 +2140,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -1602,6 +2160,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/catering": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz", @@ -1775,10 +2359,62 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/conventional-changelog-angular": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", + "integrity": "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } }, "node_modules/cookie": { "version": "0.4.2", @@ -1830,6 +2466,29 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "devOptional": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1857,6 +2516,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1873,6 +2566,18 @@ "node": ">=0.3.1" } }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -1934,6 +2639,15 @@ "node": ">=6" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2054,6 +2768,35 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2155,9 +2898,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -2185,6 +2931,83 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "dependencies": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/git-raw-commits/node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/git-raw-commits/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -2215,6 +3038,18 @@ "node": ">= 6" } }, + "node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -2240,6 +3075,15 @@ "uglify-js": "^3.1.4" } }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/hardhat": { "version": "2.12.4", "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.4.tgz", @@ -2392,6 +3236,18 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2410,6 +3266,36 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2437,6 +3323,30 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2472,6 +3382,31 @@ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.0.tgz", "integrity": "sha512-h4ujZ0OZ3kpvdFcwJAHXEdvawH7J8TYTB62e8xI03OSZhuGpuPY9DPXnonMN8s+uQ56gMUqMK71mXU8ob20xfA==" }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/imul": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", @@ -2503,6 +3438,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "node_modules/io-ts": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", @@ -2511,6 +3452,12 @@ "fp-ts": "^1.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2544,6 +3491,18 @@ "node": ">=4" } }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2588,6 +3547,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -2596,6 +3564,30 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -2607,11 +3599,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jiti": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", + "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2623,6 +3636,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2631,6 +3656,31 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/keccak": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", @@ -2645,6 +3695,15 @@ "node": ">=10.0.0" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/klaw": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", @@ -2689,6 +3748,12 @@ "node": ">=12" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -2706,6 +3771,66 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -2740,6 +3865,18 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "devOptional": true }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mcl-wasm": { "version": "0.7.9", "resolved": "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-0.7.9.tgz", @@ -2779,6 +3916,42 @@ "node": ">= 0.10.0" } }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -2808,6 +3981,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mnemonist": { "version": "0.38.5", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", @@ -3072,6 +4268,54 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3080,6 +4324,18 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -3101,6 +4357,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -3153,6 +4424,36 @@ "node": ">=4" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -3169,11 +4470,29 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -3200,6 +4519,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -3233,6 +4561,15 @@ } ] }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3255,6 +4592,153 @@ "node": ">= 0.8" } }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -3279,6 +4763,19 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3306,6 +4803,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "dependencies": { + "global-dirs": "^0.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -3445,6 +4963,27 @@ "sha.js": "bin.js" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -3458,6 +4997,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/solc": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/solc/-/solc-0.7.3.tgz", @@ -3542,6 +5087,47 @@ "source-map": "^0.6.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stacktrace-parser": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", @@ -3609,6 +5195,15 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-hex-prefix": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", @@ -3621,6 +5216,18 @@ "npm": ">=3" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3643,6 +5250,33 @@ "node": ">=4" } }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -3673,6 +5307,15 @@ "node": ">=0.6" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -3808,6 +5451,15 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3827,6 +5479,31 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -3939,59 +5616,432 @@ "node": ">=12" } }, - "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "engines": { - "node": ">=10" + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@commitlint/cli": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-18.2.0.tgz", + "integrity": "sha512-F/DCG791kMFmWg5eIdogakuGeg4OiI2kD430ed1a1Hh3epvrJdeIAgcGADAMIOmF+m0S1+VlIYUKG2dvQQ1Izw==", + "dev": true, + "requires": { + "@commitlint/format": "^18.1.0", + "@commitlint/lint": "^18.1.0", + "@commitlint/load": "^18.2.0", + "@commitlint/read": "^18.1.0", + "@commitlint/types": "^18.1.0", + "execa": "^5.0.0", + "lodash.isfunction": "^3.0.9", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^17.0.0" + } + }, + "@commitlint/config-conventional": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-18.1.0.tgz", + "integrity": "sha512-8vvvtV3GOLEMHeKc8PjRL1lfP1Y4B6BG0WroFd9PJeRiOc3nFX1J0wlJenLURzl9Qus6YXVGWf+a/ZlbCKT3AA==", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "^7.0.2" + } + }, + "@commitlint/config-validator": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-18.1.0.tgz", + "integrity": "sha512-kbHkIuItXn93o2NmTdwi5Mk1ujyuSIysRE/XHtrcps/27GuUKEIqBJp6TdJ4Sq+ze59RlzYSHMKuDKZbfg9+uQ==", + "dev": true, + "requires": { + "@commitlint/types": "^18.1.0", + "ajv": "^8.11.0" + } + }, + "@commitlint/ensure": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-18.1.0.tgz", + "integrity": "sha512-CkPzJ9UBumIo54VDcpmBlaVX81J++wzEhN3DJH9+6PaLeiIG+gkSx8t7C2gfwG7PaiW4HzQtdQlBN5ab+c4vFQ==", + "dev": true, + "requires": { + "@commitlint/types": "^18.1.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + } + }, + "@commitlint/execute-rule": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-18.1.0.tgz", + "integrity": "sha512-w3Vt4K+O7+nSr9/gFSEfZ1exKUOPSlJaRpnk7Y+XowEhvwT7AIk1HNANH+gETf0zGZ020+hfiMW/Ome+SNCUsg==", + "dev": true + }, + "@commitlint/format": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-18.1.0.tgz", + "integrity": "sha512-So/w217tGWMZZb1yXcUFNF2qFLyYtSVqbnGoMbX8a+JKcG4oB11Gc1adS0ssUOMivtiNpaLtkSHFynyiwtJtiQ==", + "dev": true, + "requires": { + "@commitlint/types": "^18.1.0", + "chalk": "^4.1.0" + } + }, + "@commitlint/is-ignored": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-18.1.0.tgz", + "integrity": "sha512-fa1fY93J/Nx2GH6r6WOLdBOiL7x9Uc1N7wcpmaJ1C5Qs6P+rPSUTkofe2IOhSJIJoboHfAH6W0ru4xtK689t0Q==", + "dev": true, + "requires": { + "@commitlint/types": "^18.1.0", + "semver": "7.5.4" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@commitlint/lint": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-18.1.0.tgz", + "integrity": "sha512-LGB3eI5UYu5LLayibNrRM4bSbowr1z9uyqvp0c7+0KaSJi+xHxy/QEhb6fy4bMAtbXEvygY0sUu9HxSWg41rVQ==", + "dev": true, + "requires": { + "@commitlint/is-ignored": "^18.1.0", + "@commitlint/parse": "^18.1.0", + "@commitlint/rules": "^18.1.0", + "@commitlint/types": "^18.1.0" + } + }, + "@commitlint/load": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-18.2.0.tgz", + "integrity": "sha512-xjX3d3CRlOALwImhOsmLYZh14/+gW/KxsY7+bPKrzmGuFailf9K7ckhB071oYZVJdACnpY4hDYiosFyOC+MpAA==", + "dev": true, + "requires": { + "@commitlint/config-validator": "^18.1.0", + "@commitlint/execute-rule": "^18.1.0", + "@commitlint/resolve-extends": "^18.1.0", + "@commitlint/types": "^18.1.0", + "@types/node": "^18.11.9", + "chalk": "^4.1.0", + "cosmiconfig": "^8.0.0", + "cosmiconfig-typescript-loader": "^5.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "requires": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + } + }, + "cosmiconfig-typescript-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", + "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", + "dev": true, + "requires": { + "jiti": "^1.19.1" + } + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "peer": true + } + } + }, + "@commitlint/message": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-18.1.0.tgz", + "integrity": "sha512-8dT/jJg73wf3o2Mut/fqEDTpBYSIEVtX5PWyuY/0uviEYeheZAczFo/VMIkeGzhJJn1IrcvAwWsvJ1lVGY2I/w==", + "dev": true + }, + "@commitlint/parse": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-18.1.0.tgz", + "integrity": "sha512-23yv8uBweXWYn8bXk4PjHIsmVA+RkbqPh2h7irupBo2LthVlzMRc4LM6UStasScJ4OlXYYaWOmuP7jcExUF50Q==", + "dev": true, + "requires": { + "@commitlint/types": "^18.1.0", + "conventional-changelog-angular": "^6.0.0", + "conventional-commits-parser": "^5.0.0" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" + "@commitlint/read": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-18.1.0.tgz", + "integrity": "sha512-rzfzoKUwxmvYO81tI5o1371Nwt3vhcQR36oTNfupPdU1jgSL3nzBIS3B93LcZh3IYKbCIMyMPN5WZ10BXdeoUg==", + "dev": true, + "requires": { + "@commitlint/top-level": "^18.1.0", + "@commitlint/types": "^18.1.0", + "fs-extra": "^11.0.0", + "git-raw-commits": "^2.0.11", + "minimist": "^1.2.6" }, - "engines": { - "node": ">=10" + "dependencies": { + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } } }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "@commitlint/resolve-extends": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-18.1.0.tgz", + "integrity": "sha512-3mZpzOEJkELt7BbaZp6+bofJyxViyObebagFn0A7IHaLARhPkWTivXdjvZHS12nAORftv88Yhbh8eCPKfSvB7g==", "dev": true, - "engines": { - "node": ">=12" + "requires": { + "@commitlint/config-validator": "^18.1.0", + "@commitlint/types": "^18.1.0", + "import-fresh": "^3.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, - "engines": { - "node": ">=6" + "@commitlint/rules": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-18.1.0.tgz", + "integrity": "sha512-VJNQ674CRv4znI0DbsjZLVnn647J+BTxHGcrDIsYv7c99gW7TUGeIe5kL80G7l8+5+N0se8v9yn+Prr8xEy6Yw==", + "dev": true, + "requires": { + "@commitlint/ensure": "^18.1.0", + "@commitlint/message": "^18.1.0", + "@commitlint/to-lines": "^18.1.0", + "@commitlint/types": "^18.1.0", + "execa": "^5.0.0" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" + "@commitlint/to-lines": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-18.1.0.tgz", + "integrity": "sha512-aHIoSDjG0ckxPLYDpODUeSLbEKmF6Jrs1B5JIssbbE9eemBtXtjm9yzdiAx9ZXcwoHlhbTp2fbndDb3YjlvJag==", + "dev": true + }, + "@commitlint/top-level": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-18.1.0.tgz", + "integrity": "sha512-1/USHlolIxJlsfLKecSXH+6PDojIvnzaJGPYwF7MtnTuuXCNQ4izkeqDsRuNMe9nU2VIKpK9OT8Q412kGNmgGw==", + "dev": true, + "requires": { + "find-up": "^5.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } } - } - }, - "dependencies": { + }, + "@commitlint/types": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-18.1.0.tgz", + "integrity": "sha512-65vGxZmbs+2OVwEItxhp3Ul7X2m2LyLfifYI/NdPwRqblmuES2w2aIRhIjb7cwUIBHHSTT8WXj4ixVHQibmvLQ==", + "dev": true, + "requires": { + "chalk": "^4.1.0" + } + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -4814,11 +6864,23 @@ "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==" }, + "@types/minimist": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.4.tgz", + "integrity": "sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==", + "dev": true + }, "@types/node": { "version": "18.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz", "integrity": "sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==" }, + "@types/normalize-package-data": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz", + "integrity": "sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==", + "dev": true + }, "@types/pbkdf2": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", @@ -4906,6 +6968,18 @@ "indent-string": "^4.0.0" } }, + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -4952,6 +7026,18 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true + }, "async": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", @@ -5124,11 +7210,36 @@ "get-intrinsic": "^1.0.2" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, "catering": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz", @@ -5259,11 +7370,51 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" }, + "compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "conventional-changelog-angular": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", + "integrity": "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==", + "dev": true, + "requires": { + "compare-func": "^2.0.0" + } + }, + "conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "requires": { + "compare-func": "^2.0.0" + } + }, + "conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "requires": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + } + }, "cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -5305,6 +7456,23 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "devOptional": true }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5318,6 +7486,30 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==" }, + "decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true + } + } + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5328,6 +7520,15 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, "dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -5379,6 +7580,15 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -5490,6 +7700,29 @@ "safe-buffer": "^5.1.1" } }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -5558,9 +7791,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -5582,6 +7815,61 @@ "has-symbols": "^1.0.3" } }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "git-raw-commits": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", + "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", + "dev": true, + "requires": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "dependencies": { + "meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + } + }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "requires": { + "readable-stream": "^3.0.0" + } + }, + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + } + } + }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -5603,6 +7891,15 @@ "is-glob": "^4.0.1" } }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -5620,6 +7917,12 @@ "wordwrap": "^1.0.0" } }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, "hardhat": { "version": "2.12.4", "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.4.tgz", @@ -5735,6 +8038,15 @@ "minimalistic-assert": "^1.0.1" } }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -5750,6 +8062,32 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -5771,6 +8109,18 @@ "debug": "4" } }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5789,6 +8139,24 @@ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.0.tgz", "integrity": "sha512-h4ujZ0OZ3kpvdFcwJAHXEdvawH7J8TYTB62e8xI03OSZhuGpuPY9DPXnonMN8s+uQ56gMUqMK71mXU8ob20xfA==" }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, "imul": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", @@ -5814,6 +8182,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "io-ts": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", @@ -5822,6 +8196,12 @@ "fp-ts": "^1.0.0" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -5835,6 +8215,15 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5863,21 +8252,60 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "requires": { + "text-extensions": "^2.0.0" + } + }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jiti": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", + "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "dev": true + }, "js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -5886,6 +8314,18 @@ "argparse": "^2.0.1" } }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -5894,6 +8334,22 @@ "graceful-fs": "^4.1.6" } }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "keccak": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", @@ -5904,6 +8360,12 @@ "readable-stream": "^3.6.0" } }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, "klaw": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", @@ -5935,6 +8397,12 @@ "module-error": "^1.0.1" } }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -5949,6 +8417,66 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -5977,6 +8505,12 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "devOptional": true }, + "map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true + }, "mcl-wasm": { "version": "0.7.9", "resolved": "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-0.7.9.tgz", @@ -6007,6 +8541,30 @@ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==" }, + "meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -6030,6 +8588,25 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "dependencies": { + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true + } + } + }, "mnemonist": { "version": "0.38.5", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", @@ -6220,11 +8797,58 @@ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==" }, + "normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -6243,6 +8867,15 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -6277,6 +8910,27 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==" }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -6287,11 +8941,23 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -6309,6 +8975,12 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -6322,6 +8994,12 @@ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6341,6 +9019,118 @@ "unpipe": "1.0.0" } }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -6359,6 +9149,16 @@ "picomatch": "^2.2.1" } }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6377,6 +9177,21 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "requires": { + "global-dirs": "^0.1.1" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -6472,6 +9287,21 @@ "safe-buffer": "^5.0.1" } }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -6482,6 +9312,12 @@ "object-inspect": "^1.9.0" } }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "solc": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/solc/-/solc-0.7.3.tgz", @@ -6553,6 +9389,44 @@ "source-map": "^0.6.0" } }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true + }, "stacktrace-parser": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", @@ -6604,6 +9478,12 @@ "ansi-regex": "^5.0.1" } }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-hex-prefix": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", @@ -6612,6 +9492,15 @@ "is-hex-prefixed": "1.0.0" } }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6625,6 +9514,27 @@ "has-flag": "^3.0.0" } }, + "text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "requires": { + "readable-stream": "3" + } + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -6646,6 +9556,12 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true + }, "ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -6730,6 +9646,15 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6746,6 +9671,25 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "devOptional": true }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/package.json b/package.json index dcd25c0c4..413cfd75b 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,15 @@ }, "homepage": "https://github.com/Layr-Labs/eigenlayer-contracts#readme", "devDependencies": { + "@commitlint/cli": "^18.2.0", + "@commitlint/config-conventional": "^18.1.0", "@types/yargs": "^17.0.28", "chalk": "^4.1.0", "dotenv": "^16.3.1", "fs": "^0.0.1-security", "hardhat": "^2.12.4", "hardhat-preprocessor": "^0.1.5", + "husky": "^8.0.3", "ts-node": "^10.9.1", "typescript": "^4.9.4", "yargs": "^17.7.2" diff --git a/script/M2_deploy.config.json b/script/M2_deploy.config.json deleted file mode 100644 index dd9cf3a00..000000000 --- a/script/M2_deploy.config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "chainInfo": { - "chainId": 31337 - }, - "oracleInitialization": { - "signers": [ - "0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF", - "0x040353E9d057689b77DF275c07FFe1A46b98a4a6" - ], - "threshold": "2" - } -} \ No newline at end of file diff --git a/script/milestone/M2Deploy.sol b/script/milestone/M2Deploy.sol deleted file mode 100644 index b149bd406..000000000 --- a/script/milestone/M2Deploy.sol +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.12; - -import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; -import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; - -import "../../src/contracts/interfaces/IETHPOSDeposit.sol"; -import "../../src/contracts/interfaces/IBeaconChainOracle.sol"; - -import "../../src/contracts/core/StrategyManager.sol"; -import "../../src/contracts/core/Slasher.sol"; -import "../../src/contracts/core/DelegationManager.sol"; - -import "../../src/contracts/strategies/StrategyBaseTVLLimits.sol"; - -import "../../src/contracts/pods/EigenPod.sol"; -import "../../src/contracts/pods/EigenPodManager.sol"; -import "../../src/contracts/pods/DelayedWithdrawalRouter.sol"; - -import "../../src/contracts/permissions/PauserRegistry.sol"; - -import "../../src/test/mocks/EmptyContract.sol"; -import "../../src/test/mocks/ETHDepositMock.sol"; - -import "forge-std/Script.sol"; -import "forge-std/Test.sol"; - -// # To load the variables in the .env file -// source .env - -// # To deploy and verify our contract -// forge script script/upgrade/M2Deploy.s.sol:M2Deploy --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast -vvvv -contract M2Deploy is Script, Test { - Vm cheats = Vm(HEVM_ADDRESS); - - string public m1DeploymentOutputPath = string(bytes("script/output/M1_deployment_goerli_2023_3_23.json")); - - IETHPOSDeposit public ethPOS; - - ISlasher public slasher; - IDelegationManager public delegation; - DelegationManager public delegationImplementation; - IStrategyManager public strategyManager; - StrategyManager public strategyManagerImplementation; - IEigenPodManager public eigenPodManager; - EigenPodManager public eigenPodManagerImplementation; - IDelayedWithdrawalRouter public delayedWithdrawalRouter; - IBeacon public eigenPodBeacon; - EigenPod public eigenPodImplementation; - - function run() external { - // read and log the chainID - uint256 chainId = block.chainid; - emit log_named_uint("You are deploying on ChainID", chainId); - - if (chainId == 1) { - m1DeploymentOutputPath = string(bytes("script/output/M1_deployment_mainnet_2023_6_9.json")); - } - - // READ JSON DEPLOYMENT DATA - string memory deployment_data = vm.readFile(m1DeploymentOutputPath); - slasher = Slasher(stdJson.readAddress(deployment_data, ".addresses.slasher")); - delegation = slasher.delegation(); - strategyManager = slasher.strategyManager(); - eigenPodManager = strategyManager.eigenPodManager(); - eigenPodBeacon = eigenPodManager.eigenPodBeacon(); - ethPOS = eigenPodManager.ethPOS(); - delayedWithdrawalRouter = EigenPod(payable(eigenPodBeacon.implementation())).delayedWithdrawalRouter(); - - vm.startBroadcast(); - delegationImplementation = new DelegationManager(strategyManager, slasher, eigenPodManager); - strategyManagerImplementation = new StrategyManager(delegation, eigenPodManager, slasher); - eigenPodManagerImplementation = new EigenPodManager( - ethPOS, - eigenPodBeacon, - strategyManager, - slasher, - delegation - ); - eigenPodImplementation = new EigenPod({ - _ethPOS: ethPOS, - _delayedWithdrawalRouter: delayedWithdrawalRouter, - _eigenPodManager: eigenPodManager, - _MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR: 32 gwei, - _GENESIS_TIME: 1616508000 - }); - - // write the output to a contract - // WRITE JSON DATA - string memory parent_object = "parent object"; - - string memory deployed_addresses = "addresses"; - vm.serializeAddress(deployed_addresses, "slasher", address(slasher)); - vm.serializeAddress(deployed_addresses, "delegation", address(delegation)); - vm.serializeAddress(deployed_addresses, "strategyManager", address(strategyManager)); - vm.serializeAddress(deployed_addresses, "delayedWithdrawalRouter", address(delayedWithdrawalRouter)); - vm.serializeAddress(deployed_addresses, "eigenPodManager", address(eigenPodManager)); - vm.serializeAddress(deployed_addresses, "eigenPodBeacon", address(eigenPodBeacon)); - vm.serializeAddress(deployed_addresses, "ethPOS", address(ethPOS)); - - vm.serializeAddress(deployed_addresses, "delegationImplementation", address(delegationImplementation)); - vm.serializeAddress( - deployed_addresses, - "strategyManagerImplementation", - address(strategyManagerImplementation) - ); - vm.serializeAddress( - deployed_addresses, - "eigenPodManagerImplementation", - address(eigenPodManagerImplementation) - ); - string memory deployed_addresses_output = vm.serializeAddress( - deployed_addresses, - "eigenPodImplementation", - address(eigenPodImplementation) - ); - - string memory chain_info = "chainInfo"; - vm.serializeUint(chain_info, "deploymentBlock", block.number); - string memory chain_info_output = vm.serializeUint(chain_info, "chainId", chainId); - - vm.serializeString(parent_object, deployed_addresses, deployed_addresses_output); - // serialize all the data - string memory finalJson = vm.serializeString(parent_object, chain_info, chain_info_output); - vm.writeJson(finalJson, "script/output/M2_deployment_data.json"); - } -} diff --git a/script/output/M2_deployment_data_goerli.json b/script/output/M2_deployment_data_goerli.json index 52409abe0..45acc38f2 100644 --- a/script/output/M2_deployment_data_goerli.json +++ b/script/output/M2_deployment_data_goerli.json @@ -2,18 +2,18 @@ "addresses": { "delayedWithdrawalRouter": "0x89581561f1F98584F88b0d57c2180fb89225388f", "delegation": "0x1b7b8F6b258f95Cf9596EabB9aa18B62940Eb0a8", - "delegationImplementation": "0x34A1D3fff3958843C43aD80F30b94c510645C316", + "delegationImplementation": "0x9b7980a32ceCe2Aa936DD2E43AF74af62581A99d", "eigenPodBeacon": "0x3093F3B560352F896F0e9567019902C9Aff8C9a5", - "eigenPodImplementation": "0xBb2180ebd78ce97360503434eD37fcf4a1Df61c3", + "eigenPodImplementation": "0x86bf376E0C0c9c6D332E13422f35Aca75C106CcA", "eigenPodManager": "0xa286b84C96aF280a49Fe1F40B9627C2A2827df41", - "eigenPodManagerImplementation": "0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496", + "eigenPodManagerImplementation": "0xdD09d95bD25299EDBF4f33d76F84dBc77b0B901b", "ethPOS": "0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b", "slasher": "0xD11d60b669Ecf7bE10329726043B3ac07B380C22", "strategyManager": "0x779d1b5315df083e3F9E94cB495983500bA8E907", - "strategyManagerImplementation": "0x90193C961A926261B756D1E5bb255e67ff9498A1" + "strategyManagerImplementation": "0x8676bb5f792ED407a237234Fe422aC6ed3540055" }, "chainInfo": { "chainId": 5, - "deploymentBlock": 9878481 + "deploymentBlock": 10002668 } } \ No newline at end of file diff --git a/script/utils/Allocator.sol b/script/utils/Allocator.sol deleted file mode 100644 index d7f27b8e4..000000000 --- a/script/utils/Allocator.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.12; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -contract Allocator { - - function allocate(IERC20 token, address[] memory recipients, uint256 amount) public { - token.transferFrom(msg.sender, address(this), recipients.length * amount); - for (uint i = 0; i < recipients.length; i++) { - token.transfer(recipients[i], amount); - } - } -} diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index 2f4d65568..d4d9824ff 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -230,9 +230,9 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg */ function undelegate(address staker) external onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE) returns (bytes32) { require(isDelegated(staker), "DelegationManager.undelegate: staker must be delegated to undelegate"); - address operator = delegatedTo[staker]; require(!isOperator(staker), "DelegationManager.undelegate: operators cannot be undelegated"); require(staker != address(0), "DelegationManager.undelegate: cannot undelegate zero address"); + address operator = delegatedTo[staker]; require( msg.sender == staker || msg.sender == operator || @@ -604,7 +604,7 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg address currentOperator = delegatedTo[msg.sender]; for (uint256 i = 0; i < withdrawal.strategies.length; ) { /** When awarding podOwnerShares in EigenPodManager, we need to be sure to only give them back to the original podOwner. - * Other strategy sharescan + will be awarded to the withdrawer. + * Other strategy shares can + will be awarded to the withdrawer. */ if (withdrawal.strategies[i] == beaconChainETHStrategy) { address staker = withdrawal.staker; @@ -666,7 +666,7 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg } function _pushOperatorStakeUpdate(address operator) internal { - // if the stake regsitry has been set + // if the stake registry has been set if (address(stakeRegistry) != address(0)) { address[] memory operators = new address[](1); operators[0] = operator; diff --git a/src/contracts/libraries/BN254.sol b/src/contracts/libraries/BN254.sol deleted file mode 100644 index 42703bda5..000000000 --- a/src/contracts/libraries/BN254.sol +++ /dev/null @@ -1,346 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 AND MIT -// several functions are taken or adapted from https://github.com/HarryR/solcrypto/blob/master/contracts/altbn128.sol (MIT license): -// Copyright 2017 Christian Reitwiessner -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -// IN THE SOFTWARE. - -// The remainder of the code is written by LayrLabs Inc. and is under the BUSL-1.1 license - -pragma solidity =0.8.12; - -/** - * @title Library for operations on the BN254 elliptic curve. - * @author Layr Labs, Inc. - * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service - * @notice Contains BN254 parameters, common operations (addition, scalar mul, pairing), and BLS signature functionality. - */ -library BN254 { - // modulus for the underlying field F_p of the elliptic curve - uint256 internal constant FP_MODULUS = - 21888242871839275222246405745257275088696311157297823662689037894645226208583; - // modulus for the underlying field F_r of the elliptic curve - uint256 internal constant FR_MODULUS = - 21888242871839275222246405745257275088548364400416034343698204186575808495617; - - struct G1Point { - uint256 X; - uint256 Y; - } - - // Encoding of field elements is: X[1] * i + X[0] - struct G2Point { - uint256[2] X; - uint256[2] Y; - } - - function generatorG1() internal pure returns (G1Point memory) { - return G1Point(1, 2); - } - - // generator of group G2 - /// @dev Generator point in F_q2 is of the form: (x0 + ix1, y0 + iy1). - uint256 internal constant G2x1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; - uint256 internal constant G2x0 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; - uint256 internal constant G2y1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; - uint256 internal constant G2y0 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; - - /// @notice returns the G2 generator - /// @dev mind the ordering of the 1s and 0s! - /// this is because of the (unknown to us) convention used in the bn254 pairing precompile contract - /// "Elements a * i + b of F_p^2 are encoded as two elements of F_p, (a, b)." - /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-197.md#encoding - function generatorG2() internal pure returns (G2Point memory) { - return G2Point([G2x1, G2x0], [G2y1, G2y0]); - } - - // negation of the generator of group G2 - /// @dev Generator point in F_q2 is of the form: (x0 + ix1, y0 + iy1). - uint256 internal constant nG2x1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; - uint256 internal constant nG2x0 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; - uint256 internal constant nG2y1 = 17805874995975841540914202342111839520379459829704422454583296818431106115052; - uint256 internal constant nG2y0 = 13392588948715843804641432497768002650278120570034223513918757245338268106653; - - function negGeneratorG2() internal pure returns (G2Point memory) { - return G2Point([nG2x1, nG2x0], [nG2y1, nG2y0]); - } - - bytes32 internal constant powersOfTauMerkleRoot = - 0x22c998e49752bbb1918ba87d6d59dd0e83620a311ba91dd4b2cc84990b31b56f; - - /** - * @param p Some point in G1. - * @return The negation of `p`, i.e. p.plus(p.negate()) should be zero. - */ - function negate(G1Point memory p) internal pure returns (G1Point memory) { - // The prime q in the base field F_q for G1 - if (p.X == 0 && p.Y == 0) { - return G1Point(0, 0); - } else { - return G1Point(p.X, FP_MODULUS - (p.Y % FP_MODULUS)); - } - } - - /** - * @return r the sum of two points of G1 - */ - function plus(G1Point memory p1, G1Point memory p2) internal view returns (G1Point memory r) { - uint256[4] memory input; - input[0] = p1.X; - input[1] = p1.Y; - input[2] = p2.X; - input[3] = p2.Y; - bool success; - - // solium-disable-next-line security/no-inline-assembly - assembly { - success := staticcall(sub(gas(), 2000), 6, input, 0x80, r, 0x40) - // Use "invalid" to make gas estimation work - switch success - case 0 { - invalid() - } - } - - require(success, "ec-add-failed"); - } - - /** - * @notice an optimized ecMul implementation that takes O(log_2(s)) ecAdds - * @param p the point to multiply - * @param s the scalar to multiply by - * @dev this function is only safe to use if the scalar is 9 bits or less - */ - function scalar_mul_tiny(BN254.G1Point memory p, uint16 s) internal view returns (BN254.G1Point memory) { - require(s < 2**9, "scalar-too-large"); - - // if s is 1 return p - if(s == 1) { - return p; - } - - // the accumulated product to return - BN254.G1Point memory acc = BN254.G1Point(0, 0); - // the 2^n*p to add to the accumulated product in each iteration - BN254.G1Point memory p2n = p; - // value of most significant bit - uint16 m = 1; - // index of most significant bit - uint8 i = 0; - - //loop until we reach the most significant bit - while(s > m){ - unchecked { - // if the current bit is 1, add the 2^n*p to the accumulated product - if ((s >> i) & 1 == 1) { - acc = plus(acc, p2n); - } - // double the 2^n*p for the next iteration - p2n = plus(p2n, p2n); - - // increment the index and double the value of the most significant bit - m <<= 1; - ++i; - } - } - - // return the accumulated product - return acc; - } - - /** - * @return r the product of a point on G1 and a scalar, i.e. - * p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all - * points p. - */ - function scalar_mul(G1Point memory p, uint256 s) internal view returns (G1Point memory r) { - uint256[3] memory input; - input[0] = p.X; - input[1] = p.Y; - input[2] = s; - bool success; - // solium-disable-next-line security/no-inline-assembly - assembly { - success := staticcall(sub(gas(), 2000), 7, input, 0x60, r, 0x40) - // Use "invalid" to make gas estimation work - switch success - case 0 { - invalid() - } - } - require(success, "ec-mul-failed"); - } - - /** - * @return The result of computing the pairing check - * e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 - * For example, - * pairing([P1(), P1().negate()], [P2(), P2()]) should return true. - */ - function pairing( - G1Point memory a1, - G2Point memory a2, - G1Point memory b1, - G2Point memory b2 - ) internal view returns (bool) { - G1Point[2] memory p1 = [a1, b1]; - G2Point[2] memory p2 = [a2, b2]; - - uint256[12] memory input; - - for (uint256 i = 0; i < 2; i++) { - uint256 j = i * 6; - input[j + 0] = p1[i].X; - input[j + 1] = p1[i].Y; - input[j + 2] = p2[i].X[0]; - input[j + 3] = p2[i].X[1]; - input[j + 4] = p2[i].Y[0]; - input[j + 5] = p2[i].Y[1]; - } - - uint256[1] memory out; - bool success; - - // solium-disable-next-line security/no-inline-assembly - assembly { - success := staticcall(sub(gas(), 2000), 8, input, mul(12, 0x20), out, 0x20) - // Use "invalid" to make gas estimation work - switch success - case 0 { - invalid() - } - } - - require(success, "pairing-opcode-failed"); - - return out[0] != 0; - } - - /** - * @notice This function is functionally the same as pairing(), however it specifies a gas limit - * the user can set, as a precompile may use the entire gas budget if it reverts. - */ - function safePairing( - G1Point memory a1, - G2Point memory a2, - G1Point memory b1, - G2Point memory b2, - uint256 pairingGas - ) internal view returns (bool, bool) { - G1Point[2] memory p1 = [a1, b1]; - G2Point[2] memory p2 = [a2, b2]; - - uint256[12] memory input; - - for (uint256 i = 0; i < 2; i++) { - uint256 j = i * 6; - input[j + 0] = p1[i].X; - input[j + 1] = p1[i].Y; - input[j + 2] = p2[i].X[0]; - input[j + 3] = p2[i].X[1]; - input[j + 4] = p2[i].Y[0]; - input[j + 5] = p2[i].Y[1]; - } - - uint256[1] memory out; - bool success; - - // solium-disable-next-line security/no-inline-assembly - assembly { - success := staticcall(pairingGas, 8, input, mul(12, 0x20), out, 0x20) - } - - //Out is the output of the pairing precompile, either 0 or 1 based on whether the two pairings are equal. - //Success is true if the precompile actually goes through (aka all inputs are valid) - - return (success, out[0] != 0); - } - - /// @return the keccak256 hash of the G1 Point - /// @dev used for BLS signatures - function hashG1Point(BN254.G1Point memory pk) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(pk.X, pk.Y)); - } - - /// @return the keccak256 hash of the G2 Point - /// @dev used for BLS signatures - function hashG2Point( - BN254.G2Point memory pk - ) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(pk.X[0], pk.X[1], pk.Y[0], pk.Y[1])); - } - - /** - * @notice adapted from https://github.com/HarryR/solcrypto/blob/master/contracts/altbn128.sol - */ - function hashToG1(bytes32 _x) internal view returns (G1Point memory) { - uint256 beta = 0; - uint256 y = 0; - - uint256 x = uint256(_x) % FP_MODULUS; - - while (true) { - (beta, y) = findYFromX(x); - - // y^2 == beta - if( beta == mulmod(y, y, FP_MODULUS) ) { - return G1Point(x, y); - } - - x = addmod(x, 1, FP_MODULUS); - } - return G1Point(0, 0); - } - - /** - * Given X, find Y - * - * where y = sqrt(x^3 + b) - * - * Returns: (x^3 + b), y - */ - function findYFromX(uint256 x) internal view returns (uint256, uint256) { - // beta = (x^3 + b) % p - uint256 beta = addmod(mulmod(mulmod(x, x, FP_MODULUS), x, FP_MODULUS), 3, FP_MODULUS); - - // y^2 = x^3 + b - // this acts like: y = sqrt(beta) = beta^((p+1) / 4) - uint256 y = expMod(beta, 0xc19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52, FP_MODULUS); - - return (beta, y); - } - - function expMod(uint256 _base, uint256 _exponent, uint256 _modulus) internal view returns (uint256 retval) { - bool success; - uint256[1] memory output; - uint[6] memory input; - input[0] = 0x20; // baseLen = new(big.Int).SetBytes(getData(input, 0, 32)) - input[1] = 0x20; // expLen = new(big.Int).SetBytes(getData(input, 32, 32)) - input[2] = 0x20; // modLen = new(big.Int).SetBytes(getData(input, 64, 32)) - input[3] = _base; - input[4] = _exponent; - input[5] = _modulus; - assembly { - success := staticcall(sub(gas(), 2000), 5, input, 0xc0, output, 0x20) - // Use "invalid" to make gas estimation work - switch success - case 0 { - invalid() - } - } - require(success, "BN254.expMod: call failure"); - return output[0]; - } -} diff --git a/src/contracts/libraries/BeaconChainProofs.sol b/src/contracts/libraries/BeaconChainProofs.sol index ccc929d21..e488eedf8 100644 --- a/src/contracts/libraries/BeaconChainProofs.sol +++ b/src/contracts/libraries/BeaconChainProofs.sol @@ -388,7 +388,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]; } @@ -401,7 +401,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/contracts/libraries/BitmapUtils.sol b/src/contracts/libraries/BitmapUtils.sol deleted file mode 100644 index 1db53985c..000000000 --- a/src/contracts/libraries/BitmapUtils.sol +++ /dev/null @@ -1,283 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity =0.8.12; - -/** - * @title Library for Bitmap utilities such as converting between an array of bytes and a bitmap and finding the number of 1s in a bitmap. - * @author Layr Labs, Inc. - */ -library BitmapUtils { - /** - * @notice Byte arrays are meant to contain unique bytes. - * If the array length exceeds 256, then it's impossible for all entries to be unique. - * This constant captures the max allowed array length (inclusive, i.e. 256 is allowed). - */ - uint256 internal constant MAX_BYTE_ARRAY_LENGTH = 256; - - /** - * @notice Converts an array of bytes into a bitmap. - * @param bytesArray The array of bytes to convert/compress into a bitmap. - * @return The resulting bitmap. - * @dev Each byte in the input is processed as indicating a single bit to flip in the bitmap - * @dev This function will also revert if the `bytesArray` input contains any duplicate entries (i.e. duplicate bytes). - */ - function bytesArrayToBitmap(bytes memory bytesArray) internal pure returns (uint256) { - // sanity-check on input. a too-long input would fail later on due to having duplicate entry(s) - require(bytesArray.length <= MAX_BYTE_ARRAY_LENGTH, - "BitmapUtils.bytesArrayToBitmap: bytesArray is too long"); - - // return empty bitmap early if length of array is 0 - if (bytesArray.length == 0) { - return uint256(0); - } - - // initialize the empty bitmap, to be built inside the loop - uint256 bitmap; - // initialize an empty uint256 to be used as a bitmask inside the loop - uint256 bitMask; - - // perform the 0-th loop iteration with the duplicate check *omitted* (since it is unnecessary / will always pass) - // construct a single-bit mask from the numerical value of the 0th byte of the array, and immediately add it to the bitmap - bitmap = uint256(1 << uint8(bytesArray[0])); - - // loop through each byte in the array to construct the bitmap - for (uint256 i = 1; i < bytesArray.length; ++i) { - // construct a single-bit mask from the numerical value of the next byte out of the array - bitMask = uint256(1 << uint8(bytesArray[i])); - // check that the entry is not a repeat - require(bitmap & bitMask == 0, "BitmapUtils.bytesArrayToBitmap: repeat entry in bytesArray"); - // add the entry to the bitmap - bitmap = (bitmap | bitMask); - } - return bitmap; - } - - /** - * @notice Converts an ordered array of bytes into a bitmap. - * @param orderedBytesArray The array of bytes to convert/compress into a bitmap. Must be in strictly ascending order. - * @return The resulting bitmap. - * @dev Each byte in the input is processed as indicating a single bit to flip in the bitmap. - * @dev This function will eventually revert in the event that the `orderedBytesArray` is not properly ordered (in ascending order). - * @dev This function will also revert if the `orderedBytesArray` input contains any duplicate entries (i.e. duplicate bytes). - */ - function orderedBytesArrayToBitmap(bytes memory orderedBytesArray) internal pure returns (uint256) { - // sanity-check on input. a too-long input would fail later on due to having duplicate entry(s) - require(orderedBytesArray.length <= MAX_BYTE_ARRAY_LENGTH, - "BitmapUtils.orderedBytesArrayToBitmap: orderedBytesArray is too long"); - - // return empty bitmap early if length of array is 0 - if (orderedBytesArray.length == 0) { - return uint256(0); - } - - // initialize the empty bitmap, to be built inside the loop - uint256 bitmap; - // initialize an empty uint256 to be used as a bitmask inside the loop - uint256 bitMask; - - // perform the 0-th loop iteration with the ordering check *omitted* (since it is unnecessary / will always pass) - // construct a single-bit mask from the numerical value of the 0th byte of the array, and immediately add it to the bitmap - bitmap = uint256(1 << uint8(orderedBytesArray[0])); - - // loop through each byte in the array to construct the bitmap - for (uint256 i = 1; i < orderedBytesArray.length; ++i) { - // construct a single-bit mask from the numerical value of the next byte of the array - bitMask = uint256(1 << uint8(orderedBytesArray[i])); - // check strictly ascending array ordering by comparing the mask to the bitmap so far (revert if mask isn't greater than bitmap) - require(bitMask > bitmap, "BitmapUtils.orderedBytesArrayToBitmap: orderedBytesArray is not ordered"); - // add the entry to the bitmap - bitmap = (bitmap | bitMask); - } - return bitmap; - } - - /** - * @notice Converts an ordered array of bytes into a bitmap. Optimized, Yul-heavy version of `orderedBytesArrayToBitmap`. - * @param orderedBytesArray The array of bytes to convert/compress into a bitmap. Must be in strictly ascending order. - * @return bitmap The resulting bitmap. - * @dev Each byte in the input is processed as indicating a single bit to flip in the bitmap. - * @dev This function will eventually revert in the event that the `orderedBytesArray` is not properly ordered (in ascending order). - * @dev This function will also revert if the `orderedBytesArray` input contains any duplicate entries (i.e. duplicate bytes). - */ - function orderedBytesArrayToBitmap_Yul(bytes calldata orderedBytesArray) internal pure returns (uint256 bitmap) { - // sanity-check on input. a too-long input would fail later on due to having duplicate entry(s) - require(orderedBytesArray.length <= MAX_BYTE_ARRAY_LENGTH, - "BitmapUtils.orderedBytesArrayToBitmap: orderedBytesArray is too long"); - - // return empty bitmap early if length of array is 0 - if (orderedBytesArray.length == 0) { - return uint256(0); - } - - assembly { - // get first entry in bitmap (single byte => single-bit mask) - bitmap := - shl( - // extract single byte to get the correct value for the left shift - shr( - 248, - calldataload( - orderedBytesArray.offset - ) - ), - 1 - ) - // loop through other entries (byte by byte) - for { let i := 1 } lt(i, orderedBytesArray.length) { i := add(i, 1) } { - // first construct the single-bit mask by left-shifting a '1' - let bitMask := - shl( - // extract single byte to get the correct value for the left shift - shr( - 248, - calldataload( - add( - orderedBytesArray.offset, - i - ) - ) - ), - 1 - ) - // check strictly ascending ordering by comparing the mask to the bitmap so far (revert if mask isn't greater than bitmap) - // TODO: revert with a good message instead of using `revert(0, 0)` - // REFERENCE: require(bitMask > bitmap, "BitmapUtils.orderedBytesArrayToBitmap: orderedBytesArray is not ordered"); - if iszero(gt(bitMask, bitmap)) { revert(0, 0) } - // update the bitmap by adding the single bit in the mask - bitmap := or(bitmap, bitMask) - } - } - } - - /** - * @notice Converts an array of bytes into a bitmap. Optimized, Yul-heavy version of `bytesArrayToBitmap`. - * @param bytesArray The array of bytes to convert/compress into a bitmap. - * @return bitmap The resulting bitmap. - * @dev Each byte in the input is processed as indicating a single bit to flip in the bitmap. - * @dev This function will eventually revert in the event that the `bytesArray` is not properly ordered (in ascending order). - * @dev This function will also revert if the `bytesArray` input contains any duplicate entries (i.e. duplicate bytes). - */ - function bytesArrayToBitmap_Yul(bytes calldata bytesArray) internal pure returns (uint256 bitmap) { - // sanity-check on input. a too-long input would fail later on due to having duplicate entry(s) - require(bytesArray.length <= MAX_BYTE_ARRAY_LENGTH, - "BitmapUtils.bytesArrayToBitmap: bytesArray is too long"); - - // return empty bitmap early if length of array is 0 - if (bytesArray.length == 0) { - return uint256(0); - } - - assembly { - // get first entry in bitmap (single byte => single-bit mask) - bitmap := - shl( - // extract single byte to get the correct value for the left shift - shr( - 248, - calldataload( - bytesArray.offset - ) - ), - 1 - ) - // loop through other entries (byte by byte) - for { let i := 1 } lt(i, bytesArray.length) { i := add(i, 1) } { - // first construct the single-bit mask by left-shifting a '1' - let bitMask := - shl( - // extract single byte to get the correct value for the left shift - shr( - 248, - calldataload( - add( - bytesArray.offset, - i - ) - ) - ), - 1 - ) - // check against duplicates by comparing the bitmask and bitmap (revert if the bitmap already contains the entry, i.e. bitmap & bitMask != 0) - // TODO: revert with a good message instead of using `revert(0, 0)` - // REFERENCE: require(bitmap & bitMask == 0, "BitmapUtils.bytesArrayToBitmap: repeat entry in bytesArray"); - if gt(and(bitmap, bitMask), 0) { revert(0, 0) } - // update the bitmap by adding the single bit in the mask - bitmap := or(bitmap, bitMask) - } - } - } - - /** - * @notice Utility function for checking if a bytes array is strictly ordered, in ascending order. - * @param bytesArray the bytes array of interest - * @return Returns 'true' if the array is ordered in strictly ascending order, and 'false' otherwise. - * @dev This function returns 'true' for the edge case of the `bytesArray` having zero length. - * It also returns 'false' early for arrays with length in excess of MAX_BYTE_ARRAY_LENGTH (i.e. so long that they cannot be strictly ordered) - */ - function isArrayStrictlyAscendingOrdered(bytes calldata bytesArray) internal pure returns (bool) { - // return 'false' early for too-long (i.e. unorderable) arrays - if (bytesArray.length > MAX_BYTE_ARRAY_LENGTH) { - return false; - } - - // return 'true' early if length of array is 0 - if (bytesArray.length == 0) { - return true; - } - - // initialize an empty byte object, to be re-used inside the loop - bytes1 singleByte; - - // perform the 0-th loop iteration with the ordering check *omitted* (otherwise it will break with an out-of-bounds error) - // pull the 0th byte out of the array - singleByte = bytesArray[0]; - - // loop through each byte in the array to construct the bitmap - for (uint256 i = 1; i < bytesArray.length; ++i) { - // check if the entry is *less than or equal to* the previous entry. if it is, then the array isn't strictly ordered! - if (uint256(uint8(bytesArray[i])) <= uint256(uint8(singleByte))) { - return false; - } - // pull the next byte out of the array - singleByte = bytesArray[i]; - } - return true; - } - - /** - * @notice Converts a bitmap into an array of bytes. - * @param bitmap The bitmap to decompress/convert to an array of bytes. - * @return bytesArray The resulting bitmap array of bytes. - * @dev Each byte in the input is processed as indicating a single bit to flip in the bitmap - */ - function bitmapToBytesArray(uint256 bitmap) internal pure returns (bytes memory bytesArray) { - // initialize an empty uint256 to be used as a bitmask inside the loop - uint256 bitMask; - // loop through each index in the bitmap to construct the array - for (uint256 i = 0; i < 256; ++i) { - // construct a single-bit mask for the i-th bit - bitMask = uint256(1 << i); - // check if the i-th bit is flipped in the bitmap - if (bitmap & bitMask != 0) { - // if the i-th bit is flipped, then add a byte encoding the value 'i' to the `bytesArray` - bytesArray = bytes.concat(bytesArray, bytes1(uint8(i))); - } - } - return bytesArray; - } - - /// @return count number of ones in binary representation of `n` - function countNumOnes(uint256 n) internal pure returns (uint16) { - uint16 count = 0; - while (n > 0) { - n &= (n - 1); - count++; - } - return count; - } - - // @notice returns 'true' if `numberToCheckForInclusion` is in `bitmap` and 'false' otherwise. - function numberIsInBitmap(uint256 bitmap, uint8 numberToCheckForInclusion) internal pure returns (bool) { - return (((bitmap >> numberToCheckForInclusion) & 1) == 1); - } -} \ No newline at end of file diff --git a/src/contracts/pods/EigenPod.sol b/src/contracts/pods/EigenPod.sol index f9eef1f25..d9a8a46bd 100644 --- a/src/contracts/pods/EigenPod.sol +++ b/src/contracts/pods/EigenPod.sol @@ -541,7 +541,7 @@ contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, Eigen uint64 currentRestakedBalanceGwei = validatorInfo.restakedBalanceGwei; uint64 newRestakedBalanceGwei; - if (validatorEffectiveBalanceGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { + if (validatorBalance > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { newRestakedBalanceGwei = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; } else { newRestakedBalanceGwei = validatorEffectiveBalanceGwei; diff --git a/src/test/Delegation.t.sol b/src/test/Delegation.t.sol index dcb5ffe59..ff21d5666 100644 --- a/src/test/Delegation.t.sol +++ b/src/test/Delegation.t.sol @@ -459,6 +459,1039 @@ contract DelegationTests is EigenLayerTestHelper { assertTrue(delegation.isOperator(_operator)); } + /************************************** + * + * Withdrawals Tests with StrategyManager, using actual SM contract instead of Mock to test + * + **************************************/ + + + // function testQueueWithdrawalRevertsMismatchedSharesAndStrategyArrayLength() external { + // IStrategy[] memory strategyArray = new IStrategy[](1); + // uint256[] memory shareAmounts = new uint256[](2); + + // { + // strategyArray[0] = eigenPodManagerMock.beaconChainETHStrategy(); + // shareAmounts[0] = 1; + // shareAmounts[1] = 1; + // } + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: strategyArray, + // shares: shareAmounts, + // withdrawer: address(this) + // }); + + // cheats.expectRevert(bytes("DelegationManager.queueWithdrawal: input length mismatch")); + // delegationManager.queueWithdrawals(params); + // } + + // function testQueueWithdrawalRevertsWithZeroAddressWithdrawer() external { + // IStrategy[] memory strategyArray = new IStrategy[](1); + // uint256[] memory shareAmounts = new uint256[](1); + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: strategyArray, + // shares: shareAmounts, + // withdrawer: address(0) + // }); + + // cheats.expectRevert(bytes("DelegationManager.queueWithdrawal: must provide valid withdrawal address")); + // delegationManager.queueWithdrawals(params); + // } + + // function testQueueWithdrawal_ToSelf( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) + // public + // returns ( + // IDelegationManager.Withdrawal memory /* queuedWithdrawal */, + // IERC20[] memory /* tokensArray */, + // bytes32 /* withdrawalRoot */ + // ) + // { + // _setUpWithdrawalTests(); + // StrategyBase strategy = strategyMock; + // IERC20 token = strategy.underlyingToken(); + + // // filtering of fuzzed inputs + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + + // _tempStrategyStorage = strategy; + + // _depositIntoStrategySuccessfully(strategy, /*staker*/ address(this), depositAmount); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // bytes32 withdrawalRoot + // ) = _setUpWithdrawalStructSingleStrat( + // /*staker*/ address(this), + // /*withdrawer*/ address(this), + // token, + // _tempStrategyStorage, + // withdrawalAmount + // ); + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(/*staker*/ address(this), _tempStrategyStorage); + // uint256 nonceBefore = delegationManager.cumulativeWithdrawalsQueued(/*staker*/ address(this)); + + // require(!delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRootPendingBefore is true!"); + + // { + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalQueued( + // withdrawalRoot, + // withdrawal + // ); + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: withdrawal.strategies, + // shares: withdrawal.shares, + // withdrawer: address(this) + // }); + // delegationManager.queueWithdrawals(params); + // } + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(/*staker*/ address(this), _tempStrategyStorage); + // uint256 nonceAfter = delegationManager.cumulativeWithdrawalsQueued(/*staker*/ address(this)); + + // require(delegationManager.pendingWithdrawals(withdrawalRoot), "pendingWithdrawalsAfter is false!"); + // require(sharesAfter == sharesBefore - withdrawalAmount, "sharesAfter != sharesBefore - withdrawalAmount"); + // require(nonceAfter == nonceBefore + 1, "nonceAfter != nonceBefore + 1"); + + // return (withdrawal, tokensArray, withdrawalRoot); + // } + + // function testQueueWithdrawal_ToSelf_TwoStrategies( + // uint256[2] memory depositAmounts, + // uint256[2] memory withdrawalAmounts + // ) + // public + // returns ( + // IDelegationManager.Withdrawal memory /* withdrawal */, + // bytes32 /* withdrawalRoot */ + // ) + // { + // _setUpWithdrawalTests(); + // // filtering of fuzzed inputs + // cheats.assume(withdrawalAmounts[0] != 0 && withdrawalAmounts[0] < depositAmounts[0]); + // cheats.assume(withdrawalAmounts[1] != 0 && withdrawalAmounts[1] < depositAmounts[1]); + // address staker = address(this); + + // IStrategy[] memory strategies = new IStrategy[](2); + // strategies[0] = IStrategy(strategyMock); + // strategies[1] = IStrategy(strategyMock2); + + // IERC20[] memory tokens = new IERC20[](2); + // tokens[0] = strategyMock.underlyingToken(); + // tokens[1] = strategyMock2.underlyingToken(); + + // uint256[] memory amounts = new uint256[](2); + // amounts[0] = withdrawalAmounts[0]; + // amounts[1] = withdrawalAmounts[1]; + + // _depositIntoStrategySuccessfully(strategies[0], staker, depositAmounts[0]); + // _depositIntoStrategySuccessfully(strategies[1], staker, depositAmounts[1]); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // bytes32 withdrawalRoot + // ) = _setUpWithdrawalStruct_MultipleStrategies( + // /* staker */ staker, + // /* withdrawer */ staker, + // strategies, + // amounts + // ); + + // uint256[] memory sharesBefore = new uint256[](2); + // sharesBefore[0] = strategyManager.stakerStrategyShares(staker, strategies[0]); + // sharesBefore[1] = strategyManager.stakerStrategyShares(staker, strategies[1]); + // uint256 nonceBefore = delegationManager.cumulativeWithdrawalsQueued(staker); + + // require(!delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRootPendingBefore is true!"); + + // { + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalQueued( + // withdrawalRoot, + // withdrawal + // ); + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: withdrawal.strategies, + // shares: withdrawal.shares, + // withdrawer: staker + // }); + + // delegationManager.queueWithdrawals( + // params + // ); + // } + + // uint256[] memory sharesAfter = new uint256[](2); + // sharesAfter[0] = strategyManager.stakerStrategyShares(staker, strategies[0]); + // sharesAfter[1] = strategyManager.stakerStrategyShares(staker, strategies[1]); + // uint256 nonceAfter = delegationManager.cumulativeWithdrawalsQueued(staker); + + // require(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRootPendingAfter is false!"); + // require( + // sharesAfter[0] == sharesBefore[0] - withdrawalAmounts[0], + // "Strat1: sharesAfter != sharesBefore - withdrawalAmount" + // ); + // require( + // sharesAfter[1] == sharesBefore[1] - withdrawalAmounts[1], + // "Strat2: sharesAfter != sharesBefore - withdrawalAmount" + // ); + // require(nonceAfter == nonceBefore + 1, "nonceAfter != nonceBefore + 1"); + + // return (withdrawal, withdrawalRoot); + // } + + // function testQueueWithdrawalPartiallyWithdraw(uint128 amount) external { + // testQueueWithdrawal_ToSelf(uint256(amount) * 2, amount); + // require(!delegationManager.isDelegated(address(this)), "should still be delegated failed"); + // } + + // function testQueueWithdrawal_ToDifferentAddress( + // address withdrawer, + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external filterFuzzedAddressInputs(withdrawer) { + // _setUpWithdrawalTests(); + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // address staker = address(this); + + // _depositIntoStrategySuccessfully(strategyMock, staker, depositAmount); + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // , + // bytes32 withdrawalRoot + // ) = _setUpWithdrawalStructSingleStrat( + // staker, + // withdrawer, + // /*token*/ strategyMock.underlyingToken(), + // strategyMock, + // withdrawalAmount + // ); + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategyMock); + // uint256 nonceBefore = delegationManager.cumulativeWithdrawalsQueued(staker); + + // require(!delegationManager.pendingWithdrawals(withdrawalRoot), "pendingWithdrawalsBefore is true!"); + + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalQueued( + // withdrawalRoot, + // withdrawal + // ); + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: withdrawal.strategies, + // shares: withdrawal.shares, + // withdrawer: withdrawer + // }); + + // delegationManager.queueWithdrawals( + // params + // ); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategyMock); + // uint256 nonceAfter = delegationManager.cumulativeWithdrawalsQueued(staker); + + // require(delegationManager.pendingWithdrawals(withdrawalRoot), "pendingWithdrawalsAfter is false!"); + // require(sharesAfter == sharesBefore - withdrawalAmount, "sharesAfter != sharesBefore - amount"); + // require(nonceAfter == nonceBefore + 1, "nonceAfter != nonceBefore + 1"); + // } + + // function testCompleteQueuedWithdrawalRevertsWhenAttemptingReentrancy( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // // replace dummyStrat with Reenterer contract + // reenterer = new Reenterer(); + // strategyMock = StrategyBase(address(reenterer)); + + // // whitelist the strategy for deposit + // cheats.startPrank(strategyManager.owner()); + // IStrategy[] memory _strategy = new IStrategy[](1); + // _strategy[0] = strategyMock; + // strategyManager.addStrategiesToDepositWhitelist(_strategy); + // cheats.stopPrank(); + + // _tempStakerStorage = address(this); + // IStrategy strategy = strategyMock; + + // reenterer.prepareReturnData(abi.encode(depositAmount)); + + + // IStrategy[] memory strategyArray = new IStrategy[](1); + // IERC20[] memory tokensArray = new IERC20[](1); + // uint256[] memory shareAmounts = new uint256[](1); + // { + // strategyArray[0] = strategy; + // shareAmounts[0] = withdrawalAmount; + // tokensArray[0] = mockToken; + // } + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // /* tokensArray */, + // /* withdrawalRoot */ + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = false; + + // address targetToUse = address(strategyManager); + // uint256 msgValueToUse = 0; + // bytes memory calldataToUse = abi.encodeWithSelector( + // DelegationManager.completeQueuedWithdrawal.selector, + // withdrawal, + // tokensArray, + // middlewareTimesIndex, + // receiveAsTokens + // ); + // reenterer.prepare(targetToUse, msgValueToUse, calldataToUse, bytes("ReentrancyGuard: reentrant call")); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + // } + + // function testCompleteQueuedWithdrawalRevertsWhenNotCallingFromWithdrawerAddress( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // _tempStakerStorage = address(this); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // IStrategy strategy = withdrawal.strategies[0]; + // IERC20 token = tokensArray[0]; + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = false; + + // cheats.startPrank(address(123456)); + // cheats.expectRevert( + // bytes( + // "DelegationManager.completeQueuedAction: only withdrawer can complete action" + // ) + // ); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + // cheats.stopPrank(); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); + + // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); + // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // } + + // function testCompleteQueuedWithdrawalRevertsWhenTryingToCompleteSameWithdrawal2X( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // _tempStakerStorage = address(this); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // bytes32 withdrawalRoot + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // IStrategy strategy = withdrawal.strategies[0]; + // IERC20 token = tokensArray[0]; + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = false; + + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalCompleted(withdrawalRoot); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); + + // require(sharesAfter == sharesBefore + withdrawalAmount, "sharesAfter != sharesBefore + withdrawalAmount"); + // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // sharesBefore = sharesAfter; + // balanceBefore = balanceAfter; + + // cheats.expectRevert( + // bytes( + // "DelegationManager.completeQueuedAction: action is not in queue" + // ) + // ); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); + // balanceAfter = token.balanceOf(address(_tempStakerStorage)); + + // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); + // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // } + + // function testCompleteQueuedWithdrawalRevertsWhenWithdrawalDelayBlocksHasNotPassed( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // _tempStakerStorage = address(this); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // uint256 valueToSet = 1; + // // set the `withdrawalDelayBlocks` variable + // cheats.startPrank(strategyManager.owner()); + // uint256 previousValue = delegationManager.withdrawalDelayBlocks(); + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalDelayBlocksSet(previousValue, valueToSet); + // delegationManager.setWithdrawalDelayBlocks(valueToSet); + // cheats.stopPrank(); + // require( + // delegationManager.withdrawalDelayBlocks() == valueToSet, + // "delegationManager.withdrawalDelayBlocks() != valueToSet" + // ); + + // cheats.expectRevert( + // bytes("DelegationManager.completeQueuedAction: withdrawalDelayBlocks period has not yet passed") + // ); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, /* middlewareTimesIndex */ 0, /* receiveAsTokens */ false); + // } + + // function testCompleteQueuedWithdrawalRevertsWhenWithdrawalDoesNotExist() external { + // uint256 withdrawalAmount = 1e18; + // IStrategy strategy = strategyMock; + // IERC20 token = strategy.underlyingToken(); + + // (IDelegationManager.Withdrawal memory withdrawal, IERC20[] memory tokensArray, ) = _setUpWithdrawalStructSingleStrat( + // /*staker*/ address(this), + // /*withdrawer*/ address(this), + // token, + // strategy, + // withdrawalAmount + // ); + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = false; + + // cheats.expectRevert(bytes("DelegationManager.completeQueuedAction: action is not in queue")); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); + + // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); + // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // } + + // function testCompleteQueuedWithdrawalRevertsWhenWithdrawalsPaused( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // _tempStakerStorage = address(this); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // IStrategy strategy = withdrawal.strategies[0]; + // IERC20 token = tokensArray[0]; + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = false; + + // // pause withdrawals + // cheats.startPrank(pauser); + // delegationManager.pause(2 ** PAUSED_EXIT_WITHDRAWAL_QUEUE); + // cheats.stopPrank(); + + // cheats.expectRevert(bytes("Pausable: index is paused")); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); + + // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); + // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // } + + // function testCompleteQueuedWithdrawalFailsWhenTokensInputLengthMismatch( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // _tempStakerStorage = address(this); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // IStrategy strategy = withdrawal.strategies[0]; + // IERC20 token = tokensArray[0]; + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = true; + // // mismatch tokens array by setting tokens array to empty array + // tokensArray = new IERC20[](0); + + // cheats.expectRevert(bytes("DelegationManager.completeQueuedAction: input length mismatch")); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); + + // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); + // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // } + + // function testCompleteQueuedWithdrawalWithNonzeroWithdrawalDelayBlocks( + // uint256 depositAmount, + // uint256 withdrawalAmount, + // uint16 valueToSet + // ) external { + // // filter fuzzed inputs to allowed *and nonzero* amounts + // cheats.assume(valueToSet <= delegationManager.MAX_WITHDRAWAL_DELAY_BLOCKS() && valueToSet != 0); + // cheats.assume(depositAmount != 0 && withdrawalAmount != 0); + // cheats.assume(depositAmount >= withdrawalAmount); + // address staker = address(this); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // IStrategy strategy = withdrawal.strategies[0]; + // IERC20 token = tokensArray[0]; + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = false; + + // // set the `withdrawalDelayBlocks` variable + // cheats.startPrank(delegationManager.owner()); + // uint256 previousValue = delegationManager.withdrawalDelayBlocks(); + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalDelayBlocksSet(previousValue, valueToSet); + // delegationManager.setWithdrawalDelayBlocks(valueToSet); + // cheats.stopPrank(); + // require( + // delegationManager.withdrawalDelayBlocks() == valueToSet, + // "strategyManager.withdrawalDelayBlocks() != valueToSet" + // ); + + // cheats.expectRevert( + // bytes("DelegationManager.completeQueuedAction: withdrawalDelayBlocks period has not yet passed") + // ); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceBefore = token.balanceOf(address(staker)); + + // // roll block number forward to one block before the withdrawal should be completeable and attempt again + // uint256 originalBlockNumber = block.number; + // cheats.roll(originalBlockNumber + valueToSet - 1); + // cheats.expectRevert( + // bytes("DelegationManager.completeQueuedAction: withdrawalDelayBlocks period has not yet passed") + // ); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // // roll block number forward to the block at which the withdrawal should be completeable, and complete it + // cheats.roll(originalBlockNumber + valueToSet); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceAfter = token.balanceOf(address(staker)); + + // require(sharesAfter == sharesBefore + withdrawalAmount, "sharesAfter != sharesBefore + withdrawalAmount"); + // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // } + + + // function testCompleteQueuedWithdrawal_ReceiveAsTokensMarkedFalse( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // address staker = address(this); + + // // testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // bytes32 withdrawalRoot + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // IStrategy strategy = withdrawal.strategies[0]; + // IERC20 token = tokensArray[0]; + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceBefore = token.balanceOf(address(staker)); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = false; + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalCompleted(withdrawalRoot); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); + // uint256 balanceAfter = token.balanceOf(address(staker)); + + // require(sharesAfter == sharesBefore + withdrawalAmount, "sharesAfter != sharesBefore + withdrawalAmount"); + // require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // } + + // function testCompleteQueuedWithdrawal_ReceiveAsTokensMarkedTrue( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); + // address staker = address(this); + + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // bytes32 withdrawalRoot + // ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + + // IStrategy strategy = withdrawal.strategies[0]; + // IERC20 token = tokensArray[0]; + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); + // uint256 balanceBefore = token.balanceOf(address(staker)); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = true; + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalCompleted(withdrawalRoot); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategy); + // uint256 balanceAfter = token.balanceOf(address(staker)); + + // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); + // require(balanceAfter == balanceBefore + withdrawalAmount, "balanceAfter != balanceBefore + withdrawalAmount"); + // if (depositAmount == withdrawalAmount) { + // // Since receiving tokens instead of shares, if withdrawal amount is entire deposit, then strategy will be removed + // // with sharesAfter being 0 + // require( + // !_isDepositedStrategy(staker, strategy), + // "Strategy still part of staker's deposited strategies" + // ); + // require(sharesAfter == 0, "staker shares is not 0"); + // } + // } + + // function testCompleteQueuedWithdrawalFullyWithdraw(uint256 amount) external { + // address staker = address(this); + // ( + // IDelegationManager.Withdrawal memory withdrawal, + // IERC20[] memory tokensArray, + // bytes32 withdrawalRoot + // ) = testQueueWithdrawal_ToSelf(amount, amount); + + // IStrategy strategy = withdrawal.strategies[0]; + // IERC20 token = tokensArray[0]; + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); + // uint256 balanceBefore = token.balanceOf(address(staker)); + + // uint256 middlewareTimesIndex = 0; + // bool receiveAsTokens = true; + // cheats.expectEmit(true, true, true, true, address(delegationManager)); + // emit WithdrawalCompleted(withdrawalRoot); + // delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategy); + // uint256 balanceAfter = token.balanceOf(address(staker)); + + // require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); + // require(balanceAfter == balanceBefore + amount, "balanceAfter != balanceBefore + withdrawalAmount"); + // require( + // !_isDepositedStrategy(staker, strategy), + // "Strategy still part of staker's deposited strategies" + // ); + // require(sharesAfter == 0, "staker shares is not 0"); + // } + + // function test_removeSharesRevertsWhenShareAmountIsZero(uint256 depositAmount) external { + // _setUpWithdrawalTests(); + // address staker = address(this); + // uint256 withdrawalAmount = 0; + + // _depositIntoStrategySuccessfully(strategyMock, staker, depositAmount); + + // (IDelegationManager.Withdrawal memory withdrawal, , ) = _setUpWithdrawalStructSingleStrat( + // /*staker*/ address(this), + // /*withdrawer*/ address(this), + // mockToken, + // strategyMock, + // withdrawalAmount + // ); + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: withdrawal.strategies, + // shares: withdrawal.shares, + // withdrawer: staker + // }); + + // cheats.expectRevert(bytes("StrategyManager._removeShares: shareAmount should not be zero!")); + // delegationManager.queueWithdrawals( + // params + // ); + // } + + // function test_removeSharesRevertsWhenShareAmountIsTooLarge( + // uint256 depositAmount, + // uint256 withdrawalAmount + // ) external { + // _setUpWithdrawalTests(); + // cheats.assume(depositAmount > 0 && withdrawalAmount > depositAmount); + // address staker = address(this); + + // _depositIntoStrategySuccessfully(strategyMock, staker, depositAmount); + + // (IDelegationManager.Withdrawal memory withdrawal, , ) = _setUpWithdrawalStructSingleStrat( + // /*staker*/ address(this), + // /*withdrawer*/ address(this), + // mockToken, + // strategyMock, + // withdrawalAmount + // ); + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: withdrawal.strategies, + // shares: withdrawal.shares, + // withdrawer: address(this) + // }); + + // cheats.expectRevert(bytes("StrategyManager._removeShares: shareAmount too high")); + // delegationManager.queueWithdrawals( + // params + // ); + // } + + // /** + // * Testing queueWithdrawal of 3 strategies, fuzzing the deposit and withdraw amounts. if the withdrawal amounts == deposit amounts + // * then the strategy should be removed from the staker StrategyList + // */ + // function test_removeStrategyFromStakerStrategyList(uint256[3] memory depositAmounts, uint256[3] memory withdrawalAmounts) external { + // _setUpWithdrawalTests(); + // // filtering of fuzzed inputs + // cheats.assume(withdrawalAmounts[0] > 0 && withdrawalAmounts[0] <= depositAmounts[0]); + // cheats.assume(withdrawalAmounts[1] > 0 && withdrawalAmounts[1] <= depositAmounts[1]); + // cheats.assume(withdrawalAmounts[2] > 0 && withdrawalAmounts[2] <= depositAmounts[2]); + // address staker = address(this); + + // // Setup input params + // IStrategy[] memory strategies = new IStrategy[](3); + // strategies[0] = strategyMock; + // strategies[1] = strategyMock2; + // strategies[2] = strategyMock3; + // uint256[] memory amounts = new uint256[](3); + // amounts[0] = withdrawalAmounts[0]; + // amounts[1] = withdrawalAmounts[1]; + // amounts[2] = withdrawalAmounts[2]; + + // _depositIntoStrategySuccessfully(strategies[0], staker, depositAmounts[0]); + // _depositIntoStrategySuccessfully(strategies[1], staker, depositAmounts[1]); + // _depositIntoStrategySuccessfully(strategies[2], staker, depositAmounts[2]); + + // ( ,bytes32 withdrawalRoot) = _setUpWithdrawalStruct_MultipleStrategies( + // /* staker */ staker, + // /* withdrawer */ staker, + // strategies, + // amounts + // ); + // require(!delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRootPendingBefore is true!"); + // delegationManager.cumulativeWithdrawalsQueued(staker); + // uint256[] memory sharesBefore = new uint256[](3); + // sharesBefore[0] = strategyManager.stakerStrategyShares(staker, strategies[0]); + // sharesBefore[1] = strategyManager.stakerStrategyShares(staker, strategies[1]); + // sharesBefore[2] = strategyManager.stakerStrategyShares(staker, strategies[2]); + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: strategies, + // shares: amounts, + // withdrawer: address(this) + // }); + + // delegationManager.queueWithdrawals( + // params + // ); + + // uint256[] memory sharesAfter = new uint256[](3); + // sharesAfter[0] = strategyManager.stakerStrategyShares(staker, strategies[0]); + // sharesAfter[1] = strategyManager.stakerStrategyShares(staker, strategies[1]); + // sharesAfter[2] = strategyManager.stakerStrategyShares(staker, strategies[2]); + // require(sharesBefore[0] == sharesAfter[0] + withdrawalAmounts[0], "Strat1: sharesBefore != sharesAfter + withdrawalAmount"); + // if (depositAmounts[0] == withdrawalAmounts[0]) { + // require(!_isDepositedStrategy(staker, strategies[0]), "Strategy still part of staker's deposited strategies"); + // } + // require(sharesBefore[1] == sharesAfter[1] + withdrawalAmounts[1], "Strat2: sharesBefore != sharesAfter + withdrawalAmount"); + // if (depositAmounts[1] == withdrawalAmounts[1]) { + // require(!_isDepositedStrategy(staker, strategies[1]), "Strategy still part of staker's deposited strategies"); + // } + // require(sharesBefore[2] == sharesAfter[2] + withdrawalAmounts[2], "Strat3: sharesBefore != sharesAfter + withdrawalAmount"); + // if (depositAmounts[2] == withdrawalAmounts[2]) { + // require(!_isDepositedStrategy(staker, strategies[2]), "Strategy still part of staker's deposited strategies"); + // } + // } + + // // ensures that when the staker and withdrawer are different and a withdrawal is completed as shares (i.e. not as tokens) + // // that the shares get added back to the right operator + // function test_completingWithdrawalAsSharesAddsSharesToCorrectOperator() external { + // address staker = address(this); + // address withdrawer = address(1000); + // address operator_for_staker = address(1001); + // address operator_for_withdrawer = address(1002); + + // // register operators + // bytes32 salt; + // IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + // earningsReceiver: operator_for_staker, + // delegationApprover: address(0), + // stakerOptOutWindowBlocks: 0 + // }); + // testRegisterAsOperator(operator_for_staker, operatorDetails, emptyStringForMetadataURI); + // testRegisterAsOperator(operator_for_withdrawer, operatorDetails, emptyStringForMetadataURI); + + // // delegate from the `staker` and withdrawer to the operators + // ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + // cheats.startPrank(staker); + // delegationManager.delegateTo(operator_for_staker, approverSignatureAndExpiry, salt); + // cheats.stopPrank(); + // cheats.startPrank(withdrawer); + // delegationManager.delegateTo(operator_for_withdrawer, approverSignatureAndExpiry, salt); + // cheats.stopPrank(); + + // // Setup input params + // IStrategy[] memory strategies = new IStrategy[](3); + // strategies[0] = strategyMock; + // strategies[1] = delegationManager.beaconChainETHStrategy(); + // strategies[2] = strategyMock3; + // uint256[] memory amounts = new uint256[](3); + // amounts[0] = 1e18; + // amounts[1] = 2e18; + // amounts[2] = 3e18; + + // (IDelegationManager.Withdrawal memory withdrawal, ) = _setUpWithdrawalStruct_MultipleStrategies({ + // staker: staker, + // withdrawer: withdrawer, + // strategyArray: strategies, + // shareAmounts: amounts + // }); + + // // give both the operators a bunch of delegated shares, so we can decrement them when queuing the withdrawal + // cheats.startPrank(address(delegationManager.strategyManager())); + // for (uint256 i = 0; i < strategies.length; ++i) { + // delegationManager.increaseDelegatedShares(staker, strategies[i], amounts[i]); + // delegationManager.increaseDelegatedShares(withdrawer, strategies[i], amounts[i]); + // } + // cheats.stopPrank(); + + // IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + + // params[0] = IDelegationManager.QueuedWithdrawalParams({ + // strategies: strategies, + // shares: amounts, + // withdrawer: withdrawer + // }); + + // // queue the withdrawal + // cheats.startPrank(staker); + // delegationManager.queueWithdrawals(params); + // cheats.stopPrank(); + + // for (uint256 i = 0; i < strategies.length; ++i) { + // require(delegationManager.operatorShares(operator_for_staker, strategies[i]) == 0, + // "staker operator shares incorrect after queueing"); + // require(delegationManager.operatorShares(operator_for_withdrawer, strategies[i]) == amounts[i], + // "withdrawer operator shares incorrect after queuing"); + // } + + // // complete the withdrawal + // cheats.startPrank(withdrawer); + // IERC20[] memory tokens; + // delegationManager.completeQueuedWithdrawal( + // withdrawal, + // tokens, + // 0 /*middlewareTimesIndex*/, + // false /*receiveAsTokens*/ + // ); + // cheats.stopPrank(); + + // for (uint256 i = 0; i < strategies.length; ++i) { + // if (strategies[i] != delegationManager.beaconChainETHStrategy()) { + // require(delegationManager.operatorShares(operator_for_staker, strategies[i]) == 0, + // "staker operator shares incorrect after completing withdrawal"); + // require(delegationManager.operatorShares(operator_for_withdrawer, strategies[i]) == 2 * amounts[i], + // "withdrawer operator shares incorrect after completing withdrawal"); + // } else { + // require(delegationManager.operatorShares(operator_for_staker, strategies[i]) == amounts[i], + // "staker operator beaconChainETHStrategy shares incorrect after completing withdrawal"); + // require(delegationManager.operatorShares(operator_for_withdrawer, strategies[i]) == amounts[i], + // "withdrawer operator beaconChainETHStrategy shares incorrect after completing withdrawal"); + // } + // } + // } + + // /** + // * Setup DelegationManager and StrategyManager contracts for testing instead of using StrategyManagerMock + // * since we need to test the actual contracts together for the withdrawal queueing tests + // */ + // function _setUpWithdrawalTests() internal { + // delegationManagerImplementation = new DelegationManager(strategyManager, slasherMock, eigenPodManagerMock); + // cheats.startPrank(eigenLayerProxyAdmin.owner()); + // eigenLayerProxyAdmin.upgrade(TransparentUpgradeableProxy(payable(address(delegationManager))), address(delegationManagerImplementation)); + // cheats.stopPrank(); + + + // strategyImplementation = new StrategyBase(strategyManager); + // mockToken = new ERC20Mock(); + // strategyMock = StrategyBase( + // address( + // new TransparentUpgradeableProxy( + // address(strategyImplementation), + // address(eigenLayerProxyAdmin), + // abi.encodeWithSelector(StrategyBase.initialize.selector, mockToken, pauserRegistry) + // ) + // ) + // ); + // strategyMock2 = StrategyBase( + // address( + // new TransparentUpgradeableProxy( + // address(strategyImplementation), + // address(eigenLayerProxyAdmin), + // abi.encodeWithSelector(StrategyBase.initialize.selector, mockToken, pauserRegistry) + // ) + // ) + // ); + // strategyMock3 = StrategyBase( + // address( + // new TransparentUpgradeableProxy( + // address(strategyImplementation), + // address(eigenLayerProxyAdmin), + // abi.encodeWithSelector(StrategyBase.initialize.selector, mockToken, pauserRegistry) + // ) + // ) + // ); + + // // whitelist the strategy for deposit + // cheats.startPrank(strategyManager.owner()); + // IStrategy[] memory _strategies = new IStrategy[](3); + // _strategies[0] = strategyMock; + // _strategies[1] = strategyMock2; + // _strategies[2] = strategyMock3; + // strategyManager.addStrategiesToDepositWhitelist(_strategies); + // cheats.stopPrank(); + + // require(delegationManager.strategyManager() == strategyManager, + // "constructor / initializer incorrect, strategyManager set wrong"); + // } + + // function _depositIntoStrategySuccessfully( + // IStrategy strategy, + // address staker, + // uint256 amount + // ) internal { + // IERC20 token = strategy.underlyingToken(); + // // IStrategy strategy = strategyMock; + + // // filter out zero case since it will revert with "StrategyManager._addShares: shares should not be zero!" + // cheats.assume(amount != 0); + // // filter out zero address because the mock ERC20 we are using will revert on using it + // cheats.assume(staker != address(0)); + // // filter out the strategy itself from fuzzed inputs + // cheats.assume(staker != address(strategy)); + // // sanity check / filter + // cheats.assume(amount <= token.balanceOf(address(this))); + // cheats.assume(amount >= 1); + + // uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); + // uint256 stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker); + + // // needed for expecting an event with the right parameters + // uint256 expectedShares = strategy.underlyingToShares(amount); + + // cheats.startPrank(staker); + // cheats.expectEmit(true, true, true, true, address(strategyManager)); + // emit Deposit(staker, token, strategy, expectedShares); + // uint256 shares = strategyManager.depositIntoStrategy(strategy, token, amount); + // cheats.stopPrank(); + + // uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategy); + // uint256 stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker); + + // require(sharesAfter == sharesBefore + shares, "sharesAfter != sharesBefore + shares"); + // if (sharesBefore == 0) { + // require( + // stakerStrategyListLengthAfter == stakerStrategyListLengthBefore + 1, + // "stakerStrategyListLengthAfter != stakerStrategyListLengthBefore + 1" + // ); + // require( + // strategyManager.stakerStrategyList(staker, stakerStrategyListLengthAfter - 1) == strategy, + // "strategyManager.stakerStrategyList(staker, stakerStrategyListLengthAfter - 1) != strategy" + // ); + // } + // } + + // /** + // * @notice internal function to help check if a strategy is part of list of deposited strategies for a staker + // * Used to check if removed correctly after withdrawing all shares for a given strategy + // */ + // function _isDepositedStrategy(address staker, IStrategy strategy) internal view returns (bool) { + // uint256 stakerStrategyListLength = strategyManager.stakerStrategyListLength(staker); + // for (uint256 i = 0; i < stakerStrategyListLength; ++i) { + // if (strategyManager.stakerStrategyList(staker, i) == strategy) { + // return true; + // } + // } + // return false; + // } + function _testRegisterAdditionalOperator(uint256 index) internal { address sender = getOperatorAddress(index); diff --git a/src/test/EigenPod.t.sol b/src/test/EigenPod.t.sol index 25ce1b782..8fe03f637 100644 --- a/src/test/EigenPod.t.sol +++ b/src/test/EigenPod.t.sol @@ -112,6 +112,9 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { /// @notice event for the claiming of delayedWithdrawals event DelayedWithdrawalsClaimed(address recipient, uint256 amountClaimed, uint256 delayedWithdrawalsCompleted); + /// @notice Emitted when ETH that was previously received via the `receive` fallback is withdrawn + event NonBeaconChainETHWithdrawn(address indexed recipient, uint256 amountWithdrawn); + modifier fuzzedAddress(address addr) virtual { cheats.assume(fuzzedAddressMapping[addr] == false); _; @@ -284,12 +287,20 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { stakeAmount, delayedWithdrawalRouter.userWithdrawalsLength(podOwner) ); + + uint timestampBeforeTx = pod.mostRecentWithdrawalTimestamp(); + pod.withdrawBeforeRestaking(); + require(_getLatestDelayedWithdrawalAmount(podOwner) == stakeAmount, "Payment amount should be stake amount"); require( pod.mostRecentWithdrawalTimestamp() == uint64(block.timestamp), "Most recent withdrawal block number not updated" ); + require( + pod.mostRecentWithdrawalTimestamp() > timestampBeforeTx, + "Most recent withdrawal block number not updated" + ); } function testDeployEigenPodWithoutActivateRestaking() public { @@ -368,6 +379,45 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { cheats.stopPrank(); } + function testWithdrawNonBeaconChainETHBalanceWei() public { + IEigenPod pod = testDeployAndVerifyNewEigenPod(); + + cheats.deal(address(podOwner), 10 ether); + emit log_named_address("Pod:", address(pod)); + + uint256 balanceBeforeDeposit = pod.nonBeaconChainETHBalanceWei(); + + (bool sent, ) = payable(address(pod)).call{value: 1 ether}(""); + + require(sent == true, "not sent"); + + uint256 balanceAfterDeposit = pod.nonBeaconChainETHBalanceWei(); + + require( + balanceBeforeDeposit < balanceAfterDeposit + && (balanceAfterDeposit - balanceBeforeDeposit) == 1 ether, + "increment checks" + ); + + cheats.startPrank(podOwner, podOwner); + cheats.expectEmit(true, true, true, true, address(pod)); + emit NonBeaconChainETHWithdrawn(podOwner, 1 ether); + pod.withdrawNonBeaconChainETHBalanceWei( + podOwner, + 1 ether + ); + + uint256 balanceAfterWithdrawal = pod.nonBeaconChainETHBalanceWei(); + + require( + balanceAfterWithdrawal < balanceAfterDeposit + && balanceAfterWithdrawal == balanceBeforeDeposit, + "decrement checks" + ); + + cheats.stopPrank(); + } + function testWithdrawFromPod() public { IEigenPod pod = eigenPodManager.getPod(podOwner); cheats.startPrank(podOwner); @@ -722,7 +772,8 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { "eigenPodManager shares not updated correctly" ); } - + + /// @notice Similar test done in EP unit test //test deploying an eigen pod with mismatched withdrawal credentials between the proof and the actual pod's address function testDeployNewEigenPodWithWrongWithdrawalCreds(address wrongWithdrawalAddress) public { // ./solidityProofGen -newBalance=32000115173 "ValidatorFieldsProof" 302913 true "data/withdrawal_proof_goerli/goerli_block_header_6399998.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "withdrawal_credential_proof_302913.json" @@ -856,40 +907,6 @@ contract EigenPodTests is ProofParsing, EigenPodPausingConstants { newPod.verifyBalanceUpdates(uint64(block.timestamp - 1), validatorIndices, stateRootProofStruct, proofs, validatorFieldsArray); } - //test that when withdrawal credentials are verified more than once, it reverts - function testDeployNewEigenPodWithActiveValidator() public { - // ./solidityProofGen -newBalance=32000115173 "ValidatorFieldsProof" 302913 true "data/withdrawal_proof_goerli/goerli_block_header_6399998.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "withdrawal_credential_proof_302913.json" - setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); - IEigenPod pod = _testDeployAndVerifyNewEigenPod(podOwner, signature, depositDataRoot); - - uint64 timestamp = 1; - - bytes32[][] memory validatorFieldsArray = new bytes32[][](1); - validatorFieldsArray[0] = getValidatorFields(); - bytes[] memory proofsArray = new bytes[](1); - proofsArray[0] = abi.encodePacked(getWithdrawalCredentialProof()); - - uint40[] memory validatorIndices = new uint40[](1); - validatorIndices[0] = uint40(getValidatorIndex()); - - BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); - - cheats.startPrank(podOwner); - cheats.expectRevert( - bytes( - "EigenPod.verifyCorrectWithdrawalCredentials: Validator must be inactive to prove withdrawal credentials" - ) - ); - pod.verifyWithdrawalCredentials( - timestamp, - stateRootProofStruct, - validatorIndices, - proofsArray, - validatorFieldsArray - ); - cheats.stopPrank(); - } - // // 3. Single withdrawal credential // // Test: Owner proves an withdrawal credential. // // validator status should be marked as ACTIVE @@ -1897,3 +1914,41 @@ contract Relayer is Test { // return (queuedWithdrawal, withdrawalRoot); // } + //Integration Test + // function testFullWithdrawalProofWithWrongWithdrawalFields(bytes32[] memory wrongWithdrawalFields) public { + // Relayer relay = new Relayer(); + // uint256 WITHDRAWAL_FIELD_TREE_HEIGHT = 2; + + // setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + // BeaconChainProofs.WithdrawalProof memory proofs = _getWithdrawalProof(); + // bytes32 beaconStateRoot = getBeaconStateRoot(); + // cheats.assume(wrongWithdrawalFields.length != 2 ** WITHDRAWAL_FIELD_TREE_HEIGHT); + // validatorFields = getValidatorFields(); + + // cheats.expectRevert(bytes("BeaconChainProofs.verifyWithdrawal: withdrawalFields has incorrect length")); + // relay.verifyWithdrawal(beaconStateRoot, wrongWithdrawalFields, proofs); + // } + + // // Integration Test + // function testMismatchedWithdrawalProofInputs(uint64 numValidators, uint64 numValidatorProofs) external { + // cheats.assume(numValidators < numValidatorProofs && numValidatorProofs < 5); + // setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + // bytes[] memory validatorFieldsProofArray = new bytes[](numValidatorProofs); + // for (uint256 index = 0; index < numValidators; index++) { + // validatorFieldsProofArray[index] = abi.encodePacked(getValidatorProof()); + // } + // bytes32[][] memory validatorFieldsArray = new bytes32[][](numValidators); + // for (uint256 index = 0; index < validatorFieldsArray.length; index++) { + // validatorFieldsArray[index] = getValidatorFields(); + // } + // BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); + // BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](1); + + // withdrawalProofsArray[0] = _getWithdrawalProof(); + + // bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); + // withdrawalFieldsArray[0] = withdrawalFields; + + // cheats.expectRevert(bytes("EigenPod.verifyAndProcessWithdrawals: inputs must be same length")); + // pod.verifyAndProcessWithdrawals(0, stateRootProofStruct, withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray); + // } \ No newline at end of file diff --git a/src/test/Registration.t.sol b/src/test/Registration.t.sol deleted file mode 100644 index 23707a5b8..000000000 --- a/src/test/Registration.t.sol +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.12; - -// import "./EigenLayerTestHelper.t.sol"; - -// import "@openzeppelin/contracts/utils/math/Math.sol"; - -// import "../contracts/libraries/BytesLib.sol"; - -// import "./mocks/MiddlewareVoteWeigherMock.sol"; -// import "./mocks/ServiceManagerMock.sol"; -// import "./mocks/PublicKeyCompendiumMock.sol"; -// import "./mocks/StrategyManagerMock.sol"; - -// import "../../src/contracts/middleware/BLSRegistry.sol"; -// import "../../src/contracts/middleware/BLSPublicKeyCompendium.sol"; - - - -// contract RegistrationTests is EigenLayerTestHelper { - -// BLSRegistry public dlRegImplementation; -// BLSPublicKeyCompendiumMock public pubkeyCompendium; - -// BLSPublicKeyCompendiumMock public pubkeyCompendiumImplementation; -// BLSRegistry public dlReg; -// ProxyAdmin public dataLayrProxyAdmin; - -// ServiceManagerMock public dlsm; -// StrategyManagerMock public strategyManagerMock; - -// function setUp() public virtual override { -// EigenLayerDeployer.setUp(); -// initializeMiddlewares(); -// } - - -// function initializeMiddlewares() public { -// dataLayrProxyAdmin = new ProxyAdmin(); -// fuzzedAddressMapping[address(dataLayrProxyAdmin)] = true; - -// pubkeyCompendium = new BLSPublicKeyCompendiumMock(); - -// strategyManagerMock = new StrategyManagerMock(); -// strategyManagerMock.setAddresses(delegation, eigenPodManager, slasher); - -// dlsm = new ServiceManagerMock(slasher); - -// dlReg = BLSRegistry( -// address(new TransparentUpgradeableProxy(address(emptyContract), address(dataLayrProxyAdmin), "")) -// ); - -// dlRegImplementation = new BLSRegistry( -// strategyManagerMock, -// dlsm, -// pubkeyCompendium -// ); - -// uint256[] memory _quorumBips = new uint256[](2); -// // split 60% ETH quorum, 40% EIGEN quorum -// _quorumBips[0] = 6000; -// _quorumBips[1] = 4000; - -// VoteWeigherBaseStorage.StrategyAndWeightingMultiplier[] memory ethStratsAndMultipliers = -// new VoteWeigherBaseStorage.StrategyAndWeightingMultiplier[](1); -// ethStratsAndMultipliers[0].strategy = wethStrat; -// ethStratsAndMultipliers[0].multiplier = 1e18; -// VoteWeigherBaseStorage.StrategyAndWeightingMultiplier[] memory eigenStratsAndMultipliers = -// new VoteWeigherBaseStorage.StrategyAndWeightingMultiplier[](1); -// eigenStratsAndMultipliers[0].strategy = eigenStrat; -// eigenStratsAndMultipliers[0].multiplier = 1e18; - -// dataLayrProxyAdmin.upgradeAndCall( -// TransparentUpgradeableProxy(payable(address(dlReg))), -// address(dlRegImplementation), -// abi.encodeWithSelector(BLSRegistry.initialize.selector, address(this), false, _quorumBips, ethStratsAndMultipliers, eigenStratsAndMultipliers) -// ); - -// } - - -// function testRegisterOperator(address operator, uint32 operatorIndex, string calldata socket) public fuzzedAddress(operator) { -// cheats.assume(operatorIndex < 15); -// BN254.G1Point memory pk = getOperatorPubkeyG1(operatorIndex); - - //register as both ETH and EIGEN operator - // uint256 wethToDeposit = 1e18; - // uint256 eigenToDeposit = 1e18; - // _testDepositWeth(operator, wethToDeposit); - // _testDepositEigen(operator, eigenToDeposit); - // IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - // earningsReceiver: operator, - // delegationApprover: address(0), - // stakerOptOutWindowBlocks: 0 - // }); - // _testRegisterAsOperator(operator, operatorDetails); - -// cheats.startPrank(operator); -// slasher.optIntoSlashing(address(dlsm)); -// pubkeyCompendium.registerPublicKey(pk); -// dlReg.registerOperator(1, pk, socket); -// cheats.stopPrank(); - -// bytes32 pubkeyHash = BN254.hashG1Point(pk); - -// (uint32 toBlockNumber, uint32 index) = dlReg.pubkeyHashToIndexHistory(pubkeyHash,0); - -// assertTrue(toBlockNumber == 0, "block number set when it shouldn't be"); -// assertTrue(index == 0, "index has been set incorrectly"); -// assertTrue(dlReg.operatorList(0) == operator, "incorrect operator added"); -// } - -// function testDeregisterOperator(address operator, uint32 operatorIndex, string calldata socket) public fuzzedAddress(operator) { -// cheats.assume(operatorIndex < 15); -// BN254.G1Point memory pk = getOperatorPubkeyG1(operatorIndex); - -// testRegisterOperator(operator, operatorIndex, socket); -// cheats.startPrank(operator); -// dlReg.deregisterOperator(pk, 0); -// cheats.stopPrank(); - -// bytes32 pubkeyHash = BN254.hashG1Point(pk); -// (uint32 toBlockNumber, /*uint32 index*/) = dlReg.pubkeyHashToIndexHistory(pubkeyHash,0); -// assertTrue(toBlockNumber == block.number, "toBlockNumber has been set incorrectly"); -// } - - -// } \ No newline at end of file diff --git a/src/test/events/IDelegationManagerEvents.sol b/src/test/events/IDelegationManagerEvents.sol new file mode 100644 index 000000000..9219523e9 --- /dev/null +++ b/src/test/events/IDelegationManagerEvents.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "src/contracts/interfaces/IDelegationManager.sol"; +import "src/test/mocks/StakeRegistryStub.sol"; + +interface IDelegationManagerEvents { + /// @notice Emitted when the StakeRegistry is set + event StakeRegistrySet(IStakeRegistryStub stakeRegistry); + + // @notice Emitted when a new operator registers in EigenLayer and provides their OperatorDetails. + event OperatorRegistered(address indexed operator, IDelegationManager.OperatorDetails operatorDetails); + + // @notice Emitted when an operator updates their OperatorDetails to @param newOperatorDetails + event OperatorDetailsModified(address indexed operator, IDelegationManager.OperatorDetails newOperatorDetails); + + /** + * @notice Emitted when @param operator indicates that they are updating their MetadataURI string + * @dev Note that these strings are *never stored in storage* and are instead purely emitted in events for off-chain indexing + */ + event OperatorMetadataURIUpdated(address indexed operator, string metadataURI); + + /// @notice Emitted whenever an operator's shares are increased for a given strategy + event OperatorSharesIncreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); + + /// @notice Emitted whenever an operator's shares are decreased for a given strategy + event OperatorSharesDecreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); + + // @notice Emitted when @param staker delegates to @param operator. + event StakerDelegated(address indexed staker, address indexed operator); + + // @notice Emitted when @param staker undelegates from @param operator. + event StakerUndelegated(address indexed staker, address indexed operator); + + /// @notice Emitted when @param staker is undelegated via a call not originating from the staker themself + event StakerForceUndelegated(address indexed staker, address indexed operator); + + /** + * @notice Emitted when a new withdrawal is queued. + * @param withdrawalRoot Is the hash of the `withdrawal`. + * @param withdrawal Is the withdrawal itself. + */ + event WithdrawalQueued(bytes32 withdrawalRoot, IDelegationManager.Withdrawal withdrawal); + + /// @notice Emitted when a queued withdrawal is completed + event WithdrawalCompleted(bytes32 withdrawalRoot); + + /// @notice Emitted when a queued withdrawal is *migrated* from the StrategyManager to the DelegationManager + event WithdrawalMigrated(bytes32 oldWithdrawalRoot, bytes32 newWithdrawalRoot); + + /// @notice Emitted when the `withdrawalDelayBlocks` variable is modified from `previousValue` to `newValue`. + event WithdrawalDelayBlocksSet(uint256 previousValue, uint256 newValue); + +} \ No newline at end of file diff --git a/src/test/events/IEigenPodEvents.sol b/src/test/events/IEigenPodEvents.sol new file mode 100644 index 000000000..c4016691b --- /dev/null +++ b/src/test/events/IEigenPodEvents.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +interface IEigenPodEvents { + /// @notice Emitted when an ETH validator stakes via this eigenPod + event EigenPodStaked(bytes pubkey); + + /// @notice Emitted when an ETH validator's withdrawal credentials are successfully verified to be pointed to this eigenPod + event ValidatorRestaked(uint40 validatorIndex); + + /// @notice Emitted when an ETH validator's balance is proven to be updated. Here newValidatorBalanceGwei + // is the validator's balance that is credited on EigenLayer. + event ValidatorBalanceUpdated(uint40 validatorIndex, uint64 balanceTimestamp, uint64 newValidatorBalanceGwei); + + /// @notice Emitted when an ETH validator is prove to have withdrawn from the beacon chain + event FullWithdrawalRedeemed( + uint40 validatorIndex, + uint64 withdrawalTimestamp, + address indexed recipient, + uint64 withdrawalAmountGwei + ); + + /// @notice Emitted when a partial withdrawal claim is successfully redeemed + event PartialWithdrawalRedeemed( + uint40 validatorIndex, + uint64 withdrawalTimestamp, + address indexed recipient, + uint64 partialWithdrawalAmountGwei + ); + + /// @notice Emitted when restaked beacon chain ETH is withdrawn from the eigenPod. + event RestakedBeaconChainETHWithdrawn(address indexed recipient, uint256 amount); + + /// @notice Emitted when podOwner enables restaking + event RestakingActivated(address indexed podOwner); + + /// @notice Emitted when ETH is received via the `receive` fallback + event NonBeaconChainETHReceived(uint256 amountReceived); + + /// @notice Emitted when ETH that was previously received via the `receive` fallback is withdrawn + event NonBeaconChainETHWithdrawn(address indexed recipient, uint256 amountWithdrawn); +} \ No newline at end of file diff --git a/src/test/harnesses/BitmapUtilsWrapper.sol b/src/test/harnesses/BitmapUtilsWrapper.sol deleted file mode 100644 index 5534cbc58..000000000 --- a/src/test/harnesses/BitmapUtilsWrapper.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.12; - -import "../../contracts/libraries/BitmapUtils.sol"; - -import "forge-std/Test.sol"; - -// wrapper around the BitmapUtils library that exposes the internal functions -contract BitmapUtilsWrapper is Test { - function bytesArrayToBitmap(bytes calldata bytesArray) external pure returns (uint256) { - return BitmapUtils.bytesArrayToBitmap(bytesArray); - } - - function orderedBytesArrayToBitmap(bytes calldata orderedBytesArray) external pure returns (uint256) { - return BitmapUtils.orderedBytesArrayToBitmap(orderedBytesArray); - } - - function isArrayStrictlyAscendingOrdered(bytes calldata bytesArray) external pure returns (bool) { - return BitmapUtils.isArrayStrictlyAscendingOrdered(bytesArray); - } - - function bitmapToBytesArray(uint256 bitmap) external pure returns (bytes memory bytesArray) { - return BitmapUtils.bitmapToBytesArray(bitmap); - } - - function orderedBytesArrayToBitmap_Yul(bytes calldata orderedBytesArray) external pure returns (uint256) { - return BitmapUtils.orderedBytesArrayToBitmap_Yul(orderedBytesArray); - } - - function bytesArrayToBitmap_Yul(bytes calldata bytesArray) external pure returns (uint256) { - return BitmapUtils.bytesArrayToBitmap_Yul(bytesArray); - } - - function countNumOnes(uint256 n) external pure returns (uint16) { - return BitmapUtils.countNumOnes(n); - } - - function numberIsInBitmap(uint256 bitmap, uint8 numberToCheckForInclusion) external pure returns (bool) { - return BitmapUtils.numberIsInBitmap(bitmap, numberToCheckForInclusion); - } -} \ No newline at end of file diff --git a/src/test/harnesses/EigenPodHarness.sol b/src/test/harnesses/EigenPodHarness.sol index d0e70fb77..1f9546d76 100644 --- a/src/test/harnesses/EigenPodHarness.sol +++ b/src/test/harnesses/EigenPodHarness.sol @@ -19,6 +19,38 @@ contract EPInternalFunctions is EigenPod { _GENESIS_TIME ) {} + function verifyWithdrawalCredentials( + uint64 oracleTimestamp, + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) public returns (uint256) { + return _verifyWithdrawalCredentials( + oracleTimestamp, + beaconStateRoot, + validatorIndex, + validatorFieldsProof, + validatorFields + ); + } + + function verifyAndProcessWithdrawal( + bytes32 beaconStateRoot, + BeaconChainProofs.WithdrawalProof calldata withdrawalProof, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields, + bytes32[] calldata withdrawalFields + ) public returns (IEigenPod.VerifiedWithdrawal memory) { + return _verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalProof, + validatorFieldsProof, + validatorFields, + withdrawalFields + ); + } + function processFullWithdrawal( uint40 validatorIndex, bytes32 validatorPubkeyHash, @@ -59,11 +91,11 @@ contract EPInternalFunctions is EigenPod { bytes32[] calldata validatorFields, uint64 mostRecentBalanceUpdateTimestamp ) - public + public returns (int256) { bytes32 pkhash = validatorFields[0]; _validatorPubkeyHashToInfo[pkhash].mostRecentBalanceUpdateTimestamp = mostRecentBalanceUpdateTimestamp; - _verifyBalanceUpdate( + return _verifyBalanceUpdate( oracleTimestamp, validatorIndex, beaconStateRoot, @@ -75,4 +107,8 @@ contract EPInternalFunctions is EigenPod { function setValidatorStatus(bytes32 pkhash, VALIDATOR_STATUS status) public { _validatorPubkeyHashToInfo[pkhash].status = status; } + + function setValidatorRestakedBalance(bytes32 pkhash, uint64 restakedBalanceGwei) public { + _validatorPubkeyHashToInfo[pkhash].restakedBalanceGwei = restakedBalanceGwei; + } } \ No newline at end of file diff --git a/src/test/harnesses/EigenPodManagerWrapper.sol b/src/test/harnesses/EigenPodManagerWrapper.sol index 4c1f96121..7377f3b7b 100644 --- a/src/test/harnesses/EigenPodManagerWrapper.sol +++ b/src/test/harnesses/EigenPodManagerWrapper.sol @@ -17,4 +17,8 @@ contract EigenPodManagerWrapper is EigenPodManager { function calculateChangeInDelegatableShares(int256 sharesBefore, int256 sharesAfter) external pure returns (int256) { return _calculateChangeInDelegatableShares(sharesBefore, sharesAfter); } + + function setPodAddress(address owner, IEigenPod pod) external { + ownerToPod[owner] = pod; + } } \ No newline at end of file diff --git a/src/test/integration/IntegrationBase.t.sol b/src/test/integration/IntegrationBase.t.sol new file mode 100644 index 000000000..e9e2b2aa8 --- /dev/null +++ b/src/test/integration/IntegrationBase.t.sol @@ -0,0 +1,527 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "forge-std/Test.sol"; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import "src/test/integration/IntegrationDeployer.t.sol"; +import "src/test/integration/TimeMachine.t.sol"; +import "src/test/integration/User.t.sol"; + +abstract contract IntegrationBase is IntegrationDeployer { + + /** + * Gen/Init methods: + */ + + /** + * @dev Create a new user according to configured random variants. + * 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(); + + assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "_newRandomStaker: failed to award token balances"); + + return (staker, strategies, tokenBalances); + } + + function _newRandomOperator() internal returns (User, IStrategy[] memory, uint[] memory) { + (User operator, IStrategy[] memory strategies, uint[] memory tokenBalances) = _randUser(); + + operator.registerAsOperator(); + operator.depositIntoEigenlayer(strategies, tokenBalances); + + assert_Snap_Added_StakerShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to add delegatable shares"); + assert_Snap_Added_OperatorShares(operator, strategies, tokenBalances, "_newRandomOperator: failed to award shares to operator"); + assertTrue(delegationManager.isOperator(address(operator)), "_newRandomOperator: operator should be registered"); + + return (operator, strategies, tokenBalances); + } + + /** + * Common assertions: + */ + + function assert_HasNoDelegatableShares(User user, string memory err) internal { + (IStrategy[] memory strategies, uint[] memory shares) = + delegationManager.getDelegatableShares(address(user)); + + assertEq(strategies.length, 0, err); + assertEq(strategies.length, shares.length, "assert_HasNoDelegatableShares: return length mismatch"); + } + + function assert_HasUnderlyingTokenBalances( + User user, + IStrategy[] memory strategies, + uint[] memory expectedBalances, + string memory err + ) internal { + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + + uint expectedBalance = expectedBalances[i]; + uint tokenBalance; + if (strat == BEACONCHAIN_ETH_STRAT) { + tokenBalance = address(user).balance; + } else { + tokenBalance = strat.underlyingToken().balanceOf(address(user)); + } + + assertEq(expectedBalance, tokenBalance, err); + } + } + + function assert_HasNoUnderlyingTokenBalance(User user, IStrategy[] memory strategies, string memory err) internal { + assert_HasUnderlyingTokenBalances(user, strategies, new uint[](strategies.length), err); + } + + function assert_HasExpectedShares( + User user, + IStrategy[] memory strategies, + uint[] memory expectedShares, + string memory err + ) internal { + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + + uint actualShares; + if (strat == BEACONCHAIN_ETH_STRAT) { + // 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); + } + + assertEq(expectedShares[i], actualShares, err); + } + } + + function assert_HasOperatorShares( + User user, + IStrategy[] memory strategies, + uint[] memory expectedShares, + string memory err + ) internal { + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + + uint actualShares = delegationManager.operatorShares(address(user), strat); + + assertEq(expectedShares[i], actualShares, err); + } + } + + /// @dev Asserts that ALL of the `withdrawalRoots` is in `delegationManager.pendingWithdrawals` + function assert_AllWithdrawalsPending(bytes32[] memory withdrawalRoots, string memory err) internal { + for (uint i = 0; i < withdrawalRoots.length; i++) { + assertTrue(delegationManager.pendingWithdrawals(withdrawalRoots[i]), err); + } + } + + /// @dev Asserts that NONE of the `withdrawalRoots` is in `delegationManager.pendingWithdrawals` + function assert_NoWithdrawalsPending(bytes32[] memory withdrawalRoots, string memory err) internal { + for (uint i = 0; i < withdrawalRoots.length; i++) { + assertFalse(delegationManager.pendingWithdrawals(withdrawalRoots[i]), err); + } + } + + /// @dev Asserts that the hash of each withdrawal corresponds to the provided withdrawal root + function assert_ValidWithdrawalHashes( + IDelegationManager.Withdrawal[] memory withdrawals, + bytes32[] memory withdrawalRoots, + string memory err + ) internal { + bytes32[] memory expectedRoots = _getWithdrawalHashes(withdrawals); + + for (uint i = 0; i < withdrawals.length; i++) { + assertEq(withdrawalRoots[i], expectedRoots[i], err); + } + } + + /******************************************************************************* + SNAPSHOT ASSERTIONS + TIME TRAVELERS ONLY BEYOND THIS POINT + *******************************************************************************/ + + /// Snapshot assertions for delegationManager.operatorShares: + + /// @dev Check that the operator has `addedShares` additional operator shares + // for each strategy since the last snapshot + function assert_Snap_Added_OperatorShares( + User operator, + IStrategy[] memory strategies, + uint[] memory addedShares, + 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++) { + assertEq(prevShares[i] + addedShares[i], curShares[i], err); + } + } + + /// @dev Check that the operator has `removedShares` fewer operator shares + /// for each strategy since the last snapshot + function assert_Snap_Removed_OperatorShares( + User operator, + IStrategy[] memory strategies, + uint[] memory removedShares, + 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 - removed == cur) + for (uint i = 0; i < strategies.length; i++) { + assertEq(prevShares[i] - removedShares[i], curShares[i], err); + } + } + + /// @dev Check that the operator's shares in ALL strategies have not changed + /// since the last snapshot + function assert_Snap_Unchanged_OperatorShares( + User operator, + string memory err + ) internal { + IStrategy[] memory strategies = allStrats; + + uint[] memory curShares = _getOperatorShares(operator, strategies); + // Use timewarp to get previous operator shares + uint[] memory prevShares = _getPrevOperatorShares(operator, strategies); + + // For each strategy, check (prev == cur) + for (uint i = 0; i < strategies.length; i++) { + assertEq(prevShares[i], curShares[i], err); + } + } + + /// Snapshot assertions for strategyMgr.stakerStrategyShares and eigenPodMgr.podOwnerShares: + + /// @dev Check that the staker has `addedShares` additional delegatable shares + /// for each strategy since the last snapshot + function assert_Snap_Added_StakerShares( + User staker, + IStrategy[] memory strategies, + uint[] memory addedShares, + string memory err + ) internal { + uint[] memory curShares = _getStakerShares(staker, strategies); + // Use timewarp to get previous staker shares + uint[] memory prevShares = _getPrevStakerShares(staker, strategies); + + // For each strategy, check (prev + added == cur) + for (uint i = 0; i < strategies.length; i++) { + assertEq(prevShares[i] + addedShares[i], curShares[i], err); + } + } + + /// @dev Check that the staker has `removedShares` fewer delegatable shares + /// for each strategy since the last snapshot + function assert_Snap_Removed_StakerShares( + User staker, + IStrategy[] memory strategies, + uint[] memory removedShares, + string memory err + ) internal { + uint[] memory curShares = _getStakerShares(staker, strategies); + // Use timewarp to get previous staker shares + uint[] memory prevShares = _getPrevStakerShares(staker, strategies); + + // For each strategy, check (prev - removed == cur) + for (uint i = 0; i < strategies.length; i++) { + assertEq(prevShares[i] - removedShares[i], curShares[i], err); + } + } + + /// @dev Check that the staker's delegatable shares in ALL strategies have not changed + /// since the last snapshot + function assert_Snap_Unchanged_StakerShares( + User staker, + string memory err + ) internal { + IStrategy[] memory strategies = allStrats; + + uint[] memory curShares = _getStakerShares(staker, strategies); + // Use timewarp to get previous staker shares + uint[] memory prevShares = _getPrevStakerShares(staker, strategies); + + // For each strategy, check (prev == cur) + for (uint i = 0; i < strategies.length; i++) { + assertEq(prevShares[i], curShares[i], err); + } + } + + /// Snapshot assertions for underlying token balances: + + /// @dev Check that the staker has `addedTokens` additional underlying tokens + // since the last snapshot + function assert_Snap_Added_TokenBalances( + User staker, + IERC20[] memory tokens, + uint[] memory addedTokens, + string memory err + ) internal { + uint[] memory curTokenBalances = _getTokenBalances(staker, tokens); + // Use timewarp to get previous token balances + uint[] memory prevTokenBalances = _getPrevTokenBalances(staker, tokens); + + for (uint i = 0; i < tokens.length; i++) { + uint prevBalance = prevTokenBalances[i]; + uint curBalance = curTokenBalances[i]; + + assertEq(prevBalance + addedTokens[i], curBalance, err); + } + } + + /// @dev Check that the staker has `removedTokens` fewer underlying tokens + // since the last snapshot + function assert_Snap_Removed_TokenBalances( + User staker, + IStrategy[] memory strategies, + uint[] memory removedTokens, + string memory err + ) internal { + IERC20[] memory tokens = _getUnderlyingTokens(strategies); + + uint[] memory curTokenBalances = _getTokenBalances(staker, tokens); + // Use timewarp to get previous token balances + uint[] memory prevTokenBalances = _getPrevTokenBalances(staker, tokens); + + for (uint i = 0; i < tokens.length; i++) { + uint prevBalance = prevTokenBalances[i]; + uint curBalance = curTokenBalances[i]; + + assertEq(prevBalance - removedTokens[i], curBalance, err); + } + } + + /// @dev Check that the staker's underlying token balance for ALL tokens have + /// not changed since the last snapshot + function assert_Snap_Unchanged_TokenBalances( + User staker, + string memory err + ) internal { + IERC20[] memory tokens = allTokens; + + uint[] memory curTokenBalances = _getTokenBalances(staker, tokens); + // Use timewarp to get previous token balances + uint[] memory prevTokenBalances = _getPrevTokenBalances(staker, tokens); + + for (uint i = 0; i < tokens.length; i++) { + assertEq(prevTokenBalances[i], curTokenBalances[i], err); + } + } + + /// Other snapshot assertions: + + function assert_Snap_Added_QueuedWithdrawals( + User staker, + IDelegationManager.Withdrawal[] memory withdrawals, + string memory err + ) internal { + uint curQueuedWithdrawals = _getCumulativeWithdrawals(staker); + // Use timewarp to get previous cumulative withdrawals + uint prevQueuedWithdrawals = _getPrevCumulativeWithdrawals(staker); + + assertEq(prevQueuedWithdrawals + withdrawals.length, curQueuedWithdrawals, err); + } + + /******************************************************************************* + UTILITY METHODS + *******************************************************************************/ + + function _randWithdrawal( + IStrategy[] memory strategies, + uint[] memory shares + ) internal returns (IStrategy[] memory, uint[] memory) { + uint stratsToWithdraw = _randUint({ min: 1, max: strategies.length }); + + IStrategy[] memory withdrawStrats = new IStrategy[](stratsToWithdraw); + uint[] memory withdrawShares = new uint[](stratsToWithdraw); + + for (uint i = 0; i < stratsToWithdraw; i++) { + uint sharesToWithdraw; + + if (strategies[i] == BEACONCHAIN_ETH_STRAT) { + // For native eth, withdraw a random amount of gwei (at least 1) + uint portion = _randUint({ min: 1, max: shares[i] / GWEI_TO_WEI }); + portion *= GWEI_TO_WEI; + + sharesToWithdraw = shares[i] - portion; + } else { + // For LSTs, withdraw a random amount of shares (at least 1) + uint portion = _randUint({ min: 1, max: shares[i] }); + + sharesToWithdraw = shares[i] - portion; + } + + withdrawStrats[i] = strategies[i]; + withdrawShares[i] = sharesToWithdraw; + } + + return (withdrawStrats, withdrawShares); + } + + /// @dev For some strategies/underlying token balances, calculate the expected shares received + /// from depositing all tokens + function _calculateExpectedShares(IStrategy[] memory strategies, uint[] memory tokenBalances) internal returns (uint[] memory) { + uint[] memory expectedShares = new uint[](strategies.length); + + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + + uint tokenBalance = tokenBalances[i]; + if (strat == BEACONCHAIN_ETH_STRAT) { + expectedShares[i] = tokenBalances[i]; + } else { + expectedShares[i] = strat.underlyingToShares(tokenBalance); + } + } + + return expectedShares; + } + + /// @dev For some strategies/underlying token balances, calculate the expected shares received + /// from depositing all tokens + function _calculateExpectedTokens(IStrategy[] memory strategies, uint[] memory shares) internal returns (uint[] memory) { + uint[] memory expectedTokens = new uint[](strategies.length); + + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + expectedTokens[i] = shares[i]; + } else { + expectedTokens[i] = strat.sharesToUnderlying(shares[i]); + } + } + + return expectedTokens; + } + + function _getWithdrawalHashes( + IDelegationManager.Withdrawal[] memory withdrawals + ) internal view returns (bytes32[] memory) { + bytes32[] memory withdrawalRoots = new bytes32[](withdrawals.length); + + for (uint i = 0; i < withdrawals.length; i++) { + withdrawalRoots[i] = delegationManager.calculateWithdrawalRoot(withdrawals[i]); + } + + return withdrawalRoots; + } + + /// @dev Converts a list of strategies to underlying tokens + function _getUnderlyingTokens(IStrategy[] memory strategies) internal view returns (IERC20[] memory) { + IERC20[] memory tokens = new IERC20[](strategies.length); + + for (uint i = 0; i < tokens.length; i++) { + IStrategy strat = strategies[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + tokens[i] = NATIVE_ETH; + } else { + tokens[i] = strat.underlyingToken(); + } + } + + return tokens; + } + + modifier timewarp() { + uint curState = timeMachine.warpToLast(); + _; + timeMachine.warpToPresent(curState); + } + + /// @dev Uses timewarp modifier to get operator shares at the last snapshot + function _getPrevOperatorShares( + User operator, + IStrategy[] memory strategies + ) internal timewarp() returns (uint[] memory) { + return _getOperatorShares(operator, strategies); + } + + /// @dev Looks up each strategy and returns a list of the operator's shares + function _getOperatorShares(User operator, IStrategy[] memory strategies) internal view returns (uint[] memory) { + uint[] memory curShares = new uint[](strategies.length); + + for (uint i = 0; i < strategies.length; i++) { + curShares[i] = delegationManager.operatorShares(address(operator), strategies[i]); + } + + return curShares; + } + + /// @dev Uses timewarp modifier to get staker shares at the last snapshot + function _getPrevStakerShares( + User staker, + IStrategy[] memory strategies + ) internal timewarp() returns (uint[] memory) { + return _getStakerShares(staker, strategies); + } + + /// @dev Looks up each strategy and returns a list of the staker's shares + function _getStakerShares(User staker, IStrategy[] memory strategies) internal view returns (uint[] memory) { + uint[] memory curShares = new uint[](strategies.length); + + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + // 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); + } + } + + return curShares; + } + + function _getPrevCumulativeWithdrawals(User staker) internal timewarp() returns (uint) { + return _getCumulativeWithdrawals(staker); + } + + function _getCumulativeWithdrawals(User staker) internal view returns (uint) { + return delegationManager.cumulativeWithdrawalsQueued(address(staker)); + } + + function _getPrevTokenBalances(User staker, IERC20[] memory tokens) internal timewarp() returns (uint[] memory) { + return _getTokenBalances(staker, tokens); + } + + function _getTokenBalances(User staker, IERC20[] memory tokens) internal view returns (uint[] memory) { + uint[] memory balances = new uint[](tokens.length); + + for (uint i = 0; i < tokens.length; i++) { + if (tokens[i] == NATIVE_ETH) { + balances[i] = address(staker).balance; + } else { + balances[i] = tokens[i].balanceOf(address(staker)); + } + } + + return balances; + } +} \ No newline at end of file diff --git a/src/test/integration/IntegrationDeployer.t.sol b/src/test/integration/IntegrationDeployer.t.sol new file mode 100644 index 000000000..ddf8dafba --- /dev/null +++ b/src/test/integration/IntegrationDeployer.t.sol @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +// Imports +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "forge-std/Test.sol"; + +import "src/contracts/core/DelegationManager.sol"; +import "src/contracts/core/StrategyManager.sol"; +import "src/contracts/core/Slasher.sol"; +import "src/contracts/strategies/StrategyBase.sol"; +import "src/contracts/pods/EigenPodManager.sol"; +import "src/contracts/pods/EigenPod.sol"; +import "src/contracts/pods/DelayedWithdrawalRouter.sol"; +import "src/contracts/permissions/PauserRegistry.sol"; + +import "src/test/mocks/EmptyContract.sol"; +import "src/test/mocks/ETHDepositMock.sol"; +import "src/test/integration/mocks/BeaconChainOracleMock.t.sol"; +import "src/test/integration/mocks/BeaconChainMock.t.sol"; + +import "src/test/integration/User.t.sol"; + +abstract contract IntegrationDeployer is Test, IUserDeployer { + + Vm cheats = Vm(HEVM_ADDRESS); + + // Core contracts to deploy + DelegationManager public delegationManager; + StrategyManager public strategyManager; + EigenPodManager public eigenPodManager; + PauserRegistry pauserRegistry; + Slasher slasher; + IBeacon eigenPodBeacon; + EigenPod pod; + DelayedWithdrawalRouter delayedWithdrawalRouter; + + // Base strategy implementation in case we want to create more strategies later + StrategyBase baseStrategyImplementation; + + TimeMachine public timeMachine; + + // Lists of strategies used in the system + // + // When we select random user assets, we use the `assetType` to determine + // which of these lists to select user assets from. + IStrategy[] lstStrats; + IStrategy[] ethStrats; // only has one strat tbh + IStrategy[] allStrats; // just a combination of the above 2 lists + IERC20[] allTokens; // `allStrats`, but contains all of the underlying tokens instead + + // Mock Contracts to deploy + ETHPOSDepositMock ethPOSDeposit; + BeaconChainOracleMock beaconChainOracle; + BeaconChainMock public beaconChain; + + // ProxyAdmin + ProxyAdmin eigenLayerProxyAdmin; + // Admin Addresses + address eigenLayerReputedMultisig = address(this); // admin address + address constant pauser = address(555); + address constant unpauser = address(556); + + // Randomness state vars + bytes32 random; + // After calling `_configRand`, these are the allowed "variants" on users that will + // be returned from `_randUser`. + bytes assetTypes; + bytes userTypes; + + // 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; + uint constant GWEI_TO_WEI = 1e9; + + // Flags + uint constant FLAG = 1; + + /// @dev Asset flags + /// These are used with _configRand to determine what assets are given + /// to a user when they are created. + 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_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 ALT_METHODS = (FLAG << 1); + + // /// @dev Withdrawal flags + // /// These are used with _configRand to determine how a user conducts a withdrawal + // uint constant FULL_WITHDRAW_SINGLE = (FLAG << 0); // stakers will withdraw all assets using a single queued withdrawal + // uint constant FULL_WITHDRAW_MULTI = (FLAG << 1); // stakers will withdraw all assets using multiple queued withdrawals + // uint constant PART_WITHDRAW_SINGLE = (FLAG << 2); // stakers will withdraw some, but not all assets + + /// Note: Thought about the following flags (but did not implement) - + /// + /// WithdrawerType (SELF_WITHDRAWER, OTHER_WITHDRAWER) + /// - especially with EPM share handling, this felt like it deserved its own test rather than a fuzzy state + /// CompletionType (AS_TOKENS, AS_SHARES) + /// - same reason as above + /// + /// WithdrawalMethod (QUEUE_WITHDRAWAL, UNDELEGATE, REDELEGATE) + /// - could still do this! + /// - This would trigger staker.queueWithdrawals to use either `queueWithdrawals` or `undelegate` under the hood + /// - "redelegate" would be like the above, but adding a new `delegateTo` step after undelegating + + mapping(uint => string) assetTypeToStr; + mapping(uint => string) userTypeToStr; + + constructor () { + assetTypeToStr[NO_ASSETS] = "NO_ASSETS"; + assetTypeToStr[HOLDS_LST] = "HOLDS_LST"; + assetTypeToStr[HOLDS_ETH] = "HOLDS_ETH"; + assetTypeToStr[HOLDS_ALL] = "HOLDS_ALL"; + + userTypeToStr[DEFAULT] = "DEFAULT"; + userTypeToStr[ALT_METHODS] = "ALT_METHODS"; + } + + function setUp() public virtual { + // Deploy ProxyAdmin + eigenLayerProxyAdmin = new ProxyAdmin(); + + // Deploy PauserRegistry + address[] memory pausers = new address[](1); + pausers[0] = pauser; + pauserRegistry = new PauserRegistry(pausers, unpauser); + + // Deploy mocks + EmptyContract emptyContract = new EmptyContract(); + ethPOSDeposit = new ETHPOSDepositMock(); + beaconChainOracle = new BeaconChainOracleMock(); + + /** + * First, deploy upgradeable proxy contracts that **will point** to the implementations. Since the implementation contracts are + * not yet deployed, we give these proxies an empty contract as the initial implementation, to act as if they have no code. + */ + delegationManager = DelegationManager( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) + ); + strategyManager = StrategyManager( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) + ); + slasher = Slasher( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) + ); + eigenPodManager = EigenPodManager( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) + ); + delayedWithdrawalRouter = DelayedWithdrawalRouter( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) + ); + + // Deploy EigenPod Contracts + pod = new EigenPod( + ethPOSDeposit, + delayedWithdrawalRouter, + eigenPodManager, + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + GOERLI_GENESIS_TIME + ); + + eigenPodBeacon = new UpgradeableBeacon(address(pod)); + + // Second, deploy the *implementation* contracts, using the *proxy contracts* as inputs + DelegationManager delegationImplementation = new DelegationManager(strategyManager, slasher, eigenPodManager); + StrategyManager strategyManagerImplementation = new StrategyManager(delegationManager, eigenPodManager, slasher); + Slasher slasherImplementation = new Slasher(strategyManager, delegationManager); + EigenPodManager eigenPodManagerImplementation = new EigenPodManager( + ethPOSDeposit, + eigenPodBeacon, + strategyManager, + slasher, + delegationManager + ); + DelayedWithdrawalRouter delayedWithdrawalRouterImplementation = new DelayedWithdrawalRouter(eigenPodManager); + + // Third, upgrade the proxy contracts to point to the implementations + // DelegationManager + eigenLayerProxyAdmin.upgradeAndCall( + TransparentUpgradeableProxy(payable(address(delegationManager))), + address(delegationImplementation), + abi.encodeWithSelector( + DelegationManager.initialize.selector, + eigenLayerReputedMultisig, // initialOwner + pauserRegistry, + 0 // initialPausedStatus + ) + ); + // StrategyManager + eigenLayerProxyAdmin.upgradeAndCall( + TransparentUpgradeableProxy(payable(address(strategyManager))), + address(strategyManagerImplementation), + abi.encodeWithSelector( + StrategyManager.initialize.selector, + eigenLayerReputedMultisig, //initialOwner + eigenLayerReputedMultisig, //initial whitelister + pauserRegistry, + 0 // initialPausedStatus + ) + ); + // Slasher + eigenLayerProxyAdmin.upgradeAndCall( + TransparentUpgradeableProxy(payable(address(slasher))), + address(slasherImplementation), + abi.encodeWithSelector( + Slasher.initialize.selector, + eigenLayerReputedMultisig, + pauserRegistry, + 0 // initialPausedStatus + ) + ); + // EigenPodManager + eigenLayerProxyAdmin.upgradeAndCall( + TransparentUpgradeableProxy(payable(address(eigenPodManager))), + address(eigenPodManagerImplementation), + abi.encodeWithSelector( + EigenPodManager.initialize.selector, + type(uint256).max, // maxPods + address(beaconChainOracle), + eigenLayerReputedMultisig, // initialOwner + pauserRegistry, + 0 // initialPausedStatus + ) + ); + // Delayed Withdrawal Router + uint256 withdrawalDelayBlocks = 7 days / 12 seconds; + eigenLayerProxyAdmin.upgradeAndCall( + TransparentUpgradeableProxy(payable(address(delayedWithdrawalRouter))), + address(delayedWithdrawalRouterImplementation), + abi.encodeWithSelector( + DelayedWithdrawalRouter.initialize.selector, + eigenLayerReputedMultisig, // initialOwner + pauserRegistry, + 0, // initialPausedStatus + withdrawalDelayBlocks + ) + ); + + // Create base strategy implementation and deploy a few strategies + baseStrategyImplementation = new StrategyBase(strategyManager); + + _newStrategyAndToken("Strategy1Token", "str1", 10e50, address(this)); // initialSupply, owner + _newStrategyAndToken("Strategy2Token", "str2", 10e50, address(this)); // initialSupply, owner + _newStrategyAndToken("Strategy3Token", "str3", 10e50, address(this)); // initialSupply, owner + + ethStrats.push(BEACONCHAIN_ETH_STRAT); + allStrats.push(BEACONCHAIN_ETH_STRAT); + allTokens.push(NATIVE_ETH); + + // 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 + /// strategy in strategyManager + function _newStrategyAndToken(string memory tokenName, string memory tokenSymbol, uint initialSupply, address owner) internal { + IERC20 underlyingToken = new ERC20PresetFixedSupply(tokenName, tokenSymbol, initialSupply, owner); + StrategyBase strategy = StrategyBase( + address( + new TransparentUpgradeableProxy( + address(baseStrategyImplementation), + address(eigenLayerProxyAdmin), + abi.encodeWithSelector(StrategyBase.initialize.selector, underlyingToken, pauserRegistry) + ) + ) + ); + + // Whitelist strategy + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = strategy; + cheats.prank(strategyManager.strategyWhitelister()); + strategyManager.addStrategiesToDepositWhitelist(strategies); + + // Add to lstStrats and allStrats + lstStrats.push(strategy); + allStrats.push(strategy); + allTokens.push(underlyingToken); + } + + function _configRand( + uint24 _randomSeed, + uint _assetTypes, + uint _userTypes + ) internal { + // Using uint24 for the seed type so that if a test fails, it's easier + // to manually use the seed to replay the same test. + emit log_named_uint("_configRand: set random seed to: ", _randomSeed); + random = keccak256(abi.encodePacked(_randomSeed)); + + // Convert flag bitmaps to bytes of set bits for easy use with _randUint + assetTypes = _bitmapToBytes(_assetTypes); + userTypes = _bitmapToBytes(_userTypes); + + emit log("_configRand: Users will be initialized with these asset types:"); + for (uint i = 0; i < assetTypes.length; i++) { + emit log(assetTypeToStr[uint(uint8(assetTypes[i]))]); + } + + emit log("_configRand: these User contracts will be initialized:"); + for (uint i = 0; i < userTypes.length; i++) { + emit log(userTypeToStr[uint(uint8(userTypes[i]))]); + } + + assertTrue(assetTypes.length != 0, "_configRand: no asset types selected"); + assertTrue(userTypes.length != 0, "_configRand: no user types selected"); + } + + /** + * @dev Create a new User with a random config using the range defined in `_configRand` + * + * Assets are pulled from `strategies` based on a random staker/operator `assetType` + */ + function _randUser() 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. + // + // The values selected here are in the ranges configured via `_configRand` + uint assetType = _randAssetType(); + uint userType = _randUserType(); + + // Create User contract based on deposit type: + User user; + if (userType == DEFAULT) { + user = new User(); + } else if (userType == ALT_METHODS) { + // User will use nonstandard methods like: + // `delegateToBySignature` and `depositIntoStrategyWithSignature` + user = User(new User_AltMethods()); + } else { + revert("_randUser: unimplemented userType"); + } + + // For the specific asset selection we made, get a random assortment of + // strategies and deal the user some corresponding underlying token balances + (IStrategy[] memory strategies, uint[] memory tokenBalances) = _dealRandAssets(user, assetType); + + _printUserInfo(assetType, userType, strategies, tokenBalances); + + return (user, strategies, tokenBalances); + } + + /// @dev For a given `assetType`, select a random assortment of strategies and assets + /// 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 BEACONCHAIN_ETH_STRAT, and + /// `tokenBalances` will contain the user's eth balance + /// 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; + uint[] memory tokenBalances; + + if (assetType == NO_ASSETS) { + strategies = new IStrategy[](0); + tokenBalances = new uint[](0); + } else if (assetType == HOLDS_LST) { + + // Select a random number of assets + uint numAssets = _randUint({ min: 1, max: lstStrats.length }); + + strategies = new IStrategy[](numAssets); + tokenBalances = new uint[](numAssets); + + // For each asset, award the user a random balance of the underlying token + for (uint i = 0; i < numAssets; 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; + } + } else if (assetType == HOLDS_ETH) { + 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("_dealRandAssets: assetType unimplemented"); + } + + return (strategies, tokenBalances); + } + + /// @dev Uses `random` to return a random uint, with a range given by `min` and `max` (inclusive) + /// @return `min` <= result <= `max` + function _randUint(uint min, uint max) internal returns (uint) { + uint range = max - min + 1; + + // calculate the number of bits needed for the range + uint bitsNeeded = 0; + uint tempRange = range; + while (tempRange > 0) { + bitsNeeded++; + tempRange >>= 1; + } + + // create a mask for the required number of bits + // and extract the value from the hash + uint mask = (1 << bitsNeeded) - 1; + uint value = uint(random) & mask; + + // in case value is out of range, wrap around or retry + while (value >= range) { + value = (value - range) & mask; + } + + // Hash `random` with itself so the next value we generate is different + random = keccak256(abi.encodePacked(random)); + return min + value; + } + + function _randAssetType() internal returns (uint) { + uint idx = _randUint({ min: 0, max: assetTypes.length - 1 }); + uint assetType = uint(uint8(assetTypes[idx])); + + return assetType; + } + + function _randUserType() internal returns (uint) { + uint idx = _randUint({ min: 0, max: userTypes.length - 1 }); + uint userType = uint(uint8(userTypes[idx])); + + return userType; + } + + /** + * @dev Converts a bitmap into an array of bytes + * @dev Each byte in the input is processed as indicating a single bit to flip in the bitmap + */ + function _bitmapToBytes(uint bitmap) internal pure returns (bytes memory bytesArray) { + for (uint i = 0; i < 256; ++i) { + // Mask for i-th bit + uint mask = uint(1 << i); + + // emit log_named_uint("mask: ", mask); + + // If the i-th bit is flipped, add a byte to the return array + if (bitmap & mask != 0) { + bytesArray = bytes.concat(bytesArray, bytes1(uint8(1 << i))); + } + } + return bytesArray; + } + + function _printUserInfo( + uint assetType, + uint userType, + IStrategy[] memory strategies, + uint[] memory tokenBalances + ) internal { + + emit log("Creating user:"); + emit log_named_string("assetType: ", assetTypeToStr[assetType]); + emit log_named_string("userType: ", userTypeToStr[userType]); + + emit log_named_uint("num assets: ", strategies.length); + + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[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 new file mode 100644 index 000000000..e1c2b75af --- /dev/null +++ b/src/test/integration/README.md @@ -0,0 +1,96 @@ +## EigenLayer Core Integration Testing + +### What the Hell? + +Good question. + +This folder contains the integration framework and tests for Eigenlayer core, which orchestrates the deployment of all EigenLayer core contracts to fuzz high-level user flows across multiple user and asset types, and supports time-travelling state lookups to quickly compare past and present states (please try to avoid preventing your own birth). + +**If you want to know where the tests are**, take a look at `/tests`. We're doing one test contract per top-level flow, and defining multiple test functions for variants on that flow. + +e.g. if you're testing the flow "deposit into strategies -> delegate to operator -> queue withdrawal -> complete withdrawal", that's it's own test contract. For variants where withdrawals are completed "as tokens" vs "as shares," those are their own functions inside that contract. + +Looking at the current tests is a good place to start. + +**If you want to know how we're fuzzing these flows**, take a look at how we're using the `_configRand` method at the start of each test, which accepts bitmaps for the types of users and assets you want to spawn during the test. + +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 `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: + +```solidity +function testFuzz_deposit_delegate_EXAMPLE(uint24 _random) public { + // When new Users are created, they will choose a random configuration from these params. + // `_randomSeed` will be the starting seed for all random lookups. + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_LST, + _userTypes: DEFAULT | ALT_METHODS + }); + + // Because of the `assetTypes` flags above, this will create two Users for our test, + // each of which holds some random assortment of LSTs. + ( + User staker, + IStrategy[] memory strategies, + uint[] memory tokenBalances + ) = _newRandomStaker(); + (User operator, ,) = _newRandomOperator(); + + // Because of the `userTypes` flags above, this user might be using either: + // - `strategyManager.depositIntoStrategy` + // - `strategyManager.depositIntoStrategyWithSignature` + staker.depositIntoEigenlayer(strategies, tokenBalances); + // assertions go here + + // Because of the `userTypes` flags above, this user might be using either: + // - `delegation.delegateTo` + // - `delegation.delegateToBySignature` + staker.delegateTo(operator); + // assertions go here +} +``` + +**If you want to know about the time travel**, there's a few things to note: + +The main feature we're using is foundry's `cheats.snapshot()` and `cheats.revertTo(snapshot)` to zip around in time. You can look at the [Cheatcodes Reference](https://book.getfoundry.sh/cheatcodes/#cheatcodes-interface) to get some idea, but the docs aren't actually correct. The best thing to do is look through our tests and see how it's being used. If you see an assertion called `assert_Snap_...`, that's using the `TimeMachine` under the hood. + +Speaking of, the `TimeMachine` is a global contract that controls the time, fate, and destiny of all who use it. +* `Users` use the `TimeMachine` to snapshot chain state *before* every action they perform. (see the [`User.createSnapshot`](https://github.com/layr-labs/eigenlayer-contracts/blob/c5193f7bff00903a4323be2a1500cbf7137a83e9/src/test/integration/User.t.sol#L43-L46) modifier). +* `IntegrationBase` uses a `timewarp` modifier to quickly fetch state "from before the last user action". These are leveraged within various `assert_Snap_XYZ` methods to allow the test to quickly compare previous and current values. ([example assertion method](https://github.com/layr-labs/eigenlayer-contracts/blob/c99e847709852d7246c73b7d72d44bba368b760e/src/test/integration/IntegrationBase.t.sol#L146-L148)) + +This means that tests can perform user actions with very little setup or "reading prior state", and perform all the important assertions after each action. For example: + +```solidity +function testFuzz_deposit_delegate_EXAMPLE(uint24 _random) public { + // ... test setup goes above here + + // This snapshots state before the deposit. + staker.depositIntoEigenlayer(strategies, tokenBalances); + // This checks the staker's shares from before `depositIntoEigenlayer`, and compares + // them to their shares after `depositIntoEigenlayer`. + assert_Snap_AddedStakerShares(staker, strategies, expectedShares, "failed to award staker shares"); + + // This snapshots state before delegating. + staker.delegateTo(operator); + // This checks the operator's `operatorShares` before the staker delegated to them, and + // compares those shares to the `operatorShares` after the staker delegated. + assert_Snap_AddedOperatorShares(operator, strategies, expectedShares, "failed to award operator shares"); +} +``` + +### Additional Important Concepts + +* Most testing logic and checks are performed at the test level. `IntegrationBase` has primarily helpers and a few sanity checks, but the current structure exists to make it clear what's being tested by reading the test itself. +* Minimal logic/assertions/cheats used in User contract. These are for carrying out user behaviors, only. Exception: + * User methods snapshot state before performing actions +* Top-level error messages are passed into helper assert methods so that it's always clear where an error came from +* User contract should have an interface as similar as possible to the contract interfaces, so it feels like calling an EigenLayer method rather than some weird abstraction. Exceptions for things like: + * `user.depositIntoEigenLayer(strats, tokenBalances)` - because this deposits all strategies/shares and may touch either Smgr or Emgr + +### What needs to be done? + +* Suggest or PR cleanup if you have ideas. Currently, the `IntegrationDeployer` contract is pretty messy. +* 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 new file mode 100644 index 000000000..86a7c8930 --- /dev/null +++ b/src/test/integration/TimeMachine.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "forge-std/Test.sol"; + +contract TimeMachine is Test { + + Vm cheats = Vm(HEVM_ADDRESS); + + bool pastExists = false; + uint lastSnapshot; + + uint64 public proofGenStartTime; + + function createSnapshot() public returns (uint) { + uint snapshot = cheats.snapshot(); + lastSnapshot = snapshot; + pastExists = true; + return snapshot; + } + + function warpToLast() public returns (uint curState) { + // Safety check to make sure createSnapshot is called before attempting to warp + // so we don't accidentally prevent our own births + assertTrue(pastExists, "Global.warpToPast: invalid usage, past does not exist"); + + curState = cheats.snapshot(); + cheats.revertTo(lastSnapshot); + return curState; + } + + 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 new file mode 100644 index 000000000..c3aa5a4fa --- /dev/null +++ b/src/test/integration/User.t.sol @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +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 { + + Vm cheats = Vm(HEVM_ADDRESS); + + DelegationManager delegationManager; + StrategyManager strategyManager; + EigenPodManager eigenPodManager; + + 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); + + delegationManager = deployer.delegationManager(); + strategyManager = deployer.strategyManager(); + eigenPodManager = deployer.eigenPodManager(); + timeMachine = deployer.timeMachine(); + + beaconChain = deployer.beaconChain(); + pod = EigenPod(payable(eigenPodManager.createPod())); + } + + modifier createSnapshot() virtual { + timeMachine.createSnapshot(); + _; + } + + receive() external payable {} + + /** + * DelegationManager methods: + */ + + function registerAsOperator() public createSnapshot virtual { + IDelegationManager.OperatorDetails memory details = IDelegationManager.OperatorDetails({ + earningsReceiver: address(this), + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + + delegationManager.registerAsOperator(details, "metadata"); + } + + /// @dev For each strategy/token balance, call the relevant deposit method + function depositIntoEigenlayer(IStrategy[] memory strategies, uint[] memory tokenBalances) public createSnapshot virtual { + + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + uint tokenBalance = tokenBalances[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + // 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); + strategyManager.depositIntoStrategy(strat, underlyingToken, tokenBalance); + } + } + } + + /// @dev Delegate to the operator without a signature + function delegateTo(User operator) public createSnapshot virtual { + ISignatureUtils.SignatureWithExpiry memory emptySig; + delegationManager.delegateTo(address(operator), emptySig, bytes32(0)); + } + + /// @dev Queues a single withdrawal for every share and strategy pair + function queueWithdrawals( + IStrategy[] memory strategies, + uint[] memory shares + ) public createSnapshot virtual returns (IDelegationManager.Withdrawal[] memory) { + + address operator = delegationManager.delegatedTo(address(this)); + address withdrawer = address(this); + uint nonce = delegationManager.cumulativeWithdrawalsQueued(address(this)); + + // Create queueWithdrawals params + IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); + params[0] = IDelegationManager.QueuedWithdrawalParams({ + strategies: strategies, + shares: shares, + withdrawer: withdrawer + }); + + // Create Withdrawal struct using same info + IDelegationManager.Withdrawal[] memory withdrawals = new IDelegationManager.Withdrawal[](1); + withdrawals[0] = IDelegationManager.Withdrawal({ + staker: address(this), + delegatedTo: operator, + withdrawer: withdrawer, + nonce: nonce, + startBlock: uint32(block.number), + strategies: strategies, + shares: shares + }); + + bytes32[] memory withdrawalRoots = delegationManager.queueWithdrawals(params); + + // Basic sanity check - we do all other checks outside this file + assertEq(withdrawals.length, withdrawalRoots.length, "User.queueWithdrawals: length mismatch"); + + return (withdrawals); + } + + function completeQueuedWithdrawal( + IDelegationManager.Withdrawal memory withdrawal, + bool receiveAsTokens + ) public createSnapshot virtual returns (IERC20[] memory) { + IERC20[] memory tokens = new IERC20[](withdrawal.strategies.length); + + for (uint i = 0; i < tokens.length; i++) { + IStrategy strat = withdrawal.strategies[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + 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(); + } + } + + delegationManager.completeQueuedWithdrawal(withdrawal, tokens, 0, receiveAsTokens); + + return tokens; + } + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(pod)); + } +} + +/// @notice A user contract that calls nonstandard methods (like xBySignature methods) +contract User_AltMethods is User { + + mapping(bytes32 => bool) public signedHashes; + + constructor() User() {} + + function delegateTo(User operator) public createSnapshot override { + // Create empty data + ISignatureUtils.SignatureWithExpiry memory emptySig; + uint256 expiry = type(uint256).max; + + // Get signature + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry; + stakerSignatureAndExpiry.expiry = expiry; + bytes32 digestHash = delegationManager.calculateCurrentStakerDelegationDigestHash(address(this), address(operator), expiry); + stakerSignatureAndExpiry.signature = bytes(abi.encodePacked(digestHash)); // dummy sig data + + // Mark hash as signed + signedHashes[digestHash] = true; + + // Delegate + delegationManager.delegateToBySignature(address(this), address(operator), stakerSignatureAndExpiry, emptySig, bytes32(0)); + + // Mark hash as used + signedHashes[digestHash] = false; + } + + function depositIntoEigenlayer(IStrategy[] memory strategies, uint[] memory tokenBalances) public createSnapshot override { + uint256 expiry = type(uint256).max; + for (uint i = 0; i < strategies.length; i++) { + IStrategy strat = strategies[i]; + uint tokenBalance = tokenBalances[i]; + + if (strat == BEACONCHAIN_ETH_STRAT) { + // 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(); + underlyingToken.approve(address(strategyManager), tokenBalance); + + // Get signature + uint256 nonceBefore = strategyManager.nonces(address(this)); + bytes32 structHash = keccak256( + abi.encode(strategyManager.DEPOSIT_TYPEHASH(), strat, underlyingToken, tokenBalance, nonceBefore, expiry) + ); + bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", strategyManager.domainSeparator(), structHash)); + bytes memory signature = bytes(abi.encodePacked(digestHash)); // dummy sig data + + // Mark hash as signed + signedHashes[digestHash] = true; + + // Deposit + strategyManager.depositIntoStrategyWithSignature( + strat, + underlyingToken, + tokenBalance, + address(this), + expiry, + signature + ); + + // Mark hash as used + signedHashes[digestHash] = false; + } + } + } + + bytes4 internal constant MAGIC_VALUE = 0x1626ba7e; + function isValidSignature(bytes32 hash, bytes memory) external view returns (bytes4) { + if(signedHashes[hash]){ + return MAGIC_VALUE; + } else { + return 0xffffffff; + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..7bd3559a2 --- /dev/null +++ b/src/test/integration/tests/Deposit_Delegate_Queue_Complete.t.sol @@ -0,0 +1,492 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "src/test/integration/IntegrationBase.t.sol"; +import "src/test/integration/User.t.sol"; + +contract Deposit_Delegate_Queue_Complete is IntegrationBase { + + /******************************************************************************* + FULL WITHDRAWALS + *******************************************************************************/ + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a ALL shares + /// 4. completes the queued withdrawal as tokens + function testFuzz_deposit_delegate_queue_completeAsTokens(uint24 _random) public { + // When new Users are created, they will choose a random configuration from these params: + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create an operator and a staker with: + // - some nonzero underlying token balances + // - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager) + // + // ... check that the staker has no delegatable shares and isn't currently delegated + ( + 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: + // For each of the assets held by the staker (either StrategyManager or EigenPodManager), + // the staker calls the relevant deposit function, depositing all held assets. + // + // ... check that all underlying tokens were transferred to the correct destination + // and that the staker now has the expected amount of delegated shares in each strategy + staker.depositIntoEigenlayer(strategies, tokenBalances); + + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + } + + { + /// 2. Delegate to an operator: + // + // ... check that the staker is now delegated to the operator, and that the operator + // was awarded the staker's shares + staker.delegateTo(operator); + + assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); + assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); + } + + IDelegationManager.Withdrawal[] memory withdrawals; + bytes32[] memory withdrawalRoots; + + { + /// 3. Queue withdrawal(s): + // The staker will queue one or more withdrawals for all strategies and shares + // + // ... check that each withdrawal was successfully enqueued, that the returned withdrawals + // match now-pending withdrawal roots, and that the staker and operator have + // reduced shares. + withdrawals = staker.queueWithdrawals(strategies, shares); + withdrawalRoots = _getWithdrawalHashes(withdrawals); + + assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots"); + assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); + assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); + assert_Snap_Removed_OperatorShares(operator, strategies, shares, "failed to remove operator shares"); + assert_Snap_Removed_StakerShares(staker, strategies, shares, "failed to remove staker shares"); + } + + // Fast forward to when we can complete the withdrawal + cheats.roll(block.number + delegationManager.withdrawalDelayBlocks()); + + { + /// 4. Complete withdrawal(s): + // The staker will complete each withdrawal as tokens + // + // ... check that the staker received their tokens + for (uint i = 0; i < withdrawals.length; i++) { + IDelegationManager.Withdrawal memory withdrawal = withdrawals[i]; + + uint[] memory expectedTokens = _calculateExpectedTokens(withdrawal.strategies, withdrawal.shares); + IERC20[] memory tokens = staker.completeQueuedWithdrawal(withdrawal, true); + + assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); + assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should not have changed"); + assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); + } + } + + // Check final state: + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_HasNoDelegatableShares(staker, "staker should have withdrawn all shares"); + assert_HasUnderlyingTokenBalances(staker, strategies, tokenBalances, "staker should once again have original token balances"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + } + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a ALL shares + /// 4. completes the queued withdrawal as shares + function testFuzz_deposit_delegate_queue_completeAsShares(uint24 _random) public { + // When new Users are created, they will choose a random configuration from these params: + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create an operator and a staker with: + // - some nonzero underlying token balances + // - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager) + // + // ... check that the staker has no delegatable shares and isn't currently delegated + ( + 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: + // For each of the assets held by the staker (either StrategyManager or EigenPodManager), + // the staker calls the relevant deposit function, depositing all held assets. + // + // ... check that all underlying tokens were transferred to the correct destination + // and that the staker now has the expected amount of delegated shares in each strategy + staker.depositIntoEigenlayer(strategies, tokenBalances); + + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + } + + { + /// 2. Delegate to an operator: + // + // ... check that the staker is now delegated to the operator, and that the operator + // was awarded the staker's shares + staker.delegateTo(operator); + + assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); + assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); + } + + IDelegationManager.Withdrawal[] memory withdrawals; + bytes32[] memory withdrawalRoots; + + { + /// 3. Queue withdrawal(s): + // The staker will queue one or more withdrawals for all strategies and shares + // + // ... check that each withdrawal was successfully enqueued, that the returned withdrawals + // match now-pending withdrawal roots, and that the staker and operator have + // reduced shares. + withdrawals = staker.queueWithdrawals(strategies, shares); + withdrawalRoots = _getWithdrawalHashes(withdrawals); + + assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots"); + assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); + assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); + assert_Snap_Removed_OperatorShares(operator, strategies, shares, "failed to remove operator shares"); + assert_Snap_Removed_StakerShares(staker, strategies, shares, "failed to remove staker shares"); + } + + // Fast forward to when we can complete the withdrawal + cheats.roll(block.number + delegationManager.withdrawalDelayBlocks()); + + { + /// 4. Complete withdrawal(s): + // The staker will complete each withdrawal as tokens + // + // ... check that the staker and operator received their shares and that neither + // have any change in token balances + for (uint i = 0; i < withdrawals.length; i++) { + IDelegationManager.Withdrawal memory withdrawal = withdrawals[i]; + + staker.completeQueuedWithdrawal(withdrawal, false); + + assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances"); + assert_Snap_Unchanged_TokenBalances(operator, "operator should not have any change in underlying token balances"); + assert_Snap_Added_StakerShares(staker, withdrawal.strategies, withdrawal.shares, "staker should have received shares"); + assert_Snap_Added_OperatorShares(operator, withdrawal.strategies, withdrawal.shares, "operator should have received shares"); + } + } + + // Check final state: + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_HasExpectedShares(staker, strategies, shares, "staker should have all original shares"); + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + } + + /******************************************************************************* + RANDOM WITHDRAWALS + *******************************************************************************/ + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a random subset of shares + /// 4. completes the queued withdrawal as tokens + function testFuzz_deposit_delegate_queueRand_completeAsTokens(uint24 _random) public { + // When new Users are created, they will choose a random configuration from these params: + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create an operator and a staker with: + // - some nonzero underlying token balances + // - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager) + // + // ... check that the staker has no delegatable shares and isn't currently delegated + ( + 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: + // For each of the assets held by the staker (either StrategyManager or EigenPodManager), + // the staker calls the relevant deposit function, depositing all held assets. + // + // ... check that all underlying tokens were transferred to the correct destination + // and that the staker now has the expected amount of delegated shares in each strategy + staker.depositIntoEigenlayer(strategies, tokenBalances); + + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + } + + { + /// 2. Delegate to an operator: + // + // ... check that the staker is now delegated to the operator, and that the operator + // was awarded the staker's shares + staker.delegateTo(operator); + + assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); + assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); + } + + // Randomly select one or more assets to withdraw + ( + IStrategy[] memory withdrawStrats, + uint[] memory withdrawShares + ) = _randWithdrawal(strategies, shares); + + IDelegationManager.Withdrawal[] memory withdrawals; + bytes32[] memory withdrawalRoots; + + { + /// 3. Queue withdrawal(s): + // 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 + // match the hashes of each withdrawal, and that the staker and operator have + // reduced shares. + withdrawals = staker.queueWithdrawals(withdrawStrats, withdrawShares); + withdrawalRoots = _getWithdrawalHashes(withdrawals); + + assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots"); + assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); + assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); + assert_Snap_Removed_OperatorShares(operator, withdrawStrats, withdrawShares, "failed to remove operator shares"); + assert_Snap_Removed_StakerShares(staker, withdrawStrats, withdrawShares, "failed to remove staker shares"); + } + + // Fast forward to when we can complete the withdrawal + cheats.roll(block.number + delegationManager.withdrawalDelayBlocks()); + + { + /// 4. Complete withdrawal(s): + // The staker will complete each withdrawal as tokens + // + // ... check that the staker received their tokens and that the staker/operator + // have unchanged share amounts + for (uint i = 0; i < withdrawals.length; i++) { + IDelegationManager.Withdrawal memory withdrawal = withdrawals[i]; + + uint[] memory expectedTokens = _calculateExpectedTokens(withdrawal.strategies, withdrawal.shares); + IERC20[] memory tokens = staker.completeQueuedWithdrawal(withdrawal, true); + + assert_Snap_Added_TokenBalances(staker, tokens, expectedTokens, "staker should have received expected tokens"); + assert_Snap_Unchanged_TokenBalances(operator, "operator token balances should not have changed"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should not have changed"); + assert_Snap_Unchanged_OperatorShares(operator, "operator shares should not have changed"); + } + } + + // Check final state: + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + } + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// 2. delegates to an operator + /// 3. queues a withdrawal for a random subset of shares + /// 4. completes the queued withdrawal as shares + function testFuzz_deposit_delegate_queueRand_completeAsShares(uint24 _random) public { + // When new Users are created, they will choose a random configuration from these params: + _configRand({ + _randomSeed: _random, + _assetTypes: HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create an operator and a staker with: + // - some nonzero underlying token balances + // - corresponding to a random subset of valid strategies (StrategyManager and/or EigenPodManager) + // + // ... check that the staker has no delegatable shares and isn't currently delegated + ( + 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: + // For each of the assets held by the staker (either StrategyManager or EigenPodManager), + // the staker calls the relevant deposit function, depositing all held assets. + // + // ... check that all underlying tokens were transferred to the correct destination + // and that the staker now has the expected amount of delegated shares in each strategy + staker.depositIntoEigenlayer(strategies, tokenBalances); + + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + } + + { + /// 2. Delegate to an operator: + // + // ... check that the staker is now delegated to the operator, and that the operator + // was awarded the staker's shares + staker.delegateTo(operator); + + assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should be delegated to operator"); + assert_Snap_Unchanged_StakerShares(staker, "staker shares should be unchanged after delegating"); + assert_Snap_Added_OperatorShares(operator, strategies, shares, "operator should have received shares"); + } + + // Randomly select one or more assets to withdraw + ( + IStrategy[] memory withdrawStrats, + uint[] memory withdrawShares + ) = _randWithdrawal(strategies, shares); + + IDelegationManager.Withdrawal[] memory withdrawals; + bytes32[] memory withdrawalRoots; + + { + /// 3. Queue withdrawal(s): + // 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 + // match the hashes of each withdrawal, and that the staker and operator have + // reduced shares. + withdrawals = staker.queueWithdrawals(withdrawStrats, withdrawShares); + withdrawalRoots = _getWithdrawalHashes(withdrawals); + + assert_ValidWithdrawalHashes(withdrawals, withdrawalRoots, "calculated withdrawals should match returned roots"); + assert_AllWithdrawalsPending(withdrawalRoots, "staker's withdrawals should now be pending"); + assert_Snap_Added_QueuedWithdrawals(staker, withdrawals, "staker should have increased nonce by withdrawals.length"); + assert_Snap_Removed_OperatorShares(operator, withdrawStrats, withdrawShares, "failed to remove operator shares"); + assert_Snap_Removed_StakerShares(staker, withdrawStrats, withdrawShares, "failed to remove staker shares"); + } + + // Fast forward to when we can complete the withdrawal + cheats.roll(block.number + delegationManager.withdrawalDelayBlocks()); + + { + /// 4. Complete withdrawal(s): + // The staker will complete each withdrawal as tokens + // + // ... check that the staker received their tokens and that the staker/operator + // have unchanged share amounts + for (uint i = 0; i < withdrawals.length; i++) { + IDelegationManager.Withdrawal memory withdrawal = withdrawals[i]; + + staker.completeQueuedWithdrawal(withdrawal, false); + + assert_Snap_Unchanged_TokenBalances(staker, "staker should not have any change in underlying token balances"); + assert_Snap_Unchanged_TokenBalances(operator, "operator should not have any change in underlying token balances"); + assert_Snap_Added_StakerShares(staker, withdrawal.strategies, withdrawal.shares, "staker should have received shares"); + assert_Snap_Added_OperatorShares(operator, withdrawal.strategies, withdrawal.shares, "operator should have received shares"); + } + } + + // Check final state: + assertEq(address(operator), delegationManager.delegatedTo(address(staker)), "staker should still be delegated to operator"); + assert_HasExpectedShares(staker, strategies, shares, "staker should have all original shares"); + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker not have any underlying tokens"); + assert_NoWithdrawalsPending(withdrawalRoots, "all withdrawals should be removed from pending"); + } + + /******************************************************************************* + UNHAPPY PATH TESTS + *******************************************************************************/ + + /// Generates a random staker and operator. The staker: + /// 1. deposits all assets into strategies + /// --- registers as an operator + /// 2. delegates to an operator + /// + /// ... we check that the final step fails + function testFuzz_deposit_delegate_revert_alreadyDelegated(uint24 _random) public { + _configRand({ + _randomSeed: _random, + _assetTypes: NO_ASSETS | HOLDS_LST | HOLDS_ETH | HOLDS_ALL, + _userTypes: DEFAULT | ALT_METHODS + }); + + /// 0. Create a staker and operator + ( + 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: + // For each of the assets held by the staker (either StrategyManager or EigenPodManager), + // the staker calls the relevant deposit function, depositing all held assets. + // + // ... check that all underlying tokens were transferred to the correct destination + // and that the staker now has the expected amount of delegated shares in each strategy + staker.depositIntoEigenlayer(strategies, tokenBalances); + + assert_HasNoUnderlyingTokenBalance(staker, strategies, "staker should have transferred all underlying tokens"); + assert_Snap_Added_StakerShares(staker, strategies, shares, "staker should expected shares in each strategy after depositing"); + } + + { + /// 2. Register the staker as an operator, then attempt to delegate to an operator. + /// This should fail as the staker is already delegated to themselves. + staker.registerAsOperator(); + assertTrue(delegationManager.isDelegated(address(staker)), "staker should be delegated"); + + cheats.expectRevert("DelegationManager._delegate: staker is already actively delegated"); + staker.delegateTo(operator); + } + } +} \ No newline at end of file diff --git a/src/test/mocks/DelayedWithdrawalRouterMock.sol b/src/test/mocks/DelayedWithdrawalRouterMock.sol index 8cf660f3f..53077cd52 100644 --- a/src/test/mocks/DelayedWithdrawalRouterMock.sol +++ b/src/test/mocks/DelayedWithdrawalRouterMock.sol @@ -3,7 +3,6 @@ pragma solidity >=0.5.0; import "../../contracts/interfaces/IDelayedWithdrawalRouter.sol"; - contract DelayedWithdrawalRouterMock is IDelayedWithdrawalRouter { /** * @notice Creates an delayed withdrawal for `msg.value` to the `recipient`. diff --git a/src/test/mocks/EigenPodManagerMock.sol b/src/test/mocks/EigenPodManagerMock.sol index 3f6d69f7a..f06730112 100644 --- a/src/test/mocks/EigenPodManagerMock.sol +++ b/src/test/mocks/EigenPodManagerMock.sol @@ -9,6 +9,8 @@ contract EigenPodManagerMock is IEigenPodManager, Test { IBeacon public eigenPodBeacon; IETHPOSDeposit public ethPOS; + mapping(address => int256) public podShares; + function slasher() external view returns(ISlasher) {} function createPod() external returns(address) {} @@ -63,7 +65,13 @@ contract EigenPodManagerMock is IEigenPodManager, Test { function unpause(uint256 /*newPausedStatus*/) external{} - function podOwnerShares(address podOwner) external view returns (int256) {} + function podOwnerShares(address podOwner) external view returns (int256) { + return podShares[podOwner]; + } + + function setPodOwnerShares(address podOwner, int256 shares) external { + podShares[podOwner] = shares; + } function addShares(address /*podOwner*/, uint256 shares) external pure returns (uint256) { // this is the "increase in delegateable tokens" diff --git a/src/test/test-data/balanceUpdateProof_balance28ETH_302913.json b/src/test/test-data/balanceUpdateProof_balance28ETH_302913.json new file mode 100644 index 000000000..b44269cf9 --- /dev/null +++ b/src/test/test-data/balanceUpdateProof_balance28ETH_302913.json @@ -0,0 +1 @@ +{"validatorIndex":302913,"beaconStateRoot":"0x8276d4d448d27e7d41da771ca7729f7a25c01b5fcd1f18f9bd9632dc7fde1388","slotRoot":"0xfea7610000000000000000000000000000000000000000000000000000000000","balanceRoot":"0x6cba5d7307000000e5d9ef840600000008bd5d7307000000239e5d7307000000","latestBlockHeaderRoot":"0x56461a3931812e3341030121e7db0f298112576ac9e898c00f4f1338c2ad647e","ValidatorBalanceProof":["0x26805d73070000002aa55d7307000000218e5d730700000056d35d7307000000","0x24846a54724508d8394a9e30f819bc19fe6727745be4f19e1e3924b985b4b8e9","0x48fb4849c31d88b413f650c17bd55bb11fe38bcef5d234dba8532737ae78f776","0x0d7d38f90a04ef49d15ca10af0e818e227134e4b46c7b22dd10e149f2a2bc612","0xdd345235f28f92719c39c9a5042b91f814d535ae5df574c0f6cd38f77791fc2b","0x0855928643f4210d9d8d0986847674139be32ebf3db984b6d81f3ed136475b6d","0x28460d51c1ce75299f8c0dff8bca9f498fef0edc57c343bbd76592b3d6b984a3","0x068a141709053afb48698290176068f448867cba9c6ff10f986b1d02691f6e70","0x9f0958275fe6ea80b1825bcd11907d1076600719ce574ae2a609e51a975dde54","0xd43fbb18b30449f5c81148848fd6579b4f6cebfe66187bd1c36a293b22085fc2","0x7bf0aac502053da7dfd5356960db63ab48d3cc6ee5f36f41639eadacd34ac272","0x866e3b213d02f1cb86149c2472789ab9d7fb3fb52371a893dc8b6f8936ebd0c1","0xcec7cc4b82fb8dd889257128af3e4433700fcc0fb0505b431aa5f592111e785b","0xdd08667067baa43e2b41b793464a591a29643b5582c8fc9c2c045f984c5848d7","0x842ec80062a4f2f7e9d80ab408374ae42817acf5036aff40b3ade345ab196090","0xfe1ae22b8ba21fce84b25f5da3e37a91db49158b7025d6371d5a54c03adba125","0x8c039a81d68952077db2cc11713ae6f5a9ecc5ebe859c0612ff7215c0ccd7aba","0x5415d3199f1da31287d646d03c2691728396d87f7ef52c142596301e27a343ca","0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4","0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0x846b080000000000000000000000000000000000000000000000000000000000","0x7d46d32db9a97974916a2114aeffc11fc031a954b2110742267054a438055c1a","0x81e1234eb27473f180f890c98019ecb7655a3e58105c7182d8daf726d75aaf8d","0x275abf1ff862082807cafed8d0a318c27439ec7159d81c831ee2832b5d6b57f6","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"],"ValidatorFields":["0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d","0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443","0xe5d9ef8406000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xea65010000000000000000000000000000000000000000000000000000000000","0xf265010000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000"],"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"],"WithdrawalCredentialProof":["0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e","0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee","0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1","0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1","0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac","0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537","0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38","0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748","0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a","0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c","0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b","0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e","0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee","0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9","0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74","0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3","0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f","0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6","0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0x9fade50f5a88a8f7a027b4e7fa019f2289075e38668e926f9909dfcd5bcb3574","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"]} \ No newline at end of file diff --git a/src/test/test-data/fullWithdrawalProof_Latest.json b/src/test/test-data/fullWithdrawalProof_Latest.json index 8c39850e5..b20a1fa31 100644 --- a/src/test/test-data/fullWithdrawalProof_Latest.json +++ b/src/test/test-data/fullWithdrawalProof_Latest.json @@ -1,6 +1,6 @@ { "slot": 6397852, - "validatorIndex": 0, + "validatorIndex": 302913, "historicalSummaryIndex": 146, "withdrawalIndex": 0, "blockHeaderRootIndex": 8092, diff --git a/src/test/test-data/fullWithdrawalProof_Latest_28ETH.json b/src/test/test-data/fullWithdrawalProof_Latest_28ETH.json new file mode 100644 index 000000000..1e40c38e4 --- /dev/null +++ b/src/test/test-data/fullWithdrawalProof_Latest_28ETH.json @@ -0,0 +1 @@ +{"slot":6397852,"validatorIndex":302913,"historicalSummaryIndex":146,"withdrawalIndex":0,"blockHeaderRootIndex":8092,"beaconStateRoot":"0xc9bc855bfd67190b654dc87cfe6945cdafa1d8bba66c3c2e81748f98c0c0e95b","slotRoot":"0x9c9f610000000000000000000000000000000000000000000000000000000000","timestampRoot":"0xb06fed6400000000000000000000000000000000000000000000000000000000","blockHeaderRoot":"0xbd83e79a38e454ece98a2f9e661792047984889db19234d11143322522e444fb","blockBodyRoot":"0x3e5c78192898e4f24aeceb181e58e4c5f390059ff6eebf0cbee6a096969a59a8","executionPayloadRoot":"0x8478de9992c86ab2617576c4b32267a2de50fd2bd35c8481a3d4a1731c0d5fe4","latestBlockHeaderRoot":"0x9ce41a41052e65ec6a0b3b021dc2f6f98f6060f22d70ac92ade6bcc3d4acca54","SlotProof":["0x89c5010000000000000000000000000000000000000000000000000000000000","0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2","0x1dfeede5317301d17b8e3ea54e055bd0567240cc27f76af3adc823ad7f6b6833"],"WithdrawalProof":["0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f","0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e","0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6","0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8","0x1000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92","0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729","0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"],"ValidatorProof":["0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e","0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee","0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1","0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1","0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac","0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537","0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38","0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748","0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a","0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c","0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b","0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e","0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee","0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9","0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74","0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3","0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f","0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6","0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0xf91f8669ffcb2c5c46ef5c23e2fe3308154469b55494145b26ca111c173e1184"],"TimestampProof":["0x28a2c80000000000000000000000000000000000000000000000000000000000","0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df","0x251a5e0b34e9eb58a44a231cfb3e817e3cf3f82cde755dc5fe94cbe36e146b5b","0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471"],"ExecutionPayloadProof":["0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474","0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053","0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1"],"ValidatorFields":["0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d","0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148","0x0040597307000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xea65010000000000000000000000000000000000000000000000000000000000","0xf265010000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000"],"WithdrawalFields":["0x45cee50000000000000000000000000000000000000000000000000000000000","0x419f040000000000000000000000000000000000000000000000000000000000","0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000","0xe5d9ef8406000000000000000000000000000000000000000000000000000000"],"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"],"HistoricalSummaryProof":["0x050b5923fe2e470849a7d467e4cbed4803d3a46d516b84552567976960ff4ccc","0x6ab9b5fc357c793cc5339398016c28ea21f0be4d56a68b554a29766dd312eeeb","0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529","0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a","0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2","0x301ab0d5d5ada4bd2e859658fc31743b5a502b26bc9b8b162e5a161e21218048","0x9d2bc97fffd61659313009e67a4c729a10274f2af26914a53bc7af6717da211e","0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e","0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f","0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15","0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3","0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf","0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8","0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be","0x0000000000000000000000000000000000000000000000000000000000000000","0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c","0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f","0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30","0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1","0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7","0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193","0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1","0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b","0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220","0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f","0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e","0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784","0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb","0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb","0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab","0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4","0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x9300000000000000000000000000000000000000000000000000000000000000","0xd9ed050000000000000000000000000000000000000000000000000000000000","0xd824a89a9d5dd329a069b394ddc6618c70ed784982061959ac77d58d48e9d7c8","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71","0x8dc0b81ebe27fb91663a74713252d2eae5deb3b983374afc3c2d2f6b254128f1","0x09c7a863a253b4163a10916db2781eb35f58c016f0a24b3331ed84af3822c341"]} \ No newline at end of file diff --git a/src/test/test-data/partialWithdrawalProof_Latest.json b/src/test/test-data/partialWithdrawalProof_Latest.json index 2da07a625..0c5160b37 100644 --- a/src/test/test-data/partialWithdrawalProof_Latest.json +++ b/src/test/test-data/partialWithdrawalProof_Latest.json @@ -1,6 +1,6 @@ { "slot": 6397852, - "validatorIndex": 0, + "validatorIndex": 302913, "historicalSummaryIndex": 146, "withdrawalIndex": 0, "blockHeaderRootIndex": 8092, diff --git a/src/test/test-data/withdrawal_credential_proof_302913_30ETHBalance.json b/src/test/test-data/withdrawal_credential_proof_302913_30ETHBalance.json new file mode 100644 index 000000000..1157b6224 --- /dev/null +++ b/src/test/test-data/withdrawal_credential_proof_302913_30ETHBalance.json @@ -0,0 +1 @@ +{"validatorIndex":302913,"beaconStateRoot":"0x9761c69195aeb853adc72f41b00232e0dccdfaaf5828bc1db56416eb1d4396df","balanceRoot":"0x6cba5d7307000000e56d25fc0600000008bd5d7307000000239e5d7307000000","latestBlockHeaderRoot":"0xbc2f3b78cf1d4d5b47d6cc987864860035088de6316b4eb1ccbe8677f31e98e1","ValidatorBalanceProof":["0x26805d73070000002aa55d7307000000218e5d730700000056d35d7307000000","0x24846a54724508d8394a9e30f819bc19fe6727745be4f19e1e3924b985b4b8e9","0x48fb4849c31d88b413f650c17bd55bb11fe38bcef5d234dba8532737ae78f776","0x0d7d38f90a04ef49d15ca10af0e818e227134e4b46c7b22dd10e149f2a2bc612","0xdd345235f28f92719c39c9a5042b91f814d535ae5df574c0f6cd38f77791fc2b","0x0855928643f4210d9d8d0986847674139be32ebf3db984b6d81f3ed136475b6d","0x28460d51c1ce75299f8c0dff8bca9f498fef0edc57c343bbd76592b3d6b984a3","0x068a141709053afb48698290176068f448867cba9c6ff10f986b1d02691f6e70","0x9f0958275fe6ea80b1825bcd11907d1076600719ce574ae2a609e51a975dde54","0xd43fbb18b30449f5c81148848fd6579b4f6cebfe66187bd1c36a293b22085fc2","0x7bf0aac502053da7dfd5356960db63ab48d3cc6ee5f36f41639eadacd34ac272","0x866e3b213d02f1cb86149c2472789ab9d7fb3fb52371a893dc8b6f8936ebd0c1","0xcec7cc4b82fb8dd889257128af3e4433700fcc0fb0505b431aa5f592111e785b","0xdd08667067baa43e2b41b793464a591a29643b5582c8fc9c2c045f984c5848d7","0x842ec80062a4f2f7e9d80ab408374ae42817acf5036aff40b3ade345ab196090","0xfe1ae22b8ba21fce84b25f5da3e37a91db49158b7025d6371d5a54c03adba125","0x8c039a81d68952077db2cc11713ae6f5a9ecc5ebe859c0612ff7215c0ccd7aba","0x5415d3199f1da31287d646d03c2691728396d87f7ef52c142596301e27a343ca","0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4","0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0x846b080000000000000000000000000000000000000000000000000000000000","0x7d46d32db9a97974916a2114aeffc11fc031a954b2110742267054a438055c1a","0x81e1234eb27473f180f890c98019ecb7655a3e58105c7182d8daf726d75aaf8d","0x738a1fb07361c872216ae655df9a1690d465e6698acd1c9230ebc3fd89412c69","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"],"WithdrawalCredentialProof":["0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e","0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee","0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1","0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1","0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac","0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537","0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38","0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748","0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a","0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c","0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b","0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e","0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee","0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9","0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74","0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3","0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f","0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6","0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528","0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3","0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa","0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c","0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167","0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7","0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0","0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544","0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765","0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4","0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1","0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636","0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c","0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7","0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff","0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5","0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d","0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c","0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327","0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74","0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76","0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f","0x846b080000000000000000000000000000000000000000000000000000000000","0x5a6c050000000000000000000000000000000000000000000000000000000000","0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e","0x8efea8a7f00e6411ae6187a6d9a2c22ad033c0ad749ce0940e5bf2fd76ac35c7","0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b","0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d"],"ValidatorFields":["0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d","0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443","0xe56d25fc06000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000","0xea65010000000000000000000000000000000000000000000000000000000000","0xf265010000000000000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000","0xffffffffffffffff000000000000000000000000000000000000000000000000"],"StateRootAgainstLatestBlockHeaderProof":["0x0000000000000000000000000000000000000000000000000000000000000000","0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b","0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"]} \ No newline at end of file diff --git a/src/test/tree/DelegationManagerUnit.tree b/src/test/tree/DelegationManagerUnit.tree new file mode 100644 index 000000000..8b974c3d0 --- /dev/null +++ b/src/test/tree/DelegationManagerUnit.tree @@ -0,0 +1,196 @@ +. +├── DelegationManager Tree (*** denotes that integration tests are needed to validate path) +├── when registerAsOperator is called +│ ├── given that the caller has already registered as operator +│ │ └── it should revert +│ ├── it should call `_setOperatorDetails` +│ │ ├── given that operatorDetails.earningsReceiver is 0 address +│ │ │ └── it should revert +│ │ ├── given operatorDetails.stakerOptOutWindowBlocks is > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS +│ │ │ └── it should revert +│ │ ├── given caller has already delegated to an operator +│ │ │ └── it should revert +│ │ └── it should emit an OperatorDetailsModified event +│ └── it should call `_delegate` +│ ├── given that delegation is paused +│ │ └── it should revert +│ ├── it should set the operator delegated to itself and emit a StakerDelegated event +│ ├── given the caller has delegateable shares +│ │ └── it should increase the operator's shares and and emit an OperatorSharesIncreased event +│ └── it should push an operator stake update +│ └── it should emit an OperatorRegistered event and OperatorMetadataURIUpdated event +├── when modifyOperatorDetails is called +│ ├── given caller is not an operator +│ │ └── it should revert +│ ├── given operatorDetails.earningsReceiver is 0 address +│ │ └── it should revert +│ ├── given operatorDetails.stakerOptOutWindowBlocks is > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS +│ │ └── it should revert +│ ├── given operatorDetails.stakerOptOutWindowBlocks is less than the current stakerOptOutWindowBlocks +│ │ └── it should revert +│ └── given caller is an operator and provides a valid earningsReceiver and stakerOptOutWindowBlocks +│ └── it should update the operatorDetails and emit an OperatorDetailsModified event +├── when updateOperatorMetadataURI is called +│ ├── given caller is not an operator +│ │ └── it should revert +│ └── given caller is an operator +│ └── it should emit an event +├── when delegateTo is called +│ └── it calls _delegate() (internal function) with msg.sender as the staker +├── when delegateToBySignature is called +│ ├── given block timestamp is > staker signature expiry +│ │ └── it should revert +│ ├── given staker signature verification fails +│ │ └── it should revert +│ └── given staker signature verification succeeds +│ └── it should call _delegate() (internal function) +├── when _delegate() is called +│ ├── given that new delegations are paused +│ │ └── it should revert +│ ├── given staker is already delegated to an operator +│ │ └── it should revert +│ ├── given passed in operator param isn't a registered operator +│ │ └── it should revert +│ ├── given operator's delegationApprover is set to zero address OR given caller is the delegationApprover +│ │ └── it should check delegatable shares and update accordingly (**below logic tree reused elsewhere**) +│ │ ├── given staker doesn't have delegatable shares +│ │ │ └── it should set staker delegated to operator, call the StakeRegistry, and emit events +│ │ └── given staker has delegatable shares +│ │ ├── given staker has EigenPod shares +│ │ │ ├── given EigenPod shares are <= 0 +│ │ │ │ └── it should set staker delegated to operator, operator beaconChainStrategy shares unchanged, call the StakeRegistry, and emit events +│ │ │ └── given EigenPod shares are > 0 +│ │ │ └── it should set staker delegated to operator, increase operator beaconChainStrategy shares, call the StakeRegistry, and emit events +│ │ ├── given staker has StrategyManager shares +│ │ │ └── it should set staker delegated to operator, increase operator StrategyManager shares, call the StakeRegistry, and emit events +│ │ └── given staker has shares in both EigenPod and StrategyManager +│ │ └── it should set staker delegated to operator, increase operator shares (EPM and SM), call the StakeRegistry, and emit events +│ └── given operator's delegationApprover is set to nonzero address AND the caller is not the delegationApprover +│ ├── given the delegationApprover is an EOA +│ │ ├── given the block timestamp is past the expiry timestamp +│ │ │ └── it should revert +│ │ ├── given the delegationApprove salt has already been used +│ │ │ └── it should revert +│ │ ├── given the signature verification fails +│ │ │ └── it should revert +│ │ └── given the signature verification succeeds +│ │ └── it should check delegatable shares and update accordingly (**logic tree reused from above**) +│ └── given the delegationApprover is a contract +│ ├── given the block timestamp is past the expiry timestamp +│ │ └── it should revert +│ ├── given the delegationApprove salt has already been used +│ │ └── it should revert +│ ├── given the contract isn't EIP1271 compliant +│ │ └── it should revert +│ ├── given the signature verification fails, isValidSignature() does not return EIP1271_MAGICVALUE +│ │ └── it should revert +│ └── given the signature verification succeeds, isValidSignature() returns EIP1271_MAGICVALUE +│ └── it should check delegatable shares and update accordingly (**logic tree reused from above**) +├── when undelegate is called +│ ├── given caller is not delegated to an operator +│ │ └── it should revert +│ ├── given that the caller is registered as operator +│ │ └── it should revert +│ ├── given that staker param is zero address +│ │ └── it should revert +│ ├── given msg.sender is neither the staker, operator, or delegationApprover (if applicable) +│ │ └── it should revert +│ ├── given the msg.sender is the operator or delegationApprover +│ │ └── it should emit a StakerForceUndelegated event +│ └── it should emit a StakerUndelegatedEvent and undelegate the staker +│ ├── given the staker doesn't have delegateable shares +│ │ └── it should return a zero withdrawal root +│ └── given the staker has delegateable shares *** +│ └── it should call _removeSharesAndQueueWithdrawal +├── when queueWithdrawals is called *** +│ ├── given that entering the withdrawal queue is paused +│ │ └── it should revert +│ └── it should loop through each withdrawal and call _removeSharesAndQueueWithdrawal +├── when _removeSharesAndQueueWithdrawal is called +│ ├── given that the staker is a zero address +│ │ └── it should revert +│ ├── given that the length of strategies is 0 +│ │ └── it should revert +│ └── it should loop through each strategy +│ ├── given that the staker is delegated to (not zero address) +│ │ └── it should decrease the operator's shares +│ ├── given that the strategy is the beacon chain strategy +│ │ └── it should remove shares from the eigen pod manager +│ ├── given that the strategy is not the beacon chain eth strategy +│ │ └── it should remove shares from the strategy manager +│ ├── given that the staker is delegated to (not zero address) +│ │ └── it should push a stake update +│ ├── it should increment the staker's cumulativeWithdrawalsQueued +│ ├── it should calculate and set the withdrawal root as pending +│ └── it should emit a WithdrawalQueued event and return the withdrawal root +├── when completeQueuedWithdrawal OR completeQueuedWithdrawals is called *** +│ ├── given that the exiting the withdrawal queue is paused +│ │ └── it should revert +│ ├── given that the function is reentered +│ │ └── it should revert +│ └── it should call _completeQueuedWithdrawal (internal function) for each withdrawal +├── when _completeQueuedWithdrawal is called *** +│ ├── given that the withdrawal root is not pending +│ │ └── it should revert +│ ├── given that the withdrawal delay blocks period has not elapsed +│ │ └── it should revert +│ ├── given that the caller is not the withdrawer +│ │ └── it should revert +│ ├── given that receiveAsTokens is true +│ │ └── given that the tokens and strategies length are not equal +│ │ └── it should revert +│ └── given that the above conditions are satisfied +│ ├── it should delete the withdrawal root from pending withdrawals +│ ├── given that receiveAsTokens is true +│ │ └── it should call _withdrawSharesAsTokens for each strategy to withdraw from +│ ├── given that receiveAsTokens is false +│ │ ├── it should loop through each strategy to withdraw from +│ │ ├── given that the strategy is the beaconChainETHStrategy +│ │ │ ├── it should call addShares on the eigenPodManager with the staker as the original pod owner +│ │ │ └── given that the staker is delegated to (operator not zero address) +│ │ │ ├── it should increase the original pod operator's shares +│ │ │ └── it should push a stake update for the original pod operator +│ │ ├── given that the strategy is not the beaconChainETHStrategy +│ │ │ ├── it should call addShares on the strategyManager with the staker as the withdrawer +│ │ │ └── it should increase the operator's shares with the staker as the withdrawer +│ │ └── it should push an operator stake update +│ └── it should emit a WithdrawalCompleted event +├── when _withdrawSharesAsTokens is called (internal function) *** +│ ├── given that the strategy is the beaconChainStrategy +│ │ └── it should call withdrawSharesAsTokens on the eigen pod manager +│ └── given that the strategy is not the beaconChainStrategy +│ └── it should call withdrawSharesAsTokens on the strategy manager +├── when migrate queued withdrawals is called +│ └── given that the withdrawal succesfully deletes from the strategy manager +│ ├── it should calculate a new root +│ │ └── given that the root is already pending +│ │ └── it should revert +│ └── it should emit WithdrawalQueued and WithdrawalMigrated events +├── when increaseDelegatedShares is called +│ ├── if the caller is not the strategy manager or eigen pod manager +│ │ └── it should revert +│ └── given that the staker is delegated +│ ├── it should increase the operator's share for the staker and its associated strategy +│ └── it should push an operator stake update +├── when decreaseDelegatedShares is called +│ ├── if the caller is not the strategy manager or eigen pod manager +│ │ └── it should revert +│ └── given that the staker is delegated +│ ├── it should increase the operator's share for the staker and its associated strategy +│ └── it should push an operator stake update +├── when setWithdrawalDelayBlocks is called +│ ├── given not called by owner +│ │ └── it should revert +│ ├── given new delay is > MAX_WITHDRAWAL_DELAY_BLOCKS +│ │ └── it should revert +│ └── given called by owner and new delay is <= MAX_WITHDRAWAL_DELAY_BLOCKS +│ └── it should set the new delay and emit event +└── when setStakeRegistry is called + ├── given not called by owner + │ └── it should revert + ├── given existing stakeRegistry address is set + │ └── it should revert + ├── given new stakeRegistry address is 0 + │ └── it should revert + └── given called by owner, existing address not set, and new address is nonzero + └── it should set the new stakeRegistry address and emit event \ No newline at end of file diff --git a/src/test/tree/EigenPodManager.tree b/src/test/tree/EigenPodManager.tree deleted file mode 100644 index 695fbb0a8..000000000 --- a/src/test/tree/EigenPodManager.tree +++ /dev/null @@ -1,93 +0,0 @@ -├── EigenPodManager Tree (*** denotes that integrationt tests are needed to validate path) -├── when contract is deployed and initialized -│ └── it should properly set storage -├── when initialize called again -│ └── it should revert -├── when createPod called -│ ├── given the user has already created a pod -│ │ └── it should revert -│ ├── given that the max number of pods has been deployed -│ │ └── it should revert -│ └── given the user has not created a pod -│ └── it should deploy a pod -├── when stake is called -│ ├── given the user has not created a pod -│ │ └── it should deploy a pod -│ └── given the user has already created a pod -│ └── it should call stake on the eigenPod -├── when setMaxPods is called -│ ├── given the user is not the pauser -│ │ └── it should revert -│ └── given the user is the pauser -│ └── it should set the max pods -├── when updateBeaconChainOracle is called -│ ├── given the user is not the owner -│ │ └── it should revert -│ └── given the user is the owner -│ └── it should set the beacon chain oracle -├── when addShares is called -│ ├── given that the caller is not the delegationManager -│ │ └── it should revert -│ ├── given that the podOwner address is 0 -│ │ └── it should revert -│ ├── given that the shares amount is negative -│ │ └── it should revert -│ ├── given that the shares is not a whole gwei amount -│ │ └── it should revert -│ └── given that all of the above conditions are satisfied -│ └── it should update the podOwnerShares -├── when removeShares is called -│ ├── given that the caller is not the delegationManager -│ │ └── it should revert -│ ├── given that the shares amount is negative -│ │ └── it should revert -│ ├── given that the shares is not a whole gwei amount -│ │ └── it should revert -│ ├── given that removing shares results in the pod owner having negative shares -│ │ └── it should revert -│ └── given that all of the above conditions are satisfied -│ └── it should update the podOwnerShares -├── when withdrawSharesAsTokens is called -│ ├── given that the podOwner is address 0 -│ │ └── it should revert -│ ├── given that the destination is address 0 -│ │ └── it should revert -│ ├── given that the shares amount is negative -│ │ └── it should revert -│ ├── given that the shares is not a whole gwei amount -│ │ └── it should revert -│ ├── given that the current podOwner shares are negative -│ │ ├── given that the shares to withdraw are larger in magnitude than the shares of the podOwner -│ │ │ └── it should set the podOwnerShares to 0 and decrement shares to withdraw by the share deficit -│ │ └── given that the shares to withdraw are smaller in magnitude than shares of the podOwner -│ │ └── it should increment the podOwner shares by the shares to withdraw -│ └── given that the pod owner shares are positive -│ └── it should withdraw restaked ETH from the eigenPod -├── when shares are adjusted *** -│ ├── given that sharesBefore is negative or 0 -│ │ ├── given that sharesAfter is negative or zero -│ │ │ └── the change in delegateable shares should be 0 -│ │ └── given that sharesAfter is positive -│ │ └── the change in delegateable shares should be positive -│ └── given that sharesBefore is positive -│ ├── given that sharesAfter is negative or zero -│ │ └── the change in delegateable shares is negative sharesBefore -│ └── given that sharesAfter is positive -│ └── the change in delegateable shares is the difference between sharesAfter and sharesBefore -└── when recordBeaconChainETHBalanceUpdate is called - ├── given that the podOwner's eigenPod is not the caller - │ └── it should revert - ├── given that the podOwner is a zero address - │ └── it should revert - ├── given that sharesDelta is not a whole gwei amount - │ ├── it should revert - │ └── given that the shares delta is valid - │ └── it should update the podOwnerShares - ├── given that the change in delegateable shares is positive *** - │ └── it should increase delegated shares on the delegationManager - ├── given that the change in delegateable shares is negative *** - │ └── it should decrease delegated shares on the delegationManager - ├── given that the change in delegateable shares is 0 *** - │ └── it should only update the podOwnerShares - └── given that the function is reentered *** - └── it should revert \ No newline at end of file diff --git a/src/test/tree/EigenPodUnit.tree b/src/test/tree/EigenPodUnit.tree new file mode 100644 index 000000000..92a2c4e45 --- /dev/null +++ b/src/test/tree/EigenPodUnit.tree @@ -0,0 +1,171 @@ +. +├── EigenPod Tree (*** denotes that integration tests are needed to validate path) +├── when the contract is deployed and initialized +│ └── it should properly set storage +├── when initialize called again +│ └── it should revert +├── // Balance Update Tree +├── when verifyBalanceUpdates is called *** +│ ├── given that balance updates are paused +│ │ └── it should revert +│ ├── given that the indices and proofs are different lengths +│ │ └── it should revert +│ ├── given that the oracle timestamp is stale +│ │ └── it should revert +│ ├── given that the beacon state root proof is invalid +│ │ └── it should revert +│ └── given that the above conditions are satisfied +│ ├── it should call _verifyBalanceUpdate for each balance update +│ └── it should record a beaconChainETH balance update in the EPM +├── when _verifyBalanceUpdate is called (internal function) +│ ├── given that the most recent balance update timestamp is greater than or equal to the oracle timestamp +│ │ └── it should revert +│ ├── given that the validator status is not active +│ │ └── it should revert +│ ├── given that the validator withdrawable epoch is less than or equal to the epoch of the oracle timestamp +│ │ └── given that the validator balance is equal to 0 +│ │ └── it should revert +│ ├── given that the validator fields proof is not valid +│ │ └── it should revert +│ ├── given that the validator balances proof is not valid +│ │ └── it should revert +│ └── given that the above conditions are satisfied +│ ├── given that the validator restaked balance is greater than the max restaked balance per validator +│ │ └── it should set the new restaked balance to the validator restaked balance +│ ├── given that the validator restaked balance is less than or equal to the max restaked balance per validator +│ │ └── it should set the new restaked balance to the validator restaked balance +│ ├── it should update the _validatorPubkeyHashToInfo mapping with the new restaked balance +│ └── given that the new restaked balance is not equal to the validator restaked balance +│ └── it should emit a validator balance updated event and return a non-zero sharesDeltaGwei +├── // EigenPodManager Caller Tree +├── when stake is called +│ ├── given the caller is not the EigenPodManager +│ │ └── it should revert +│ ├── given the value staked is not 32 ETH +│ │ └── it should revert +│ └── given that all of the above conditions are satisfied +│ └── it should stake ETH in the beacon chain deposit contract +├── when withdrawRestakedBeaconChainETH is called - function only relevant when `withdrawableRestakedExecutionLayerGwei` is incremented after a full withdrawal +│ ├── given that the caller is not the EigenPodManager +│ │ └── it should revert +│ ├── given that the amount to withdraw is not a whole Gwei amount +│ │ └── it should revert +│ ├── given that the amount to withdraw is greater than the withdrawable restaked execution layer amount +│ │ └── it should revert +│ └── given the above conditions are satisfied +│ └── it should send eth from the pod to the recipient +├── // EigenPodOwner Caller Tree +├── when verifyWithdrawalCredentials is called *** +│ ├── given that the caller is not the eigen pod Owner +│ │ └── it should revert +│ ├── given that verify credentials is paused +│ │ └── it should revert +│ ├── given that the proof is not valid for the timestamp +│ │ └── it should revert +│ ├── given that restaking is not enabled +│ │ └── it should revert +│ ├── given that the validator indices, proofs, and validator fields are different lengths +│ │ └── it should revert +│ ├── given that the withdrawal credential proof is stale +│ │ └── it should revert +│ ├── given that the beacon state root proof is invalid +│ │ └── it should revert +│ ├── it should call _verifyWithdrawalCredentials for each validator +│ └── it should record a beaconChainETH balance update in the EPM +├── when _verifyWithdrawalCredentials is called (internal function) +│ ├── given that the validators status is inactive +│ │ └── it should revert +│ ├── given that validator's withdrawal credentials does not correspond to the pod withdrawal credentials +│ │ └── it should revert +│ ├── given that the validator fields proof is not valid +│ │ └── it should revert +│ └── given that all the above conditions are satisfied +│ ├── given that the validator effective balance is greater than the max restaked balance +│ │ └── it should set the validator restaked balance to the max restaked balance +│ ├── given that the validator effective balance is less than or equal to the max restaked balance +│ │ └── it should set the validator restaked balance to the validator effective balance +│ ├── it should update the _validatorPubkeyHashToInfo mapping with an active validator and restaked balance +│ ├── it should emit ValidatorRestaked and ValidatorBalanceUpdated Events +│ └── It should return the validator's restakedBalance in wei +├── when withdrawNonBeaconChainETHBalanceWei is called +│ ├── given that the caller is not the eigen pod owner +│ │ └── it should revert +│ ├── given that the amount to withdraw is greater than the non-beacon chain eth balance +│ │ └── it should revert +│ └── given the above conditions pass +│ └── it should emit a non beacon chain eth withdrawn event and send eth to the delayed withdrawal router +├── when recoverTokens is called +│ ├── given that the caller is not the eigen pod owner +│ │ └── it should revert +│ ├── given that the tokens and amounts to withdraw are different lengths +│ │ └── it should revert +│ └── given that the above conditions pass +│ └── it should transfer tokens to the recipient +├── when activate restaking is called +│ ├── given that the eigenpods verify credentials is not paused *** +│ │ └── it should revert +│ ├── given that the caller is not the eigen pod owner +│ │ └── it should revert +│ ├── given that hasRestaked is true +│ │ └── it should revert +│ └── given that all the above conditions pass +│ └── it should set hasRestaked to true, process a withdrawal of ETH to the delayed withdrawal router, and emit a RestakingActivated event +├── when withdrawBeforeRestaking is called +│ ├── given that the caller is not the eigen pod owner +│ │ └── it should revert +│ ├── given that has restaked is true +│ │ └── it should revert +│ └── given that the above conditions pass +│ └── it should process a withdrawal of eth to the delayed withdrawal router +├── // Withdrawal Tree +├── when verifyAndProcessWithdrawals is called *** +│ ├── given that verifying withdrawals are paused +│ │ └── it should revert +│ ├── given that validatorFields, validatorProofs, withdrawalProofs, withdrawalFields, are different lengths +│ │ └── it should revert +│ ├── given that the beacon state root proof is invalid +│ │ └── it should revert +│ ├── given that the above conditions are satisfied +│ ├── it should call _verifyAndProcessWithdrawal +│ ├── given that the amount of ETH to withdraw immediately is greater than 0 +│ │ └── it should send the ETH to the delayed withdrawal router +│ └── given that the pod's shares have are not 0 +│ └── it should record a beacon chain balance update in the EPM +└── when _verifyAndProcessWithdrawal is called (internal function) + ├── given that the proof timestamp is stale + │ └── it should revert + ├── given that the status of the validator is inactive + │ └── it should revert + ├── given that the withdrawalTimestamp has already been proven for the validator + │ └── it should revert + ├── given that the withdrawal proof is invalid + │ └── it should revert + ├── given that the validator fields proof is invalid + │ └── it should revert + └── given that the above conditions are satisfied + ├── it should set the withdrawal timestamp as proven for the validator pubKey + ├── given that the epoch of the proof is after the withdrawable epoch + │ ├── it should call _processFullWithdrawal + │ └── when _processFullWithdrawal is called (internal function) + │ ├── given that the withdrawalAmount is greater than the max restaked balance per validator + │ │ └── it should set the amount to queue to the max restaked balance per validator + │ ├── given that the withdrawalAmount is less than or equal to the max restaked balance per validator + │ │ └── it should set the amount to queue to the withdrawal amount + │ ├── it should set the amount of ETH to withdraw via the delayed withdrawal router as the difference between the withdrawalAmount and amount to queue + │ ├── it should increment withdrawableRestakedExecutionLayerGwei by the amount to queue + │ ├── it should update the sharesDelta of the withdrawal as the difference between the amount to queue and the previous restaked balance + │ ├── it should update the _validatorPubkeyHashToInfo mapping with a restaked balance of 0 and status as withdrawn + │ └── it should emit a FullWithdrawalRedeemed event and return the verified withdrawal struct + └── given that the epoch of the proof is before the withdrawable epoch + ├── it should call _processPartialWithdrawal + └── when _processPartialWithdrawal is called (internal function) + ├── it should emit a PartialWithdrawalRedeemed event + ├── it should increment the sumOfPartialWithdrawalsClaimedGwei variable + └── it should return the verified withdrawal struct + +// Tests in Integration +// Pausing Functionality +// verifyWithdrawalCredentials (external) +// verifyBalanceUpdates (external) +// verifyAndProcessWithdrawals(external) +// Withdraw restaked beacon chain ETH after withdrawing \ No newline at end of file diff --git a/src/test/tree/StrategyManangerUnit.tree b/src/test/tree/StrategyManagerUnit.tree similarity index 100% rename from src/test/tree/StrategyManangerUnit.tree rename to src/test/tree/StrategyManagerUnit.tree diff --git a/src/test/unit/BitmapUtils.t.sol b/src/test/unit/BitmapUtils.t.sol deleted file mode 100644 index 76225aa97..000000000 --- a/src/test/unit/BitmapUtils.t.sol +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.12; - -import "../harnesses/BitmapUtilsWrapper.sol"; -// import "../../contracts/libraries/BitmapUtils.sol"; - -import "forge-std/Test.sol"; - -contract BitmapUtilsUnitTests is Test { - Vm cheats = Vm(HEVM_ADDRESS); - - BitmapUtilsWrapper public bitmapUtilsWrapper; - - function setUp() public { - bitmapUtilsWrapper = new BitmapUtilsWrapper(); - } - - // ensure that the bitmap encoding of an emtpy bytes array is an emtpy bitmap (function doesn't revert and approriately returns uint256(0)) - function testEmptyArrayEncoding() public view { - bytes memory emptyBytesArray; - uint256 returnedBitMap = bitmapUtilsWrapper.bytesArrayToBitmap(emptyBytesArray); - require(returnedBitMap == 0, "BitmapUtilsUnitTests.testEmptyArrayEncoding: empty array not encoded to empty bitmap"); - } - - // ensure that the bitmap encoding of a single uint8 (i.e. a single byte) matches the expected output - function testSingleByteEncoding(uint8 fuzzedNumber) public view { - bytes1 singleByte = bytes1(fuzzedNumber); - bytes memory bytesArray = abi.encodePacked(singleByte); - uint256 returnedBitMap = bitmapUtilsWrapper.bytesArrayToBitmap(bytesArray); - uint256 bitMask = uint256(1 << fuzzedNumber); - require(returnedBitMap == bitMask, "BitmapUtilsUnitTests.testSingleByteEncoding: non-equivalence"); - } - - // ensure that the bitmap encoding of a two uint8's (i.e. a two byte array) matches the expected output - function testTwoByteEncoding(uint8 firstFuzzedNumber, uint8 secondFuzzedNumber) public { - bytes1 firstSingleByte = bytes1(firstFuzzedNumber); - bytes1 secondSingleByte = bytes1(secondFuzzedNumber); - bytes memory bytesArray = abi.encodePacked(firstSingleByte, secondSingleByte); - if (firstFuzzedNumber == secondFuzzedNumber) { - cheats.expectRevert(bytes("BitmapUtils.bytesArrayToBitmap: repeat entry in bytesArray")); - bitmapUtilsWrapper.bytesArrayToBitmap(bytesArray); - } else { - uint256 returnedBitMap = bitmapUtilsWrapper.bytesArrayToBitmap(bytesArray); - uint256 firstBitMask = uint256(1 << firstFuzzedNumber); - uint256 secondBitMask = uint256(1 << secondFuzzedNumber); - uint256 combinedBitMask = firstBitMask | secondBitMask; - require(returnedBitMap == combinedBitMask, "BitmapUtilsUnitTests.testTwoByteEncoding: non-equivalence"); - } - } - - // ensure that converting bytes array => bitmap => bytes array is returns the original bytes array (i.e. is lossless and artifactless) - // note that this only works on ordered arrays, because unordered arrays will be returned ordered - function testBytesArrayToBitmapToBytesArray(bytes memory originalBytesArray) public view { - // filter down to only ordered inputs - cheats.assume(bitmapUtilsWrapper.isArrayStrictlyAscendingOrdered(originalBytesArray)); - uint256 bitmap = bitmapUtilsWrapper.bytesArrayToBitmap(originalBytesArray); - bytes memory returnedBytesArray = bitmapUtilsWrapper.bitmapToBytesArray(bitmap); - // emit log_named_bytes("originalBytesArray", originalBytesArray); - // emit log_named_uint("bitmap", bitmap); - // emit log_named_bytes("returnedBytesArray", returnedBytesArray); - require(keccak256(abi.encodePacked(originalBytesArray)) == keccak256(abi.encodePacked(returnedBytesArray)), - "BitmapUtilsUnitTests.testBytesArrayToBitmapToBytesArray: output doesn't match input"); - } - - // ensure that converting bytes array => bitmap => bytes array is returns the original bytes array (i.e. is lossless and artifactless) - // note that this only works on ordered arrays, because unordered arrays will be returned ordered - function testBytesArrayToBitmapToBytesArray_Yul(bytes memory originalBytesArray) public view { - // filter down to only ordered inputs - cheats.assume(bitmapUtilsWrapper.isArrayStrictlyAscendingOrdered(originalBytesArray)); - uint256 bitmap = bitmapUtilsWrapper.bytesArrayToBitmap_Yul(originalBytesArray); - bytes memory returnedBytesArray = bitmapUtilsWrapper.bitmapToBytesArray(bitmap); - // emit log_named_bytes("originalBytesArray", originalBytesArray); - // emit log_named_uint("bitmap", bitmap); - // emit log_named_bytes("returnedBytesArray", returnedBytesArray); - require(keccak256(abi.encodePacked(originalBytesArray)) == keccak256(abi.encodePacked(returnedBytesArray)), - "BitmapUtilsUnitTests.testBytesArrayToBitmapToBytesArray: output doesn't match input"); - } - - // ensure that converting bytes array => bitmap => bytes array is returns the original bytes array (i.e. is lossless and artifactless) - // note that this only works on ordered arrays - function testBytesArrayToBitmapToBytesArray_OrderedVersion(bytes memory originalBytesArray) public view { - // filter down to only ordered inputs - cheats.assume(bitmapUtilsWrapper.isArrayStrictlyAscendingOrdered(originalBytesArray)); - uint256 bitmap = bitmapUtilsWrapper.orderedBytesArrayToBitmap(originalBytesArray); - bytes memory returnedBytesArray = bitmapUtilsWrapper.bitmapToBytesArray(bitmap); - require(keccak256(abi.encodePacked(originalBytesArray)) == keccak256(abi.encodePacked(returnedBytesArray)), - "BitmapUtilsUnitTests.testBytesArrayToBitmapToBytesArray: output doesn't match input"); - } - - // ensure that converting bytes array => bitmap => bytes array is returns the original bytes array (i.e. is lossless and artifactless) - // note that this only works on ordered arrays - function testBytesArrayToBitmapToBytesArray_OrderedVersion_Yul(bytes memory originalBytesArray) public view { - // filter down to only ordered inputs - cheats.assume(bitmapUtilsWrapper.isArrayStrictlyAscendingOrdered(originalBytesArray)); - uint256 bitmap = bitmapUtilsWrapper.orderedBytesArrayToBitmap(originalBytesArray); - bytes memory returnedBytesArray = bitmapUtilsWrapper.bitmapToBytesArray(bitmap); - require(keccak256(abi.encodePacked(originalBytesArray)) == keccak256(abi.encodePacked(returnedBytesArray)), - "BitmapUtilsUnitTests.testBytesArrayToBitmapToBytesArray: output doesn't match input"); - } - - // ensure that converting bitmap => bytes array => bitmap is returns the original bitmap (i.e. is lossless and artifactless) - function testBitMapToBytesArrayToBitmap(uint256 originalBitmap) public view { - bytes memory bytesArray = bitmapUtilsWrapper.bitmapToBytesArray(originalBitmap); - uint256 returnedBitMap = bitmapUtilsWrapper.bytesArrayToBitmap(bytesArray); - require(returnedBitMap == originalBitmap, "BitmapUtilsUnitTests.testBitMapToArrayToBitmap: output doesn't match input"); - } - - // testing one function for a specific input. used for comparing gas costs - function testBytesArrayToBitmap_OrderedVersion_Yul_SpecificInput(/*bytes memory originalBytesArray*/) public { - bytes memory originalBytesArray = - abi.encodePacked(bytes1(uint8(5)), bytes1(uint8(6)), bytes1(uint8(7)), bytes1(uint8(8)), bytes1(uint8(9)), bytes1(uint8(10)), bytes1(uint8(11)), bytes1(uint8(12))); - // bytes memory originalBytesArray = abi.encodePacked(bytes1(uint8(5))); - uint256 gasLeftBefore = gasleft(); - bitmapUtilsWrapper.orderedBytesArrayToBitmap_Yul(originalBytesArray); - uint256 gasLeftAfter = gasleft(); - uint256 gasSpent = gasLeftBefore - gasLeftAfter; - emit log_named_uint("gasSpent", gasSpent); - } - - // testing one function for a specific input. used for comparing gas costs - function testBytesArrayToBitmap_OrderedVersion_SpecificInput(/*bytes memory originalBytesArray*/) public { - bytes memory originalBytesArray = - abi.encodePacked(bytes1(uint8(5)), bytes1(uint8(6)), bytes1(uint8(7)), bytes1(uint8(8)), bytes1(uint8(9)), bytes1(uint8(10)), bytes1(uint8(11)), bytes1(uint8(12))); - // bytes memory originalBytesArray = abi.encodePacked(bytes1(uint8(5))); - uint256 gasLeftBefore = gasleft(); - bitmapUtilsWrapper.orderedBytesArrayToBitmap(originalBytesArray); - uint256 gasLeftAfter = gasleft(); - uint256 gasSpent = gasLeftBefore - gasLeftAfter; - emit log_named_uint("gasSpent", gasSpent); - } - - // testing one function for a specific input. used for comparing gas costs - function testBytesArrayToBitmap_SpecificInput(/*bytes memory originalBytesArray*/) public { - bytes memory originalBytesArray = - abi.encodePacked(bytes1(uint8(5)), bytes1(uint8(6)), bytes1(uint8(7)), bytes1(uint8(8)), bytes1(uint8(9)), bytes1(uint8(10)), bytes1(uint8(11)), bytes1(uint8(12))); - // bytes memory originalBytesArray = abi.encodePacked(bytes1(uint8(5))); - uint256 gasLeftBefore = gasleft(); - bitmapUtilsWrapper.bytesArrayToBitmap(originalBytesArray); - uint256 gasLeftAfter = gasleft(); - uint256 gasSpent = gasLeftBefore - gasLeftAfter; - emit log_named_uint("gasSpent", gasSpent); - } - - // testing one function for a specific input. used for comparing gas costs - function testBytesArrayToBitmap_Yul_SpecificInput(/*bytes memory originalBytesArray*/) public { - bytes memory originalBytesArray = - abi.encodePacked(bytes1(uint8(5)), bytes1(uint8(6)), bytes1(uint8(7)), bytes1(uint8(8)), bytes1(uint8(9)), bytes1(uint8(10)), bytes1(uint8(11)), bytes1(uint8(12))); - // bytes memory originalBytesArray = abi.encodePacked(bytes1(uint8(5))); - uint256 gasLeftBefore = gasleft(); - bitmapUtilsWrapper.bytesArrayToBitmap_Yul(originalBytesArray); - uint256 gasLeftAfter = gasleft(); - uint256 gasSpent = gasLeftBefore - gasLeftAfter; - emit log_named_uint("gasSpent", gasSpent); - } - - // @notice check for consistency of `countNumOnes` function - function testCountNumOnes(uint256 input) public view { - uint16 libraryOutput = bitmapUtilsWrapper.countNumOnes(input); - // run dumb routine - uint16 numOnes = 0; - for (uint256 i = 0; i < 256; ++i) { - if ((input >> i) & 1 == 1) { - ++numOnes; - } - } - require(libraryOutput == numOnes, "inconsistency in countNumOnes function"); - } - - // @notice some simple sanity checks on the `numberIsInBitmap` function - function testNumberIsInBitmap() public view { - require(bitmapUtilsWrapper.numberIsInBitmap(2 ** 6, 6), "numberIsInBitmap function is broken 0"); - require(bitmapUtilsWrapper.numberIsInBitmap(1, 0), "numberIsInBitmap function is broken 1"); - require(bitmapUtilsWrapper.numberIsInBitmap(255, 7), "numberIsInBitmap function is broken 2"); - require(bitmapUtilsWrapper.numberIsInBitmap(1024, 10), "numberIsInBitmap function is broken 3"); - for (uint256 i = 0; i < 256; ++i) { - require(bitmapUtilsWrapper.numberIsInBitmap(type(uint256).max, uint8(i)), "numberIsInBitmap function is broken 4"); - require(!bitmapUtilsWrapper.numberIsInBitmap(0, uint8(i)), "numberIsInBitmap function is broken 5"); - } - } -} \ No newline at end of file diff --git a/src/test/unit/DelegationUnit.t.sol b/src/test/unit/DelegationUnit.t.sol index 8798b425d..ca98d5834 100644 --- a/src/test/unit/DelegationUnit.t.sol +++ b/src/test/unit/DelegationUnit.t.sol @@ -1,44 +1,37 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.9; +pragma solidity =0.8.12; import "@openzeppelin/contracts/mocks/ERC1271WalletMock.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; - -import "forge-std/Test.sol"; - -import "../mocks/StrategyManagerMock.sol"; -import "../mocks/SlasherMock.sol"; -import "../mocks/EigenPodManagerMock.sol"; -import "../mocks/StakeRegistryStub.sol"; -import "../EigenLayerTestHelper.t.sol"; -import "../mocks/ERC20Mock.sol"; -import "../mocks/Reenterer.sol"; -import "../Delegation.t.sol"; -import "src/contracts/core/StrategyManager.sol"; - -contract DelegationUnitTests is EigenLayerTestHelper { - - StrategyManagerMock strategyManagerMock; - SlasherMock slasherMock; +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; + +import "src/contracts/core/DelegationManager.sol"; +import "src/contracts/strategies/StrategyBase.sol"; + +import "src/test/events/IDelegationManagerEvents.sol"; +import "src/test/mocks/StakeRegistryStub.sol"; +import "src/test/utils/EigenLayerUnitTestSetup.sol"; + +/** + * @notice Unit testing of the DelegationManager contract. Withdrawals are tightly coupled + * with EigenPodManager and StrategyManager and are part of integration tests. + * Contracts tested: DelegationManager + * Contracts not mocked: StrategyBase, PauserRegistry + */ +contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManagerEvents { + // Contract under test DelegationManager delegationManager; DelegationManager delegationManagerImplementation; - StrategyManager strategyManagerImplementation; + + // Mocks StrategyBase strategyImplementation; StrategyBase strategyMock; - StrategyBase strategyMock2; - StrategyBase strategyMock3; IERC20 mockToken; - EigenPodManagerMock eigenPodManagerMock; + uint256 mockTokenInitialSupply = 10e50; StakeRegistryStub stakeRegistryMock; - Reenterer public reenterer; - - // used as transient storage to fix stack-too-deep errors - IStrategy public _tempStrategyStorage; - address public _tempStakerStorage; - + // Delegation signer uint256 delegationSignerPrivateKey = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); - uint256 stakerPrivateKey = uint256(123456789); + uint256 stakerPrivateKey = uint256(123_456_789); // empty string reused across many tests string emptyStringForMetadataURI; @@ -47,275 +40,455 @@ contract DelegationUnitTests is EigenLayerTestHelper { bytes32 emptySalt; // reused in various tests. in storage to help handle stack-too-deep errors - address _operator = address(this); + address defaultStaker = cheats.addr(uint256(123_456_789)); + address defaultOperator = address(this); - /** - * @dev Index for flag that pauses new delegations when set - */ + IStrategy public constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0); + + // Index for flag that pauses new delegations when set. uint8 internal constant PAUSED_NEW_DELEGATION = 0; - // @dev Index for flag that pauses queuing new withdrawals when set. + // Index for flag that pauses queuing new withdrawals when set. uint8 internal constant PAUSED_ENTER_WITHDRAWAL_QUEUE = 1; - // @dev Index for flag that pauses completing existing withdrawals when set. + // Index for flag that pauses completing existing withdrawals when set. uint8 internal constant PAUSED_EXIT_WITHDRAWAL_QUEUE = 2; - /// @notice Emitted when the StakeRegistry is set - event StakeRegistrySet(IStakeRegistryStub stakeRegistry); - - // @notice Emitted when a new operator registers in EigenLayer and provides their OperatorDetails. - event OperatorRegistered(address indexed operator, IDelegationManager.OperatorDetails operatorDetails); - - // @notice Emitted when an operator updates their OperatorDetails to @param newOperatorDetails - event OperatorDetailsModified(address indexed operator, IDelegationManager.OperatorDetails newOperatorDetails); - - /** - * @notice Emitted when @param operator indicates that they are updating their MetadataURI string - * @dev Note that these strings are *never stored in storage* and are instead purely emitted in events for off-chain indexing - */ - event OperatorMetadataURIUpdated(address indexed operator, string metadataURI); - - /// @notice Emitted whenever an operator's shares are increased for a given strategy - event OperatorSharesIncreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); - - /// @notice Emitted whenever an operator's shares are decreased for a given strategy - event OperatorSharesDecreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); - - // @notice Emitted when @param staker delegates to @param operator. - event StakerDelegated(address indexed staker, address indexed operator); - - // @notice Emitted when @param staker undelegates from @param operator. - event StakerUndelegated(address indexed staker, address indexed operator); - - /** - * @notice Emitted when a new withdrawal is queued. - * @param withdrawalRoot Is the hash of the `withdrawal`. - * @param withdrawal Is the withdrawal itself. - */ - event WithdrawalQueued(bytes32 withdrawalRoot, IDelegationManager.Withdrawal withdrawal); - - /// @notice Emitted when a queued withdrawal is completed - event WithdrawalCompleted(bytes32 withdrawalRoot); - - /// @notice Emitted when a queued withdrawal is *migrated* from the StrategyManager to the DelegationManager - event WithdrawalMigrated(bytes32 oldWithdrawalRoot, bytes32 newWithdrawalRoot); - - /// @notice Emitted when the `withdrawalDelayBlocks` variable is modified from `previousValue` to `newValue`. - event WithdrawalDelayBlocksSet(uint256 previousValue, uint256 newValue); - - /// StrategyManager Events - - /** - * @notice Emitted when a new deposit occurs on behalf of `depositor`. - * @param depositor Is the staker who is depositing funds into EigenLayer. - * @param strategy Is the strategy that `depositor` has deposited into. - * @param token Is the token that `depositor` deposited. - * @param shares Is the number of new shares `depositor` has been granted in `strategy`. - */ - event Deposit(address depositor, IERC20 token, IStrategy strategy, uint256 shares); - - // @notice reuseable modifier + associated mapping for filtering out weird fuzzed inputs, like making calls from the ProxyAdmin or the zero address - mapping(address => bool) public addressIsExcludedFromFuzzedInputs; - modifier filterFuzzedAddressInputs(address fuzzedAddress) { - cheats.assume(!addressIsExcludedFromFuzzedInputs[fuzzedAddress]); - _; - } - - // @notice mappings used to handle duplicate entries in fuzzed address array input + /// @notice mappings used to handle duplicate entries in fuzzed address array input mapping(address => uint256) public totalSharesForStrategyInArray; mapping(IStrategy => uint256) public delegatedSharesBefore; - function setUp() override virtual public{ - EigenLayerDeployer.setUp(); - - slasherMock = new SlasherMock(); - delegationManager = DelegationManager(address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), ""))); - strategyManagerMock = new StrategyManagerMock(); - eigenPodManagerMock = new EigenPodManagerMock(); + function setUp() public virtual override { + // Setup + EigenLayerUnitTestSetup.setUp(); + // Deploy DelegationManager implmentation and proxy delegationManagerImplementation = new DelegationManager(strategyManagerMock, slasherMock, eigenPodManagerMock); - - cheats.startPrank(eigenLayerProxyAdmin.owner()); - eigenLayerProxyAdmin.upgrade(TransparentUpgradeableProxy(payable(address(delegationManager))), address(delegationManagerImplementation)); - cheats.stopPrank(); - - address initialOwner = address(this); - uint256 initialPausedStatus = 0; - delegationManager.initialize(initialOwner, eigenLayerPauserReg, initialPausedStatus); - - strategyManagerImplementation = new StrategyManager(delegationManager, eigenPodManagerMock, slasherMock); - strategyManager = StrategyManager( + delegationManager = DelegationManager( address( new TransparentUpgradeableProxy( - address(strategyManagerImplementation), + address(delegationManagerImplementation), address(eigenLayerProxyAdmin), - abi.encodeWithSelector( - StrategyManager.initialize.selector, - initialOwner, - initialOwner, - eigenLayerPauserReg, - initialPausedStatus - ) + abi.encodeWithSelector(DelegationManager.initialize.selector, address(this), pauserRegistry, 0) // 0 is initialPausedStatus ) ) ); + // Deploy mock stake registry and set stakeRegistryMock = new StakeRegistryStub(); cheats.expectEmit(true, true, true, true, address(delegationManager)); emit StakeRegistrySet(stakeRegistryMock); - delegationManager.setStakeRegistry(stakeRegistryMock); + // Deploy mock token and strategy + mockToken = new ERC20PresetFixedSupply("Mock Token", "MOCK", mockTokenInitialSupply, address(this)); strategyImplementation = new StrategyBase(strategyManagerMock); strategyMock = StrategyBase( address( new TransparentUpgradeableProxy( address(strategyImplementation), address(eigenLayerProxyAdmin), - abi.encodeWithSelector(StrategyBase.initialize.selector, weth, eigenLayerPauserReg) + abi.encodeWithSelector(StrategyBase.initialize.selector, mockToken, pauserRegistry) ) ) ); - // excude the zero address and the proxyAdmin from fuzzed inputs - addressIsExcludedFromFuzzedInputs[address(0)] = true; - addressIsExcludedFromFuzzedInputs[address(eigenLayerProxyAdmin)] = true; - addressIsExcludedFromFuzzedInputs[address(strategyManagerMock)] = true; + // Exclude delegation manager from fuzzed tests addressIsExcludedFromFuzzedInputs[address(delegationManager)] = true; - addressIsExcludedFromFuzzedInputs[address(slasherMock)] = true; - - // check setup (constructor + initializer) - require(delegationManager.strategyManager() == strategyManagerMock, - "constructor / initializer incorrect, strategyManager set wrong"); - require(delegationManager.slasher() == slasherMock, - "constructor / initializer incorrect, slasher set wrong"); - require(delegationManager.pauserRegistry() == eigenLayerPauserReg, - "constructor / initializer incorrect, pauserRegistry set wrong"); - require(delegationManager.owner() == initialOwner, - "constructor / initializer incorrect, owner set wrong"); - require(delegationManager.paused() == initialPausedStatus, - "constructor / initializer incorrect, paused status set wrong"); } - /// @notice Verifies that the DelegationManager cannot be iniitalized multiple times - function testCannotReinitializeDelegationManager() public { - cheats.expectRevert(bytes("Initializable: contract is already initialized")); - delegationManager.initialize(address(this), eigenLayerPauserReg, 0); - } + /** + * INTERNAL / HELPER FUNCTIONS + */ - /// @notice Verifies that the stakeRegistry cannot be set after it has already been set - function testCannotSetStakeRegistryTwice() public { - cheats.expectRevert(bytes("DelegationManager.setStakeRegistry: stakeRegistry already set")); - delegationManager.setStakeRegistry(stakeRegistryMock); + /** + * @notice internal function for calculating a signature from the delegationSigner corresponding to `_delegationSignerPrivateKey`, approving + * the `staker` to delegate to `operator`, with the specified `salt`, and expiring at `expiry`. + */ + function _getApproverSignature( + uint256 _delegationSignerPrivateKey, + address staker, + address operator, + bytes32 salt, + uint256 expiry + ) internal view returns (ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry) { + approverSignatureAndExpiry.expiry = expiry; + { + bytes32 digestHash = delegationManager.calculateDelegationApprovalDigestHash( + staker, + operator, + delegationManager.delegationApprover(operator), + salt, + expiry + ); + (uint8 v, bytes32 r, bytes32 s) = cheats.sign(_delegationSignerPrivateKey, digestHash); + approverSignatureAndExpiry.signature = abi.encodePacked(r, s, v); + } + return approverSignatureAndExpiry; } /** - * @notice `operator` registers via calling `DelegationManager.registerAsOperator(operatorDetails, metadataURI)` - * Should be able to set any parameters, other than setting their `earningsReceiver` to the zero address or too high value for `stakerOptOutWindowBlocks` - * The set parameters should match the desired parameters (correct storage update) - * Operator becomes delegated to themselves - * Properly emits events – especially the `OperatorRegistered` event, but also `StakerDelegated` & `OperatorDetailsModified` events - * Reverts appropriately if operator was already delegated to someone (including themselves, i.e. they were already an operator) - * @param operator and @param operatorDetails are fuzzed inputs + * @notice internal function for calculating a signature from the staker corresponding to `_stakerPrivateKey`, delegating them to + * the `operator`, and expiring at `expiry`. */ - function testRegisterAsOperator(address operator, IDelegationManager.OperatorDetails memory operatorDetails, string memory metadataURI) public - filterFuzzedAddressInputs(operator) - { + function _getStakerSignature( + uint256 _stakerPrivateKey, + address operator, + uint256 expiry + ) internal view returns (ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry) { + address staker = cheats.addr(stakerPrivateKey); + stakerSignatureAndExpiry.expiry = expiry; + { + bytes32 digestHash = delegationManager.calculateCurrentStakerDelegationDigestHash(staker, operator, expiry); + (uint8 v, bytes32 r, bytes32 s) = cheats.sign(_stakerPrivateKey, digestHash); + stakerSignatureAndExpiry.signature = abi.encodePacked(r, s, v); + } + return stakerSignatureAndExpiry; + } + + // @notice Assumes operator does not have a delegation approver & staker != approver + function _delegateToOperatorWhoAcceptsAllStakers(address staker, address operator) internal { + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + cheats.prank(staker); + delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt); + } + + function _delegateToOperatorWhoRequiresSig(address staker, address operator, bytes32 salt) internal { + uint256 expiry = type(uint256).max; + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + operator, + salt, + expiry + ); + cheats.prank(staker); + delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt); + } + + function _delegateToOperatorWhoRequiresSig(address staker, address operator) internal { + _delegateToOperatorWhoRequiresSig(staker, operator, emptySalt); + } + + function _delegateToBySignatureOperatorWhoAcceptsAllStakers( + address staker, + address caller, + address operator, + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry, + bytes32 salt + ) internal { + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + cheats.prank(caller); + delegationManager.delegateToBySignature( + staker, + operator, + stakerSignatureAndExpiry, + approverSignatureAndExpiry, + salt + ); + } + + function _delegateToBySignatureOperatorWhoRequiresSig( + address staker, + address caller, + address operator, + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry, + bytes32 salt + ) internal { + uint256 expiry = type(uint256).max; + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + operator, + salt, + expiry + ); + cheats.prank(caller); + delegationManager.delegateToBySignature( + staker, + operator, + stakerSignatureAndExpiry, + approverSignatureAndExpiry, + salt + ); + } + + function _registerOperatorWithBaseDetails(address operator) internal { + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + _registerOperator(operator, operatorDetails, emptyStringForMetadataURI); + } + + function _registerOperatorWithDelegationApprover(address operator) internal { + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: operator, + delegationApprover: cheats.addr(delegationSignerPrivateKey), + stakerOptOutWindowBlocks: 0 + }); + _registerOperator(operator, operatorDetails, emptyStringForMetadataURI); + } + + function _registerOperatorWith1271DelegationApprover(address operator) internal { + address delegationSigner = cheats.addr(delegationSignerPrivateKey); + /** + * deploy a ERC1271WalletMock contract with the `delegationSigner` address as the owner, + * so that we can create valid signatures from the `delegationSigner` for the contract to check when called + */ + ERC1271WalletMock wallet = new ERC1271WalletMock(delegationSigner); + + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: defaultOperator, + delegationApprover: address(wallet), + stakerOptOutWindowBlocks: 0 + }); + _registerOperator(defaultOperator, operatorDetails, emptyStringForMetadataURI); + } + + function _registerOperator( + address operator, + IDelegationManager.OperatorDetails memory operatorDetails, + string memory metadataURI + ) internal filterFuzzedAddressInputs(operator) { + _filterOperatorDetails(operator, operatorDetails); + cheats.prank(operator); + delegationManager.registerAsOperator(operatorDetails, metadataURI); + } + + function _filterOperatorDetails( + address operator, + IDelegationManager.OperatorDetails memory operatorDetails + ) internal view { // filter out zero address since people can't delegate to the zero address and operators are delegated to themselves cheats.assume(operator != address(0)); // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) cheats.assume(operatorDetails.earningsReceiver != address(0)); // filter out disallowed stakerOptOutWindowBlocks values cheats.assume(operatorDetails.stakerOptOutWindowBlocks <= delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()); + } +} - cheats.startPrank(operator); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit OperatorDetailsModified(operator, operatorDetails); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(operator, operator); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit OperatorRegistered(operator, operatorDetails); +contract DelegationManagerUnitTests_Initialization_Setters is DelegationManagerUnitTests { + function test_initialization() public { + assertEq( + address(delegationManager.strategyManager()), + address(strategyManagerMock), + "constructor / initializer incorrect, strategyManager set wrong" + ); + assertEq( + address(delegationManager.slasher()), + address(slasherMock), + "constructor / initializer incorrect, slasher set wrong" + ); + assertEq( + address(delegationManager.pauserRegistry()), + address(pauserRegistry), + "constructor / initializer incorrect, pauserRegistry set wrong" + ); + assertEq(delegationManager.owner(), address(this), "constructor / initializer incorrect, owner set wrong"); + assertEq(delegationManager.paused(), 0, "constructor / initializer incorrect, paused status set wrong"); + } + + /// @notice Verifies that the DelegationManager cannot be iniitalized multiple times + function test_initialize_revert_reinitialization() public { + cheats.expectRevert("Initializable: contract is already initialized"); + delegationManager.initialize(address(this), pauserRegistry, 0); + } + + /// @notice Verifies that the stakeRegistry cannot be set after it has already been set + function test_setStakeRegistry_revert_alreadySet() public { + cheats.expectRevert("DelegationManager.setStakeRegistry: stakeRegistry already set"); + delegationManager.setStakeRegistry(stakeRegistryMock); + } + + function testFuzz_setWithdrawalDelayBlocks_revert_notOwner( + address invalidCaller + ) public filterFuzzedAddressInputs(invalidCaller) { + cheats.prank(invalidCaller); + cheats.expectRevert("Ownable: caller is not the owner"); + delegationManager.setWithdrawalDelayBlocks(0); + } + + function testFuzz_setWithdrawalDelayBlocks_revert_tooLarge(uint256 newWithdrawalDelayBlocks) external { + // filter fuzzed inputs to disallowed amounts + cheats.assume(newWithdrawalDelayBlocks > delegationManager.MAX_WITHDRAWAL_DELAY_BLOCKS()); + + // attempt to set the `withdrawalDelayBlocks` variable + cheats.expectRevert("DelegationManager.setWithdrawalDelayBlocks: newWithdrawalDelayBlocks too high"); + delegationManager.setWithdrawalDelayBlocks(newWithdrawalDelayBlocks); + } + + function testFuzz_setWithdrawalDelayBlocks(uint256 newWithdrawalDelayBlocks) public { + cheats.assume(newWithdrawalDelayBlocks <= delegationManager.MAX_WITHDRAWAL_DELAY_BLOCKS()); + + // set the `withdrawalDelayBlocks` variable + uint256 previousDelayBlocks = delegationManager.withdrawalDelayBlocks(); cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit OperatorMetadataURIUpdated(operator, metadataURI); + emit WithdrawalDelayBlocksSet(previousDelayBlocks, newWithdrawalDelayBlocks); + delegationManager.setWithdrawalDelayBlocks(newWithdrawalDelayBlocks); + + // Check storage + assertEq( + delegationManager.withdrawalDelayBlocks(), + newWithdrawalDelayBlocks, + "withdrawalDelayBlocks not set correctly" + ); + } +} - delegationManager.registerAsOperator(operatorDetails, metadataURI); +contract DelegationManagerUnitTests_RegisterModifyOperator is DelegationManagerUnitTests { + function test_registerAsOperator_revert_paused() public { + // set the pausing flag + cheats.prank(pauser); + delegationManager.pause(2 ** PAUSED_NEW_DELEGATION); - require(operatorDetails.earningsReceiver == delegationManager.earningsReceiver(operator), "earningsReceiver not set correctly"); - require(operatorDetails.delegationApprover == delegationManager.delegationApprover(operator), "delegationApprover not set correctly"); - require(operatorDetails.stakerOptOutWindowBlocks == delegationManager.stakerOptOutWindowBlocks(operator), "stakerOptOutWindowBlocks not set correctly"); - require(delegationManager.delegatedTo(operator) == operator, "operator not delegated to self"); - cheats.stopPrank(); + cheats.expectRevert("Pausable: index is paused"); + delegationManager.registerAsOperator( + IDelegationManager.OperatorDetails({ + earningsReceiver: defaultOperator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }), + emptyStringForMetadataURI + ); } - /** - * @notice Verifies that an operator cannot register with `stakerOptOutWindowBlocks` set larger than `MAX_STAKER_OPT_OUT_WINDOW_BLOCKS` - * @param operatorDetails is a fuzzed input - */ - function testCannotRegisterAsOperatorWithDisallowedStakerOptOutWindowBlocks(IDelegationManager.OperatorDetails memory operatorDetails) public { - // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) - cheats.assume(operatorDetails.earningsReceiver != address(0)); - // filter out *allowed* stakerOptOutWindowBlocks values - cheats.assume(operatorDetails.stakerOptOutWindowBlocks > delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()); + // @notice Verifies that someone cannot successfully call `DelegationManager.registerAsOperator(operatorDetails)` again after registering for the first time + function testFuzz_registerAsOperator_revert_cannotRegisterMultipleTimes( + address operator, + IDelegationManager.OperatorDetails memory operatorDetails + ) public filterFuzzedAddressInputs(operator) { + _filterOperatorDetails(operator, operatorDetails); + // Register once cheats.startPrank(operator); - cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS")); + delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); + + // Expect revert when register again + cheats.expectRevert("DelegationManager.registerAsOperator: operator has already registered"); delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); cheats.stopPrank(); } - + /** * @notice Verifies that an operator cannot register with `earningsReceiver` set to the zero address * @dev This is an important check since we check `earningsReceiver != address(0)` to check if an address is an operator! */ - function testCannotRegisterAsOperatorWithZeroAddressAsEarningsReceiver() public { - cheats.startPrank(operator); + function testFuzz_registerAsOperator_revert_earningsReceiverZeroAddress() public { IDelegationManager.OperatorDetails memory operatorDetails; - cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: cannot set `earningsReceiver` to zero address")); + + cheats.prank(defaultOperator); + cheats.expectRevert("DelegationManager._setOperatorDetails: cannot set `earningsReceiver` to zero address"); delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); - cheats.stopPrank(); } - // @notice Verifies that someone cannot successfully call `DelegationManager.registerAsOperator(operatorDetails)` again after registering for the first time - function testCannotRegisterAsOperatorMultipleTimes(address operator, IDelegationManager.OperatorDetails memory operatorDetails) public - filterFuzzedAddressInputs(operator) - { - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); - cheats.startPrank(operator); - cheats.expectRevert(bytes("DelegationManager.registerAsOperator: operator has already registered")); + /** + * @notice Verifies that an operator cannot register with `stakerOptOutWindowBlocks` set larger than `MAX_STAKER_OPT_OUT_WINDOW_BLOCKS` + */ + function testFuzz_registerAsOperator_revert_optOutBlocksTooLarge( + IDelegationManager.OperatorDetails memory operatorDetails + ) public { + // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) + cheats.assume(operatorDetails.earningsReceiver != address(0)); + // filter out *allowed* stakerOptOutWindowBlocks values + cheats.assume(operatorDetails.stakerOptOutWindowBlocks > delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()); + + cheats.prank(defaultOperator); + cheats.expectRevert("DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS"); delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); - cheats.stopPrank(); + } + + /** + * @notice `operator` registers via calling `DelegationManager.registerAsOperator(operatorDetails, metadataURI)` + * Should be able to set any parameters, other than setting their `earningsReceiver` to the zero address or too high value for `stakerOptOutWindowBlocks` + * The set parameters should match the desired parameters (correct storage update) + * Operator becomes delegated to themselves + * Properly emits events – especially the `OperatorRegistered` event, but also `StakerDelegated` & `OperatorDetailsModified` events + * Reverts appropriately if operator was already delegated to someone (including themselves, i.e. they were already an operator) + * @param operator and @param operatorDetails are fuzzed inputs + */ + function testFuzz_registerAsOperator( + address operator, + IDelegationManager.OperatorDetails memory operatorDetails, + string memory metadataURI + ) public filterFuzzedAddressInputs(operator) { + _filterOperatorDetails(operator, operatorDetails); + + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorDetailsModified(operator, operatorDetails); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(operator, operator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorRegistered(operator, operatorDetails); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorMetadataURIUpdated(operator, metadataURI); + + cheats.prank(operator); + delegationManager.registerAsOperator(operatorDetails, metadataURI); + + // Storage checks + assertEq( + operatorDetails.earningsReceiver, + delegationManager.earningsReceiver(operator), + "earningsReceiver not set correctly" + ); + assertEq( + operatorDetails.delegationApprover, + delegationManager.delegationApprover(operator), + "delegationApprover not set correctly" + ); + assertEq( + operatorDetails.stakerOptOutWindowBlocks, + delegationManager.stakerOptOutWindowBlocks(operator), + "stakerOptOutWindowBlocks not set correctly" + ); + assertEq(delegationManager.delegatedTo(operator), operator, "operator not delegated to self"); } // @notice Verifies that a staker who is actively delegated to an operator cannot register as an operator (without first undelegating, at least) - function testCannotRegisterAsOperatorWhileDelegated(address staker, IDelegationManager.OperatorDetails memory operatorDetails) public - filterFuzzedAddressInputs(staker) - { - // filter out disallowed stakerOptOutWindowBlocks values - cheats.assume(operatorDetails.stakerOptOutWindowBlocks <= delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()); - // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) - cheats.assume(operatorDetails.earningsReceiver != address(0)); - // filter out case where staker *is* the operator - cheats.assume(staker != _operator); + function testFuzz_registerAsOperator_cannotRegisterWhileDelegated( + address staker, + IDelegationManager.OperatorDetails memory operatorDetails + ) public filterFuzzedAddressInputs(staker) { + cheats.assume(staker != defaultOperator); + // Staker becomes an operator, so filter against staker's address + _filterOperatorDetails(staker, operatorDetails); // register *this contract* as an operator - IDelegationManager.OperatorDetails memory _operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: _operator, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(_operator, _operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithBaseDetails(defaultOperator); // delegate from the `staker` to the operator cheats.startPrank(staker); ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; - delegationManager.delegateTo(_operator, approverSignatureAndExpiry, emptySalt); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt); - cheats.expectRevert(bytes("DelegationManager._delegate: staker is already actively delegated")); + // expect revert if attempt to register as operator + cheats.expectRevert("DelegationManager._delegate: staker is already actively delegated"); delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); cheats.stopPrank(); } + /** + * @notice Verifies that an operator cannot modify their `earningsReceiver` address to set it to the zero address + * @dev This is an important check since we check `earningsReceiver != address(0)` to check if an address is an operator! + */ + function test_modifyOperatorParameters_revert_earningsReceiverZeroAddress() public { + // register *this contract* as an operator + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ + earningsReceiver: defaultOperator, + delegationApprover: address(0), + stakerOptOutWindowBlocks: 0 + }); + _registerOperator(defaultOperator, operatorDetails, emptyStringForMetadataURI); + + operatorDetails.earningsReceiver = address(0); + cheats.expectRevert("DelegationManager._setOperatorDetails: cannot set `earningsReceiver` to zero address"); + delegationManager.modifyOperatorDetails(operatorDetails); + } + /** * @notice Tests that an operator can modify their OperatorDetails by calling `DelegationManager.modifyOperatorDetails` * Should be able to set any parameters, other than setting their `earningsReceiver` to the zero address or too high value for `stakerOptOutWindowBlocks` @@ -325,83 +498,63 @@ contract DelegationUnitTests is EigenLayerTestHelper { * Reverts if operator tries to decrease their `stakerOptOutWindowBlocks` parameter * @param initialOperatorDetails and @param modifiedOperatorDetails are fuzzed inputs */ - function testModifyOperatorParameters( + function testFuzz_modifyOperatorParameters( IDelegationManager.OperatorDetails memory initialOperatorDetails, IDelegationManager.OperatorDetails memory modifiedOperatorDetails ) public { - testRegisterAsOperator(_operator, initialOperatorDetails, emptyStringForMetadataURI); - // filter out zero address since people can't set their earningsReceiver address to the zero address (special test case to verify) + // filter out zero address since people can't set their earningsReceiver address to the zero address (test case verified above) cheats.assume(modifiedOperatorDetails.earningsReceiver != address(0)); - cheats.startPrank(_operator); + _registerOperator(defaultOperator, initialOperatorDetails, emptyStringForMetadataURI); + + cheats.startPrank(defaultOperator); // either it fails for trying to set the stakerOptOutWindowBlocks if (modifiedOperatorDetails.stakerOptOutWindowBlocks > delegationManager.MAX_STAKER_OPT_OUT_WINDOW_BLOCKS()) { - cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS")); + cheats.expectRevert( + "DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be > MAX_STAKER_OPT_OUT_WINDOW_BLOCKS" + ); delegationManager.modifyOperatorDetails(modifiedOperatorDetails); - // or the transition is allowed, - } else if (modifiedOperatorDetails.stakerOptOutWindowBlocks >= initialOperatorDetails.stakerOptOutWindowBlocks) { + // or the transition is allowed, + } else if ( + modifiedOperatorDetails.stakerOptOutWindowBlocks >= initialOperatorDetails.stakerOptOutWindowBlocks + ) { cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit OperatorDetailsModified(_operator, modifiedOperatorDetails); + emit OperatorDetailsModified(defaultOperator, modifiedOperatorDetails); delegationManager.modifyOperatorDetails(modifiedOperatorDetails); - require(modifiedOperatorDetails.earningsReceiver == delegationManager.earningsReceiver(_operator), "earningsReceiver not set correctly"); - require(modifiedOperatorDetails.delegationApprover == delegationManager.delegationApprover(_operator), "delegationApprover not set correctly"); - require(modifiedOperatorDetails.stakerOptOutWindowBlocks == delegationManager.stakerOptOutWindowBlocks(_operator), "stakerOptOutWindowBlocks not set correctly"); - require(delegationManager.delegatedTo(_operator) == _operator, "operator not delegated to self"); - // or else the transition is disallowed + assertEq( + modifiedOperatorDetails.earningsReceiver, + delegationManager.earningsReceiver(defaultOperator), + "earningsReceiver not set correctly" + ); + assertEq( + modifiedOperatorDetails.delegationApprover, + delegationManager.delegationApprover(defaultOperator), + "delegationApprover not set correctly" + ); + assertEq( + modifiedOperatorDetails.stakerOptOutWindowBlocks, + delegationManager.stakerOptOutWindowBlocks(defaultOperator), + "stakerOptOutWindowBlocks not set correctly" + ); + assertEq(delegationManager.delegatedTo(defaultOperator), defaultOperator, "operator not delegated to self"); + // or else the transition is disallowed } else { - cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be decreased")); + cheats.expectRevert("DelegationManager._setOperatorDetails: stakerOptOutWindowBlocks cannot be decreased"); delegationManager.modifyOperatorDetails(modifiedOperatorDetails); } cheats.stopPrank(); } - // @notice Tests that an operator who calls `updateOperatorMetadataURI` will correctly see an `OperatorMetadataURIUpdated` event emitted with their input - function testUpdateOperatorMetadataURI(string memory metadataURI) public { - // register *this contract* as an operator - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: _operator, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(_operator, operatorDetails, emptyStringForMetadataURI); - - // call `updateOperatorMetadataURI` and check for event - cheats.startPrank(_operator); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit OperatorMetadataURIUpdated(_operator, metadataURI); - delegationManager.updateOperatorMetadataURI(metadataURI); - cheats.stopPrank(); - } - // @notice Tests that an address which is not an operator cannot successfully call `updateOperatorMetadataURI`. - function testCannotUpdateOperatorMetadataURIWithoutRegisteringFirst() public { - require(!delegationManager.isOperator(_operator), "bad test setup"); + function test_updateOperatorMetadataUri_notRegistered() public { + assertFalse(delegationManager.isOperator(defaultOperator), "bad test setup"); - cheats.startPrank(_operator); - cheats.expectRevert(bytes("DelegationManager.updateOperatorMetadataURI: caller must be an operator")); + cheats.prank(defaultOperator); + cheats.expectRevert("DelegationManager.updateOperatorMetadataURI: caller must be an operator"); delegationManager.updateOperatorMetadataURI(emptyStringForMetadataURI); - cheats.stopPrank(); - } - - /** - * @notice Verifies that an operator cannot modify their `earningsReceiver` address to set it to the zero address - * @dev This is an important check since we check `earningsReceiver != address(0)` to check if an address is an operator! - */ - function testCannotModifyEarningsReceiverAddressToZeroAddress() public { - // register *this contract* as an operator - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: _operator, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(_operator, operatorDetails, emptyStringForMetadataURI); - - operatorDetails.earningsReceiver = address(0); - cheats.expectRevert(bytes("DelegationManager._setOperatorDetails: cannot set `earningsReceiver` to zero address")); - delegationManager.modifyOperatorDetails(operatorDetails); } /** @@ -409,206 +562,334 @@ contract DelegationUnitTests is EigenLayerTestHelper { * @dev This is an important check to ensure that our definition of 'operator' remains consistent, in particular for preserving the * invariant that 'operators' are always delegated to themselves */ - function testCannotModifyOperatorDetailsWithoutRegistering(IDelegationManager.OperatorDetails memory operatorDetails) public { - cheats.expectRevert(bytes("DelegationManager.modifyOperatorDetails: caller must be an operator")); + function testFuzz_updateOperatorMetadataUri_revert_notOperator( + IDelegationManager.OperatorDetails memory operatorDetails + ) public { + cheats.expectRevert("DelegationManager.modifyOperatorDetails: caller must be an operator"); delegationManager.modifyOperatorDetails(operatorDetails); } - /** - * @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address) - * via the `staker` calling `DelegationManager.delegateTo` - * The function should pass with any `operatorSignature` input (since it should be unused) - * Properly emits a `StakerDelegated` event - * Staker is correctly delegated after the call (i.e. correct storage update) - * Reverts if the staker is already delegated (to the operator or to anyone else) - * Reverts if the ‘operator’ is not actually registered as an operator - */ - function testDelegateToOperatorWhoAcceptsAllStakers(address staker, ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 salt) public - filterFuzzedAddressInputs(staker) - { + // @notice Tests that an operator who calls `updateOperatorMetadataURI` will correctly see an `OperatorMetadataURIUpdated` event emitted with their input + function testFuzz_UpdateOperatorMetadataURI(string memory metadataURI) public { // register *this contract* as an operator - // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != _operator); - - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: _operator, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(_operator, operatorDetails, emptyStringForMetadataURI); - - // verify that the salt hasn't been used before - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(_operator), salt), "salt somehow spent too early?"); + _registerOperatorWithBaseDetails(defaultOperator); - IStrategy[] memory strategiesToReturn = new IStrategy[](1); - strategiesToReturn[0] = strategyMock; - uint256[] memory sharesToReturn = new uint256[](1); - sharesToReturn[0] = 1; - strategyManagerMock.setDeposits(strategiesToReturn, sharesToReturn); - // delegate from the `staker` to the operator - cheats.startPrank(staker); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(staker, _operator); + // call `updateOperatorMetadataURI` and check for event + cheats.prank(defaultOperator); cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit OperatorSharesIncreased(_operator, staker, strategyMock, 1); - delegationManager.delegateTo(_operator, approverSignatureAndExpiry, salt); - cheats.stopPrank(); + emit OperatorMetadataURIUpdated(defaultOperator, metadataURI); + delegationManager.updateOperatorMetadataURI(metadataURI); + } +} - require(delegationManager.isDelegated(staker), "staker not delegated correctly"); - require(delegationManager.delegatedTo(staker) == _operator, "staker delegated to the wrong address"); - require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); +contract DelegationManagerUnitTests_delegateTo is DelegationManagerUnitTests { + function test_revert_paused() public { + // set the pausing flag + cheats.prank(pauser); + delegationManager.pause(2 ** PAUSED_NEW_DELEGATION); - // verify that the salt is still marked as unused (since it wasn't checked or used) - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(_operator), salt), "salt somehow spent too early?"); + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + cheats.prank(defaultStaker); + cheats.expectRevert("Pausable: index is paused"); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt); } /** * @notice Delegates from `staker` to an operator, then verifies that the `staker` cannot delegate to another `operator` (at least without first undelegating) */ - function testCannotDelegateWhileDelegated(address staker, address operator, ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 salt) public - filterFuzzedAddressInputs(staker) - filterFuzzedAddressInputs(operator) - { + function testFuzz_Revert_WhenDelegateWhileDelegated( + address staker, + address operator, + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, + bytes32 salt + ) public filterFuzzedAddressInputs(staker) filterFuzzedAddressInputs(operator) { // filter out input since if the staker tries to delegate again after registering as an operator, we will revert earlier than this test is designed to check cheats.assume(staker != operator); // delegate from the staker to an operator - testDelegateToOperatorWhoAcceptsAllStakers(staker, approverSignatureAndExpiry, salt); - - // register another operator - // filter out this contract, since we already register it as an operator in the above step cheats.assume(operator != address(this)); - IDelegationManager.OperatorDetails memory _operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, _operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithBaseDetails(operator); + _delegateToOperatorWhoAcceptsAllStakers(staker, operator); // try to delegate again and check that the call reverts cheats.startPrank(staker); - cheats.expectRevert(bytes("DelegationManager._delegate: staker is already actively delegated")); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt); + cheats.expectRevert("DelegationManager._delegate: staker is already actively delegated"); + delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt); cheats.stopPrank(); } // @notice Verifies that `staker` cannot delegate to an unregistered `operator` - function testCannotDelegateToUnregisteredOperator(address staker, address operator) public - filterFuzzedAddressInputs(staker) - filterFuzzedAddressInputs(operator) - { - require(!delegationManager.isOperator(operator), "incorrect test input?"); + function testFuzz_Revert_WhenDelegateToUnregisteredOperator( + address staker, + address operator + ) public filterFuzzedAddressInputs(staker) filterFuzzedAddressInputs(operator) { + assertFalse(delegationManager.isOperator(operator), "incorrect test input?"); // try to delegate and check that the call reverts cheats.startPrank(staker); - cheats.expectRevert(bytes("DelegationManager._delegate: operator is not registered in EigenLayer")); + cheats.expectRevert("DelegationManager._delegate: operator is not registered in EigenLayer"); ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; - delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt); + delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt); cheats.stopPrank(); } /** - * @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) + * @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address) * via the `staker` calling `DelegationManager.delegateTo` - * The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves + * The function should pass with any `operatorSignature` input (since it should be unused) * Properly emits a `StakerDelegated` event * Staker is correctly delegated after the call (i.e. correct storage update) * Reverts if the staker is already delegated (to the operator or to anyone else) * Reverts if the ‘operator’ is not actually registered as an operator */ - function testDelegateToOperatorWhoRequiresECDSASignature(address staker, bytes32 salt, uint256 expiry) public - filterFuzzedAddressInputs(staker) - { - // filter to only valid `expiry` values - cheats.assume(expiry >= block.timestamp); - - address delegationApprover = cheats.addr(delegationSignerPrivateKey); - + function testFuzz_OperatorWhoAcceptsAllStakers_StrategyManagerShares( + address staker, + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, + bytes32 salt, + uint256 shares + ) public filterFuzzedAddressInputs(staker) { // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); + cheats.assume(staker != defaultOperator); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: delegationApprover, - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithBaseDetails(defaultOperator); // verify that the salt hasn't been used before - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent too early?"); - // calculate the delegationSigner's signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, salt, expiry); - + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); + // Set staker shares in StrategyManager + IStrategy[] memory strategiesToReturn = new IStrategy[](1); + strategiesToReturn[0] = strategyMock; + uint256[] memory sharesToReturn = new uint256[](1); + sharesToReturn[0] = shares; + strategyManagerMock.setDeposits(strategiesToReturn, sharesToReturn); + uint256 operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock); // delegate from the `staker` to the operator cheats.startPrank(staker); cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(staker, operator); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt); + emit StakerDelegated(staker, defaultOperator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, staker, strategyMock, shares); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); + uint256 operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock); - require(delegationManager.isDelegated(staker), "staker not delegated correctly"); - require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); - require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + assertEq(operatorSharesBefore + shares, operatorSharesAfter, "operator shares not increased correctly"); + assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); + } - if (staker == operator || staker == delegationManager.delegationApprover(operator)) { - // verify that the salt is still marked as unused (since it wasn't checked or used) - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent too incorrectly?"); + /** + * @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address) + * via the `staker` calling `DelegationManager.delegateTo` + * The function should pass with any `operatorSignature` input (since it should be unused) + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * OperatorSharesIncreased event should only be emitted if beaconShares is > 0. Since a staker can have negative shares nothing should happen in that case + */ + function testFuzz_OperatorWhoAcceptsAllStakers_BeaconChainStrategyShares( + address staker, + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, + bytes32 salt, + int256 beaconShares + ) public filterFuzzedAddressInputs(staker) { + // register *this contract* as an operator + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != defaultOperator); + + _registerOperatorWithBaseDetails(defaultOperator); + + // verify that the salt hasn't been used before + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); + // Set staker shares in BeaconChainStrategy + eigenPodManagerMock.setPodOwnerShares(staker, beaconShares); + uint256 beaconSharesBefore = delegationManager.operatorShares(staker, beaconChainETHStrategy); + // delegate from the `staker` to the operator + cheats.startPrank(staker); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, defaultOperator); + if (beaconShares > 0) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, staker, beaconChainETHStrategy, uint256(beaconShares)); + } + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); + cheats.stopPrank(); + uint256 beaconSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy); + if (beaconShares <= 0) { + assertEq( + beaconSharesBefore, + beaconSharesAfter, + "operator beaconchain shares should not have increased with negative shares" + ); } else { - // verify that the salt is marked as used - require(delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent not spent?"); + assertEq( + beaconSharesBefore + uint256(beaconShares), + beaconSharesAfter, + "operator beaconchain shares not increased correctly" + ); } + assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); } /** - * @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an incorrect signature on purpose and checking that reversion occurs + * @notice `staker` delegates to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address) + * via the `staker` calling `DelegationManager.delegateTo` + * Similar to tests above but now with staker who has both EigenPod and StrategyManager shares. */ - function testDelegateToOperatorWhoRequiresECDSASignature_RevertsWithBadSignature(address staker, uint256 expiry) public - filterFuzzedAddressInputs(staker) - { - // filter to only valid `expiry` values - cheats.assume(expiry >= block.timestamp); + function testFuzz_OperatorWhoAcceptsAllStakers_BeaconChainAndStrategyManagerShares( + address staker, + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, + bytes32 salt, + int256 beaconShares, + uint256 shares + ) public filterFuzzedAddressInputs(staker) { + // register *this contract* as an operator + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != defaultOperator); - address delegationApprover = cheats.addr(delegationSignerPrivateKey); + _registerOperatorWithBaseDetails(defaultOperator); + + // verify that the salt hasn't been used before + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); + // Set staker shares in BeaconChainStrategy and StrategyMananger + IStrategy[] memory strategiesToReturn = new IStrategy[](1); + strategiesToReturn[0] = strategyMock; + uint256[] memory sharesToReturn = new uint256[](1); + sharesToReturn[0] = shares; + strategyManagerMock.setDeposits(strategiesToReturn, sharesToReturn); + eigenPodManagerMock.setPodOwnerShares(staker, beaconShares); + uint256 operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesBefore = delegationManager.operatorShares(staker, beaconChainETHStrategy); + // delegate from the `staker` to the operator + cheats.startPrank(staker); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, defaultOperator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, staker, strategyMock, shares); + if (beaconShares > 0) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, staker, beaconChainETHStrategy, uint256(beaconShares)); + } + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); + cheats.stopPrank(); + uint256 operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy); + if (beaconShares <= 0) { + assertEq( + beaconSharesBefore, + beaconSharesAfter, + "operator beaconchain shares should not have increased with negative shares" + ); + } else { + assertEq( + beaconSharesBefore + uint256(beaconShares), + beaconSharesAfter, + "operator beaconchain shares not increased correctly" + ); + } + assertEq(operatorSharesBefore + shares, operatorSharesAfter, "operator shares not increased correctly"); + assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); + } + /** + * @notice `staker` delegates to a operator who does not require any signature verification similar to test above. + * In this scenario, staker doesn't have any delegatable shares and operator shares should not increase. Staker + * should still be correctly delegated to the operator after the call. + */ + function testFuzz_OperatorWhoAcceptsAllStakers_ZeroDelegatableShares( + address staker, + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, + bytes32 salt + ) public filterFuzzedAddressInputs(staker) { // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); + cheats.assume(staker != defaultOperator); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: delegationApprover, - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithBaseDetails(defaultOperator); - // calculate the signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; - approverSignatureAndExpiry.expiry = expiry; - { - bytes32 digestHash = - delegationManager.calculateDelegationApprovalDigestHash(staker, operator, delegationManager.delegationApprover(operator), emptySalt, expiry); - (uint8 v, bytes32 r, bytes32 s) = cheats.sign(delegationSignerPrivateKey, digestHash); - // mess up the signature by flipping v's parity - v = (v == 27 ? 28 : 27); - approverSignatureAndExpiry.signature = abi.encodePacked(r, s, v); - } + // verify that the salt hasn't been used before + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); - // try to delegate from the `staker` to the operator, and check reversion + // delegate from the `staker` to the operator cheats.startPrank(staker); - cheats.expectRevert(bytes("EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer")); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, defaultOperator); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); + + assertTrue(delegationManager.isOperator(defaultOperator), "staker not registered as operator"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); } /** * @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an invalid expiry on purpose and checking that reversion occurs */ - function testDelegateToOperatorWhoRequiresECDSASignature_RevertsWithExpiredDelegationApproverSignature(address staker, bytes32 salt, uint256 expiry) public - filterFuzzedAddressInputs(staker) - { + function testFuzz_Revert_WhenOperatorWhoRequiresECDSASignature_ExpiredDelegationApproverSignature( + address staker, + bytes32 salt, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { // roll to a very late timestamp cheats.roll(type(uint256).max / 2); // filter to only *invalid* `expiry` values @@ -616,2003 +897,1647 @@ contract DelegationUnitTests is EigenLayerTestHelper { address delegationApprover = cheats.addr(delegationSignerPrivateKey); - // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); + cheats.assume(staker != defaultOperator); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: delegationApprover, - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithDelegationApprover(defaultOperator); // calculate the delegationSigner's signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, salt, expiry); + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); // delegate from the `staker` to the operator cheats.startPrank(staker); - cheats.expectRevert(bytes("DelegationManager._delegate: approver signature expired")); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt); + cheats.expectRevert("DelegationManager._delegate: approver signature expired"); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); } /** - * @notice `staker` delegates to an operator who requires signature verification through an EIP1271-compliant contract (i.e. the operator’s `delegationApprover` address is - * set to a nonzero and code-containing address) via the `staker` calling `DelegationManager.delegateTo` - * The function uses OZ's ERC1271WalletMock contract, and thus should pass *only when a valid ECDSA signature from the `owner` of the ERC1271WalletMock contract, - * OR if called by the operator or their delegationApprover themselves - * Properly emits a `StakerDelegated` event - * Staker is correctly delegated after the call (i.e. correct storage update) - * Reverts if the staker is already delegated (to the operator or to anyone else) - * Reverts if the ‘operator’ is not actually registered as an operator + * @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but undelegating after delegating and trying the same approveSignature + * and checking that reversion occurs with the same salt */ - function testDelegateToOperatorWhoRequiresEIP1271Signature(address staker, bytes32 salt, uint256 expiry) public - filterFuzzedAddressInputs(staker) - { + function testFuzz_Revert_WhenOperatorWhoRequiresECDSASignature_PreviouslyUsedSalt( + address staker, + bytes32 salt, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { // filter to only valid `expiry` values cheats.assume(expiry >= block.timestamp); + address delegationApprover = cheats.addr(delegationSignerPrivateKey); - address delegationSigner = cheats.addr(delegationSignerPrivateKey); - - // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); - - /** - * deploy a ERC1271WalletMock contract with the `delegationSigner` address as the owner, - * so that we can create valid signatures from the `delegationSigner` for the contract to check when called - */ - ERC1271WalletMock wallet = new ERC1271WalletMock(delegationSigner); + // staker also must not be the delegationApprover so that signature verification process takes place + cheats.assume(staker != defaultOperator); + cheats.assume(staker != delegationApprover); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: address(wallet), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithDelegationApprover(defaultOperator); // verify that the salt hasn't been used before - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent too early?"); + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); // calculate the delegationSigner's signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, salt, expiry); + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); - // delegate from the `staker` to the operator + // delegate from the `staker` to the operator, undelegate, and then try to delegate again with same approversalt + // to check that call reverts cheats.startPrank(staker); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(staker, operator); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); + assertTrue( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent not spent?" + ); + delegationManager.undelegate(staker); + cheats.expectRevert("DelegationManager._delegate: approverSalt already spent"); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); - - require(delegationManager.isDelegated(staker), "staker not delegated correctly"); - require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); - require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); - - // check that the nonce incremented appropriately - if (staker == operator || staker == delegationManager.delegationApprover(operator)) { - // verify that the salt is still marked as unused (since it wasn't checked or used) - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent too incorrectly?"); - } else { - // verify that the salt is marked as used - require(delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent not spent?"); - } } /** - * @notice Like `testDelegateToOperatorWhoRequiresEIP1271Signature` but using a contract that - * returns a value other than the EIP1271 "magic bytes" and checking that reversion occurs appropriately + * @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an incorrect signature on purpose and checking that reversion occurs */ - function testDelegateToOperatorWhoRequiresEIP1271Signature_RevertsOnBadReturnValue(address staker, uint256 expiry) public - filterFuzzedAddressInputs(staker) - { + function testFuzz_Revert_WhenOperatorWhoRequiresECDSASignature_WithBadSignature( + address staker, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { // filter to only valid `expiry` values cheats.assume(expiry >= block.timestamp); - - // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); - - // deploy a ERC1271MaliciousMock contract that will return an incorrect value when called - ERC1271MaliciousMock wallet = new ERC1271MaliciousMock(); - - // filter fuzzed input, since otherwise we can get a flaky failure here. if the caller itself is the 'delegationApprover' - // then we don't even trigger the signature verification call, so we won't get a revert as expected - cheats.assume(staker != address(wallet)); + cheats.assume(staker != defaultOperator); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: address(wallet), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithDelegationApprover(defaultOperator); - // create the signature struct + // calculate the signature ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; approverSignatureAndExpiry.expiry = expiry; + { + bytes32 digestHash = delegationManager.calculateDelegationApprovalDigestHash( + staker, + defaultOperator, + delegationManager.delegationApprover(defaultOperator), + emptySalt, + expiry + ); + (uint8 v, bytes32 r, bytes32 s) = cheats.sign(delegationSignerPrivateKey, digestHash); + // mess up the signature by flipping v's parity + v = (v == 27 ? 28 : 27); + approverSignatureAndExpiry.signature = abi.encodePacked(r, s, v); + } // try to delegate from the `staker` to the operator, and check reversion cheats.startPrank(staker); - // because the ERC1271MaliciousMock contract returns the wrong amount of data, we get a low-level "EvmError: Revert" message here rather than the error message bubbling up - // cheats.expectRevert(bytes("EIP1271SignatureUtils.checkSignature_EIP1271: ERC1271 signature verification failed")); - cheats.expectRevert(); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt); + cheats.expectRevert("EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer"); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt); cheats.stopPrank(); } /** - * @notice `staker` becomes delegated to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address) - * via the `caller` calling `DelegationManager.delegateToBySignature` - * The function should pass with any `operatorSignature` input (since it should be unused) - * The function should pass only with a valid `stakerSignatureAndExpiry` input + * @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) + * via the `staker` calling `DelegationManager.delegateTo` + * The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves * Properly emits a `StakerDelegated` event * Staker is correctly delegated after the call (i.e. correct storage update) * Reverts if the staker is already delegated (to the operator or to anyone else) * Reverts if the ‘operator’ is not actually registered as an operator */ - function testDelegateBySignatureToOperatorWhoAcceptsAllStakers(address caller, bytes32 salt, uint256 expiry) public - filterFuzzedAddressInputs(caller) - { + function testFuzz_OperatorWhoRequiresECDSASignature( + address staker, + bytes32 salt, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { // filter to only valid `expiry` values cheats.assume(expiry >= block.timestamp); - - address staker = cheats.addr(stakerPrivateKey); - - // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); + cheats.assume(staker != defaultOperator); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithDelegationApprover(defaultOperator); // verify that the salt hasn't been used before - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent too early?"); - // fetch the staker's current nonce - uint256 currentStakerNonce = delegationManager.stakerNonce(staker); - // calculate the staker signature - ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature(stakerPrivateKey, operator, expiry); + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); + // calculate the delegationSigner's signature + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); - // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` - cheats.startPrank(caller); + // delegate from the `staker` to the operator + cheats.startPrank(staker); cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(staker, operator); - // use an empty approver signature input since none is needed / the input is unchecked - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; - delegationManager.delegateToBySignature(staker, operator, stakerSignatureAndExpiry, approverSignatureAndExpiry, emptySalt); + emit StakerDelegated(staker, defaultOperator); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); - // check all the delegation status changes - require(delegationManager.isDelegated(staker), "staker not delegated correctly"); - require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); - require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); - // check that the staker nonce incremented appropriately - require(delegationManager.stakerNonce(staker) == currentStakerNonce + 1, - "staker nonce did not increment"); - // verify that the salt is still marked as unused (since it wasn't checked or used) - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent too incorrectly?"); + if (staker == delegationManager.delegationApprover(defaultOperator)) { + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too incorrectly?" + ); + } else { + // verify that the salt is marked as used + assertTrue( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent not spent?" + ); + } } /** - * @notice `staker` becomes delegated to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) - * via the `caller` calling `DelegationManager.delegateToBySignature` + * @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) + * via the `staker` calling `DelegationManager.delegateTo` * The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves - * AND with a valid `stakerSignatureAndExpiry` input * Properly emits a `StakerDelegated` event * Staker is correctly delegated after the call (i.e. correct storage update) + * Operator shares should increase by the amount of shares delegated * Reverts if the staker is already delegated (to the operator or to anyone else) * Reverts if the ‘operator’ is not actually registered as an operator */ - function testDelegateBySignatureToOperatorWhoRequiresECDSASignature(address caller, bytes32 salt, uint256 expiry) public - filterFuzzedAddressInputs(caller) - { + function testFuzz_OperatorWhoRequiresECDSASignature_StrategyManagerShares( + address staker, + bytes32 salt, + uint256 expiry, + uint256 shares + ) public filterFuzzedAddressInputs(staker) { // filter to only valid `expiry` values cheats.assume(expiry >= block.timestamp); - address staker = cheats.addr(stakerPrivateKey); address delegationApprover = cheats.addr(delegationSignerPrivateKey); - - // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); + cheats.assume(staker != defaultOperator); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: delegationApprover, - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithDelegationApprover(defaultOperator); // verify that the salt hasn't been used before - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent too early?"); + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); // calculate the delegationSigner's signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, operator, salt, expiry); - - // fetch the staker's current nonce - uint256 currentStakerNonce = delegationManager.stakerNonce(staker); - // calculate the staker signature - ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature(stakerPrivateKey, operator, expiry); + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); - // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` - cheats.startPrank(caller); + // Set staker shares in StrategyManager + IStrategy[] memory strategiesToReturn = new IStrategy[](1); + strategiesToReturn[0] = strategyMock; + uint256[] memory sharesToReturn = new uint256[](1); + sharesToReturn[0] = shares; + strategyManagerMock.setDeposits(strategiesToReturn, sharesToReturn); + uint256 operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock); + // delegate from the `staker` to the operator + cheats.startPrank(staker); cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(staker, operator); - delegationManager.delegateToBySignature(staker, operator, stakerSignatureAndExpiry, approverSignatureAndExpiry, salt); + emit StakerDelegated(staker, defaultOperator); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); + uint256 operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock); + assertEq(operatorSharesBefore + shares, operatorSharesAfter, "operator shares not increased correctly"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); - require(delegationManager.isDelegated(staker), "staker not delegated correctly"); - require(delegationManager.delegatedTo(staker) == operator, "staker delegated to the wrong address"); - require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); - - // check that the delegationApprover nonce incremented appropriately - if (caller == operator || caller == delegationManager.delegationApprover(operator)) { + if (staker == delegationManager.delegationApprover(defaultOperator)) { // verify that the salt is still marked as unused (since it wasn't checked or used) - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent too incorrectly?"); + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too incorrectly?" + ); } else { // verify that the salt is marked as used - require(delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(operator), salt), "salt somehow spent not spent?"); + assertTrue( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent not spent?" + ); } - - // check that the staker nonce incremented appropriately - require(delegationManager.stakerNonce(staker) == currentStakerNonce + 1, - "staker nonce did not increment"); } /** - * @notice `staker` becomes delegated to an operatorwho requires signature verification through an EIP1271-compliant contract (i.e. the operator’s `delegationApprover` address is - * set to a nonzero and code-containing address) via the `caller` calling `DelegationManager.delegateToBySignature` - * The function uses OZ's ERC1271WalletMock contract, and thus should pass *only when a valid ECDSA signature from the `owner` of the ERC1271WalletMock contract, - * OR if called by the operator or their delegationApprover themselves - * AND with a valid `stakerSignatureAndExpiry` input + * @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) + * via the `staker` calling `DelegationManager.delegateTo` + * The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves * Properly emits a `StakerDelegated` event * Staker is correctly delegated after the call (i.e. correct storage update) + * Operator beaconShares should increase by the amount of shares delegated if beaconShares > 0 * Reverts if the staker is already delegated (to the operator or to anyone else) * Reverts if the ‘operator’ is not actually registered as an operator */ - function testDelegateBySignatureToOperatorWhoRequiresEIP1271Signature(address caller, bytes32 salt, uint256 expiry) public - filterFuzzedAddressInputs(caller) - { + function testFuzz_OperatorWhoRequiresECDSASignature_BeaconChainStrategyShares( + address staker, + bytes32 salt, + uint256 expiry, + int256 beaconShares + ) public filterFuzzedAddressInputs(staker) { // filter to only valid `expiry` values cheats.assume(expiry >= block.timestamp); - - address staker = cheats.addr(stakerPrivateKey); - address delegationSigner = cheats.addr(delegationSignerPrivateKey); - - // register *this contract* as an operator // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != _operator); + cheats.assume(staker != defaultOperator); - /** - * deploy a ERC1271WalletMock contract with the `delegationSigner` address as the owner, - * so that we can create valid signatures from the `delegationSigner` for the contract to check when called - */ - ERC1271WalletMock wallet = new ERC1271WalletMock(delegationSigner); - - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: _operator, - delegationApprover: address(wallet), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(_operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithDelegationApprover(defaultOperator); // verify that the salt hasn't been used before - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(_operator), salt), "salt somehow spent too early?"); + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); // calculate the delegationSigner's signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature(delegationSignerPrivateKey, staker, _operator, salt, expiry); - - // fetch the staker's current nonce - uint256 currentStakerNonce = delegationManager.stakerNonce(staker); - // calculate the staker signature - ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature(stakerPrivateKey, _operator, expiry); + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); - // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` - cheats.startPrank(caller); + // Set staker shares in BeaconChainStrategy + eigenPodManagerMock.setPodOwnerShares(staker, beaconShares); + uint256 beaconSharesBefore = delegationManager.operatorShares(staker, beaconChainETHStrategy); + // delegate from the `staker` to the operator + cheats.startPrank(staker); cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(staker, _operator); - delegationManager.delegateToBySignature(staker, _operator, stakerSignatureAndExpiry, approverSignatureAndExpiry, salt); + emit StakerDelegated(staker, defaultOperator); + if (beaconShares > 0) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, staker, beaconChainETHStrategy, uint256(beaconShares)); + } + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); + uint256 beaconSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy); + if (beaconShares <= 0) { + assertEq( + beaconSharesBefore, + beaconSharesAfter, + "operator beaconchain shares should not have increased with negative shares" + ); + } else { + assertEq( + beaconSharesBefore + uint256(beaconShares), + beaconSharesAfter, + "operator beaconchain shares not increased correctly" + ); + } + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); - require(delegationManager.isDelegated(staker), "staker not delegated correctly"); - require(delegationManager.delegatedTo(staker) == _operator, "staker delegated to the wrong address"); - require(!delegationManager.isOperator(staker), "staker incorrectly registered as operator"); - - // check that the delegationApprover nonce incremented appropriately - if (caller == _operator || caller == delegationManager.delegationApprover(_operator)) { + if (staker == delegationManager.delegationApprover(defaultOperator)) { // verify that the salt is still marked as unused (since it wasn't checked or used) - require(!delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(_operator), salt), "salt somehow spent too incorrectly?"); + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too incorrectly?" + ); } else { // verify that the salt is marked as used - require(delegationManager.delegationApproverSaltIsSpent(delegationManager.delegationApprover(_operator), salt), "salt somehow spent not spent?"); + assertTrue( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent not spent?" + ); } - - // check that the staker nonce incremented appropriately - require(delegationManager.stakerNonce(staker) == currentStakerNonce + 1, - "staker nonce did not increment"); - } - - // @notice Checks that `DelegationManager.delegateToBySignature` reverts if the staker's signature has expired - function testDelegateBySignatureRevertsWhenStakerSignatureExpired(address staker, address operator, uint256 expiry, bytes memory signature) public{ - cheats.assume(expiry < block.timestamp); - cheats.expectRevert(bytes("DelegationManager.delegateToBySignature: staker signature expired")); - ISignatureUtils.SignatureWithExpiry memory signatureWithExpiry = ISignatureUtils.SignatureWithExpiry({ - signature: signature, - expiry: expiry - }); - delegation.delegateToBySignature(staker, operator, signatureWithExpiry, signatureWithExpiry, emptySalt); } - // @notice Checks that `DelegationManager.delegateToBySignature` reverts if the delegationApprover's signature has expired and their signature is checked - function testDelegateBySignatureRevertsWhenDelegationApproverSignatureExpired(address caller, uint256 stakerExpiry, uint256 delegationApproverExpiry) public - filterFuzzedAddressInputs(caller) - { - // filter to only valid `stakerExpiry` values - cheats.assume(stakerExpiry >= block.timestamp); - // roll to a very late timestamp - cheats.roll(type(uint256).max / 2); - // filter to only *invalid* `delegationApproverExpiry` values - cheats.assume(delegationApproverExpiry < block.timestamp); - - address staker = cheats.addr(stakerPrivateKey); - address delegationApprover = cheats.addr(delegationSignerPrivateKey); + /** + * @notice `staker` delegates to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) + * via the `staker` calling `DelegationManager.delegateTo` + * The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Operator beaconshares should increase by the amount of beaconShares delegated if beaconShares > 0 + * Operator strategy manager shares should icnrease by amount of shares + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testFuzz_OperatorWhoRequiresECDSASignature_BeaconChainAndStrategyManagerShares( + address staker, + bytes32 salt, + uint256 expiry, + int256 beaconShares, + uint256 shares + ) public filterFuzzedAddressInputs(staker) { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); - // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); - - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: delegationApprover, - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + cheats.assume(staker != defaultOperator); + _registerOperatorWithDelegationApprover(defaultOperator); + // verify that the salt hasn't been used before + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); // calculate the delegationSigner's signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = - _getApproverSignature(delegationSignerPrivateKey, staker, operator, emptySalt, delegationApproverExpiry); - - // calculate the staker signature - ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature(stakerPrivateKey, operator, stakerExpiry); + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); - // try delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature`, and check for reversion - cheats.startPrank(caller); - cheats.expectRevert(bytes("DelegationManager._delegate: approver signature expired")); - delegationManager.delegateToBySignature(staker, operator, stakerSignatureAndExpiry, approverSignatureAndExpiry, emptySalt); + // Set staker shares in BeaconChainStrategy and StrategyMananger + { + IStrategy[] memory strategiesToReturn = new IStrategy[](1); + strategiesToReturn[0] = strategyMock; + uint256[] memory sharesToReturn = new uint256[](1); + sharesToReturn[0] = shares; + strategyManagerMock.setDeposits(strategiesToReturn, sharesToReturn); + eigenPodManagerMock.setPodOwnerShares(staker, beaconShares); + } + uint256 operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesBefore = delegationManager.operatorShares(staker, beaconChainETHStrategy); + // delegate from the `staker` to the operator + cheats.startPrank(staker); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, defaultOperator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, staker, strategyMock, shares); + if (beaconShares > 0) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, staker, beaconChainETHStrategy, uint256(beaconShares)); + } + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); + uint256 operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy); + if (beaconShares <= 0) { + assertEq( + beaconSharesBefore, + beaconSharesAfter, + "operator beaconchain shares should not have increased with negative shares" + ); + } else { + assertEq( + beaconSharesBefore + uint256(beaconShares), + beaconSharesAfter, + "operator beaconchain shares not increased correctly" + ); + } + assertEq(operatorSharesBefore + shares, operatorSharesAfter, "operator shares not increased correctly"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); + + if (staker == delegationManager.delegationApprover(defaultOperator)) { + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too incorrectly?" + ); + } else { + // verify that the salt is marked as used + assertTrue( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent not spent?" + ); + } } /** - * @notice Like `testDelegateToOperatorWhoRequiresECDSASignature` but using an invalid expiry on purpose and checking that reversion occurs + * @notice delegateTo test with operator's delegationApprover address set to a contract address + * and check that reversion occurs when the signature is expired */ - function testDelegateToOperatorWhoRequiresECDSASignature_RevertsWithExpiredSignature(address staker, uint256 expiry) public - filterFuzzedAddressInputs(staker) - { + function testFuzz_Revert_WhenOperatorWhoRequiresEIP1271Signature_ExpiredDelegationApproverSignature( + address staker, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { // roll to a very late timestamp cheats.roll(type(uint256).max / 2); // filter to only *invalid* `expiry` values cheats.assume(expiry < block.timestamp); - address delegationApprover = cheats.addr(delegationSignerPrivateKey); - - // register *this contract* as an operator - address operator = address(this); // filter inputs, since this will fail when the staker is already registered as an operator - cheats.assume(staker != operator); + cheats.assume(staker != defaultOperator); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: delegationApprover, - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperatorWithDelegationApprover(defaultOperator); - // calculate the delegationSigner's signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = - _getApproverSignature(delegationSignerPrivateKey, staker, operator, emptySalt, expiry); + // create the signature struct + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + approverSignatureAndExpiry.expiry = expiry; - // delegate from the `staker` to the operator + // try to delegate from the `staker` to the operator, and check reversion cheats.startPrank(staker); - cheats.expectRevert(bytes("DelegationManager._delegate: approver signature expired")); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt); + cheats.expectRevert("DelegationManager._delegate: approver signature expired"); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt); cheats.stopPrank(); } /** - * Staker is undelegated from an operator, via a call to `undelegate`, properly originating from the staker's address. - * Reverts if the staker is themselves an operator (i.e. they are delegated to themselves) - * Does nothing if the staker is already undelegated - * Properly undelegates the staker, i.e. the staker becomes “delegated to” the zero address, and `isDelegated(staker)` returns ‘false’ - * Emits a `StakerUndelegated` event + * @notice delegateTo test with operator's delegationApprover address set to a contract address + * and check that reversion occurs when the signature approverSalt is already used. + * Performed by delegating to operator, undelegating, and trying to reuse the same signature */ - function testUndelegateFromOperator(address staker) public { - // register *this contract* as an operator and delegate from the `staker` to them (already filters out case when staker is the operator since it will revert) - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; - testDelegateToOperatorWhoAcceptsAllStakers(staker, approverSignatureAndExpiry, emptySalt); + function testFuzz_Revert_WhenOperatorWhoRequiresEIP1271Signature_PreviouslyUsedSalt( + address staker, + bytes32 salt, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); + + // register *this contract* as an operator + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != defaultOperator); + _registerOperatorWith1271DelegationApprover(defaultOperator); + + // calculate the delegationSigner's signature + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); + // delegate from the `staker` to the operator cheats.startPrank(staker); cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerUndelegated(staker, delegationManager.delegatedTo(staker)); + emit StakerDelegated(staker, defaultOperator); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); delegationManager.undelegate(staker); + // Reusing same signature should revert with salt already being used + cheats.expectRevert("DelegationManager._delegate: approverSalt already spent"); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); - - require(!delegationManager.isDelegated(staker), "staker not undelegated!"); - require(delegationManager.delegatedTo(staker) == address(0), "undelegated staker should be delegated to zero address"); } - // @notice Verifies that an operator cannot undelegate from themself (this should always be forbidden) - function testOperatorCannotUndelegateFromThemself(address operator) public fuzzedAddress(operator) { - cheats.startPrank(operator); + /** + * @notice delegateTo test with operator's delegationApprover address set to a contract address that + * is non compliant with EIP1271 + */ + function testFuzz_Revert_WhenOperatorWhoRequiresEIP1271Signature_NonCompliantWallet( + address staker, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); + + // register *this contract* as an operator + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != defaultOperator); + + // deploy a ERC1271MaliciousMock contract that will return an incorrect value when called + ERC1271MaliciousMock wallet = new ERC1271MaliciousMock(); + + // filter fuzzed input, since otherwise we can get a flaky failure here. if the caller itself is the 'delegationApprover' + // then we don't even trigger the signature verification call, so we won't get a revert as expected + cheats.assume(staker != address(wallet)); + IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: address(0), + earningsReceiver: defaultOperator, + delegationApprover: address(wallet), stakerOptOutWindowBlocks: 0 }); - delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); - cheats.stopPrank(); - cheats.expectRevert(bytes("DelegationManager.undelegate: operators cannot be undelegated")); - - cheats.startPrank(operator); - delegationManager.undelegate(operator); + _registerOperator(defaultOperator, operatorDetails, emptyStringForMetadataURI); + + // create the signature struct + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + approverSignatureAndExpiry.expiry = expiry; + + // try to delegate from the `staker` to the operator, and check reversion + cheats.startPrank(staker); + // because the ERC1271MaliciousMock contract returns the wrong amount of data, we get a low-level "EvmError: Revert" message here rather than the error message bubbling up + cheats.expectRevert(); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt); cheats.stopPrank(); } /** - * @notice Verifies that `DelegationManager.increaseDelegatedShares` properly increases the delegated `shares` that the operator - * who the `staker` is delegated to has in the strategy - * @dev Checks that there is no change if the staker is not delegated + * @notice delegateTo test with operator's delegationApprover address set to a contract address that + * returns a value other than the EIP1271 "magic bytes" and checking that reversion occurs appropriately */ - function testIncreaseDelegatedShares(address staker, uint256 shares, bool delegateFromStakerToOperator) public { - IStrategy strategy = strategyMock; + function testFuzz_Revert_WhenOperatorWhoRequiresEIP1271Signature_IsValidSignatureFails( + address staker, + bytes32 salt, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); // register *this contract* as an operator - address operator = address(this); - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; - // filter inputs, since delegating to the operator will fail when the staker is already registered as an operator - cheats.assume(staker != operator); + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != defaultOperator); + + // deploy a ERC1271WalletMock contract that will return an incorrect value when called + // owner is the 0 address + ERC1271WalletMock wallet = new ERC1271WalletMock(address(1)); + + // filter fuzzed input, since otherwise we can get a flaky failure here. if the caller itself is the 'delegationApprover' + // then we don't even trigger the signature verification call, so we won't get a revert as expected + cheats.assume(staker != address(wallet)); IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: address(0), + earningsReceiver: defaultOperator, + delegationApprover: address(wallet), stakerOptOutWindowBlocks: 0 }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + _registerOperator(defaultOperator, operatorDetails, emptyStringForMetadataURI); + + // calculate the delegationSigner's but this is not the correct signature from the wallet contract + // since the wallet owner is address(1) + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); - // delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'* - if (delegateFromStakerToOperator) { - cheats.startPrank(staker); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(staker, operator); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt); - cheats.stopPrank(); - } - - uint256 _delegatedSharesBefore = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategy); - - if(delegationManager.isDelegated(staker)) { - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit OperatorSharesIncreased(operator, staker, strategy, shares); - } - - cheats.startPrank(address(strategyManagerMock)); - delegationManager.increaseDelegatedShares(staker, strategy, shares); + // try to delegate from the `staker` to the operator, and check reversion + cheats.startPrank(staker); + // Signature should fail as the wallet will not return EIP1271_MAGICVALUE + cheats.expectRevert("EIP1271SignatureUtils.checkSignature_EIP1271: ERC1271 signature verification failed"); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, emptySalt); cheats.stopPrank(); - - uint256 delegatedSharesAfter = delegationManager.operatorShares(delegationManager.delegatedTo(staker), strategy); - - if (delegationManager.isDelegated(staker)) { - require(delegatedSharesAfter == _delegatedSharesBefore + shares, "delegated shares did not increment correctly"); - } else { - require(delegatedSharesAfter == _delegatedSharesBefore, "delegated shares incremented incorrectly"); - require(_delegatedSharesBefore == 0, "nonzero shares delegated to zero address!"); - } } /** - * @notice Verifies that `DelegationManager.decreaseDelegatedShares` properly decreases the delegated `shares` that the operator - * who the `staker` is delegated to has in the strategies - * @dev Checks that there is no change if the staker is not delegated + * @notice `staker` delegates to an operator who requires signature verification through an EIP1271-compliant contract (i.e. the operator’s `delegationApprover` address is + * set to a nonzero and code-containing address) via the `staker` calling `DelegationManager.delegateTo` + * The function uses OZ's ERC1271WalletMock contract, and thus should pass *only when a valid ECDSA signature from the `owner` of the ERC1271WalletMock contract, + * OR if called by the operator or their delegationApprover themselves + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator */ - function testDecreaseDelegatedShares(address staker, IStrategy[] memory strategies, uint128 shares, bool delegateFromStakerToOperator) public filterFuzzedAddressInputs(staker) { - // sanity-filtering on fuzzed input length - cheats.assume(strategies.length <= 32); - // register *this contract* as an operator - address operator = address(this); - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; - // filter inputs, since delegating to the operator will fail when the staker is already registered as an operator - cheats.assume(staker != operator); - - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, operatorDetails, emptyStringForMetadataURI); + function testFuzz_OperatorWhoRequiresEIP1271Signature( + address staker, + bytes32 salt, + uint256 expiry + ) public filterFuzzedAddressInputs(staker) { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); - // delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'* - if (delegateFromStakerToOperator) { - cheats.startPrank(staker); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit StakerDelegated(staker, operator); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, emptySalt); - cheats.stopPrank(); - } + // register *this contract* as an operator + // filter inputs, since this will fail when the staker is already registered as an operator + cheats.assume(staker != defaultOperator); - uint256[] memory sharesInputArray = new uint256[](strategies.length); + _registerOperatorWith1271DelegationApprover(defaultOperator); - address delegatedTo = delegationManager.delegatedTo(staker); + // verify that the salt hasn't been used before + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); + // calculate the delegationSigner's signature + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + staker, + defaultOperator, + salt, + expiry + ); - // for each strategy in `strategies`, increase delegated shares by `shares` - cheats.startPrank(address(strategyManagerMock)); - for (uint256 i = 0; i < strategies.length; ++i) { - delegationManager.increaseDelegatedShares(staker, strategies[i], shares); - // store delegated shares in a mapping - delegatedSharesBefore[strategies[i]] = delegationManager.operatorShares(delegatedTo, strategies[i]); - // also construct an array which we'll use in another loop - sharesInputArray[i] = shares; - totalSharesForStrategyInArray[address(strategies[i])] += sharesInputArray[i]; - } + // delegate from the `staker` to the operator + cheats.startPrank(staker); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(staker, defaultOperator); + delegationManager.delegateTo(defaultOperator, approverSignatureAndExpiry, salt); cheats.stopPrank(); - // for each strategy in `strategies`, decrease delegated shares by `shares` - { - cheats.startPrank(address(strategyManagerMock)); - address operatorToDecreaseSharesOf = delegationManager.delegatedTo(staker); - if (delegationManager.isDelegated(staker)) { - for (uint256 i = 0; i < strategies.length; ++i) { - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit OperatorSharesDecreased(operatorToDecreaseSharesOf, staker, strategies[i], sharesInputArray[i]); - delegationManager.decreaseDelegatedShares(staker, strategies[i], sharesInputArray[i]); - } - } - cheats.stopPrank(); - } - - // check shares after call to `decreaseDelegatedShares` - bool isDelegated = delegationManager.isDelegated(staker); - for (uint256 i = 0; i < strategies.length; ++i) { - uint256 delegatedSharesAfter = delegationManager.operatorShares(delegatedTo, strategies[i]); + assertTrue(delegationManager.isDelegated(staker), "staker not delegated correctly"); + assertEq(delegationManager.delegatedTo(staker), defaultOperator, "staker delegated to the wrong address"); + assertFalse(delegationManager.isOperator(staker), "staker incorrectly registered as operator"); - if (isDelegated) { - require(delegatedSharesAfter + totalSharesForStrategyInArray[address(strategies[i])] == delegatedSharesBefore[strategies[i]], - "delegated shares did not decrement correctly"); - } else { - require(delegatedSharesAfter == delegatedSharesBefore[strategies[i]], "delegated shares decremented incorrectly"); - require(delegatedSharesBefore[strategies[i]] == 0, "nonzero shares delegated to zero address!"); - } + // check that the nonce incremented appropriately + if (staker == defaultOperator || staker == delegationManager.delegationApprover(defaultOperator)) { + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too incorrectly?" + ); + } else { + // verify that the salt is marked as used + assertTrue( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent not spent?" + ); } } +} - // @notice Verifies that `DelegationManager.increaseDelegatedShares` reverts if not called by the StrategyManager nor EigenPodManager - function testCannotCallIncreaseDelegatedSharesFromNonPermissionedAddress(address operator, uint256 shares) public fuzzedAddress(operator) { - cheats.assume(operator != address(strategyManagerMock)); - cheats.assume(operator != address(eigenPodManagerMock)); - cheats.expectRevert(bytes("DelegationManager: onlyStrategyManagerOrEigenPodManager")); - cheats.startPrank(operator); - delegationManager.increaseDelegatedShares(operator, strategyMock, shares); - } +contract DelegationManagerUnitTests_delegateToBySignature is DelegationManagerUnitTests { + function test_revert_paused() public { + // set the pausing flag + cheats.prank(pauser); + delegationManager.pause(2 ** PAUSED_NEW_DELEGATION); - // @notice Verifies that `DelegationManager.decreaseDelegatedShares` reverts if not called by the StrategyManager nor EigenPodManager - function testCannotCallDecreaseDelegatedSharesFromNonPermissionedAddress( - address operator, - IStrategy strategy, - uint256 shares - ) public fuzzedAddress(operator) { - cheats.assume(operator != address(strategyManagerMock)); - cheats.assume(operator != address(eigenPodManagerMock)); - cheats.expectRevert(bytes("DelegationManager: onlyStrategyManagerOrEigenPodManager")); - cheats.startPrank(operator); - delegationManager.decreaseDelegatedShares(operator, strategy, shares); + uint256 expiry = type(uint256).max; + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + expiry + ); + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + cheats.expectRevert("Pausable: index is paused"); + delegationManager.delegateToBySignature( + defaultStaker, + defaultOperator, + stakerSignatureAndExpiry, + approverSignatureAndExpiry, + emptySalt + ); } - // @notice Verifies that it is not possible for a staker to delegate to an operator when they are already delegated to an operator - function testCannotDelegateWhenStakerHasExistingDelegation(address staker, address operator, address operator2) public - fuzzedAddress(staker) - fuzzedAddress(operator) - fuzzedAddress(operator2) - { - cheats.assume(operator != operator2); - cheats.assume(staker != operator); - cheats.assume(staker != operator2); - - cheats.startPrank(operator); - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 + /// @notice Checks that `DelegationManager.delegateToBySignature` reverts if the staker's signature has expired + function testFuzz_Revert_WhenStakerSignatureExpired( + address staker, + address operator, + uint256 expiry, + bytes memory signature + ) public filterFuzzedAddressInputs(staker) filterFuzzedAddressInputs(operator) { + cheats.assume(expiry < block.timestamp); + cheats.expectRevert("DelegationManager.delegateToBySignature: staker signature expired"); + ISignatureUtils.SignatureWithExpiry memory signatureWithExpiry = ISignatureUtils.SignatureWithExpiry({ + signature: signature, + expiry: expiry }); - delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); - cheats.stopPrank(); - - cheats.startPrank(operator2); - delegationManager.registerAsOperator(operatorDetails, emptyStringForMetadataURI); - cheats.stopPrank(); - - cheats.startPrank(staker); - ISignatureUtils.SignatureWithExpiry memory signatureWithExpiry; - delegationManager.delegateTo(operator, signatureWithExpiry, emptySalt); - cheats.stopPrank(); - - cheats.startPrank(staker); - cheats.expectRevert(bytes("DelegationManager._delegate: staker is already actively delegated")); - delegationManager.delegateTo(operator2, signatureWithExpiry, emptySalt); - cheats.stopPrank(); - } - - // @notice Verifies that it is not possible to delegate to an unregistered operator - function testCannotDelegateToUnregisteredOperator(address operator) public { - cheats.expectRevert(bytes("DelegationManager._delegate: operator is not registered in EigenLayer")); - ISignatureUtils.SignatureWithExpiry memory signatureWithExpiry; - delegationManager.delegateTo(operator, signatureWithExpiry, emptySalt); + delegationManager.delegateToBySignature(staker, operator, signatureWithExpiry, signatureWithExpiry, emptySalt); } - // @notice Verifies that delegating is not possible when the "new delegations paused" switch is flipped - function testCannotDelegateWhenPausedNewDelegationIsSet(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { - // set the pausing flag - cheats.startPrank(pauser); - delegationManager.pause(2 ** PAUSED_NEW_DELEGATION); - cheats.stopPrank(); - - cheats.startPrank(staker); - cheats.expectRevert(bytes("Pausable: index is paused")); - ISignatureUtils.SignatureWithExpiry memory signatureWithExpiry; - delegationManager.delegateTo(operator, signatureWithExpiry, emptySalt); - cheats.stopPrank(); - } + /// @notice Checks that `DelegationManager.delegateToBySignature` reverts if the staker's ECDSA signature verification fails + function test_Revert_EOAStaker_WhenStakerSignatureVerificationFails() public { + address invalidStaker = address(1000); + address caller = address(2000); + uint256 expiry = type(uint256).max; - // @notice Verifies that undelegating is not possible when the "undelegation paused" switch is flipped - function testCannotUndelegateWhenPausedUndelegationIsSet(address operator, address staker) public fuzzedAddress(operator) fuzzedAddress(staker) { - // register *this contract* as an operator and delegate from the `staker` to them (already filters out case when staker is the operator since it will revert) - IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry; - testDelegateToOperatorWhoAcceptsAllStakers(staker, approverSignatureAndExpiry, emptySalt); + _registerOperatorWithBaseDetails(defaultOperator); - // set the pausing flag - cheats.startPrank(pauser); - delegationManager.pause(2 ** PAUSED_ENTER_WITHDRAWAL_QUEUE); - cheats.stopPrank(); + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + expiry + ); - cheats.startPrank(staker); - cheats.expectRevert(bytes("Pausable: index is paused")); - delegationManager.undelegate(staker); + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` + // Should revert from invalid signature as staker is not set as the address of signer + cheats.startPrank(caller); + cheats.expectRevert("EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer"); + // use an empty approver signature input since none is needed / the input is unchecked + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + delegationManager.delegateToBySignature( + invalidStaker, + defaultOperator, + stakerSignatureAndExpiry, + approverSignatureAndExpiry, + emptySalt + ); cheats.stopPrank(); } - // special event purely used in the StrategyManagerMock contract, inside of `undelegate` function to verify that the correct call is made - event ForceTotalWithdrawalCalled(address staker); - - /** - * @notice Verifies that the `undelegate` function properly calls `strategyManager.forceTotalWithdrawal` when necessary - * @param callFromOperatorOrApprover -- calls from the operator if 'false' and the 'approver' if true - */ - function testForceUndelegation(address staker, bytes32 salt, bool callFromOperatorOrApprover) public - fuzzedAddress(staker) - { - address delegationApprover = cheats.addr(delegationSignerPrivateKey); - address operator = address(this); - - // filtering since you can't delegate to yourself after registering as an operator - cheats.assume(staker != operator); - - // register this contract as an operator and delegate from the staker to it + /// @notice Checks that `DelegationManager.delegateToBySignature` reverts if the staker's contract signature verification fails + function test_Revert_ERC1271Staker_WhenStakerSignatureVerficationFails() public { + address staker = address(new ERC1271WalletMock(address(1))); + address caller = address(2000); uint256 expiry = type(uint256).max; - testDelegateToOperatorWhoRequiresECDSASignature(staker, salt, expiry); - address caller; - if (callFromOperatorOrApprover) { - caller = delegationApprover; - } else { - caller = operator; - } + _registerOperatorWithBaseDetails(defaultOperator); - // call the `undelegate` function - cheats.startPrank(caller); - // check that the correct calldata is forwarded by looking for an event emitted by the StrategyManagerMock contract - if (strategyManagerMock.stakerStrategyListLength(staker) != 0) { - cheats.expectEmit(true, true, true, true, address(strategyManagerMock)); - emit ForceTotalWithdrawalCalled(staker); - } - // withdrawal root - (IStrategy[] memory strategies, uint256[] memory shares) = delegationManager.getDelegatableShares(staker); - IDelegationManager.Withdrawal memory fullWithdrawal = IDelegationManager.Withdrawal({ - staker: staker, - delegatedTo: operator, - withdrawer: staker, - nonce: delegationManager.cumulativeWithdrawalsQueued(staker), - startBlock: uint32(block.number), - strategies: strategies, - shares: shares - }); - bytes32 withdrawalRoot = delegationManager.calculateWithdrawalRoot(fullWithdrawal); - - (bytes32 returnValue) = delegationManager.undelegate(staker); - - if (strategies.length == 0) { - withdrawalRoot = bytes32(0); - } + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + expiry + ); - // check that the return value is the withdrawal root - require(returnValue == withdrawalRoot, "contract returned wrong return value"); + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` + // Should revert from invalid signature as staker is not set as the address of signer + cheats.startPrank(caller); + cheats.expectRevert("EIP1271SignatureUtils.checkSignature_EIP1271: ERC1271 signature verification failed"); + // use an empty approver signature input since none is needed / the input is unchecked + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + delegationManager.delegateToBySignature( + staker, + defaultOperator, + stakerSignatureAndExpiry, + approverSignatureAndExpiry, + emptySalt + ); cheats.stopPrank(); } - /** - * @notice Verifies that the `undelegate` function has proper access controls (can only be called by the operator who the `staker` has delegated - * to or the operator's `delegationApprover`), or the staker themselves - */ - function testCannotCallUndelegateFromImproperAddress(address staker, address caller) public - fuzzedAddress(staker) - fuzzedAddress(caller) - { - address delegationApprover = cheats.addr(delegationSignerPrivateKey); - address operator = address(this); - - // filtering since you can't delegate to yourself after registering as an operator - cheats.assume(staker != operator); + /// @notice Checks that `DelegationManager.delegateToBySignature` reverts when the staker is already delegated + function test_Revert_Staker_WhenAlreadyDelegated() public { + address staker = cheats.addr(stakerPrivateKey); + address caller = address(2000); + uint256 expiry = type(uint256).max; - // filter out addresses that are actually allowed to call the function - cheats.assume(caller != operator); - cheats.assume(caller != delegationApprover); - cheats.assume(caller != staker); + _registerOperatorWithBaseDetails(defaultOperator); - // register this contract as an operator and delegate from the staker to it - uint256 expiry = type(uint256).max; - testDelegateToOperatorWhoRequiresECDSASignature(staker, emptySalt, expiry); + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + expiry + ); - // try to call the `undelegate` function and check for reversion + _delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator); + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` + // Should revert as `staker` has already delegated to `operator` cheats.startPrank(caller); - cheats.expectRevert(bytes("DelegationManager.undelegate: caller cannot undelegate staker")); - delegationManager.undelegate(staker); + cheats.expectRevert("DelegationManager._delegate: staker is already actively delegated"); + // use an empty approver signature input since none is needed / the input is unchecked + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + delegationManager.delegateToBySignature( + staker, + defaultOperator, + stakerSignatureAndExpiry, + approverSignatureAndExpiry, + emptySalt + ); cheats.stopPrank(); } - /** - * @notice verifies that `DelegationManager.undelegate` reverts if trying to undelegate an operator from themselves - * @param callFromOperatorOrApprover -- calls from the operator if 'false' and the 'approver' if true - */ - function testOperatorCannotForceUndelegateThemself(address delegationApprover, bool callFromOperatorOrApprover) public { - // register *this contract* as an operator - address operator = address(this); - IDelegationManager.OperatorDetails memory _operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator, - delegationApprover: delegationApprover, - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator, _operatorDetails, emptyStringForMetadataURI); + /// @notice Checks that `delegateToBySignature` reverts when operator is not registered after successful staker signature verification + function test_Revert_EOAStaker_OperatorNotRegistered() public { + address staker = cheats.addr(stakerPrivateKey); + address caller = address(2000); + uint256 expiry = type(uint256).max; - address caller; - if (callFromOperatorOrApprover) { - caller = delegationApprover; - } else { - caller = operator; - } + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + expiry + ); - // try to call the `undelegate` function and check for reversion + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` + // Should revert as `operator` is not registered cheats.startPrank(caller); - cheats.expectRevert(bytes("DelegationManager.undelegate: operators cannot be undelegated")); - delegationManager.undelegate(operator); + cheats.expectRevert("DelegationManager._delegate: operator is not registered in EigenLayer"); + // use an empty approver signature input since none is needed / the input is unchecked + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; + delegationManager.delegateToBySignature( + staker, + defaultOperator, + stakerSignatureAndExpiry, + approverSignatureAndExpiry, + emptySalt + ); cheats.stopPrank(); } /** - * @notice Verifies that the reversion occurs when trying to reuse an 'approverSalt' + * @notice Checks that `DelegationManager.delegateToBySignature` reverts if the delegationApprover's signature has expired + * after successful staker signature verification */ - function test_Revert_WhenTryingToReuseSalt(address staker_one, address staker_two, bytes32 salt) public - fuzzedAddress(staker_one) - fuzzedAddress(staker_two) - { - // address delegationApprover = cheats.addr(delegationSignerPrivateKey); - address operator = address(this); - - // filtering since you can't delegate to yourself after registering as an operator - cheats.assume(staker_one != operator); - cheats.assume(staker_two != operator); - - // filtering since you can't delegate twice - cheats.assume(staker_one != staker_two); - - address delegationApprover = cheats.addr(delegationSignerPrivateKey); - // filter out the case where `staker` *is* the 'delegationApprover', since in this case the salt won't get used - cheats.assume(staker_one != delegationApprover); - cheats.assume(staker_two != delegationApprover); + function testFuzz_Revert_WhenDelegationApproverSignatureExpired( + address caller, + uint256 stakerExpiry, + uint256 delegationApproverExpiry + ) public filterFuzzedAddressInputs(caller) { + // filter to only valid `stakerExpiry` values + cheats.assume(stakerExpiry >= block.timestamp); + // roll to a very late timestamp + cheats.roll(type(uint256).max / 2); + // filter to only *invalid* `delegationApproverExpiry` values + cheats.assume(delegationApproverExpiry < block.timestamp); - // register this contract as an operator and delegate from `staker_one` to it, using the `salt` - uint256 expiry = type(uint256).max; - testDelegateToOperatorWhoRequiresECDSASignature(staker_one, salt, expiry); + _registerOperatorWithDelegationApprover(defaultOperator); // calculate the delegationSigner's signature - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = - _getApproverSignature(delegationSignerPrivateKey, staker_two, operator, salt, expiry); - - // try to delegate to the operator from `staker_two`, and verify that the call reverts for the proper reason (trying to reuse a salt) - cheats.startPrank(staker_two); - cheats.expectRevert(bytes("DelegationManager._delegate: approverSalt already spent")); - delegationManager.delegateTo(operator, approverSignatureAndExpiry, salt); - cheats.stopPrank(); - } - - function testSetWithdrawalDelayBlocks(uint16 valueToSet) external { - // filter fuzzed inputs to allowed amounts - cheats.assume(valueToSet <= delegationManager.MAX_WITHDRAWAL_DELAY_BLOCKS()); - - // set the `withdrawalDelayBlocks` variable - cheats.startPrank(delegationManager.owner()); - uint256 previousValue = delegationManager.withdrawalDelayBlocks(); - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalDelayBlocksSet(previousValue, valueToSet); - delegationManager.setWithdrawalDelayBlocks(valueToSet); - cheats.stopPrank(); - require(delegationManager.withdrawalDelayBlocks() == valueToSet, "DelegationManager.withdrawalDelayBlocks() != valueToSet"); - } + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry = _getApproverSignature( + delegationSignerPrivateKey, + defaultStaker, + defaultOperator, + emptySalt, + delegationApproverExpiry + ); - function testSetWithdrawalDelayBlocksRevertsWhenCalledByNotOwner(address notOwner) filterFuzzedAddressInputs(notOwner) external { - cheats.assume(notOwner != delegationManager.owner()); + // calculate the staker signature + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + stakerExpiry + ); - uint256 valueToSet = 1; - // set the `withdrawalDelayBlocks` variable - cheats.startPrank(notOwner); - cheats.expectRevert(bytes("Ownable: caller is not the owner")); - delegationManager.setWithdrawalDelayBlocks(valueToSet); + // try delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature`, and check for reversion + cheats.startPrank(caller); + cheats.expectRevert("DelegationManager._delegate: approver signature expired"); + delegationManager.delegateToBySignature( + defaultStaker, + defaultOperator, + stakerSignatureAndExpiry, + approverSignatureAndExpiry, + emptySalt + ); cheats.stopPrank(); } - function testSetWithdrawalDelayBlocksRevertsWhenInputValueTooHigh(uint256 valueToSet) external { - // filter fuzzed inputs to disallowed amounts - cheats.assume(valueToSet > delegationManager.MAX_WITHDRAWAL_DELAY_BLOCKS()); - - // attempt to set the `withdrawalDelayBlocks` variable - cheats.startPrank(delegationManager.owner()); - cheats.expectRevert(bytes("DelegationManager.setWithdrawalDelayBlocks: newWithdrawalDelayBlocks too high")); - delegationManager.setWithdrawalDelayBlocks(valueToSet); - } - - /************************************** - * - * Withdrawals Tests with StrategyManager, using actual SM contract instead of Mock to test - * - **************************************/ + /** + * @notice `staker` becomes delegated to an operator who does not require any signature verification (i.e. the operator’s `delegationApprover` address is set to the zero address) + * via the `caller` calling `DelegationManager.delegateToBySignature` + * The function should pass with any `operatorSignature` input (since it should be unused) + * The function should pass only with a valid `stakerSignatureAndExpiry` input + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * BeaconChainStrategy and StrategyManager operator shares should increase for operator + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testFuzz_EOAStaker_OperatorWhoAcceptsAllStakers( + address caller, + uint256 expiry, + int256 beaconShares, + uint256 shares + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(expiry >= block.timestamp); + cheats.assume(shares > 0); - - function testQueueWithdrawalRevertsMismatchedSharesAndStrategyArrayLength() external { - IStrategy[] memory strategyArray = new IStrategy[](1); - uint256[] memory shareAmounts = new uint256[](2); + _registerOperatorWithBaseDetails(defaultOperator); + // verify that the salt hasn't been used before + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + emptySalt + ), + "salt somehow spent too early?" + ); { - strategyArray[0] = eigenPodManagerMock.beaconChainETHStrategy(); - shareAmounts[0] = 1; - shareAmounts[1] = 1; + // Set staker shares in BeaconChainStrategy and StrategyMananger + IStrategy[] memory strategiesToReturn = new IStrategy[](1); + strategiesToReturn[0] = strategyMock; + uint256[] memory sharesToReturn = new uint256[](1); + sharesToReturn[0] = shares; + strategyManagerMock.setDeposits(strategiesToReturn, sharesToReturn); + eigenPodManagerMock.setPodOwnerShares(defaultStaker, beaconShares); } - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: strategyArray, - shares: shareAmounts, - withdrawer: address(this) - }); - - cheats.expectRevert(bytes("DelegationManager.queueWithdrawal: input length mismatch")); - delegationManager.queueWithdrawals(params); - } - - function testQueueWithdrawalRevertsWithZeroAddressWithdrawer() external { - IStrategy[] memory strategyArray = new IStrategy[](1); - uint256[] memory shareAmounts = new uint256[](1); - - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: strategyArray, - shares: shareAmounts, - withdrawer: address(0) - }); - - cheats.expectRevert(bytes("DelegationManager.queueWithdrawal: must provide valid withdrawal address")); - delegationManager.queueWithdrawals(params); - } - - function testQueueWithdrawal_ToSelf( - uint256 depositAmount, - uint256 withdrawalAmount - ) - public - returns ( - IDelegationManager.Withdrawal memory /* queuedWithdrawal */, - IERC20[] memory /* tokensArray */, - bytes32 /* withdrawalRoot */ - ) - { - _setUpWithdrawalTests(); - StrategyBase strategy = strategyMock; - IERC20 token = strategy.underlyingToken(); - - // filtering of fuzzed inputs - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - - _tempStrategyStorage = strategy; - - _depositIntoStrategySuccessfully(strategy, /*staker*/ address(this), depositAmount); - - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - bytes32 withdrawalRoot - ) = _setUpWithdrawalStructSingleStrat( - /*staker*/ address(this), - /*withdrawer*/ address(this), - token, - _tempStrategyStorage, - withdrawalAmount - ); - - uint256 sharesBefore = strategyManager.stakerStrategyShares(/*staker*/ address(this), _tempStrategyStorage); - uint256 nonceBefore = delegationManager.cumulativeWithdrawalsQueued(/*staker*/ address(this)); - - require(!delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRootPendingBefore is true!"); + uint256 operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesBefore = delegationManager.operatorShares(defaultStaker, beaconChainETHStrategy); + // fetch the staker's current nonce + uint256 currentStakerNonce = delegationManager.stakerNonce(defaultStaker); + // calculate the staker signature + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + expiry + ); - { + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerDelegated(defaultStaker, defaultOperator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, defaultStaker, strategyMock, shares); + if (beaconShares > 0) { cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalQueued( - withdrawalRoot, - withdrawal - ); - - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: withdrawal.strategies, - shares: withdrawal.shares, - withdrawer: address(this) - }); - delegationManager.queueWithdrawals(params); + emit OperatorSharesIncreased(defaultOperator, defaultStaker, beaconChainETHStrategy, uint256(beaconShares)); } + _delegateToBySignatureOperatorWhoAcceptsAllStakers( + defaultStaker, + caller, + defaultOperator, + stakerSignatureAndExpiry, + emptySalt + ); - uint256 sharesAfter = strategyManager.stakerStrategyShares(/*staker*/ address(this), _tempStrategyStorage); - uint256 nonceAfter = delegationManager.cumulativeWithdrawalsQueued(/*staker*/ address(this)); - - require(delegationManager.pendingWithdrawals(withdrawalRoot), "pendingWithdrawalsAfter is false!"); - require(sharesAfter == sharesBefore - withdrawalAmount, "sharesAfter != sharesBefore - withdrawalAmount"); - require(nonceAfter == nonceBefore + 1, "nonceAfter != nonceBefore + 1"); - - return (withdrawal, tokensArray, withdrawalRoot); - } - - function testQueueWithdrawal_ToSelf_TwoStrategies( - uint256[2] memory depositAmounts, - uint256[2] memory withdrawalAmounts - ) - public - returns ( - IDelegationManager.Withdrawal memory /* withdrawal */, - bytes32 /* withdrawalRoot */ - ) - { - _setUpWithdrawalTests(); - // filtering of fuzzed inputs - cheats.assume(withdrawalAmounts[0] != 0 && withdrawalAmounts[0] < depositAmounts[0]); - cheats.assume(withdrawalAmounts[1] != 0 && withdrawalAmounts[1] < depositAmounts[1]); - address staker = address(this); - - IStrategy[] memory strategies = new IStrategy[](2); - strategies[0] = IStrategy(strategyMock); - strategies[1] = IStrategy(strategyMock2); - - IERC20[] memory tokens = new IERC20[](2); - tokens[0] = strategyMock.underlyingToken(); - tokens[1] = strategyMock2.underlyingToken(); - - uint256[] memory amounts = new uint256[](2); - amounts[0] = withdrawalAmounts[0]; - amounts[1] = withdrawalAmounts[1]; - - _depositIntoStrategySuccessfully(strategies[0], staker, depositAmounts[0]); - _depositIntoStrategySuccessfully(strategies[1], staker, depositAmounts[1]); - - ( - IDelegationManager.Withdrawal memory withdrawal, - bytes32 withdrawalRoot - ) = _setUpWithdrawalStruct_MultipleStrategies( - /* staker */ staker, - /* withdrawer */ staker, - strategies, - amounts - ); - - uint256[] memory sharesBefore = new uint256[](2); - sharesBefore[0] = strategyManager.stakerStrategyShares(staker, strategies[0]); - sharesBefore[1] = strategyManager.stakerStrategyShares(staker, strategies[1]); - uint256 nonceBefore = delegationManager.cumulativeWithdrawalsQueued(staker); - - require(!delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRootPendingBefore is true!"); - - { - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalQueued( - withdrawalRoot, - withdrawal + // Check operator shares increases + uint256 operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy); + if (beaconShares <= 0) { + assertEq( + beaconSharesBefore, + beaconSharesAfter, + "operator beaconchain shares should not have increased with negative shares" ); - - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: withdrawal.strategies, - shares: withdrawal.shares, - withdrawer: staker - }); - - delegationManager.queueWithdrawals( - params + } else { + assertEq( + beaconSharesBefore + uint256(beaconShares), + beaconSharesAfter, + "operator beaconchain shares not increased correctly" ); } - - uint256[] memory sharesAfter = new uint256[](2); - sharesAfter[0] = strategyManager.stakerStrategyShares(staker, strategies[0]); - sharesAfter[1] = strategyManager.stakerStrategyShares(staker, strategies[1]); - uint256 nonceAfter = delegationManager.cumulativeWithdrawalsQueued(staker); - - require(delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRootPendingAfter is false!"); - require( - sharesAfter[0] == sharesBefore[0] - withdrawalAmounts[0], - "Strat1: sharesAfter != sharesBefore - withdrawalAmount" - ); - require( - sharesAfter[1] == sharesBefore[1] - withdrawalAmounts[1], - "Strat2: sharesAfter != sharesBefore - withdrawalAmount" + assertEq(operatorSharesBefore + shares, operatorSharesAfter, "operator shares not increased correctly"); + // check all the delegation status changes + assertTrue(delegationManager.isDelegated(defaultStaker), "staker not delegated correctly"); + assertEq( + delegationManager.delegatedTo(defaultStaker), + defaultOperator, + "staker delegated to the wrong address" ); - require(nonceAfter == nonceBefore + 1, "nonceAfter != nonceBefore + 1"); - - return (withdrawal, withdrawalRoot); - } - - function testQueueWithdrawalPartiallyWithdraw(uint128 amount) external { - testQueueWithdrawal_ToSelf(uint256(amount) * 2, amount); - require(!delegationManager.isDelegated(address(this)), "should still be delegated failed"); - } - - function testQueueWithdrawal_ToDifferentAddress( - address withdrawer, - uint256 depositAmount, - uint256 withdrawalAmount - ) external filterFuzzedAddressInputs(withdrawer) { - _setUpWithdrawalTests(); - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - address staker = address(this); - - _depositIntoStrategySuccessfully(strategyMock, staker, depositAmount); - ( - IDelegationManager.Withdrawal memory withdrawal, - , - bytes32 withdrawalRoot - ) = _setUpWithdrawalStructSingleStrat( - staker, - withdrawer, - /*token*/ strategyMock.underlyingToken(), - strategyMock, - withdrawalAmount - ); - - uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategyMock); - uint256 nonceBefore = delegationManager.cumulativeWithdrawalsQueued(staker); - - require(!delegationManager.pendingWithdrawals(withdrawalRoot), "pendingWithdrawalsBefore is true!"); - - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalQueued( - withdrawalRoot, - withdrawal + assertFalse(delegationManager.isOperator(defaultStaker), "staker incorrectly registered as operator"); + // check that the staker nonce incremented appropriately + assertEq( + delegationManager.stakerNonce(defaultStaker), + currentStakerNonce + 1, + "staker nonce did not increment" ); - - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: withdrawal.strategies, - shares: withdrawal.shares, - withdrawer: withdrawer - }); - - delegationManager.queueWithdrawals( - params + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + emptySalt + ), + "salt somehow spent too incorrectly?" ); - - uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategyMock); - uint256 nonceAfter = delegationManager.cumulativeWithdrawalsQueued(staker); - - require(delegationManager.pendingWithdrawals(withdrawalRoot), "pendingWithdrawalsAfter is false!"); - require(sharesAfter == sharesBefore - withdrawalAmount, "sharesAfter != sharesBefore - amount"); - require(nonceAfter == nonceBefore + 1, "nonceAfter != nonceBefore + 1"); } - function testCompleteQueuedWithdrawalRevertsWhenAttemptingReentrancy( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - // replace dummyStrat with Reenterer contract - reenterer = new Reenterer(); - strategyMock = StrategyBase(address(reenterer)); - - // whitelist the strategy for deposit - cheats.startPrank(strategyManager.owner()); - IStrategy[] memory _strategy = new IStrategy[](1); - _strategy[0] = strategyMock; - strategyManager.addStrategiesToDepositWhitelist(_strategy); - cheats.stopPrank(); - - _tempStakerStorage = address(this); - IStrategy strategy = strategyMock; - - reenterer.prepareReturnData(abi.encode(depositAmount)); + /** + * @notice `staker` becomes delegated to an operator who requires signature verification through an EOA (i.e. the operator’s `delegationApprover` address is set to a nonzero EOA) + * via the `caller` calling `DelegationManager.delegateToBySignature` + * The function should pass *only with a valid ECDSA signature from the `delegationApprover`, OR if called by the operator or their delegationApprover themselves + * AND with a valid `stakerSignatureAndExpiry` input + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * BeaconChainStrategy and StrategyManager operator shares should increase for operator + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testFuzz_EOAStaker_OperatorWhoRequiresECDSASignature( + address caller, + bytes32 salt, + uint256 expiry, + int256 beaconShares, + uint256 shares + ) public filterFuzzedAddressInputs(caller) { + // filter to only valid `expiry` values + cheats.assume(expiry >= block.timestamp); + cheats.assume(shares > 0); + _registerOperatorWithDelegationApprover(defaultOperator); - IStrategy[] memory strategyArray = new IStrategy[](1); - IERC20[] memory tokensArray = new IERC20[](1); - uint256[] memory shareAmounts = new uint256[](1); + // verify that the salt hasn't been used before + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); { - strategyArray[0] = strategy; - shareAmounts[0] = withdrawalAmount; - tokensArray[0] = mockToken; + // Set staker shares in BeaconChainStrategy and StrategyMananger + IStrategy[] memory strategiesToReturn = new IStrategy[](1); + strategiesToReturn[0] = strategyMock; + uint256[] memory sharesToReturn = new uint256[](1); + sharesToReturn[0] = shares; + strategyManagerMock.setDeposits(strategiesToReturn, sharesToReturn); + eigenPodManagerMock.setPodOwnerShares(defaultStaker, beaconShares); } - ( - IDelegationManager.Withdrawal memory withdrawal, - /* tokensArray */, - /* withdrawalRoot */ - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = false; - - address targetToUse = address(strategyManager); - uint256 msgValueToUse = 0; - bytes memory calldataToUse = abi.encodeWithSelector( - DelegationManager.completeQueuedWithdrawal.selector, - withdrawal, - tokensArray, - middlewareTimesIndex, - receiveAsTokens - ); - reenterer.prepare(targetToUse, msgValueToUse, calldataToUse, bytes("ReentrancyGuard: reentrant call")); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - } - - function testCompleteQueuedWithdrawalRevertsWhenNotCallingFromWithdrawerAddress( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - _tempStakerStorage = address(this); - - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - IStrategy strategy = withdrawal.strategies[0]; - IERC20 token = tokensArray[0]; - - uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); - - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = false; - - cheats.startPrank(address(123456)); - cheats.expectRevert( - bytes( - "DelegationManager.completeQueuedAction: only withdrawer can complete action" - ) + uint256 operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesBefore = delegationManager.operatorShares(defaultStaker, beaconChainETHStrategy); + // fetch the staker's current nonce + uint256 currentStakerNonce = delegationManager.stakerNonce(defaultStaker); + // calculate the staker signature + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + expiry ); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - cheats.stopPrank(); - - uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); - - require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); - } - - function testCompleteQueuedWithdrawalRevertsWhenTryingToCompleteSameWithdrawal2X( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - _tempStakerStorage = address(this); - - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - bytes32 withdrawalRoot - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - IStrategy strategy = withdrawal.strategies[0]; - IERC20 token = tokensArray[0]; - - uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); - - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = false; + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalCompleted(withdrawalRoot); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); - - require(sharesAfter == sharesBefore + withdrawalAmount, "sharesAfter != sharesBefore + withdrawalAmount"); - require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); - sharesBefore = sharesAfter; - balanceBefore = balanceAfter; - - cheats.expectRevert( - bytes( - "DelegationManager.completeQueuedAction: action is not in queue" - ) - ); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - balanceAfter = token.balanceOf(address(_tempStakerStorage)); - - require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); - } - - function testCompleteQueuedWithdrawalRevertsWhenWithdrawalDelayBlocksHasNotPassed( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - _tempStakerStorage = address(this); - - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - uint256 valueToSet = 1; - // set the `withdrawalDelayBlocks` variable - cheats.startPrank(strategyManager.owner()); - uint256 previousValue = delegationManager.withdrawalDelayBlocks(); + emit StakerDelegated(defaultStaker, defaultOperator); cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalDelayBlocksSet(previousValue, valueToSet); - delegationManager.setWithdrawalDelayBlocks(valueToSet); - cheats.stopPrank(); - require( - delegationManager.withdrawalDelayBlocks() == valueToSet, - "delegationManager.withdrawalDelayBlocks() != valueToSet" + emit OperatorSharesIncreased(defaultOperator, defaultStaker, strategyMock, shares); + if (beaconShares > 0) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, defaultStaker, beaconChainETHStrategy, uint256(beaconShares)); + } + _delegateToBySignatureOperatorWhoRequiresSig( + defaultStaker, + caller, + defaultOperator, + stakerSignatureAndExpiry, + salt ); - - cheats.expectRevert( - bytes("DelegationManager.completeQueuedAction: withdrawalDelayBlocks period has not yet passed") + { + // Check operator shares increases + uint256 operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy); + if (beaconShares <= 0) { + assertEq( + beaconSharesBefore, + beaconSharesAfter, + "operator beaconchain shares should not have increased with negative shares" + ); + } else { + assertEq( + beaconSharesBefore + uint256(beaconShares), + beaconSharesAfter, + "operator beaconchain shares not increased correctly" + ); + } + assertEq(operatorSharesBefore + shares, operatorSharesAfter, "operator shares not increased correctly"); + } + assertTrue(delegationManager.isDelegated(defaultStaker), "staker not delegated correctly"); + assertEq( + delegationManager.delegatedTo(defaultStaker), + defaultOperator, + "staker delegated to the wrong address" ); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, /* middlewareTimesIndex */ 0, /* receiveAsTokens */ false); - } - - function testCompleteQueuedWithdrawalRevertsWhenWithdrawalDoesNotExist() external { - uint256 withdrawalAmount = 1e18; - IStrategy strategy = strategyMock; - IERC20 token = strategy.underlyingToken(); + assertFalse(delegationManager.isOperator(defaultStaker), "staker incorrectly registered as operator"); - (IDelegationManager.Withdrawal memory withdrawal, IERC20[] memory tokensArray, ) = _setUpWithdrawalStructSingleStrat( - /*staker*/ address(this), - /*withdrawer*/ address(this), - token, - strategy, - withdrawalAmount + // check that the delegationApprover nonce incremented appropriately + if (caller == defaultOperator || caller == delegationManager.delegationApprover(defaultOperator)) { + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too incorrectly?" ); + } else { + // verify that the salt is marked as used + assertTrue( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent not spent?" + ); + } - uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); - - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = false; - - cheats.expectRevert(bytes("DelegationManager.completeQueuedAction: action is not in queue")); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); - - require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); - } - - function testCompleteQueuedWithdrawalRevertsWhenWithdrawalsPaused( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - _tempStakerStorage = address(this); - - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - IStrategy strategy = withdrawal.strategies[0]; - IERC20 token = tokensArray[0]; - - uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); - - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = false; - - // pause withdrawals - cheats.startPrank(pauser); - delegationManager.pause(2 ** PAUSED_EXIT_WITHDRAWAL_QUEUE); - cheats.stopPrank(); - - cheats.expectRevert(bytes("Pausable: index is paused")); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); - - require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + // check that the staker nonce incremented appropriately + assertEq( + delegationManager.stakerNonce(defaultStaker), + currentStakerNonce + 1, + "staker nonce did not increment" + ); } - function testCompleteQueuedWithdrawalFailsWhenTokensInputLengthMismatch( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - _tempStakerStorage = address(this); - - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - IStrategy strategy = withdrawal.strategies[0]; - IERC20 token = tokensArray[0]; - - uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceBefore = token.balanceOf(address(_tempStakerStorage)); - - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = true; - // mismatch tokens array by setting tokens array to empty array - tokensArray = new IERC20[](0); - - cheats.expectRevert(bytes("DelegationManager.completeQueuedAction: input length mismatch")); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceAfter = token.balanceOf(address(_tempStakerStorage)); - - require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); - } + /** + * @notice `staker` becomes delegated to an operatorwho requires signature verification through an EIP1271-compliant contract (i.e. the operator’s `delegationApprover` address is + * set to a nonzero and code-containing address) via the `caller` calling `DelegationManager.delegateToBySignature` + * The function uses OZ's ERC1271WalletMock contract, and thus should pass *only when a valid ECDSA signature from the `owner` of the ERC1271WalletMock contract, + * OR if called by the operator or their delegationApprover themselves + * AND with a valid `stakerSignatureAndExpiry` input + * Properly emits a `StakerDelegated` event + * Staker is correctly delegated after the call (i.e. correct storage update) + * Reverts if the staker is already delegated (to the operator or to anyone else) + * Reverts if the ‘operator’ is not actually registered as an operator + */ + function testFuzz_EOAStaker_OperatorWhoRequiresEIP1271Signature( + address caller, + bytes32 salt, + uint256 expiry, + int256 beaconShares, + uint256 shares + ) public filterFuzzedAddressInputs(caller) { + cheats.assume(expiry >= block.timestamp); + cheats.assume(shares > 0); - function testCompleteQueuedWithdrawalWithNonzeroWithdrawalDelayBlocks( - uint256 depositAmount, - uint256 withdrawalAmount, - uint16 valueToSet - ) external { - // filter fuzzed inputs to allowed *and nonzero* amounts - cheats.assume(valueToSet <= delegationManager.MAX_WITHDRAWAL_DELAY_BLOCKS() && valueToSet != 0); - cheats.assume(depositAmount != 0 && withdrawalAmount != 0); - cheats.assume(depositAmount >= withdrawalAmount); - address staker = address(this); + _registerOperatorWith1271DelegationApprover(defaultOperator); - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + // verify that the salt hasn't been used before + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too early?" + ); + { + // Set staker shares in BeaconChainStrategy and StrategyMananger + IStrategy[] memory strategiesToReturn = new IStrategy[](1); + strategiesToReturn[0] = strategyMock; + uint256[] memory sharesToReturn = new uint256[](1); + sharesToReturn[0] = shares; + strategyManagerMock.setDeposits(strategiesToReturn, sharesToReturn); + eigenPodManagerMock.setPodOwnerShares(defaultStaker, beaconShares); + } - IStrategy strategy = withdrawal.strategies[0]; - IERC20 token = tokensArray[0]; - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = false; + uint256 operatorSharesBefore = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesBefore = delegationManager.operatorShares(defaultStaker, beaconChainETHStrategy); + // fetch the staker's current nonce + uint256 currentStakerNonce = delegationManager.stakerNonce(defaultStaker); + // calculate the staker signature + ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry = _getStakerSignature( + stakerPrivateKey, + defaultOperator, + expiry + ); - // set the `withdrawalDelayBlocks` variable - cheats.startPrank(delegationManager.owner()); - uint256 previousValue = delegationManager.withdrawalDelayBlocks(); + // delegate from the `staker` to the operator, via having the `caller` call `DelegationManager.delegateToBySignature` cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalDelayBlocksSet(previousValue, valueToSet); - delegationManager.setWithdrawalDelayBlocks(valueToSet); - cheats.stopPrank(); - require( - delegationManager.withdrawalDelayBlocks() == valueToSet, - "strategyManager.withdrawalDelayBlocks() != valueToSet" + emit StakerDelegated(defaultStaker, defaultOperator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, defaultStaker, strategyMock, shares); + if (beaconShares > 0) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, defaultStaker, beaconChainETHStrategy, uint256(beaconShares)); + } + _delegateToBySignatureOperatorWhoRequiresSig( + defaultStaker, + caller, + defaultOperator, + stakerSignatureAndExpiry, + salt ); - cheats.expectRevert( - bytes("DelegationManager.completeQueuedAction: withdrawalDelayBlocks period has not yet passed") + { + // Check operator shares increases + uint256 operatorSharesAfter = delegationManager.operatorShares(defaultOperator, strategyMock); + uint256 beaconSharesAfter = delegationManager.operatorShares(defaultOperator, beaconChainETHStrategy); + if (beaconShares <= 0) { + assertEq( + beaconSharesBefore, + beaconSharesAfter, + "operator beaconchain shares should not have increased with negative shares" + ); + } else { + assertEq( + beaconSharesBefore + uint256(beaconShares), + beaconSharesAfter, + "operator beaconchain shares not increased correctly" + ); + } + assertEq(operatorSharesBefore + shares, operatorSharesAfter, "operator shares not increased correctly"); + } + assertTrue(delegationManager.isDelegated(defaultStaker), "staker not delegated correctly"); + assertEq( + delegationManager.delegatedTo(defaultStaker), + defaultOperator, + "staker delegated to the wrong address" ); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + assertFalse(delegationManager.isOperator(defaultStaker), "staker incorrectly registered as operator"); - uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceBefore = token.balanceOf(address(staker)); + // check that the delegationApprover nonce incremented appropriately + if (caller == defaultOperator || caller == delegationManager.delegationApprover(defaultOperator)) { + // verify that the salt is still marked as unused (since it wasn't checked or used) + assertFalse( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent too incorrectly?" + ); + } else { + // verify that the salt is marked as used + assertTrue( + delegationManager.delegationApproverSaltIsSpent( + delegationManager.delegationApprover(defaultOperator), + salt + ), + "salt somehow spent not spent?" + ); + } - // roll block number forward to one block before the withdrawal should be completeable and attempt again - uint256 originalBlockNumber = block.number; - cheats.roll(originalBlockNumber + valueToSet - 1); - cheats.expectRevert( - bytes("DelegationManager.completeQueuedAction: withdrawalDelayBlocks period has not yet passed") + // check that the staker nonce incremented appropriately + assertEq( + delegationManager.stakerNonce(defaultStaker), + currentStakerNonce + 1, + "staker nonce did not increment" ); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - // roll block number forward to the block at which the withdrawal should be completeable, and complete it - cheats.roll(originalBlockNumber + valueToSet); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceAfter = token.balanceOf(address(staker)); - - require(sharesAfter == sharesBefore + withdrawalAmount, "sharesAfter != sharesBefore + withdrawalAmount"); - require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); } + /** + * @notice Calls same delegateToBySignature test but with the staker address being a ERC1271WalletMock + * Generates valid signatures from the staker to delegate to operator `defaultOperator` + */ + function testFuzz_ERC1271Staker_OperatorWhoAcceptsAllStakers( + address caller, + uint256 expiry, + int256 beaconShares, + uint256 shares + ) public filterFuzzedAddressInputs(caller) { + defaultStaker = address(ERC1271WalletMock(cheats.addr(stakerPrivateKey))); + testFuzz_EOAStaker_OperatorWhoAcceptsAllStakers(caller, expiry, beaconShares, shares); + } - function testCompleteQueuedWithdrawal_ReceiveAsTokensMarkedFalse( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - address staker = address(this); - - // testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - bytes32 withdrawalRoot - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); - - IStrategy strategy = withdrawal.strategies[0]; - IERC20 token = tokensArray[0]; - - uint256 sharesBefore = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceBefore = token.balanceOf(address(staker)); + /** + * @notice Calls same delegateToBySignature test but with the staker address being a ERC1271WalletMock + * Generates valid signatures from the staker to delegate to operator `defaultOperator` who has + * a delegationApprover address set to a nonzero EOA + */ + function testFuzz_ERC1271Staker_OperatorWhoRequiresECDSASignature( + address caller, + bytes32 salt, + uint256 expiry, + int256 beaconShares, + uint256 shares + ) public filterFuzzedAddressInputs(caller) { + // Call same test but with the staker address being a ERC1271WalletMock + defaultStaker = address(ERC1271WalletMock(cheats.addr(stakerPrivateKey))); + testFuzz_EOAStaker_OperatorWhoRequiresECDSASignature(caller, salt, expiry, beaconShares, shares); + } - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = false; - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalCompleted(withdrawalRoot); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + /** + * @notice Calls same delegateToBySignature test but with the staker address being a ERC1271WalletMock + * Generates valid signatures from the staker to delegate to operator `defaultOperator` who has + * a delegationApprover address set to a nonzero ERC1271 compliant contract + */ + function testFuzz_ERC1271Staker_OperatorWhoRequiresEIP1271Signature( + address caller, + bytes32 salt, + uint256 expiry, + int256 beaconShares, + uint256 shares + ) public filterFuzzedAddressInputs(caller) { + // Call same test but with the staker address being a ERC1271WalletMock + defaultStaker = address(ERC1271WalletMock(cheats.addr(stakerPrivateKey))); + testFuzz_EOAStaker_OperatorWhoRequiresEIP1271Signature(caller, salt, expiry, beaconShares, shares); + } +} - uint256 sharesAfter = strategyManager.stakerStrategyShares(address(this), strategy); - uint256 balanceAfter = token.balanceOf(address(staker)); +contract DelegationManagerUnitTests_ShareAdjustment is DelegationManagerUnitTests { + // @notice Verifies that `DelegationManager.increaseDelegatedShares` reverts if not called by the StrategyManager nor EigenPodManager + function testFuzz_increaseDelegatedShares_revert_invalidCaller( + address invalidCaller, + uint256 shares + ) public filterFuzzedAddressInputs(invalidCaller) { + cheats.assume(invalidCaller != address(strategyManagerMock)); + cheats.assume(invalidCaller != address(eigenPodManagerMock)); - require(sharesAfter == sharesBefore + withdrawalAmount, "sharesAfter != sharesBefore + withdrawalAmount"); - require(balanceAfter == balanceBefore, "balanceAfter != balanceBefore"); + cheats.prank(invalidCaller); + cheats.expectRevert("DelegationManager: onlyStrategyManagerOrEigenPodManager"); + delegationManager.increaseDelegatedShares(invalidCaller, strategyMock, shares); } - function testCompleteQueuedWithdrawal_ReceiveAsTokensMarkedTrue( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - cheats.assume(withdrawalAmount != 0 && withdrawalAmount <= depositAmount); - address staker = address(this); + // @notice Verifies that there is no change in shares if the staker is not delegated + function testFuzz_increaseDelegatedShares_noop(address staker) public { + cheats.assume(staker != defaultOperator); + _registerOperatorWithBaseDetails(defaultOperator); + assertFalse(delegationManager.isDelegated(staker), "bad test setup"); - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - bytes32 withdrawalRoot - ) = testQueueWithdrawal_ToSelf(depositAmount, withdrawalAmount); + cheats.prank(address(strategyManagerMock)); + delegationManager.increaseDelegatedShares(staker, strategyMock, 1); + assertEq(delegationManager.operatorShares(defaultOperator, strategyMock), 0, "shares should not have changed"); + } - IStrategy strategy = withdrawal.strategies[0]; - IERC20 token = tokensArray[0]; + /** + * @notice Verifies that `DelegationManager.increaseDelegatedShares` properly increases the delegated `shares` that the operator + * who the `staker` is delegated to has in the strategy + * @dev Checks that there is no change if the staker is not delegated + */ + function testFuzz_increaseDelegatedShares( + address staker, + uint256 shares, + bool delegateFromStakerToOperator + ) public { + // filter inputs, since delegating to the operator will fail when the staker is already registered as an operator + cheats.assume(staker != defaultOperator); - uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); - uint256 balanceBefore = token.balanceOf(address(staker)); + // Register operator + _registerOperatorWithBaseDetails(defaultOperator); - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = true; - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalCompleted(withdrawalRoot); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); - - uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategy); - uint256 balanceAfter = token.balanceOf(address(staker)); - - require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - require(balanceAfter == balanceBefore + withdrawalAmount, "balanceAfter != balanceBefore + withdrawalAmount"); - if (depositAmount == withdrawalAmount) { - // Since receiving tokens instead of shares, if withdrawal amount is entire deposit, then strategy will be removed - // with sharesAfter being 0 - require( - !_isDepositedStrategy(staker, strategy), - "Strategy still part of staker's deposited strategies" - ); - require(sharesAfter == 0, "staker shares is not 0"); + // delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'* + if (delegateFromStakerToOperator) { + _delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator); } - } - - function testCompleteQueuedWithdrawalFullyWithdraw(uint256 amount) external { - address staker = address(this); - ( - IDelegationManager.Withdrawal memory withdrawal, - IERC20[] memory tokensArray, - bytes32 withdrawalRoot - ) = testQueueWithdrawal_ToSelf(amount, amount); - - IStrategy strategy = withdrawal.strategies[0]; - IERC20 token = tokensArray[0]; - uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); - uint256 balanceBefore = token.balanceOf(address(staker)); + uint256 _delegatedSharesBefore = delegationManager.operatorShares( + delegationManager.delegatedTo(staker), + strategyMock + ); - uint256 middlewareTimesIndex = 0; - bool receiveAsTokens = true; - cheats.expectEmit(true, true, true, true, address(delegationManager)); - emit WithdrawalCompleted(withdrawalRoot); - delegationManager.completeQueuedWithdrawal(withdrawal, tokensArray, middlewareTimesIndex, receiveAsTokens); + if (delegationManager.isDelegated(staker)) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesIncreased(defaultOperator, staker, strategyMock, shares); + } - uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategy); - uint256 balanceAfter = token.balanceOf(address(staker)); + cheats.prank(address(strategyManagerMock)); + delegationManager.increaseDelegatedShares(staker, strategyMock, shares); - require(sharesAfter == sharesBefore, "sharesAfter != sharesBefore"); - require(balanceAfter == balanceBefore + amount, "balanceAfter != balanceBefore + withdrawalAmount"); - require( - !_isDepositedStrategy(staker, strategy), - "Strategy still part of staker's deposited strategies" + uint256 delegatedSharesAfter = delegationManager.operatorShares( + delegationManager.delegatedTo(staker), + strategyMock ); - require(sharesAfter == 0, "staker shares is not 0"); - } - - function test_removeSharesRevertsWhenShareAmountIsZero(uint256 depositAmount) external { - _setUpWithdrawalTests(); - address staker = address(this); - uint256 withdrawalAmount = 0; - - _depositIntoStrategySuccessfully(strategyMock, staker, depositAmount); - (IDelegationManager.Withdrawal memory withdrawal, , ) = _setUpWithdrawalStructSingleStrat( - /*staker*/ address(this), - /*withdrawer*/ address(this), - mockToken, - strategyMock, - withdrawalAmount + if (delegationManager.isDelegated(staker)) { + assertEq( + delegatedSharesAfter, + _delegatedSharesBefore + shares, + "delegated shares did not increment correctly" ); - - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: withdrawal.strategies, - shares: withdrawal.shares, - withdrawer: staker - }); - - cheats.expectRevert(bytes("StrategyManager._removeShares: shareAmount should not be zero!")); - delegationManager.queueWithdrawals( - params - ); + } else { + assertEq(delegatedSharesAfter, _delegatedSharesBefore, "delegated shares incremented incorrectly"); + assertEq(_delegatedSharesBefore, 0, "nonzero shares delegated to zero address!"); + } } - function test_removeSharesRevertsWhenShareAmountIsTooLarge( - uint256 depositAmount, - uint256 withdrawalAmount - ) external { - _setUpWithdrawalTests(); - cheats.assume(depositAmount > 0 && withdrawalAmount > depositAmount); - address staker = address(this); - - _depositIntoStrategySuccessfully(strategyMock, staker, depositAmount); + // @notice Verifies that `DelegationManager.decreaseDelegatedShares` reverts if not called by the StrategyManager nor EigenPodManager + function testFuzz_decreaseDelegatedShares_revert_invalidCaller( + address invalidCaller, + uint256 shares + ) public filterFuzzedAddressInputs(invalidCaller) { + cheats.assume(invalidCaller != address(strategyManagerMock)); + cheats.assume(invalidCaller != address(eigenPodManagerMock)); - (IDelegationManager.Withdrawal memory withdrawal, , ) = _setUpWithdrawalStructSingleStrat( - /*staker*/ address(this), - /*withdrawer*/ address(this), - mockToken, - strategyMock, - withdrawalAmount - ); + cheats.startPrank(invalidCaller); + cheats.expectRevert("DelegationManager: onlyStrategyManagerOrEigenPodManager"); + delegationManager.decreaseDelegatedShares(invalidCaller, strategyMock, shares); + } - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: withdrawal.strategies, - shares: withdrawal.shares, - withdrawer: address(this) - }); + // @notice Verifies that there is no change in shares if the staker is not delegated + function testFuzz_decreaseDelegatedShares_noop(address staker) public { + cheats.assume(staker != defaultOperator); + _registerOperatorWithBaseDetails(defaultOperator); + assertFalse(delegationManager.isDelegated(staker), "bad test setup"); - cheats.expectRevert(bytes("StrategyManager._removeShares: shareAmount too high")); - delegationManager.queueWithdrawals( - params - ); + cheats.prank(address(strategyManagerMock)); + delegationManager.decreaseDelegatedShares(staker, strategyMock, 1); + assertEq(delegationManager.operatorShares(defaultOperator, strategyMock), 0, "shares should not have changed"); } /** - * Testing queueWithdrawal of 3 strategies, fuzzing the deposit and withdraw amounts. if the withdrawal amounts == deposit amounts - * then the strategy should be removed from the staker StrategyList + * @notice Verifies that `DelegationManager.decreaseDelegatedShares` properly decreases the delegated `shares` that the operator + * who the `staker` is delegated to has in the strategies + * @dev Checks that there is no change if the staker is not delegated */ - function test_removeStrategyFromStakerStrategyList(uint256[3] memory depositAmounts, uint256[3] memory withdrawalAmounts) external { - _setUpWithdrawalTests(); - // filtering of fuzzed inputs - cheats.assume(withdrawalAmounts[0] > 0 && withdrawalAmounts[0] <= depositAmounts[0]); - cheats.assume(withdrawalAmounts[1] > 0 && withdrawalAmounts[1] <= depositAmounts[1]); - cheats.assume(withdrawalAmounts[2] > 0 && withdrawalAmounts[2] <= depositAmounts[2]); - address staker = address(this); - - // Setup input params - IStrategy[] memory strategies = new IStrategy[](3); - strategies[0] = strategyMock; - strategies[1] = strategyMock2; - strategies[2] = strategyMock3; - uint256[] memory amounts = new uint256[](3); - amounts[0] = withdrawalAmounts[0]; - amounts[1] = withdrawalAmounts[1]; - amounts[2] = withdrawalAmounts[2]; - - _depositIntoStrategySuccessfully(strategies[0], staker, depositAmounts[0]); - _depositIntoStrategySuccessfully(strategies[1], staker, depositAmounts[1]); - _depositIntoStrategySuccessfully(strategies[2], staker, depositAmounts[2]); - - ( ,bytes32 withdrawalRoot) = _setUpWithdrawalStruct_MultipleStrategies( - /* staker */ staker, - /* withdrawer */ staker, - strategies, - amounts - ); - require(!delegationManager.pendingWithdrawals(withdrawalRoot), "withdrawalRootPendingBefore is true!"); - delegationManager.cumulativeWithdrawalsQueued(staker); - uint256[] memory sharesBefore = new uint256[](3); - sharesBefore[0] = strategyManager.stakerStrategyShares(staker, strategies[0]); - sharesBefore[1] = strategyManager.stakerStrategyShares(staker, strategies[1]); - sharesBefore[2] = strategyManager.stakerStrategyShares(staker, strategies[2]); - - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: strategies, - shares: amounts, - withdrawer: address(this) - }); + function testFuzz_decreaseDelegatedShares( + address staker, + IStrategy[] memory strategies, + uint128 shares, + bool delegateFromStakerToOperator + ) public filterFuzzedAddressInputs(staker) { + // sanity-filtering on fuzzed input length & staker + cheats.assume(strategies.length <= 32); + cheats.assume(staker != defaultOperator); - delegationManager.queueWithdrawals( - params - ); + // Register operator + _registerOperatorWithBaseDetails(defaultOperator); - uint256[] memory sharesAfter = new uint256[](3); - sharesAfter[0] = strategyManager.stakerStrategyShares(staker, strategies[0]); - sharesAfter[1] = strategyManager.stakerStrategyShares(staker, strategies[1]); - sharesAfter[2] = strategyManager.stakerStrategyShares(staker, strategies[2]); - require(sharesBefore[0] == sharesAfter[0] + withdrawalAmounts[0], "Strat1: sharesBefore != sharesAfter + withdrawalAmount"); - if (depositAmounts[0] == withdrawalAmounts[0]) { - require(!_isDepositedStrategy(staker, strategies[0]), "Strategy still part of staker's deposited strategies"); - } - require(sharesBefore[1] == sharesAfter[1] + withdrawalAmounts[1], "Strat2: sharesBefore != sharesAfter + withdrawalAmount"); - if (depositAmounts[1] == withdrawalAmounts[1]) { - require(!_isDepositedStrategy(staker, strategies[1]), "Strategy still part of staker's deposited strategies"); - } - require(sharesBefore[2] == sharesAfter[2] + withdrawalAmounts[2], "Strat3: sharesBefore != sharesAfter + withdrawalAmount"); - if (depositAmounts[2] == withdrawalAmounts[2]) { - require(!_isDepositedStrategy(staker, strategies[2]), "Strategy still part of staker's deposited strategies"); + // delegate from the `staker` to the operator *if `delegateFromStakerToOperator` is 'true'* + if (delegateFromStakerToOperator) { + _delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator); } - } - - // ensures that when the staker and withdrawer are different and a withdrawal is completed as shares (i.e. not as tokens) - // that the shares get added back to the right operator - function test_completingWithdrawalAsSharesAddsSharesToCorrectOperator() external { - address staker = address(this); - address withdrawer = address(1000); - address operator_for_staker = address(1001); - address operator_for_withdrawer = address(1002); - // register operators - bytes32 salt; - IDelegationManager.OperatorDetails memory operatorDetails = IDelegationManager.OperatorDetails({ - earningsReceiver: operator_for_staker, - delegationApprover: address(0), - stakerOptOutWindowBlocks: 0 - }); - testRegisterAsOperator(operator_for_staker, operatorDetails, emptyStringForMetadataURI); - testRegisterAsOperator(operator_for_withdrawer, operatorDetails, emptyStringForMetadataURI); + uint256[] memory sharesInputArray = new uint256[](strategies.length); - // delegate from the `staker` and withdrawer to the operators - ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry; - cheats.startPrank(staker); - delegationManager.delegateTo(operator_for_staker, approverSignatureAndExpiry, salt); - cheats.stopPrank(); - cheats.startPrank(withdrawer); - delegationManager.delegateTo(operator_for_withdrawer, approverSignatureAndExpiry, salt); - cheats.stopPrank(); + address delegatedTo = delegationManager.delegatedTo(staker); - // Setup input params - IStrategy[] memory strategies = new IStrategy[](3); - strategies[0] = strategyMock; - strategies[1] = delegationManager.beaconChainETHStrategy(); - strategies[2] = strategyMock3; - uint256[] memory amounts = new uint256[](3); - amounts[0] = 1e18; - amounts[1] = 2e18; - amounts[2] = 3e18; - - (IDelegationManager.Withdrawal memory withdrawal, ) = _setUpWithdrawalStruct_MultipleStrategies({ - staker: staker, - withdrawer: withdrawer, - strategyArray: strategies, - shareAmounts: amounts - }); - - // give both the operators a bunch of delegated shares, so we can decrement them when queuing the withdrawal - cheats.startPrank(address(delegationManager.strategyManager())); + // for each strategy in `strategies`, increase delegated shares by `shares` + // noop if the staker is not delegated + cheats.startPrank(address(strategyManagerMock)); for (uint256 i = 0; i < strategies.length; ++i) { - delegationManager.increaseDelegatedShares(staker, strategies[i], amounts[i]); - delegationManager.increaseDelegatedShares(withdrawer, strategies[i], amounts[i]); + delegationManager.increaseDelegatedShares(staker, strategies[i], shares); + // store delegated shares in a mapping + delegatedSharesBefore[strategies[i]] = delegationManager.operatorShares(delegatedTo, strategies[i]); + // also construct an array which we'll use in another loop + sharesInputArray[i] = shares; + totalSharesForStrategyInArray[address(strategies[i])] += sharesInputArray[i]; } cheats.stopPrank(); - IDelegationManager.QueuedWithdrawalParams[] memory params = new IDelegationManager.QueuedWithdrawalParams[](1); - - params[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: strategies, - shares: amounts, - withdrawer: withdrawer - }); - - // queue the withdrawal - cheats.startPrank(staker); - delegationManager.queueWithdrawals(params); - cheats.stopPrank(); + bool isDelegated = delegationManager.isDelegated(staker); - for (uint256 i = 0; i < strategies.length; ++i) { - require(delegationManager.operatorShares(operator_for_staker, strategies[i]) == 0, - "staker operator shares incorrect after queueing"); - require(delegationManager.operatorShares(operator_for_withdrawer, strategies[i]) == amounts[i], - "withdrawer operator shares incorrect after queuing"); + // for each strategy in `strategies`, decrease delegated shares by `shares` + { + cheats.startPrank(address(strategyManagerMock)); + address operatorToDecreaseSharesOf = delegationManager.delegatedTo(staker); + if (isDelegated) { + for (uint256 i = 0; i < strategies.length; ++i) { + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit OperatorSharesDecreased( + operatorToDecreaseSharesOf, + staker, + strategies[i], + sharesInputArray[i] + ); + delegationManager.decreaseDelegatedShares(staker, strategies[i], sharesInputArray[i]); + } + } + cheats.stopPrank(); } - // complete the withdrawal - cheats.startPrank(withdrawer); - IERC20[] memory tokens; - delegationManager.completeQueuedWithdrawal( - withdrawal, - tokens, - 0 /*middlewareTimesIndex*/, - false /*receiveAsTokens*/ - ); - cheats.stopPrank(); - + // check shares after call to `decreaseDelegatedShares` for (uint256 i = 0; i < strategies.length; ++i) { - if (strategies[i] != delegationManager.beaconChainETHStrategy()) { - require(delegationManager.operatorShares(operator_for_staker, strategies[i]) == 0, - "staker operator shares incorrect after completing withdrawal"); - require(delegationManager.operatorShares(operator_for_withdrawer, strategies[i]) == 2 * amounts[i], - "withdrawer operator shares incorrect after completing withdrawal"); + uint256 delegatedSharesAfter = delegationManager.operatorShares(delegatedTo, strategies[i]); + + if (isDelegated) { + assertEq( + delegatedSharesAfter + totalSharesForStrategyInArray[address(strategies[i])], + delegatedSharesBefore[strategies[i]], + "delegated shares did not decrement correctly" + ); + assertEq(delegatedSharesAfter, 0, "nonzero shares delegated to"); } else { - require(delegationManager.operatorShares(operator_for_staker, strategies[i]) == amounts[i], - "staker operator beaconChainETHStrategy shares incorrect after completing withdrawal"); - require(delegationManager.operatorShares(operator_for_withdrawer, strategies[i]) == amounts[i], - "withdrawer operator beaconChainETHStrategy shares incorrect after completing withdrawal"); + assertEq( + delegatedSharesAfter, + delegatedSharesBefore[strategies[i]], + "delegated shares decremented incorrectly" + ); + assertEq(delegatedSharesBefore[strategies[i]], 0, "nonzero shares delegated to zero address!"); } } } +} - /** - * INTERNAL / HELPER FUNCTIONS - */ - - /** - * Setup DelegationManager and StrategyManager contracts for testing instead of using StrategyManagerMock - * since we need to test the actual contracts together for the withdrawal queueing tests - */ - function _setUpWithdrawalTests() internal { - delegationManagerImplementation = new DelegationManager(strategyManager, slasherMock, eigenPodManagerMock); - cheats.startPrank(eigenLayerProxyAdmin.owner()); - eigenLayerProxyAdmin.upgrade(TransparentUpgradeableProxy(payable(address(delegationManager))), address(delegationManagerImplementation)); - cheats.stopPrank(); - - - strategyImplementation = new StrategyBase(strategyManager); - mockToken = new ERC20Mock(); - strategyMock = StrategyBase( - address( - new TransparentUpgradeableProxy( - address(strategyImplementation), - address(eigenLayerProxyAdmin), - abi.encodeWithSelector(StrategyBase.initialize.selector, mockToken, eigenLayerPauserReg) - ) - ) - ); - strategyMock2 = StrategyBase( - address( - new TransparentUpgradeableProxy( - address(strategyImplementation), - address(eigenLayerProxyAdmin), - abi.encodeWithSelector(StrategyBase.initialize.selector, mockToken, eigenLayerPauserReg) - ) - ) - ); - strategyMock3 = StrategyBase( - address( - new TransparentUpgradeableProxy( - address(strategyImplementation), - address(eigenLayerProxyAdmin), - abi.encodeWithSelector(StrategyBase.initialize.selector, mockToken, eigenLayerPauserReg) - ) - ) - ); - - // whitelist the strategy for deposit - cheats.startPrank(strategyManager.owner()); - IStrategy[] memory _strategies = new IStrategy[](3); - _strategies[0] = strategyMock; - _strategies[1] = strategyMock2; - _strategies[2] = strategyMock3; - strategyManager.addStrategiesToDepositWhitelist(_strategies); - cheats.stopPrank(); +contract DelegationManagerUnitTests_Undelegate is DelegationManagerUnitTests { + // @notice Verifies that undelegating is not possible when the "undelegation paused" switch is flipped + function test_undelegate_revert_paused(address staker) public filterFuzzedAddressInputs(staker) { + // set the pausing flag + cheats.prank(pauser); + delegationManager.pause(2 ** PAUSED_ENTER_WITHDRAWAL_QUEUE); - require(delegationManager.strategyManager() == strategyManager, - "constructor / initializer incorrect, strategyManager set wrong"); + cheats.prank(staker); + cheats.expectRevert("Pausable: index is paused"); + delegationManager.undelegate(staker); } - function _depositIntoStrategySuccessfully( - IStrategy strategy, - address staker, - uint256 amount - ) internal { - IERC20 token = strategy.underlyingToken(); - // IStrategy strategy = strategyMock; - - // filter out zero case since it will revert with "StrategyManager._addShares: shares should not be zero!" - cheats.assume(amount != 0); - // filter out zero address because the mock ERC20 we are using will revert on using it - cheats.assume(staker != address(0)); - // filter out the strategy itself from fuzzed inputs - cheats.assume(staker != address(strategy)); - // sanity check / filter - cheats.assume(amount <= token.balanceOf(address(this))); - cheats.assume(amount >= 1); + function testFuzz_undelegate_revert_notDelgated( + address undelegatedStaker + ) public filterFuzzedAddressInputs(undelegatedStaker) { + cheats.assume(undelegatedStaker != defaultOperator); + assertFalse(delegationManager.isDelegated(undelegatedStaker), "bad test setup"); - uint256 sharesBefore = strategyManager.stakerStrategyShares(staker, strategy); - uint256 stakerStrategyListLengthBefore = strategyManager.stakerStrategyListLength(staker); + cheats.prank(undelegatedStaker); + cheats.expectRevert("DelegationManager.undelegate: staker must be delegated to undelegate"); + delegationManager.undelegate(undelegatedStaker); + } - // needed for expecting an event with the right parameters - uint256 expectedShares = strategy.underlyingToShares(amount); + // @notice Verifies that an operator cannot undelegate from themself (this should always be forbidden) + function testFuzz_undelegate_revert_stakerIsOperator(address operator) public filterFuzzedAddressInputs(operator) { + _registerOperatorWithBaseDetails(operator); - cheats.startPrank(staker); - cheats.expectEmit(true, true, true, true, address(strategyManager)); - emit Deposit(staker, token, strategy, expectedShares); - uint256 shares = strategyManager.depositIntoStrategy(strategy, token, amount); - cheats.stopPrank(); + cheats.prank(operator); + cheats.expectRevert("DelegationManager.undelegate: operators cannot be undelegated"); + delegationManager.undelegate(operator); + } - uint256 sharesAfter = strategyManager.stakerStrategyShares(staker, strategy); - uint256 stakerStrategyListLengthAfter = strategyManager.stakerStrategyListLength(staker); + /** + * @notice verifies that `DelegationManager.undelegate` reverts if trying to undelegate an operator from themselves + * @param callFromOperatorOrApprover -- calls from the operator if 'false' and the 'approver' if true + */ + function testFuzz_undelegate_operatorCannotForceUndelegateThemself( + address delegationApprover, + bool callFromOperatorOrApprover + ) public { + // register *this contract* as an operator with the default `delegationApprover` + _registerOperatorWithDelegationApprover(defaultOperator); - require(sharesAfter == sharesBefore + shares, "sharesAfter != sharesBefore + shares"); - if (sharesBefore == 0) { - require( - stakerStrategyListLengthAfter == stakerStrategyListLengthBefore + 1, - "stakerStrategyListLengthAfter != stakerStrategyListLengthBefore + 1" - ); - require( - strategyManager.stakerStrategyList(staker, stakerStrategyListLengthAfter - 1) == strategy, - "strategyManager.stakerStrategyList(staker, stakerStrategyListLengthAfter - 1) != strategy" - ); + address caller; + if (callFromOperatorOrApprover) { + caller = delegationApprover; + } else { + caller = defaultOperator; } - } - function _setUpWithdrawalStructSingleStrat(address staker, address withdrawer, IERC20 token, IStrategy strategy, uint256 shareAmount) - internal view returns (IDelegationManager.Withdrawal memory queuedWithdrawal, IERC20[] memory tokensArray, bytes32 withdrawalRoot) - { - IStrategy[] memory strategyArray = new IStrategy[](1); - tokensArray = new IERC20[](1); - uint256[] memory shareAmounts = new uint256[](1); - strategyArray[0] = strategy; - tokensArray[0] = token; - shareAmounts[0] = shareAmount; - queuedWithdrawal = - IDelegationManager.Withdrawal({ - strategies: strategyArray, - shares: shareAmounts, - staker: staker, - withdrawer: withdrawer, - nonce: delegationManager.cumulativeWithdrawalsQueued(staker), - startBlock: uint32(block.number), - delegatedTo: delegationManager.delegatedTo(staker) - } - ); - // calculate the withdrawal root - withdrawalRoot = delegationManager.calculateWithdrawalRoot(queuedWithdrawal); - return (queuedWithdrawal, tokensArray, withdrawalRoot); + // try to call the `undelegate` function and check for reversion + cheats.prank(caller); + cheats.expectRevert("DelegationManager.undelegate: operators cannot be undelegated"); + delegationManager.undelegate(defaultOperator); } - function _setUpWithdrawalStruct_MultipleStrategies( - address staker, - address withdrawer, - IStrategy[] memory strategyArray, - uint256[] memory shareAmounts - ) - internal view returns (IDelegationManager.Withdrawal memory withdrawal, bytes32 withdrawalRoot) - { - withdrawal = - IDelegationManager.Withdrawal({ - strategies: strategyArray, - shares: shareAmounts, - staker: staker, - withdrawer: withdrawer, - nonce: delegationManager.cumulativeWithdrawalsQueued(staker), - startBlock: uint32(block.number), - delegatedTo: delegationManager.delegatedTo(staker) - } - ); - // calculate the withdrawal root - withdrawalRoot = delegationManager.calculateWithdrawalRoot(withdrawal); - return (withdrawal, withdrawalRoot); - } + //TODO: verify that this check is even needed + function test_undelegate_revert_zeroAddress() public { + _registerOperatorWithBaseDetails(defaultOperator); + _delegateToOperatorWhoAcceptsAllStakers(address(0), defaultOperator); - function _setUpWithdrawalStructSingleStrat_MultipleStrategies( - address staker, - address withdrawer, - IStrategy[] memory strategyArray, - uint256[] memory shareAmounts - ) - internal view returns (IDelegationManager.Withdrawal memory queuedWithdrawal, bytes32 withdrawalRoot) - { - queuedWithdrawal = - IDelegationManager.Withdrawal({ - staker: staker, - withdrawer: withdrawer, - nonce: delegationManager.cumulativeWithdrawalsQueued(staker), - startBlock: uint32(block.number), - delegatedTo: delegationManager.delegatedTo(staker), - strategies: strategyArray, - shares: shareAmounts - } - ); - // calculate the withdrawal root - withdrawalRoot = delegationManager.calculateWithdrawalRoot(queuedWithdrawal); - return (queuedWithdrawal, withdrawalRoot); + cheats.prank(address(0)); + cheats.expectRevert("DelegationManager.undelegate: cannot undelegate zero address"); + delegationManager.undelegate(address(0)); } /** - * @notice internal function for calculating a signature from the delegationSigner corresponding to `_delegationSignerPrivateKey`, approving - * the `staker` to delegate to `operator`, with the specified `salt`, and expiring at `expiry`. + * @notice Verifies that the `undelegate` function has proper access controls (can only be called by the operator who the `staker` has delegated + * to or the operator's `delegationApprover`), or the staker themselves */ - function _getApproverSignature(uint256 _delegationSignerPrivateKey, address staker, address operator, bytes32 salt, uint256 expiry) - internal view returns (ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry) - { - approverSignatureAndExpiry.expiry = expiry; - { - bytes32 digestHash = - delegationManager.calculateDelegationApprovalDigestHash(staker, operator, delegationManager.delegationApprover(operator), salt, expiry); - (uint8 v, bytes32 r, bytes32 s) = cheats.sign(_delegationSignerPrivateKey, digestHash); - approverSignatureAndExpiry.signature = abi.encodePacked(r, s, v); - } - return approverSignatureAndExpiry; + function testFuzz_undelegate_revert_invalidCaller( + address invalidCaller + ) public filterFuzzedAddressInputs(invalidCaller) { + address staker = address(0x123); + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + // filter out addresses that are actually allowed to call the function + cheats.assume(invalidCaller != staker); + cheats.assume(invalidCaller != defaultOperator); + cheats.assume(invalidCaller != delegationApprover); + + _registerOperatorWithDelegationApprover(defaultOperator); + _delegateToOperatorWhoRequiresSig(staker, defaultOperator); + + cheats.prank(invalidCaller); + cheats.expectRevert("DelegationManager.undelegate: caller cannot undelegate staker"); + delegationManager.undelegate(staker); } /** - * @notice internal function for calculating a signature from the staker corresponding to `_stakerPrivateKey`, delegating them to - * the `operator`, and expiring at `expiry`. + * Staker is undelegated from an operator, via a call to `undelegate`, properly originating from the staker's address. + * Reverts if the staker is themselves an operator (i.e. they are delegated to themselves) + * Does nothing if the staker is already undelegated + * Properly undelegates the staker, i.e. the staker becomes “delegated to” the zero address, and `isDelegated(staker)` returns ‘false’ + * Emits a `StakerUndelegated` event */ - function _getStakerSignature(uint256 _stakerPrivateKey, address operator, uint256 expiry) - internal view returns (ISignatureUtils.SignatureWithExpiry memory stakerSignatureAndExpiry) - { - address staker = cheats.addr(stakerPrivateKey); - stakerSignatureAndExpiry.expiry = expiry; - { - bytes32 digestHash = delegationManager.calculateCurrentStakerDelegationDigestHash(staker, operator, expiry); - (uint8 v, bytes32 r, bytes32 s) = cheats.sign(_stakerPrivateKey, digestHash); - stakerSignatureAndExpiry.signature = abi.encodePacked(r, s, v); - } - return stakerSignatureAndExpiry; + function testFuzz_undelegate_noDelegateableShares(address staker) public filterFuzzedAddressInputs(staker) { + // register *this contract* as an operator and delegate from the `staker` to them + _registerOperatorWithBaseDetails(defaultOperator); + _delegateToOperatorWhoAcceptsAllStakers(staker, defaultOperator); + + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerUndelegated(staker, delegationManager.delegatedTo(staker)); + cheats.prank(staker); + bytes32 withdrawalRoot = delegationManager.undelegate(staker); + + assertEq(withdrawalRoot, bytes32(0), "withdrawalRoot should be zero"); + assertEq( + delegationManager.delegatedTo(staker), + address(0), + "undelegated staker should be delegated to zero address" + ); + assertFalse(delegationManager.isDelegated(staker), "staker not undelegated"); } /** - * @notice internal function to help check if a strategy is part of list of deposited strategies for a staker - * Used to check if removed correctly after withdrawing all shares for a given strategy + * @notice Verifies that the `undelegate` function allows for a force undelegation */ - function _isDepositedStrategy(address staker, IStrategy strategy) internal view returns (bool) { - uint256 stakerStrategyListLength = strategyManager.stakerStrategyListLength(staker); - for (uint256 i = 0; i < stakerStrategyListLength; ++i) { - if (strategyManager.stakerStrategyList(staker, i) == strategy) { - return true; - } + function testFuzz_undelegate_forceUndelegation_noDelegateableShares( + address staker, + bytes32 salt, + bool callFromOperatorOrApprover + ) public filterFuzzedAddressInputs(staker) { + cheats.assume(staker != defaultOperator); + address delegationApprover = cheats.addr(delegationSignerPrivateKey); + + _registerOperatorWithDelegationApprover(defaultOperator); + _delegateToOperatorWhoRequiresSig(staker, defaultOperator, salt); + + address caller; + if (callFromOperatorOrApprover) { + caller = delegationApprover; + } else { + caller = defaultOperator; } - return false; + + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerForceUndelegated(staker, defaultOperator); + cheats.expectEmit(true, true, true, true, address(delegationManager)); + emit StakerUndelegated(staker, defaultOperator); + cheats.prank(caller); + bytes32 withdrawalRoot = delegationManager.undelegate(staker); + + assertEq(withdrawalRoot, bytes32(0), "withdrawalRoot should be zero"); + assertEq( + delegationManager.delegatedTo(staker), + address(0), + "undelegated staker should be delegated to zero address" + ); + assertFalse(delegationManager.isDelegated(staker), "staker not undelegated"); } } diff --git a/src/test/unit/EigenPod-PodManagerUnit.t.sol b/src/test/unit/EigenPod-PodManagerUnit.t.sol index 5a0cea727..2fdd83fb4 100644 --- a/src/test/unit/EigenPod-PodManagerUnit.t.sol +++ b/src/test/unit/EigenPod-PodManagerUnit.t.sol @@ -1,53 +1,653 @@ -///@notice Placeholder for future unit tests that combine interaction between the EigenPod & EigenPodManager - -// TODO: salvage / re-implement a check for reentrancy guard on functions, as possible - // function testRecordBeaconChainETHBalanceUpdateFailsWhenReentering() public { - // uint256 amount = 1e18; - // uint256 amount2 = 2e18; - // address staker = address(this); - // uint256 beaconChainETHStrategyIndex = 0; - - // _beaconChainReentrancyTestsSetup(); - - // testRestakeBeaconChainETHSuccessfully(staker, amount); - - // address targetToUse = address(strategyManager); - // uint256 msgValueToUse = 0; - - // int256 amountDelta = int256(amount2 - amount); - // // reference: function recordBeaconChainETHBalanceUpdate(address podOwner, uint256 beaconChainETHStrategyIndex, uint256 sharesDelta, bool isNegative) - // bytes memory calldataToUse = abi.encodeWithSelector(StrategyManager.recordBeaconChainETHBalanceUpdate.selector, staker, beaconChainETHStrategyIndex, amountDelta); - // reenterer.prepare(targetToUse, msgValueToUse, calldataToUse, bytes("ReentrancyGuard: reentrant call")); - - // cheats.startPrank(address(reenterer)); - // eigenPodManager.recordBeaconChainETHBalanceUpdate(staker, amountDelta); - // cheats.stopPrank(); - // } - - // function _beaconChainReentrancyTestsSetup() internal { - // // prepare EigenPodManager with StrategyManager and Delegation replaced with a Reenterer contract - // reenterer = new Reenterer(); - // eigenPodManagerImplementation = new EigenPodManager( - // ethPOSMock, - // eigenPodBeacon, - // IStrategyManager(address(reenterer)), - // slasherMock, - // IDelegationManager(address(reenterer)) - // ); - // eigenPodManager = EigenPodManager( - // address( - // new TransparentUpgradeableProxy( - // address(eigenPodManagerImplementation), - // address(proxyAdmin), - // abi.encodeWithSelector( - // EigenPodManager.initialize.selector, - // type(uint256).max /*maxPods*/, - // IBeaconChainOracle(address(0)) /*beaconChainOracle*/, - // initialOwner, - // pauserRegistry, - // 0 /*initialPausedStatus*/ - // ) - // ) - // ) - // ); - // } +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +import "src/contracts/pods/EigenPodManager.sol"; +import "src/contracts/pods/EigenPod.sol"; +import "src/contracts/pods/EigenPodPausingConstants.sol"; + +import "src/test/utils/EigenLayerUnitTestSetup.sol"; +import "src/test/utils/ProofParsing.sol"; +import "src/test/harnesses/EigenPodManagerWrapper.sol"; +import "src/test/mocks/EigenPodMock.sol"; +import "src/test/mocks/Dummy.sol"; +import "src/test/mocks/ETHDepositMock.sol"; +import "src/test/mocks/DelayedWithdrawalRouterMock.sol"; +import "src/test/mocks/BeaconChainOracleMock.sol"; +import "src/test/mocks/Reenterer.sol"; +import "src/test/events/IEigenPodEvents.sol"; +import "src/test/events/IEigenPodManagerEvents.sol"; + +contract EigenPod_PodManager_UnitTests is EigenLayerUnitTestSetup { + // Contracts Under Test: EigenPodManager & EigenPod + EigenPod public eigenPod; + EigenPod public podImplementation; + IBeacon public eigenPodBeacon; + EigenPodManager public eigenPodManager; + EigenPodManager public eigenPodManagerImplementation; + EigenPodManagerWrapper public eigenPodManagerWrapper; // Implementation contract + + // Mocks + IETHPOSDeposit public ethPOSMock; + IDelayedWithdrawalRouter public delayedWithdrawalRouterMock; + BeaconChainOracleMock beaconChainOracle; + + // Constants + uint64 public constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; + uint64 public constant GOERLI_GENESIS_TIME = 1616508000; + address public initialOwner = address(this); + + // Owner for which proofs are generated; eigenPod above is owned by this address + bytes internal constant beaconProxyBytecode = + hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; + address public constant podOwner = address(42000094993494); + + address public constant podAddress = address(0x49c486E3f4303bc11C02F952Fe5b08D0AB22D443); + + function setUp() public override virtual { + // Setup + EigenLayerUnitTestSetup.setUp(); + + // Deploy Mocks + ethPOSMock = new ETHPOSDepositMock(); + delayedWithdrawalRouterMock = new DelayedWithdrawalRouterMock(); + beaconChainOracle = new BeaconChainOracleMock(); + + // Deploy proxy contract for EPM + EmptyContract emptyContract = new EmptyContract(); + eigenPodManager = EigenPodManager( + address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) + ); + + // Deploy EigenPod Implementation and beacon + podImplementation = new EigenPod( + ethPOSMock, + delayedWithdrawalRouterMock, + eigenPodManager, + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + GOERLI_GENESIS_TIME + ); + + eigenPodBeacon = new UpgradeableBeacon(address(podImplementation)); + + // Deploy EigenPodManager implementation + eigenPodManagerWrapper = new EigenPodManagerWrapper( + ethPOSMock, + eigenPodBeacon, + strategyManagerMock, + slasherMock, + delegationManagerMock + ); + + eigenLayerProxyAdmin.upgradeAndCall( + TransparentUpgradeableProxy(payable(address(eigenPodManager))), + address(eigenPodManagerWrapper), + abi.encodeWithSelector( + EigenPodManager.initialize.selector, + type(uint256).max /*maxPods*/, + beaconChainOracle, + initialOwner, + pauserRegistry, + 0 /*initialPausedStatus*/ + ) + ); + + // Below is a hack to get the eigenPod address that proofs prove against + + // Deploy Proxy same way as EigenPodManager does + eigenPod = EigenPod(payable( + Create2.deploy( + 0, + bytes32(uint256(uint160(address(podOwner)))), + // set the beacon address to the eigenPodBeacon + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ))); + + // Etch the eigenPod code to the address for which proofs are generated + bytes memory code = address(eigenPod).code; + cheats.etch(podAddress, code); + eigenPod = EigenPod(payable(podAddress)); + + // Store the eigenPodBeacon address in the eigenPod beacon proxy + bytes32 beaconSlot = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + cheats.store(address(eigenPod), beaconSlot, bytes32(uint256(uint160(address(eigenPodBeacon))))); + + // Initialize pod + eigenPod.initialize(address(podOwner)); + + // Set storage in EPM + EigenPodManagerWrapper(address(eigenPodManager)).setPodAddress(podOwner, eigenPod); + } +} + +contract EigenPod_PodManager_UnitTests_EigenPodPausing is EigenPod_PodManager_UnitTests { + /** + * 1. verifyBalanceUpdates revert when PAUSED_EIGENPODS_VERIFY_BALANCE_UPDATE set + * 2. verifyAndProcessWithdrawals revert when PAUSED_EIGENPODS_VERIFY_WITHDRAWAL set + * 3. verifyWithdrawalCredentials revert when PAUSED_EIGENPODS_VERIFY_CREDENTIALS set + * 4. activateRestaking revert when PAUSED_EIGENPODS_VERIFY_CREDENTIALS set + */ + + /// @notice Index for flag that pauses creation of new EigenPods when set. See EigenPodManager code for details. + uint8 internal constant PAUSED_NEW_EIGENPODS = 0; + /// @notice Index for flag that pauses all withdrawal-of-restaked ETH related functionality `function *of the EigenPodManager* when set. See EigenPodManager code for details. + uint8 internal constant PAUSED_WITHDRAW_RESTAKED_ETH = 1; + + /// @notice Index for flag that pauses the deposit related functions *of the EigenPods* when set. see EigenPod code for details. + uint8 internal constant PAUSED_EIGENPODS_VERIFY_CREDENTIALS = 2; + /// @notice Index for flag that pauses the `verifyBalanceUpdate` function *of the EigenPods* when set. see EigenPod code for details. + uint8 internal constant PAUSED_EIGENPODS_VERIFY_BALANCE_UPDATE = 3; + /// @notice Index for flag that pauses the `verifyBeaconChainFullWithdrawal` function *of the EigenPods* when set. see EigenPod code for details. + uint8 internal constant PAUSED_EIGENPODS_VERIFY_WITHDRAWAL = 4; + + function test_verifyBalanceUpdates_revert_pausedEigenVerifyBalanceUpdate() public { + bytes32[][] memory validatorFieldsArray = new bytes32[][](1); + + uint40[] memory validatorIndices = new uint40[](1); + + BeaconChainProofs.BalanceUpdateProof[] memory proofs = new BeaconChainProofs.BalanceUpdateProof[](1); + + BeaconChainProofs.StateRootProof memory stateRootProofStruct; + + // pause the contract + cheats.prank(address(pauser)); + eigenPodManager.pause(2 ** PAUSED_EIGENPODS_VERIFY_BALANCE_UPDATE); + + cheats.prank(address(podOwner)); + cheats.expectRevert(bytes("EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager")); + eigenPod.verifyBalanceUpdates(0, validatorIndices, stateRootProofStruct, proofs, validatorFieldsArray); + } + + function test_verifyAndProcessWithdrawals_revert_pausedEigenVerifyWithdrawal() public { + BeaconChainProofs.StateRootProof memory stateRootProofStruct; + BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray; + + bytes[] memory validatorFieldsProofArray = new bytes[](1); + bytes32[][] memory validatorFieldsArray = new bytes32[][](1); + bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); + + // pause the contract + cheats.prank(address(pauser)); + eigenPodManager.pause(2 ** PAUSED_EIGENPODS_VERIFY_WITHDRAWAL); + + cheats.prank(address(podOwner)); + cheats.expectRevert(bytes("EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager")); + eigenPod.verifyAndProcessWithdrawals( + 0, + stateRootProofStruct, + withdrawalProofsArray, + validatorFieldsProofArray, + validatorFieldsArray, + withdrawalFieldsArray + ); + } + + function test_verifyWithdrawalCredentials_revert_pausedEigenVerifyCredentials() public { + BeaconChainProofs.StateRootProof memory stateRootProofStruct; + + bytes32[][] memory validatorFieldsArray = new bytes32[][](1); + bytes[] memory proofsArray = new bytes[](1); + uint40[] memory validatorIndices = new uint40[](1); + + // pause the contract + cheats.prank(address(pauser)); + eigenPodManager.pause(2 ** PAUSED_EIGENPODS_VERIFY_CREDENTIALS); + + cheats.prank(address(podOwner)); + cheats.expectRevert(bytes("EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager")); + eigenPod.verifyWithdrawalCredentials( + 0, + stateRootProofStruct, + validatorIndices, + proofsArray, + validatorFieldsArray + ); + } + + function test_activateRestaking_revert_pausedEigenVerifyCredentials() public { + // pause the contract + cheats.prank(address(pauser)); + eigenPodManager.pause(2 ** PAUSED_EIGENPODS_VERIFY_CREDENTIALS); + + cheats.prank(address(podOwner)); + cheats.expectRevert(bytes("EigenPod.onlyWhenNotPaused: index is paused in EigenPodManager")); + eigenPod.activateRestaking(); + } +} + +contract EigenPod_PodManager_UnitTests_EigenPod is EigenPod_PodManager_UnitTests { + /** + * @notice Tests function calls from EPM to EigenPod + * 1. Stake works when pod is deployed + * 2. Stake when pod is not deployed -> check that ethPOS deposit contract is correct for this and above test + */ + + bytes public constant pubkey = hex"88347ed1c492eedc97fc8c506a35d44d81f27a0c7a1c661b35913cfd15256c0cccbd34a83341f505c7de2983292f2cab"; + + function test_stake_podAlreadyDeployed(bytes memory signature, bytes32 depositDataRoot) public { + uint256 stakeAmount = 32e18; + + uint256 maxPods = eigenPodManager.maxPods(); + uint256 numPods = eigenPodManager.numPods(); + emit log_named_uint("maxPods", maxPods); + emit log_named_uint("numPods", numPods); + + cheats.startPrank(podOwner); + cheats.deal(podOwner, stakeAmount); + eigenPodManager.stake{value: stakeAmount}(pubkey, signature, depositDataRoot); + cheats.stopPrank(); + } + + function test_stake_podNotDeployed(bytes memory signature, bytes32 depositDataRoot) public { + address newPodOwner = address(69696969696); + + uint256 stakeAmount = 32e18; + + cheats.startPrank(newPodOwner); + cheats.deal(newPodOwner, stakeAmount); + eigenPodManager.stake{value: stakeAmount}(pubkey, signature, depositDataRoot); + cheats.stopPrank(); + } +} + +contract EigenPod_PodManager_UnitTests_EigenPodManager is EigenPod_PodManager_UnitTests, ProofParsing, IEigenPodEvents { + /** + * @notice Tests function calls from EigenPod to EigenPodManager + * 1. Verify withdrawal credentials and call `recordBeaconChainETHBalanceUpdate` -> assert shares are updated + * 2. Do a full withdrawal and call `recordBeaconChainETHBalanceUpdate` -> assert shares are updated + * 3. Do a partial withdrawal and call `recordBeaconChainETHBalanceUpdate` -> assert shares are updated + * 4. Verify balance updates and call `recordBeaconChainEThBalanceUpdate` -> assert shares are updated + * 5. Withdraw restaked beacon chain ETH + */ + + using BeaconChainProofs for *; + + // Params to verify withdrawal credentials + BeaconChainProofs.StateRootProof stateRootProofStruct; + uint40[] validatorIndices; + bytes[] validatorFieldsProofs; + bytes32[][] validatorFields; + BeaconChainProofs.BalanceUpdateProof[] balanceUpdateProof; + BeaconChainProofs.WithdrawalProof[] withdrawalProofs; + bytes32[][] withdrawalFields; + + function test_verifyWithdrawalCredentials() public { + // Arrange: Set up conditions to verify withdrawal credentials + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _setWithdrawalCredentialParams(); + + // Set oracle block root and warp time + _setOracleBlockRoot(); + cheats.warp(GOERLI_GENESIS_TIME + 1 days); + uint64 oracleTimestamp = uint64(block.timestamp); + + // Save state for checks + int256 initialShares = eigenPodManager.podOwnerShares(podOwner); + + // Act: Verify withdrawal credentials and record the balance update + cheats.prank(podOwner); + eigenPod.verifyWithdrawalCredentials( + oracleTimestamp, + stateRootProofStruct, + validatorIndices, + validatorFieldsProofs, + validatorFields + ); + + // Assert: Check that the shares are updated correctly + int256 updatedShares = eigenPodManager.podOwnerShares(podOwner); + assertTrue(updatedShares != initialShares, "Shares should be updated after verifying withdrawal credentials"); + assertEq(updatedShares, 32e18, "Shares should be 32ETH in wei after verifying withdrawal credentials"); + } + + function test_balanceUpdate_negativeSharesDelta() public { + // Verify withdrawal credentials + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _verifyWithdrawalCredentials(); + + // Set JSON + setJSON("src/test/test-data/balanceUpdateProof_balance28ETH_302913.json"); + bytes32 validatorPubkeyHash = validatorFields[0].getPubkeyHash(); + + // Set proof params, oracle block root, and warp time + _setBalanceUpdateParams(); + _setOracleBlockRoot(); + cheats.warp(GOERLI_GENESIS_TIME); + uint64 oracleTimestamp = uint64(block.timestamp); + + // Save state for checks + int256 initialShares = eigenPodManager.podOwnerShares(podOwner); + uint64 newValidatorBalance = balanceUpdateProof[0].balanceRoot.getBalanceAtIndex(validatorIndices[0]); + + // Verify balance update + eigenPod.verifyBalanceUpdates( + oracleTimestamp, + validatorIndices, + stateRootProofStruct, + balanceUpdateProof, + validatorFields + ); + + // Checks + int256 updatedShares = eigenPodManager.podOwnerShares(podOwner); + IEigenPod.ValidatorInfo memory validatorInfo = eigenPod.validatorPubkeyHashToInfo(validatorPubkeyHash); + assertEq(validatorInfo.restakedBalanceGwei, newValidatorBalance, "Restaked balance gwei is incorrect"); + assertLt(updatedShares - initialShares, 0, "Shares delta should be negative"); + int256 expectedSharesDiff = (int256(uint256(newValidatorBalance)) - int256(uint256(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR))) * 1e9; + assertEq(updatedShares - initialShares, expectedSharesDiff, "Shares delta should be equal to restaked balance"); + } + + function test_balanceUpdate_positiveSharesDelta() public { + // Verify withdrawal credentials + setJSON("./src/test/test-data/withdrawal_credential_proof_302913_30ETHBalance.json"); + _verifyWithdrawalCredentials(); + + // Set JSON + setJSON("src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); + bytes32 validatorPubkeyHash = validatorFields[0].getPubkeyHash(); + + // Set proof params, oracle block root, and warp time + _setBalanceUpdateParams(); + _setOracleBlockRoot(); + cheats.warp(GOERLI_GENESIS_TIME); + uint64 oracleTimestamp = uint64(block.timestamp); + + // Save state for checks + int256 initialShares = eigenPodManager.podOwnerShares(podOwner); + uint64 newValidatorBalance = balanceUpdateProof[0].balanceRoot.getBalanceAtIndex(validatorIndices[0]); + + // Verify balance update + eigenPod.verifyBalanceUpdates( + oracleTimestamp, + validatorIndices, + stateRootProofStruct, + balanceUpdateProof, + validatorFields + ); + + // Checks + int256 updatedShares = eigenPodManager.podOwnerShares(podOwner); + IEigenPod.ValidatorInfo memory validatorInfo = eigenPod.validatorPubkeyHashToInfo(validatorPubkeyHash); + assertEq(validatorInfo.restakedBalanceGwei, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Restaked balance gwei should be max"); + assertGt(updatedShares - initialShares, 0, "Shares delta should be positive"); + assertEq(updatedShares, 32e18, "Shares should be 32ETH"); + } + + function test_fullWithdrawal_excess32ETH() public { + // Verify withdrawal credentials + setJSON("./src/test/test-data/withdrawal_credential_proof_302913_30ETHBalance.json"); + _verifyWithdrawalCredentials(); + + // Set JSON + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + bytes32 validatorPubkeyHash = validatorFields[0].getPubkeyHash(); + + // Set proof params, block root + _setWithdrawalProofParams(); + _setOracleBlockRoot(); + + // Save state for checks; deal EigenPod withdrawal router balance + uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64( + withdrawalFields[0][BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX] + ); + uint64 leftOverBalanceWEI = uint64(withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * 1e9; + cheats.deal(address(eigenPod), leftOverBalanceWEI); + int256 initialShares = eigenPodManager.podOwnerShares(podOwner); + + // Withdraw + eigenPod.verifyAndProcessWithdrawals( + 0, + stateRootProofStruct, + withdrawalProofs, + validatorFieldsProofs, + validatorFields, + withdrawalFields + ); + + // Checks + int256 updatedShares = eigenPodManager.podOwnerShares(podOwner); + IEigenPod.ValidatorInfo memory validatorInfo = eigenPod.validatorPubkeyHashToInfo(validatorPubkeyHash); + assertEq(validatorInfo.restakedBalanceGwei, 0, "Restaked balance gwei should be 0"); + assertGt(updatedShares - initialShares, 0, "Shares diff should be positive"); + int256 expectedSharesDiff = (int256(uint256(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR))*1e9) - initialShares; + assertEq(updatedShares - initialShares, expectedSharesDiff, "Shares delta incorrect"); + assertEq(updatedShares, 32e18, "Shares should be 32e18"); + assertEq(address(delayedWithdrawalRouterMock).balance, leftOverBalanceWEI, "Incorrect amount sent to delayed withdrawal router"); + } + + function test_withdrawRestakedBeaconChainETH() public { + test_fullWithdrawal_excess32ETH(); + + // Deal eigenPod balance - max restaked balance + cheats.deal(address(eigenPod), 32 ether); + + cheats.startPrank(address(delegationManagerMock)); + vm.expectEmit(true, true, true, true); + emit RestakedBeaconChainETHWithdrawn(podOwner, 32 ether); + eigenPodManager.withdrawSharesAsTokens( + podOwner, + podOwner, + uint256(eigenPodManager.podOwnerShares(podOwner)) + ); + cheats.stopPrank(); + + // Checks + assertEq(address(podOwner).balance, 32 ether, "EigenPod balance should be 0"); + assertEq(address(eigenPod).balance, 0, "EigenPod balance should be 0"); + assertEq(eigenPod.withdrawableRestakedExecutionLayerGwei(), 0, "Restaked execution layer gwei should be 0"); + } + + function test_fullWithdrawal_less32ETH() public { + // Verify withdrawal credentials + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _verifyWithdrawalCredentials(); + + // Set JSON + setJSON("src/test/test-data/fullWithdrawalProof_Latest_28ETH.json"); + bytes32 validatorPubkeyHash = validatorFields[0].getPubkeyHash(); + + // Set proof params, block root + _setWithdrawalProofParams(); + _setOracleBlockRoot(); + + // Save State + uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64( + withdrawalFields[0][BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX] + ); + int256 initialShares = eigenPodManager.podOwnerShares(podOwner); + + // Withdraw + eigenPod.verifyAndProcessWithdrawals( + 0, + stateRootProofStruct, + withdrawalProofs, + validatorFieldsProofs, + validatorFields, + withdrawalFields + ); + + // Checks + int256 updatedShares = eigenPodManager.podOwnerShares(podOwner); + IEigenPod.ValidatorInfo memory validatorInfo = eigenPod.validatorPubkeyHashToInfo(validatorPubkeyHash); + assertEq(validatorInfo.restakedBalanceGwei, 0, "Restaked balance gwei is incorrect"); + assertLt(updatedShares - initialShares, 0, "Shares delta should be negative"); + int256 expectedSharesDiff = (int256(uint256(withdrawalAmountGwei)) - int256(uint256(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR))) * 1e9; + assertEq(updatedShares - initialShares, expectedSharesDiff, "Shares delta incorrect"); + } + + function test_partialWithdrawal() public { + // Set JSON & params + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _verifyWithdrawalCredentials(); + + // Set JSON + setJSON("./src/test/test-data/partialWithdrawalProof_Latest.json"); + bytes32 validatorPubkeyHash = validatorFields[0].getPubkeyHash(); + + // Set proof params, block root + _setWithdrawalProofParams(); + _setOracleBlockRoot(); + + // Assert that partial withdrawal code path will be tested + assertLt(withdrawalProofs[0].getWithdrawalEpoch(), validatorFields[0].getWithdrawableEpoch(), "Withdrawal epoch should be less than the withdrawable epoch"); + + // Save state for checks; deal EigenPod withdrawal router balance + uint64 withdrawalAmountGwei = Endian.fromLittleEndianUint64( + withdrawalFields[0][BeaconChainProofs.WITHDRAWAL_VALIDATOR_AMOUNT_INDEX] + ); + cheats.deal(address(eigenPod), withdrawalAmountGwei * 1e9); // deal full withdrawal amount since it's a partial withdrawal + uint64 initialRestakedBalance = (eigenPod.validatorPubkeyHashToInfo(validatorPubkeyHash)).restakedBalanceGwei; + int256 initialShares = eigenPodManager.podOwnerShares(podOwner); + + // Withdraw + eigenPod.verifyAndProcessWithdrawals( + 0, + stateRootProofStruct, + withdrawalProofs, + validatorFieldsProofs, + validatorFields, + withdrawalFields + ); + + // Checks + int256 updatedShares = eigenPodManager.podOwnerShares(podOwner); + IEigenPod.ValidatorInfo memory validatorInfo = eigenPod.validatorPubkeyHashToInfo(validatorPubkeyHash); + assertEq(validatorInfo.restakedBalanceGwei, initialRestakedBalance, "Restaked balance gwei should be unchanged"); + assertEq(updatedShares - initialShares, 0, "Shares diff should be 0"); + assertEq(address(delayedWithdrawalRouterMock).balance, withdrawalAmountGwei * 1e9, "Incorrect amount sent to delayed withdrawal router"); + } + + // Helper Functions + function _getStateRootProof() internal returns (BeaconChainProofs.StateRootProof memory) { + return BeaconChainProofs.StateRootProof(getBeaconStateRoot(), abi.encodePacked(getStateRootProof())); + } + + function _setOracleBlockRoot() internal { + bytes32 latestBlockRoot = getLatestBlockRoot(); + //set beaconStateRoot + beaconChainOracle.setOracleBlockRootAtTimestamp(latestBlockRoot); + } + + function _verifyWithdrawalCredentials() internal { + _setWithdrawalCredentialParams(); + + // Set oracle block root and warp time + uint64 oracleTimestamp = 0; + _setOracleBlockRoot(); + cheats.warp(oracleTimestamp+=1); + + // Save state for checks + int256 initialShares = eigenPodManager.podOwnerShares(podOwner); + + // Act: Verify withdrawal credentials and record the balance update + cheats.prank(podOwner); + eigenPod.verifyWithdrawalCredentials( + oracleTimestamp, + stateRootProofStruct, + validatorIndices, + validatorFieldsProofs, + validatorFields + ); + } + + function _setWithdrawalCredentialParams() internal { + // Reset arrays + delete validatorIndices; + delete validatorFields; + delete validatorFieldsProofs; + + // Set state proof struct + stateRootProofStruct = _getStateRootProof(); + + // Set validator indices + uint40 validatorIndex = uint40(getValidatorIndex()); + validatorIndices.push(validatorIndex); + + // Set validatorFieldsArray + validatorFields.push(getValidatorFields()); + + // Set validator fields proof + validatorFieldsProofs.push(abi.encodePacked(getWithdrawalCredentialProof())); // Validator fields are proven here + } + + function _setBalanceUpdateParams() internal { + // Reset arrays + delete validatorIndices; + delete validatorFields; + delete balanceUpdateProof; + + // Set state proof struct + stateRootProofStruct = _getStateRootProof(); + + // Set validator index, beacon state root, balance update proof, and validator fields + uint40 validatorIndex = uint40(getValidatorIndex()); + validatorIndices.push(validatorIndex); + + // Set validatorFields array + validatorFields.push(getValidatorFields()); + + // Set balance update proof + balanceUpdateProof.push(_getBalanceUpdateProof()); + } + + function _setWithdrawalProofParams() internal { + // Reset arrays + delete validatorFields; + delete validatorFieldsProofs; + delete withdrawalFields; + delete withdrawalProofs; + + // Set state proof struct + stateRootProofStruct = _getStateRootProof(); + + // Set validatorFields + validatorFields.push(getValidatorFields()); + + // Set validator fields proof + validatorFieldsProofs.push(abi.encodePacked(getValidatorProof())); + + // Set withdrawal fields + withdrawalFields.push(getWithdrawalFields()); + + // Set withdrawal proofs + withdrawalProofs.push(_getWithdrawalProof()); + } + + function _getBalanceUpdateProof() internal returns (BeaconChainProofs.BalanceUpdateProof memory) { + bytes32 balanceRoot = getBalanceRoot(); + BeaconChainProofs.BalanceUpdateProof memory proofs = BeaconChainProofs.BalanceUpdateProof( + abi.encodePacked(getValidatorBalanceProof()), + abi.encodePacked(getWithdrawalCredentialProof()), //technically this is to verify validator pubkey in the validator fields, but the WC proof is effectively the same so we use it here again. + balanceRoot + ); + return proofs; + } + + /// @notice this function just generates a valid proof so that we can test other functionalities of the withdrawal flow + function _getWithdrawalProof() internal returns (BeaconChainProofs.WithdrawalProof memory) { + { + bytes32 blockRoot = getBlockRoot(); + bytes32 slotRoot = getSlotRoot(); + bytes32 timestampRoot = getTimestampRoot(); + bytes32 executionPayloadRoot = getExecutionPayloadRoot(); + + return + BeaconChainProofs.WithdrawalProof( + abi.encodePacked(getWithdrawalProof()), + abi.encodePacked(getSlotProof()), + abi.encodePacked(getExecutionPayloadProof()), + abi.encodePacked(getTimestampProof()), + abi.encodePacked(getHistoricalSummaryProof()), + uint64(getBlockRootIndex()), + uint64(getHistoricalSummaryIndex()), + uint64(getWithdrawalIndex()), + blockRoot, + slotRoot, + timestampRoot, + executionPayloadRoot + ); + } + } +} + diff --git a/src/test/unit/EigenPodManagerUnit.t.sol b/src/test/unit/EigenPodManagerUnit.t.sol index a4cdcf1e3..94645d78f 100644 --- a/src/test/unit/EigenPodManagerUnit.t.sol +++ b/src/test/unit/EigenPodManagerUnit.t.sol @@ -2,16 +2,15 @@ pragma solidity =0.8.12; import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import "forge-std/Test.sol"; -import "../../contracts/pods/EigenPodManager.sol"; -import "../../contracts/pods/EigenPodPausingConstants.sol"; +import "src/contracts/pods/EigenPodManager.sol"; +import "src/contracts/pods/EigenPodPausingConstants.sol"; -import "../events/IEigenPodManagerEvents.sol"; -import "../utils/EigenLayerUnitTestSetup.sol"; -import "../harnesses/EigenPodManagerWrapper.sol"; -import "../mocks/EigenPodMock.sol"; -import "../mocks/ETHDepositMock.sol"; +import "src/test/events/IEigenPodManagerEvents.sol"; +import "src/test/utils/EigenLayerUnitTestSetup.sol"; +import "src/test/harnesses/EigenPodManagerWrapper.sol"; +import "src/test/mocks/EigenPodMock.sol"; +import "src/test/mocks/ETHDepositMock.sol"; contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { // Contracts Under Test: EigenPodManager @@ -24,7 +23,6 @@ contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { IETHPOSDeposit public ethPOSMock; IEigenPod public eigenPodMockImplementation; IBeacon public eigenPodBeacon; // Proxy for eigenPodMockImplementation - IStrategy public beaconChainETHStrategy; // Constants uint256 public constant GWEI_TO_WEI = 1e9; @@ -65,9 +63,6 @@ contract EigenPodManagerUnitTests is EigenLayerUnitTestSetup { ) ); - // Set beaconChainETHStrategy - beaconChainETHStrategy = eigenPodManager.beaconChainETHStrategy(); - // Set defaultPod defaultPod = eigenPodManager.getPod(defaultStaker); diff --git a/src/test/unit/EigenPodUnit.t.sol b/src/test/unit/EigenPodUnit.t.sol index c23312d82..207aa37e6 100644 --- a/src/test/unit/EigenPodUnit.t.sol +++ b/src/test/unit/EigenPodUnit.t.sol @@ -1,233 +1,544 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.12; -import "./../EigenPod.t.sol"; -import "./../utils/ProofParsing.sol"; -import "./../mocks/StrategyManagerMock.sol"; -import "./../mocks/SlasherMock.sol"; -import "./../mocks/DelegationManagerMock.sol"; -import "./../mocks/DelayedWithdrawalRouterMock.sol"; - -import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; + +import "src/contracts/pods/EigenPod.sol"; + +import "src/test/mocks/ETHDepositMock.sol"; +import "src/test/mocks/DelayedWithdrawalRouterMock.sol"; +import "src/test/mocks/ERC20Mock.sol"; +import "src/test/harnesses/EigenPodHarness.sol"; +import "src/test/utils/ProofParsing.sol"; +import "src/test/utils/EigenLayerUnitTestSetup.sol"; +import "src/test/events/IEigenPodEvents.sol"; + +contract EigenPodUnitTests is EigenLayerUnitTestSetup { + // Contract Under Test: EigenPod + EigenPod public eigenPod; + EigenPod public podImplementation; + IBeacon public eigenPodBeacon; -contract EigenPodUnitTests is Test, ProofParsing { + // Mocks + IETHPOSDeposit public ethPOSDepositMock; + IDelayedWithdrawalRouter public delayedWithdrawalRouterMock; - using BytesLib for bytes; - using BeaconChainProofs for *; + // Address of pod for which proofs were generated + address podAddress = address(0x49c486E3f4303bc11C02F952Fe5b08D0AB22D443); + + // Constants + // uint32 public constant WITHDRAWAL_DELAY_BLOCKS = 7 days / 12 seconds; + uint64 public constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; + // uint64 public constant RESTAKED_BALANCE_OFFSET_GWEI = 75e7; + uint64 public constant GOERLI_GENESIS_TIME = 1616508000; + // uint64 public constant SECONDS_PER_SLOT = 12; + + bytes internal constant beaconProxyBytecode = + hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; + address public podOwner = address(this); - uint256 internal constant GWEI_TO_WEI = 1e9; + function setUp() public override virtual { + // Setup + EigenLayerUnitTestSetup.setUp(); - bytes pubkey = + // Deploy mocks + ethPOSDepositMock = new ETHPOSDepositMock(); + delayedWithdrawalRouterMock = new DelayedWithdrawalRouterMock(); + + // Deploy EigenPod + podImplementation = new EigenPod( + ethPOSDepositMock, + delayedWithdrawalRouterMock, + eigenPodManagerMock, + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + GOERLI_GENESIS_TIME + ); + + // Deploy Beacon + eigenPodBeacon = new UpgradeableBeacon(address(podImplementation)); + + // Deploy Proxy same way as EigenPodManager does + eigenPod = EigenPod(payable( + Create2.deploy( + 0, + bytes32(uint256(uint160(address(this)))), + // set the beacon address to the eigenPodBeacon + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ))); + + // Etch the eigenPod code to the address for which proofs are generated + bytes memory code = address(eigenPod).code; + cheats.etch(podAddress, code); + eigenPod = EigenPod(payable(podAddress)); + + // Store the eigenPodBeacon address in the eigenPod beacon proxy + bytes32 beaconSlot = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + cheats.store(address(eigenPod), beaconSlot, bytes32(uint256(uint160(address(eigenPodBeacon))))); + + // Initialize pod + eigenPod.initialize(address(this)); + } + + + /// @notice Post-M2, all new deployed eigen pods will have restaked set to true + modifier hasNotRestaked() { + // Write hasRestaked as false. hasRestaked in slot 52 + bytes32 slot = bytes32(uint256(52)); + bytes32 value = bytes32(0); // 0 == false + cheats.store(address(eigenPod), slot, value); + _; + } +} + +contract EigenPodUnitTests_Initialization is EigenPodUnitTests, IEigenPodEvents { + + function test_initialization() public { + // Check podOwner and restaked + assertEq(eigenPod.podOwner(), podOwner, "Pod owner incorrectly set"); + assertTrue(eigenPod.hasRestaked(), "hasRestaked incorrectly set"); + // Check immutable storage + assertEq(address(eigenPod.ethPOS()), address(ethPOSDepositMock), "EthPOS incorrectly set"); + assertEq(address(eigenPod.delayedWithdrawalRouter()), address(delayedWithdrawalRouterMock), "DelayedWithdrawalRouter incorrectly set"); + assertEq(address(eigenPod.eigenPodManager()), address(eigenPodManagerMock), "EigenPodManager incorrectly set"); + assertEq(eigenPod.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR(), MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Max restaked balance incorrectly set"); + assertEq(eigenPod.GENESIS_TIME(), GOERLI_GENESIS_TIME, "Goerli genesis time incorrectly set"); + } + + function test_initialize_revert_alreadyInitialized() public { + cheats.expectRevert("Initializable: contract is already initialized"); + eigenPod.initialize(podOwner); + } + + function test_initialize_eventEmitted() public { + address newPodOwner = address(0x123); + + // Deploy new pod + EigenPod newEigenPod = EigenPod(payable( + Create2.deploy( + 0, + bytes32(uint256(uint160(newPodOwner))), + // set the beacon address to the eigenPodBeacon + abi.encodePacked(beaconProxyBytecode, abi.encode(eigenPodBeacon, "")) + ))); + + // Expect emit and Initialize new pod + vm.expectEmit(true, true, true, true); + emit RestakingActivated(newPodOwner); + newEigenPod.initialize(newPodOwner); + } +} + +contract EigenPodUnitTests_Stake is EigenPodUnitTests, IEigenPodEvents { + + // Beacon chain staking constnats + bytes public constant pubkey = hex"88347ed1c492eedc97fc8c506a35d44d81f27a0c7a1c661b35913cfd15256c0cccbd34a83341f505c7de2983292f2cab"; - uint40 validatorIndex0 = 0; - uint40 validatorIndex1 = 1; + bytes public signature; + bytes32 public depositDataRoot; - address podOwner = address(42000094993494); + function testFuzz_stake_revert_notEigenPodManager(address invalidCaller) public { + cheats.assume(invalidCaller != address(eigenPodManagerMock)); + cheats.deal(invalidCaller, 32 ether); - Vm cheats = Vm(HEVM_ADDRESS); + cheats.prank(invalidCaller); + cheats.expectRevert("EigenPod.onlyEigenPodManager: not eigenPodManager"); + eigenPod.stake{value: 32 ether}(pubkey, signature, depositDataRoot); + } - ProxyAdmin public eigenLayerProxyAdmin; - IEigenPodManager public eigenPodManager; - IEigenPod public podImplementation; - IDelayedWithdrawalRouter public delayedWithdrawalRouter; - IETHPOSDeposit public ethPOSDeposit; - IBeacon public eigenPodBeacon; - EPInternalFunctions public podInternalFunctionTester; - - IDelegationManager public delegation; - IStrategyManager public strategyManager; - ISlasher public slasher; - IEigenPod public pod; - PauserRegistry public pauserReg; - - BeaconChainOracleMock public beaconChainOracle; - address[] public slashingContracts; - address pauser = address(69); - address unpauser = address(489); - address podManagerAddress = 0x212224D2F2d262cd093eE13240ca4873fcCBbA3C; - address podAddress = address(123); - uint256 stakeAmount = 32e18; - mapping(address => bool) fuzzedAddressMapping; - bytes signature; - bytes32 depositDataRoot; + function testFuzz_stake_revert_invalidValue(uint256 value) public { + cheats.assume(value != 32 ether); + cheats.deal(address(eigenPodManagerMock), value); - bytes32[] withdrawalFields; - bytes32[] validatorFields; + cheats.prank(address(eigenPodManagerMock)); + cheats.expectRevert("EigenPod.stake: must initially stake for any validator with 32 ether"); + eigenPod.stake{value: value}(pubkey, signature, depositDataRoot); + } - uint32 WITHDRAWAL_DELAY_BLOCKS = 7 days / 12 seconds; - uint64 MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 31e9; - uint64 RESTAKED_BALANCE_OFFSET_GWEI = 75e7; - uint64 internal constant GOERLI_GENESIS_TIME = 1616508000; - uint64 internal constant SECONDS_PER_SLOT = 12; + function test_stake() public { + cheats.deal(address(eigenPodManagerMock), 32 ether); - EigenPodTests test; + // Expect emit + vm.expectEmit(true, true, true, true); + emit EigenPodStaked(pubkey); - // EIGENPOD EVENTS - /// @notice Emitted when an ETH validator stakes via this eigenPod - event EigenPodStaked(bytes pubkey); + // Stake + cheats.prank(address(eigenPodManagerMock)); + eigenPod.stake{value: 32 ether}(pubkey, signature, depositDataRoot); - /// @notice Emitted when an ETH validator's withdrawal credentials are successfully verified to be pointed to this eigenPod - event ValidatorRestaked(uint40 validatorIndex); + // Check eth transferred + assertEq(address(ethPOSDepositMock).balance, 32 ether, "Incorrect amount transferred"); + } +} - /// @notice Emitted when an ETH validator's balance is updated in EigenLayer - event ValidatorBalanceUpdated(uint40 validatorIndex, uint64 balanceTimestamp, uint64 newBalanceGwei); +contract EigenPodUnitTests_PodOwnerFunctions is EigenPodUnitTests, IEigenPodEvents { + + /******************************************************************************* + Withdraw Non Beacon Chain ETH Tests + *******************************************************************************/ - /// @notice Emitted when an ETH validator is prove to have withdrawn from the beacon chain - event FullWithdrawalRedeemed( - uint40 validatorIndex, - uint64 withdrawalTimestamp, - address indexed recipient, - uint64 withdrawalAmountGwei - ); + function testFuzz_withdrawNonBeaconChainETH_revert_notPodOwner(address invalidCaller) public { + cheats.assume(invalidCaller != podOwner); - /// @notice Emitted when a partial withdrawal claim is successfully redeemed - event PartialWithdrawalRedeemed( - uint40 validatorIndex, - uint64 withdrawalTimestamp, - address indexed recipient, - uint64 partialWithdrawalAmountGwei - ); + cheats.prank(invalidCaller); + cheats.expectRevert("EigenPod.onlyEigenPodOwner: not podOwner"); + eigenPod.withdrawNonBeaconChainETHBalanceWei(invalidCaller, 1 ether); + } - modifier fuzzedAddress(address addr) virtual { - cheats.assume(fuzzedAddressMapping[addr] == false); - _; + function test_withdrawNonBeaconChainETH_revert_tooMuchWithdrawn() public { + // Send EigenPod 0.9 ether + _seedPodWithETH(0.9 ether); + + // Withdraw 1 ether + cheats.expectRevert("EigenPod.withdrawnonBeaconChainETHBalanceWei: amountToWithdraw is greater than nonBeaconChainETHBalanceWei"); + eigenPod.withdrawNonBeaconChainETHBalanceWei(podOwner, 1 ether); } - function setUp() public { - ethPOSDeposit = new ETHPOSDepositMock(); - beaconChainOracle = new BeaconChainOracleMock(); - EmptyContract emptyContract = new EmptyContract(); - // deploy proxy admin for ability to upgrade proxy contracts - eigenLayerProxyAdmin = new ProxyAdmin(); + function testFuzz_withdrawNonBeaconChainETH(uint256 ethAmount) public { + _seedPodWithETH(ethAmount); + assertEq(eigenPod.nonBeaconChainETHBalanceWei(), ethAmount, "Incorrect amount incremented in receive function"); - // this contract is deployed later to keep its address the same (for these tests) - eigenPodManager = EigenPodManager( - address(new TransparentUpgradeableProxy(address(emptyContract), address(eigenLayerProxyAdmin), "")) - ); + cheats.expectEmit(true, true, true, true); + emit NonBeaconChainETHWithdrawn(podOwner, ethAmount); + eigenPod.withdrawNonBeaconChainETHBalanceWei(podOwner, ethAmount); - delayedWithdrawalRouter = new DelayedWithdrawalRouterMock(); + // Checks + assertEq(address(eigenPod).balance, 0, "Incorrect amount withdrawn from eigenPod"); + assertEq(address(delayedWithdrawalRouterMock).balance, ethAmount, "Incorrect amount set to delayed withdrawal router"); + } - podImplementation = new EigenPod( - ethPOSDeposit, - delayedWithdrawalRouter, - eigenPodManager, - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, - GOERLI_GENESIS_TIME - ); + /******************************************************************************* + Recover Tokens Tests + *******************************************************************************/ + + function testFuzz_recoverTokens_revert_notPodOwner(address invalidCaller) public { + cheats.assume(invalidCaller != podOwner); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = IERC20(address(0x123)); + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1; + + cheats.prank(invalidCaller); + cheats.expectRevert("EigenPod.onlyEigenPodOwner: not podOwner"); + eigenPod.recoverTokens(tokens, amounts, podOwner); + } - eigenPodBeacon = new UpgradeableBeacon(address(podImplementation)); + function test_recoverTokens_revert_invalidLengths() public { + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = IERC20(address(0x123)); + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1; + amounts[1] = 1; - test = new EigenPodTests(); - eigenPodBeacon = new UpgradeableBeacon(address(podImplementation)); - strategyManager = new StrategyManagerMock(); - slasher = new SlasherMock(); - delegation = new DelegationManagerMock(); - // deploy pauser registry - address[] memory pausers = new address[](1); - pausers[0] = pauser; - pauserReg = new PauserRegistry(pausers, unpauser); - - EigenPodManager eigenPodManagerImplementation = new EigenPodManager( - ethPOSDeposit, - eigenPodBeacon, - strategyManager, - slasher, - delegation - ); + cheats.expectRevert("EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length"); + eigenPod.recoverTokens(tokens, amounts, podOwner); + } - eigenLayerProxyAdmin.upgradeAndCall( - TransparentUpgradeableProxy(payable(address(eigenPodManager))), - address(eigenPodManagerImplementation), - abi.encodeWithSelector( - EigenPodManager.initialize.selector, - type(uint256).max, // maxPods - beaconChainOracle, - address(this), - pauserReg, - 0 /*initialPausedStatus*/ - ) - ); + function test_recoverTokens() public { + // Deploy dummy token + IERC20 dummyToken = new ERC20Mock(); + dummyToken.transfer(address(eigenPod), 1e18); + + // Recover tokens + address recipient = address(0x123); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = dummyToken; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1e18; - cheats.startPrank(podOwner); - eigenPodManager.createPod(); - cheats.stopPrank(); - pod = eigenPodManager.getPod(podOwner); + eigenPod.recoverTokens(tokens, amounts, recipient); - // Filter 0 address - fuzzedAddressMapping[address(0x0)] = true; + // Checks + assertEq(dummyToken.balanceOf(recipient), 1e18, "Incorrect amount recovered"); } - function testStakingWithInvalidAmount () public { - cheats.deal(address(eigenPodManager), 10e18); - cheats.startPrank(address(eigenPodManager)); - cheats.expectRevert(bytes("EigenPod.stake: must initially stake for any validator with 32 ether")); - pod.stake{value: 10e18}(pubkey, signature, depositDataRoot); - cheats.stopPrank(); + /******************************************************************************* + Activate Restaking Tests + *******************************************************************************/ + + function testFuzz_activateRestaking_revert_notPodOwner(address invalidCaller) public { + cheats.assume(invalidCaller != podOwner); + + cheats.prank(invalidCaller); + cheats.expectRevert("EigenPod.onlyEigenPodOwner: not podOwner"); + eigenPod.activateRestaking(); } - function testFullWithdrawalProofWithWrongWithdrawalFields(bytes32[] memory wrongWithdrawalFields) public { - Relayer relay = new Relayer(); - uint256 WITHDRAWAL_FIELD_TREE_HEIGHT = 2; + function test_activateRestaking_revert_alreadyRestaked() public { + cheats.expectRevert("EigenPod.hasNeverRestaked: restaking is enabled"); + eigenPod.activateRestaking(); + } - setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); - BeaconChainProofs.WithdrawalProof memory proofs = _getWithdrawalProof(); - bytes32 beaconStateRoot = getBeaconStateRoot(); - cheats.assume(wrongWithdrawalFields.length != 2 ** WITHDRAWAL_FIELD_TREE_HEIGHT); - validatorFields = getValidatorFields(); + function testFuzz_activateRestaking(uint256 ethAmount) public hasNotRestaked { + // Seed some ETH + _seedPodWithETH(ethAmount); + + // Activate restaking + vm.expectEmit(true, true, true, true); + emit RestakingActivated(podOwner); + eigenPod.activateRestaking(); - cheats.expectRevert(bytes("BeaconChainProofs.verifyWithdrawal: withdrawalFields has incorrect length")); - relay.verifyWithdrawal(beaconStateRoot, wrongWithdrawalFields, proofs); + // Checks + assertTrue(eigenPod.hasRestaked(), "hasRestaked incorrectly set"); + _assertWithdrawalProcessed(ethAmount); } - function testCheckThatHasRestakedIsSetToTrue() public returns (IEigenPod){ - require(pod.hasRestaked() == true, "Pod should not be restaked"); - return pod; + /** + * This is a regression test for a bug (EIG-14) found by Hexens. Lets say podOwner sends 32 ETH to the EigenPod, + * the nonBeaconChainETHBalanceWei increases by 32 ETH. podOwner calls withdrawBeforeRestaking, which + * will simply send the entire ETH balance (32 ETH) to the owner. The owner activates restaking, + * creates a validator and verifies the withdrawal credentials, receiving 32 ETH in shares. + * They can exit the validator, the pod gets the 32ETH and they can call withdrawNonBeaconChainETHBalanceWei + * And simply withdraw the 32ETH because nonBeaconChainETHBalanceWei is 32ETH. This was an issue because + * nonBeaconChainETHBalanceWei was never zeroed out in _processWithdrawalBeforeRestaking + */ + function test_regression_validatorBalance_cannotBeRemoved_viaNonBeaconChainETHBalanceWei() external hasNotRestaked { + // Assert that the pod has not restaked + assertFalse(eigenPod.hasRestaked(), "hasRestaked should be false"); + + // Simulate podOwner sending 32 ETH to eigenPod + _seedPodWithETH(32 ether); + assertEq(eigenPod.nonBeaconChainETHBalanceWei(), 32 ether, "nonBeaconChainETHBalanceWei should be 32 ETH"); + + // Pod owner calls withdrawBeforeRestaking, sending 32 ETH to owner + eigenPod.withdrawBeforeRestaking(); + assertEq(address(eigenPod).balance, 0, "eigenPod balance should be 0"); + assertEq(address(delayedWithdrawalRouterMock).balance, 32 ether, "withdrawal router balance should be 32 ETH"); + + // Upgrade from m1 to m2 + + // Activate restaking on the pod + eigenPod.activateRestaking(); + + // Simulate a withdrawal by increasing eth balance without code execution + cheats.deal(address(eigenPod), 32 ether); + + // Try calling withdrawNonBeaconChainETHBalanceWei, should fail since `nonBeaconChainETHBalanceWei` + // was set to 0 when calling `_processWithdrawalBeforeRestaking` from `activateRestaking` + cheats.expectRevert("EigenPod.withdrawnonBeaconChainETHBalanceWei: amountToWithdraw is greater than nonBeaconChainETHBalanceWei"); + eigenPod.withdrawNonBeaconChainETHBalanceWei(podOwner, 32 ether); } + + /******************************************************************************* + Withdraw Before Restaking Tests + *******************************************************************************/ + + function testFuzz_withdrawBeforeRestaking_revert_notPodOwner(address invalidCaller) public filterFuzzedAddressInputs(invalidCaller) { + cheats.assume(invalidCaller != podOwner); - function testActivateRestakingWithM2Pods() external { - IEigenPod pod = testCheckThatHasRestakedIsSetToTrue(); - cheats.startPrank(podOwner); - cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); - pod.activateRestaking(); - cheats.stopPrank(); + cheats.prank(invalidCaller); + cheats.expectRevert("EigenPod.onlyEigenPodOwner: not podOwner"); + eigenPod.withdrawBeforeRestaking(); } - function testWithdrawBeforeRestakingWithM2Pods() external { - IEigenPod pod = testCheckThatHasRestakedIsSetToTrue(); - cheats.startPrank(podOwner); - cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); - pod.withdrawBeforeRestaking(); - cheats.stopPrank(); + function test_withdrawBeforeRestaking_revert_alreadyRestaked() public { + cheats.expectRevert("EigenPod.hasNeverRestaked: restaking is enabled"); + eigenPod.withdrawBeforeRestaking(); } - function testAttemptedWithdrawalAfterVerifyingWithdrawalCredentials() public { - cheats.startPrank(podOwner); - cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); - IEigenPod(pod).withdrawBeforeRestaking(); - cheats.stopPrank(); + function testFuzz_withdrawBeforeRestaking(uint256 ethAmount) public hasNotRestaked { + // Seed some ETH + _seedPodWithETH(ethAmount); + + // Withdraw + eigenPod.withdrawBeforeRestaking(); + + // Checks + _assertWithdrawalProcessed(ethAmount); } - function testTooSoonBalanceUpdate(uint64 oracleTimestamp, uint64 mostRecentBalanceUpdateTimestamp) external { - cheats.assume(oracleTimestamp < mostRecentBalanceUpdateTimestamp); - _deployInternalFunctionTester(); + // Helpers + function _assertWithdrawalProcessed(uint256 amount) internal { + assertEq(eigenPod.mostRecentWithdrawalTimestamp(), uint32(block.timestamp), "Incorrect mostRecentWithdrawalTimestamp"); + assertEq(eigenPod.nonBeaconChainETHBalanceWei(), 0, "Incorrect nonBeaconChainETHBalanceWei"); + assertEq(address(delayedWithdrawalRouterMock).balance, amount, "Incorrect amount sent to delayed withdrawal router"); + } - setJSON("./src/test/test-data/balanceUpdateProof_notOverCommitted_302913.json"); - validatorFields = getValidatorFields(); + function _seedPodWithETH(uint256 ethAmount) internal { + cheats.deal(address(this), ethAmount); + address(eigenPod).call{value: ethAmount}(""); + } +} + +contract EigenPodHarnessSetup is EigenPodUnitTests { + // Harness that exposes internal functions for test + EPInternalFunctions public eigenPodHarnessImplementation; + EPInternalFunctions public eigenPodHarness; + + function setUp() public virtual override { + EigenPodUnitTests.setUp(); + + // Deploy EP Harness + eigenPodHarnessImplementation = new EPInternalFunctions( + ethPOSDepositMock, + delayedWithdrawalRouterMock, + eigenPodManagerMock, + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, + GOERLI_GENESIS_TIME + ); + + // Upgrade eigenPod to harness + UpgradeableBeacon(address(eigenPodBeacon)).upgradeTo(address(eigenPodHarnessImplementation)); + eigenPodHarness = EPInternalFunctions(payable(eigenPod)); + } +} + +contract EigenPodUnitTests_VerifyWithdrawalCredentialsTests is EigenPodHarnessSetup, ProofParsing, IEigenPodEvents { + using BytesLib for bytes; + using BeaconChainProofs for *; + + // Params to _verifyWithdrawalCredentials, can be set in test or helper function + uint64 oracleTimestamp; + bytes32 beaconStateRoot; + uint40 validatorIndex; + bytes validatorFieldsProof; + bytes32[] validatorFields; + + function test_revert_validatorActive() public { + // Set JSON & params + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _setWithdrawalCredentialParams(); + + // Set validator status to active + eigenPodHarness.setValidatorStatus(validatorFields[0], IEigenPod.VALIDATOR_STATUS.ACTIVE); + + // Expect revert + cheats.expectRevert( + "EigenPod.verifyCorrectWithdrawalCredentials: Validator must be inactive to prove withdrawal credentials" + ); + eigenPodHarness.verifyWithdrawalCredentials( + oracleTimestamp, + beaconStateRoot, + validatorIndex, + validatorFieldsProof, + validatorFields + ); + } - uint40[] memory validatorIndices = new uint40[](1); - validatorIndices[0] = uint40(getValidatorIndex()); - bytes memory balanceUpdateProof = abi.encodePacked(getBalanceUpdateProof()); + function testFuzz_revert_invalidValidatorFields(address wrongWithdrawalAddress) public { + // Set JSON and params + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _setWithdrawalCredentialParams(); - bytes32 newBeaconStateRoot = getBeaconStateRoot(); - emit log_named_bytes32("newBeaconStateRoot", newBeaconStateRoot); - BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(newBeaconStateRoot); + // Change the withdrawal credentials in validatorFields, which is at index 1 + validatorFields[1] = abi.encodePacked(bytes1(uint8(1)), bytes11(0), wrongWithdrawalAddress).toBytes32(0); + // Expect revert cheats.expectRevert( - bytes("EigenPod.verifyBalanceUpdate: Validators balance has already been updated for this timestamp") + "EigenPod.verifyCorrectWithdrawalCredentials: Proof is not for this EigenPod" + ); + eigenPodHarness.verifyWithdrawalCredentials( + oracleTimestamp, + beaconStateRoot, + validatorIndex, + validatorFieldsProof, + validatorFields + ); + } + + function test_effectiveBalanceGreaterThan32ETH() public { + // Set JSON and params + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + _setWithdrawalCredentialParams(); + + // Check that restaked balance greater than 32 ETH + uint64 effectiveBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + assertGt(effectiveBalanceGwei, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Proof file has an effective balance less than 32 ETH"); + + // Verify withdrawal credentials + vm.expectEmit(true, true, true, true); + emit ValidatorRestaked(validatorIndex); + vm.expectEmit(true, true, true, true); + emit ValidatorBalanceUpdated(validatorIndex, oracleTimestamp, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR); + uint256 restakedBalanceWei = eigenPodHarness.verifyWithdrawalCredentials( + oracleTimestamp, + beaconStateRoot, + validatorIndex, + validatorFieldsProof, + validatorFields ); - podInternalFunctionTester.verifyBalanceUpdate( + + // Checks + assertEq(restakedBalanceWei, uint256(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * uint256(1e9), "Returned restaked balance gwei should be max"); + _assertWithdrawalCredentialsSet(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR); + } + + function test_effectiveBalanceLessThan32ETH() public { + // Set JSON and params + setJSON("./src/test/test-data/withdrawal_credential_proof_302913_30ETHBalance.json"); + _setWithdrawalCredentialParams(); + + // Check that restaked balance less than 32 ETH + uint64 effectiveBalanceGwei = validatorFields.getEffectiveBalanceGwei(); + assertLt(effectiveBalanceGwei, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Proof file has an effective balance greater than 32 ETH"); + + // Verify withdrawal credentials + vm.expectEmit(true, true, true, true); + emit ValidatorRestaked(validatorIndex); + vm.expectEmit(true, true, true, true); + emit ValidatorBalanceUpdated(validatorIndex, oracleTimestamp, effectiveBalanceGwei); + uint256 restakedBalanceWei = eigenPodHarness.verifyWithdrawalCredentials( oracleTimestamp, + beaconStateRoot, + validatorIndex, + validatorFieldsProof, + validatorFields + ); + + // Checks + assertEq(restakedBalanceWei, uint256(effectiveBalanceGwei) * uint256(1e9), "Returned restaked balance gwei incorrect"); + _assertWithdrawalCredentialsSet(effectiveBalanceGwei); + } + + function _assertWithdrawalCredentialsSet(uint256 restakedBalanceGwei) internal { + IEigenPod.ValidatorInfo memory validatorInfo = eigenPodHarness.validatorPubkeyHashToInfo(validatorFields[0]); + assertEq(uint8(validatorInfo.status), uint8(IEigenPod.VALIDATOR_STATUS.ACTIVE), "Validator status should be active"); + assertEq(validatorInfo.validatorIndex, validatorIndex, "Validator index incorrectly set"); + assertEq(validatorInfo.mostRecentBalanceUpdateTimestamp, oracleTimestamp, "Most recent balance update timestamp incorrectly set"); + assertEq(validatorInfo.restakedBalanceGwei, restakedBalanceGwei, "Restaked balance gwei not set correctly"); + } + + + function _setWithdrawalCredentialParams() public { + // Set beacon state root, validatorIndex + beaconStateRoot = getBeaconStateRoot(); + validatorIndex = uint40(getValidatorIndex()); + validatorFieldsProof = abi.encodePacked(getWithdrawalCredentialProof()); // Validator fields are proven here + validatorFields = getValidatorFields(); + + // Get an oracle timestamp + cheats.warp(GOERLI_GENESIS_TIME + 1 days); + oracleTimestamp = uint64(block.timestamp); + } +} + +/// @notice In practice, this function should be called after a validator has verified their withdrawal credentials +contract EigenPodUnitTests_VerifyBalanceUpdateTests is EigenPodHarnessSetup, ProofParsing, IEigenPodEvents { + using BeaconChainProofs for *; + + // Params to verifyBalanceUpdate, can be set in test or helper function + uint64 oracleTimestamp; + uint40 validatorIndex; + bytes32 beaconStateRoot; + BeaconChainProofs.BalanceUpdateProof balanceUpdateProof; + bytes32[] validatorFields; + + function testFuzz_revert_oracleTimestampStale(uint64 oracleFuzzTimestamp, uint64 mostRecentBalanceUpdateTimestamp) public { + // Constain inputs and set proof file + cheats.assume(oracleFuzzTimestamp < mostRecentBalanceUpdateTimestamp); + setJSON("src/test/test-data/balanceUpdateProof_notOverCommitted_302913.json"); + + // Get validator fields and balance update root + validatorFields = getValidatorFields(); + BeaconChainProofs.BalanceUpdateProof memory proof = _getBalanceUpdateProof(); + + // Balance update reversion + cheats.expectRevert( + "EigenPod.verifyBalanceUpdate: Validators balance has already been updated for this timestamp" + ); + eigenPodHarness.verifyBalanceUpdate( + oracleFuzzTimestamp, 0, bytes32(0), balanceUpdateProof, @@ -236,262 +547,444 @@ contract EigenPodUnitTests is Test, ProofParsing { ); } - function testProcessFullWithdrawalForLessThanMaxRestakedBalance(uint64 withdrawalAmount) public { - _deployInternalFunctionTester(); - cheats.assume(withdrawalAmount > 0 && withdrawalAmount < MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR); - IEigenPod.ValidatorInfo memory validatorInfo = IEigenPod.ValidatorInfo({ - validatorIndex: 0, - restakedBalanceGwei: 0, - mostRecentBalanceUpdateTimestamp: 0, - status: IEigenPod.VALIDATOR_STATUS.ACTIVE - }); - uint64 balanceBefore = podInternalFunctionTester.withdrawableRestakedExecutionLayerGwei(); - podInternalFunctionTester.processFullWithdrawal(0, bytes32(0), 0, podOwner, withdrawalAmount, validatorInfo); - require(podInternalFunctionTester.withdrawableRestakedExecutionLayerGwei() - balanceBefore == withdrawalAmount, "withdrawableRestakedExecutionLayerGwei hasn't been updated correctly"); - } - function testWithdrawBeforeRestakingAfterRestaking() public { - cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); - cheats.startPrank(podOwner); - pod.withdrawBeforeRestaking(); - cheats.stopPrank(); - } - - //post M2, all new pods deployed will have "hasRestaked = true". THis tests that - function testDeployedPodIsRestaked(address podOwner) public fuzzedAddress(podOwner) { - cheats.startPrank(podOwner); - eigenPodManager.createPod(); - cheats.stopPrank(); - IEigenPod pod = eigenPodManager.getPod(podOwner); - require(pod.hasRestaked() == true, "Pod should be restaked"); - } - function testTryToActivateRestakingAfterHasRestakedIsSet() public { - require(pod.hasRestaked() == true, "Pod should be restaked"); - cheats.startPrank(podOwner); - cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); - pod.activateRestaking(); - } - function testTryToWithdrawBeforeRestakingAfterHasRestakedIsSet() public { - require(pod.hasRestaked() == true, "Pod should be restaked"); - cheats.startPrank(podOwner); - cheats.expectRevert(bytes("EigenPod.hasNeverRestaked: restaking is enabled")); - pod.withdrawBeforeRestaking(); - } - function testMismatchedWithdrawalProofInputs(uint64 numValidators, uint64 numValidatorProofs) external { - cheats.assume(numValidators < numValidatorProofs && numValidatorProofs < 5); - setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); - bytes[] memory validatorFieldsProofArray = new bytes[](numValidatorProofs); - for (uint256 index = 0; index < numValidators; index++) { - validatorFieldsProofArray[index] = abi.encodePacked(getValidatorProof()); - } - bytes32[][] memory validatorFieldsArray = new bytes32[][](numValidators); - for (uint256 index = 0; index < validatorFieldsArray.length; index++) { - validatorFieldsArray[index] = getValidatorFields(); - } - BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); - BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](1); + function test_revert_validatorInactive() public { + // Set proof file + setJSON("src/test/test-data/balanceUpdateProof_notOverCommitted_302913.json"); - withdrawalProofsArray[0] = _getWithdrawalProof(); + // Set proof params + _setBalanceUpdateParams(); - bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); - withdrawalFieldsArray[0] = withdrawalFields; + // Set validator status to inactive + eigenPodHarness.setValidatorStatus(validatorFields[0], IEigenPod.VALIDATOR_STATUS.INACTIVE); - cheats.expectRevert(bytes("EigenPod.verifyAndProcessWithdrawals: inputs must be same length")); - pod.verifyAndProcessWithdrawals(0, stateRootProofStruct, withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray); + // Balance update reversion + cheats.expectRevert( + "EigenPod.verifyBalanceUpdate: Validator not active" + ); + eigenPodHarness.verifyBalanceUpdate( + oracleTimestamp, + validatorIndex, + beaconStateRoot, + balanceUpdateProof, + validatorFields, + 0 // Most recent balance update timestamp set to 0 + ); } - function testProveWithdrawalFromBeforeLastWithdrawBeforeRestaking() external { - cheats.store(address(pod), bytes32(uint256(52)), bytes32(uint256(1))); - require(pod.hasRestaked() != true, "Pod should not be restaked"); - setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); - BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(getLatestBlockRoot()); - BeaconChainProofs.WithdrawalProof[] memory withdrawalProofsArray = new BeaconChainProofs.WithdrawalProof[](1); - withdrawalProofsArray[0] = _getWithdrawalProof(); - - uint64 timestampOfWithdrawal = Endian.fromLittleEndianUint64(withdrawalProofsArray[0].timestampRoot); - uint256 newTimestamp = timestampOfWithdrawal + 2500; - cheats.warp(newTimestamp); - cheats.startPrank(podOwner); - pod.withdrawBeforeRestaking(); - cheats.stopPrank(); - - bytes[] memory validatorFieldsProofArray = new bytes[](1); - validatorFieldsProofArray[0] = abi.encodePacked(getValidatorProof()); - bytes32[][] memory validatorFieldsArray = new bytes32[][](1); - validatorFieldsArray[0] = getValidatorFields(); - BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); - bytes32[][] memory withdrawalFieldsArray = new bytes32[][](1); - withdrawalFieldsArray[0] = withdrawalFields; - - cheats.warp(timestampOfWithdrawal); - cheats.expectRevert(bytes("EigenPod.proofIsForValidTimestamp: beacon chain proof must be for timestamp after mostRecentWithdrawalTimestamp")); - pod.verifyAndProcessWithdrawals(0, stateRootProofStruct, withdrawalProofsArray, validatorFieldsProofArray, validatorFieldsArray, withdrawalFieldsArray); - } - function testPodReceiveFallBack(uint256 amountETH) external { - cheats.assume(amountETH > 0); - cheats.deal(address(this), amountETH); - Address.sendValue(payable(address(pod)), amountETH); - require(address(pod).balance == amountETH, "Pod should have received ETH"); - } /** - * This is a regression test for a bug (EIG-14) found by Hexens. Lets say podOwner sends 32 ETH to the EigenPod, - * the nonBeaconChainETHBalanceWei increases by 32 ETH. podOwner calls withdrawBeforeRestaking, which - * will simply send the entire ETH balance (32 ETH) to the owner. The owner activates restaking, - * creates a validator and verifies the withdrawal credentials, receiving 32 ETH in shares. - * They can exit the validator, the pod gets the 32ETH and they can call withdrawNonBeaconChainETHBalanceWei - * And simply withdraw the 32ETH because nonBeaconChainETHBalanceWei is 32ETH. This was an issue because - * nonBeaconChainETHBalanceWei was never zeroed out in _processWithdrawalBeforeRestaking + * Regression test for a bug that allowed balance updates to be made for withdrawn validators. Thus + * the validator's balance could be maliciously proven to be 0 before the validator themselves are + * able to prove their withdrawal. */ - function testValidatorBalanceCannotBeRemovedFromPodViaNonBeaconChainETHBalanceWei() external { + function test_revert_balanceUpdateAfterWithdrawableEpoch() external { + // Set Json proof + setJSON("src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); + + // Set proof params + _setBalanceUpdateParams(); - uint256 amount = 32 ether; - cheats.store(address(pod), bytes32(uint256(52)), bytes32(0)); - cheats.deal(address(this), amount); - // simulate a withdrawal processed on the beacon chain, pod balance goes to 32 ETH - Address.sendValue(payable(address(pod)), amount); - require(pod.nonBeaconChainETHBalanceWei() == amount, "nonBeaconChainETHBalanceWei should be 32 ETH"); - //simulate that hasRestaked is set to false, so that we can test withdrawBeforeRestaking for pods deployed before M2 activation - cheats.store(address(pod), bytes32(uint256(52)), bytes32(uint256(1))); - //this is an M1 pod so hasRestaked should be false - require(pod.hasRestaked() == false, "Pod should be restaked"); - cheats.startPrank(podOwner); - pod.activateRestaking(); - cheats.stopPrank(); - require(pod.nonBeaconChainETHBalanceWei() == 0, "nonBeaconChainETHBalanceWei should be 32 ETH"); + // Set balance root and withdrawable epoch + balanceUpdateProof.balanceRoot = bytes32(uint256(0)); + validatorFields[7] = bytes32(uint256(0)); // per consensus spec, slot 7 is withdrawable epoch == 0 + + // Expect revert on balance update + cheats.expectRevert(bytes("EigenPod.verifyBalanceUpdate: validator is withdrawable but has not withdrawn")); + eigenPodHarness.verifyBalanceUpdate(oracleTimestamp, validatorIndex, beaconStateRoot, balanceUpdateProof, validatorFields, 0); } - /** - * Regression test for a bug that allowed balance updates to be made for withdrawn validators. Thus - * the validator's balance could be maliciously proven to be 0 before the validator themselves are - * able to prove their withdrawal. - */ - function testBalanceUpdateMadeAfterWithdrawableEpochFails() external { - _deployInternalFunctionTester(); - cheats.roll(block.number + 1); - // ./solidityProofGen "BalanceUpdateProof" 302913 true 0 "data/withdrawal_proof_goerli/goerli_slot_6399999.json" "data/withdrawal_proof_goerli/goerli_slot_6399998.json" "balanceUpdateProof_overCommitted_302913.json" - setJSON("./src/test/test-data/balanceUpdateProof_updated_to_0ETH_302913.json"); - bytes32[] memory validatorFields = getValidatorFields(); - uint40 validatorIndex = uint40(getValidatorIndex()); + /// @notice Rest of tests assume beacon chain proofs are correct; Now we update the validator's balance + + ///@notice Balance of validator is > 32e9 + function test_positiveSharesDelta() public { + // Set JSON + setJSON("src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); + + // Set proof params + _setBalanceUpdateParams(); + + // Verify balance update + vm.expectEmit(true, true, true, true); + emit ValidatorBalanceUpdated(validatorIndex, oracleTimestamp, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR); + int256 sharesDeltaGwei = eigenPodHarness.verifyBalanceUpdate( + oracleTimestamp, + validatorIndex, + beaconStateRoot, + balanceUpdateProof, + validatorFields, + 0 // Most recent balance update timestamp set to 0 + ); + + // Checks + IEigenPod.ValidatorInfo memory validatorInfo = eigenPodHarness.validatorPubkeyHashToInfo(validatorFields[0]); + assertEq(validatorInfo.restakedBalanceGwei, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Restaked balance gwei should be max"); + assertGt(sharesDeltaGwei, 0, "Shares delta should be positive"); + assertEq(sharesDeltaGwei, int256(uint256(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR)), "Shares delta should be equal to restaked balance"); + } + + function test_negativeSharesDelta() public { + // Set JSON + setJSON("src/test/test-data/balanceUpdateProof_balance28ETH_302913.json"); + + // Set proof params + _setBalanceUpdateParams(); + uint64 newValidatorBalance = balanceUpdateProof.balanceRoot.getBalanceAtIndex(validatorIndex); + + // Set balance of validator to max ETH + eigenPodHarness.setValidatorRestakedBalance(validatorFields[0], MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR); + + // Verify balance update + int256 sharesDeltaGwei = eigenPodHarness.verifyBalanceUpdate( + oracleTimestamp, + validatorIndex, + beaconStateRoot, + balanceUpdateProof, + validatorFields, + 0 // Most recent balance update timestamp set to 0 + ); + + // Checks + IEigenPod.ValidatorInfo memory validatorInfo = eigenPodHarness.validatorPubkeyHashToInfo(validatorFields[0]); + assertEq(validatorInfo.restakedBalanceGwei, newValidatorBalance, "Restaked balance gwei should be max"); + assertLt(sharesDeltaGwei, 0, "Shares delta should be negative"); + int256 expectedSharesDiff = int256(uint256(newValidatorBalance)) - int256(uint256(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR)); + assertEq(sharesDeltaGwei, expectedSharesDiff, "Shares delta should be equal to restaked balance"); + } + + function test_zeroSharesDelta() public { + // Set JSON + setJSON("src/test/test-data/balanceUpdateProof_overCommitted_302913.json"); + + // Set proof params + _setBalanceUpdateParams(); - bytes memory proof = abi.encodePacked(getBalanceUpdateProof()); - bytes32 newLatestBlockRoot = getLatestBlockRoot(); + // Set previous restaked balance to max restaked balance + eigenPodHarness.setValidatorRestakedBalance(validatorFields[0], MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR); + + // Verify balance update + int256 sharesDeltaGwei = eigenPodHarness.verifyBalanceUpdate( + oracleTimestamp, + validatorIndex, + beaconStateRoot, + balanceUpdateProof, + validatorFields, + 0 // Most recent balance update timestamp set to 0 + ); - BeaconChainOracleMock(address(beaconChainOracle)).setOracleBlockRootAtTimestamp(newLatestBlockRoot); + // Checks + assertEq(sharesDeltaGwei, 0, "Shares delta should be 0"); + } - BeaconChainProofs.StateRootProof memory stateRootProofStruct = _getStateRootProof(); + function _setBalanceUpdateParams() internal { + // Set validator index, beacon state root, balance update proof, and validator fields + validatorIndex = uint40(getValidatorIndex()); + beaconStateRoot = getBeaconStateRoot(); + balanceUpdateProof = _getBalanceUpdateProof(); + validatorFields = getValidatorFields(); - validatorFields[7] = bytes32(uint256(0)); // set withdrawable epoch timestamp to 0 + // Get an oracle timestamp cheats.warp(GOERLI_GENESIS_TIME + 1 days); - uint64 oracleTimestamp = uint64(block.timestamp); + oracleTimestamp = uint64(block.timestamp); - podInternalFunctionTester.setValidatorStatus(validatorFields[0], IEigenPod.VALIDATOR_STATUS.ACTIVE); - cheats.expectRevert(bytes("EigenPod.verifyBalanceUpdate: validator is withdrawable but has not withdrawn")); - podInternalFunctionTester.verifyBalanceUpdate(oracleTimestamp, validatorIndex, newLatestBlockRoot, proof, validatorFields, 0); + // Set validator status to active + eigenPodHarness.setValidatorStatus(validatorFields[0], IEigenPod.VALIDATOR_STATUS.ACTIVE); } - function testWithdrawlBeforeRestakingFromNonPodOwnerAddress(uint256 amount, address nonPodOwner) external { - cheats.assume(nonPodOwner != podOwner); - uint256 amount = 32 ether; + function _getBalanceUpdateProof() internal returns (BeaconChainProofs.BalanceUpdateProof memory) { + bytes32 balanceRoot = getBalanceRoot(); + BeaconChainProofs.BalanceUpdateProof memory proofs = BeaconChainProofs.BalanceUpdateProof( + abi.encodePacked(getValidatorBalanceProof()), + abi.encodePacked(getWithdrawalCredentialProof()), //technically this is to verify validator pubkey in the validator fields, but the WC proof is effectively the same so we use it here again. + balanceRoot + ); + return proofs; + } +} + +contract EigenPodUnitTests_WithdrawalTests is EigenPodHarnessSetup, ProofParsing, IEigenPodEvents { + using BeaconChainProofs for *; + + // Params to process withdrawal + bytes32 beaconStateRoot; + BeaconChainProofs.WithdrawalProof withdrawalToProve; + bytes validatorFieldsProof; + bytes32[] validatorFields; + bytes32[] withdrawalFields; - cheats.store(address(pod), bytes32(uint256(52)), bytes32(0)); + // Most recent withdrawal timestamp incremented when withdrawal processed before restaking OR when staking activated + function test_verifyAndProcessWithdrawal_revert_staleProof() public hasNotRestaked { + // Set JSON & params + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + _setWithdrawalProofParams(); - cheats.startPrank(nonPodOwner); - cheats.expectRevert(bytes("EigenPod.onlyEigenPodOwner: not podOwner")); - pod.withdrawBeforeRestaking(); - cheats.stopPrank(); + // Set timestamp to after withdrawal timestamp + uint64 timestampOfWithdrawal = Endian.fromLittleEndianUint64(withdrawalToProve.timestampRoot); + uint256 newTimestamp = timestampOfWithdrawal + 2500; + cheats.warp(newTimestamp); + + // Activate restaking, setting `mostRecentWithdrawalTimestamp` + eigenPodHarness.activateRestaking(); + + // Expect revert + cheats.expectRevert("EigenPod.proofIsForValidTimestamp: beacon chain proof must be for timestamp after mostRecentWithdrawalTimestamp"); + eigenPodHarness.verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalToProve, + validatorFieldsProof, + validatorFields, + withdrawalFields + ); } - function testFullWithdrawalAmounts(bytes32 pubkeyHash, uint64 withdrawalAmount) external { - _deployInternalFunctionTester(); - IEigenPod.ValidatorInfo memory validatorInfo = IEigenPod.ValidatorInfo({ - validatorIndex: 0, - restakedBalanceGwei: 0, - mostRecentBalanceUpdateTimestamp: 0, - status: IEigenPod.VALIDATOR_STATUS.ACTIVE - }); - IEigenPod.VerifiedWithdrawal memory vw = podInternalFunctionTester.processFullWithdrawal(0, pubkeyHash, 0, podOwner, withdrawalAmount, validatorInfo); + function test_verifyAndProcessWithdrawal_revert_statusInactive() public { + // Set JSON & params + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + _setWithdrawalProofParams(); - if(withdrawalAmount > podInternalFunctionTester.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR()){ - require(vw.amountToSendGwei == withdrawalAmount - podInternalFunctionTester.MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR(), "newAmount should be MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR"); - } - else{ - require(vw.amountToSendGwei == 0, "newAmount should be withdrawalAmount"); - } + // Set status to inactive + eigenPodHarness.setValidatorStatus(validatorFields[0], IEigenPod.VALIDATOR_STATUS.INACTIVE); + + // Expect revert + cheats.expectRevert("EigenPod._verifyAndProcessWithdrawal: Validator never proven to have withdrawal credentials pointed to this contract"); + eigenPodHarness.verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalToProve, + validatorFieldsProof, + validatorFields, + withdrawalFields + ); } - function testProcessPartialWithdrawal( - uint40 validatorIndex, - uint64 withdrawalTimestamp, - address recipient, - uint64 partialWithdrawalAmountGwei - ) external { - _deployInternalFunctionTester(); - cheats.expectEmit(true, true, true, true, address(podInternalFunctionTester)); - emit PartialWithdrawalRedeemed( - validatorIndex, - withdrawalTimestamp, - recipient, - partialWithdrawalAmountGwei + function test_verifyAndProcessWithdrawal_withdrawalAlreadyProcessed() public setWithdrawalCredentialsExcess { + // Set JSON & params + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + _setWithdrawalProofParams(); + + // Process withdrawal + eigenPodHarness.verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalToProve, + validatorFieldsProof, + validatorFields, + withdrawalFields ); - IEigenPod.VerifiedWithdrawal memory vw = podInternalFunctionTester.processPartialWithdrawal(validatorIndex, withdrawalTimestamp, recipient, partialWithdrawalAmountGwei); - require(vw.amountToSendGwei == partialWithdrawalAmountGwei, "newAmount should be partialWithdrawalAmountGwei"); + // Attempt to process again + cheats.expectRevert("EigenPod._verifyAndProcessWithdrawal: withdrawal has already been proven for this timestamp"); + eigenPodHarness.verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalToProve, + validatorFieldsProof, + validatorFields, + withdrawalFields + ); } - function testRecoverTokens(uint256 amount, address recipient) external fuzzedAddress(recipient) { - cheats.assume(amount > 0 && amount < 1e30); - IERC20 randomToken = new ERC20PresetFixedSupply( - "rand", - "RAND", - 1e30, - address(this) + function test_verifyAndProcessWithdrawal_excess() public setWithdrawalCredentialsExcess { + // Set JSON & params + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + _setWithdrawalProofParams(); + + // Process withdrawal + eigenPodHarness.verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalToProve, + validatorFieldsProof, + validatorFields, + withdrawalFields ); - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = randomToken; - uint256[] memory amounts = new uint256[](1); - amounts[0] = amount; + // Verify storage + bytes32 validatorPubKeyHash = validatorFields.getPubkeyHash(); + uint64 withdrawalTimestamp = withdrawalToProve.getWithdrawalTimestamp(); + assertTrue(eigenPodHarness.provenWithdrawal(validatorPubKeyHash, withdrawalTimestamp), "Withdrawal not set to proven"); + } - randomToken.transfer(address(pod), amount); - require(randomToken.balanceOf(address(pod)) == amount, "randomToken balance should be amount"); + /// @notice Tests processing a full withdrawal > MAX_RESTAKED_GWEI_PER_VALIDATOR + function test_processFullWithdrawal_excess32ETH() public setWithdrawalCredentialsExcess { + // Set JSON & params + setJSON("./src/test/test-data/fullWithdrawalProof_Latest.json"); + _setWithdrawalProofParams(); + + // Get params to check against + uint64 withdrawalTimestamp = withdrawalToProve.getWithdrawalTimestamp(); + uint40 validatorIndex = uint40(getValidatorIndex()); + uint64 withdrawalAmountGwei = withdrawalFields.getWithdrawalAmountGwei(); + assertGt(withdrawalAmountGwei, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Withdrawal amount should be greater than max restaked balance for this test"); + + // Process full withdrawal + vm.expectEmit(true, true, true, true); + emit FullWithdrawalRedeemed(validatorIndex, withdrawalTimestamp, podOwner, withdrawalAmountGwei); + IEigenPod.VerifiedWithdrawal memory vw = eigenPodHarness.verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalToProve, + validatorFieldsProof, + validatorFields, + withdrawalFields + ); - uint256 recipientBalanceBefore = randomToken.balanceOf(recipient); + // Storage checks in _verifyAndProcessWithdrawal + bytes32 validatorPubKeyHash = validatorFields.getPubkeyHash(); + assertTrue(eigenPodHarness.provenWithdrawal(validatorPubKeyHash, withdrawalTimestamp), "Withdrawal not set to proven"); - cheats.startPrank(podOwner); - pod.recoverTokens(tokens, amounts, recipient); - cheats.stopPrank(); - require(randomToken.balanceOf(address(recipient)) - recipientBalanceBefore == amount, "recipient should have received amount"); + // Checks from _processFullWithdrawal + assertEq(eigenPod.withdrawableRestakedExecutionLayerGwei(), MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Incorrect withdrawable restaked execution layer gwei"); + // Excess withdrawal amount is diff between restaked balance and total withdrawal amount + uint64 excessWithdrawalAmount = withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; + assertEq(vw.amountToSendGwei, excessWithdrawalAmount, "Amount to send via router is not correct"); + assertEq(vw.sharesDeltaGwei, 0, "Shares delta not correct"); // Shares delta is 0 since restaked balance and amount to withdraw were max + + // ValidatorInfo storage update checks + IEigenPod.ValidatorInfo memory validatorInfo = eigenPodHarness.validatorPubkeyHashToInfo(validatorFields[0]); + assertEq(uint8(validatorInfo.status), uint8(IEigenPod.VALIDATOR_STATUS.WITHDRAWN), "Validator status should be withdrawn"); + assertEq(validatorInfo.restakedBalanceGwei, 0, "Restaked balance gwei should be 0"); } - function testRecoverTokensMismatchedInputs() external { - uint256 tokenListLen = 5; - uint256 amountsToWithdrawLen = 2; + function test_processFullWithdrawal_lessThan32ETH() public setWithdrawalCredentialsExcess { + // Set JSON & params + setJSON("src/test/test-data/fullWithdrawalProof_Latest_28ETH.json"); + _setWithdrawalProofParams(); + + // Get params to check against + uint64 withdrawalTimestamp = withdrawalToProve.getWithdrawalTimestamp(); + uint40 validatorIndex = uint40(getValidatorIndex()); + uint64 withdrawalAmountGwei = withdrawalFields.getWithdrawalAmountGwei(); + assertLt(withdrawalAmountGwei, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Withdrawal amount should be greater than max restaked balance for this test"); + + // Process full withdrawal + IEigenPod.VerifiedWithdrawal memory vw = eigenPodHarness.verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalToProve, + validatorFieldsProof, + validatorFields, + withdrawalFields + ); - IERC20[] memory tokens = new IERC20[](tokenListLen); - uint256[] memory amounts = new uint256[](amountsToWithdrawLen); + // Storage checks in _verifyAndProcessWithdrawal + bytes32 validatorPubKeyHash = validatorFields.getPubkeyHash(); + // assertTrue(eigenPodHarness.provenWithdrawal(validatorPubKeyHash, withdrawalTimestamp), "Withdrawal not set to proven"); - cheats.expectRevert(bytes("EigenPod.recoverTokens: tokenList and amountsToWithdraw must be same length")); - cheats.startPrank(podOwner); - pod.recoverTokens(tokens, amounts, address(this)); - cheats.stopPrank(); + // Checks from _processFullWithdrawal + assertEq(eigenPod.withdrawableRestakedExecutionLayerGwei(), withdrawalAmountGwei, "Incorrect withdrawable restaked execution layer gwei"); + // Excess withdrawal amount should be 0 since balance is < MAX + assertEq(vw.amountToSendGwei, 0, "Amount to send via router is not correct"); + int256 expectedSharesDiff = int256(uint256(withdrawalAmountGwei)) - int256(uint256(MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR)); + assertEq(vw.sharesDeltaGwei, expectedSharesDiff, "Shares delta not correct"); // Shares delta is 0 since restaked balance and amount to withdraw were max + + // ValidatorInfo storage update checks + IEigenPod.ValidatorInfo memory validatorInfo = eigenPodHarness.validatorPubkeyHashToInfo(validatorFields[0]); + assertEq(uint8(validatorInfo.status), uint8(IEigenPod.VALIDATOR_STATUS.WITHDRAWN), "Validator status should be withdrawn"); + assertEq(validatorInfo.restakedBalanceGwei, 0, "Restaked balance gwei should be 0"); } - function _deployInternalFunctionTester() internal { - podInternalFunctionTester = new EPInternalFunctions( - ethPOSDeposit, - delayedWithdrawalRouter, - IEigenPodManager(podManagerAddress), - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, - GOERLI_GENESIS_TIME + function test_processPartialWithdrawal() public setWithdrawalCredentialsExcess { + // Set JSON & params + setJSON("./src/test/test-data/partialWithdrawalProof_Latest.json"); + _setWithdrawalProofParams(); + + // Get params to check against + uint64 withdrawalTimestamp = withdrawalToProve.getWithdrawalTimestamp(); + uint40 validatorIndex = uint40(getValidatorIndex()); + uint64 withdrawalAmountGwei = withdrawalFields.getWithdrawalAmountGwei(); + + // Assert that partial withdrawal code path will be tested + assertLt(withdrawalToProve.getWithdrawalEpoch(), validatorFields.getWithdrawableEpoch(), "Withdrawal epoch should be less than the withdrawable epoch"); + + // Process partial withdrawal + vm.expectEmit(true, true, true, true); + emit PartialWithdrawalRedeemed(validatorIndex, withdrawalTimestamp, podOwner, withdrawalAmountGwei); + IEigenPod.VerifiedWithdrawal memory vw = eigenPodHarness.verifyAndProcessWithdrawal( + beaconStateRoot, + withdrawalToProve, + validatorFieldsProof, + validatorFields, + withdrawalFields ); + + // Storage checks in _verifyAndProcessWithdrawal + bytes32 validatorPubKeyHash = validatorFields.getPubkeyHash(); + assertTrue(eigenPodHarness.provenWithdrawal(validatorPubKeyHash, withdrawalTimestamp), "Withdrawal not set to proven"); + + // Checks from _processPartialWithdrawal + assertEq(eigenPod.sumOfPartialWithdrawalsClaimedGwei(), withdrawalAmountGwei, "Incorrect partial withdrawal amount"); + assertEq(vw.amountToSendGwei, withdrawalAmountGwei, "Amount to send via router is not correct"); + assertEq(vw.sharesDeltaGwei, 0, "Shares delta should be 0"); + + // Assert validator still has same restaked balance and status + IEigenPod.ValidatorInfo memory validatorInfo = eigenPodHarness.validatorPubkeyHashToInfo(validatorFields[0]); + assertEq(uint8(validatorInfo.status), uint8(IEigenPod.VALIDATOR_STATUS.ACTIVE), "Validator status should be active"); + assertEq(validatorInfo.restakedBalanceGwei, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR, "Restaked balance gwei should be max"); + } + + function testFuzz_processFullWithdrawal(bytes32 pubkeyHash, uint64 restakedAmount, uint64 withdrawalAmount) public { + // Format validatorInfo struct + IEigenPod.ValidatorInfo memory validatorInfo = IEigenPod.ValidatorInfo({ + validatorIndex: 0, + restakedBalanceGwei: restakedAmount, + mostRecentBalanceUpdateTimestamp: 0, + status: IEigenPod.VALIDATOR_STATUS.ACTIVE + }); + + // Process full withdrawal + IEigenPod.VerifiedWithdrawal memory vw = eigenPodHarness.processFullWithdrawal(0, pubkeyHash, 0, podOwner, withdrawalAmount, validatorInfo); + + // Get expected amounts based on withdrawalAmount + uint64 amountETHToQueue; + uint64 amountETHToSend; + if (withdrawalAmount > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR){ + amountETHToQueue = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; + amountETHToSend = withdrawalAmount - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; + } else { + amountETHToQueue = withdrawalAmount; + amountETHToSend = 0; + } + + // Check invariant-> amountToQueue + amountToSend = withdrawalAmount + assertEq(vw.amountToSendGwei + eigenPod.withdrawableRestakedExecutionLayerGwei(), withdrawalAmount, "Amount to queue and send must add up to total withdrawal amount"); + + // Check amount to queue and send + assertEq(vw.amountToSendGwei, amountETHToSend, "Amount to queue is not correct"); + assertEq(eigenPod.withdrawableRestakedExecutionLayerGwei(), amountETHToQueue, "Incorrect withdrawable restaked execution layer gwei"); + + // Check shares delta + int256 expectedSharesDelta = int256(uint256(amountETHToQueue)) - int256(uint256(restakedAmount)); + assertEq(vw.sharesDeltaGwei, expectedSharesDelta, "Shares delta not correct"); + + // Storage checks + IEigenPod.ValidatorInfo memory validatorInfoAfter = eigenPodHarness.validatorPubkeyHashToInfo(pubkeyHash); + assertEq(uint8(validatorInfoAfter.status), uint8(IEigenPod.VALIDATOR_STATUS.WITHDRAWN), "Validator status should be withdrawn"); + assertEq(validatorInfoAfter.restakedBalanceGwei, 0, "Restaked balance gwei should be 0"); + } + + function testFuzz_processFullWithdrawal_lessMaxRestakedBalance(bytes32 pubkeyHash, uint64 restakedAmount, uint64 withdrawalAmount) public { + withdrawalAmount = uint64(bound(withdrawalAmount, 0, MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR)); + testFuzz_processFullWithdrawal(pubkeyHash, restakedAmount, withdrawalAmount); + } + + function testFuzz_processPartialWithdrawal( + uint40 validatorIndex, + uint64 withdrawalTimestamp, + address recipient, + uint64 partialWithdrawalAmountGwei + ) public { + IEigenPod.VerifiedWithdrawal memory vw = eigenPodHarness.processPartialWithdrawal(validatorIndex, withdrawalTimestamp, recipient, partialWithdrawalAmountGwei); + + // Checks + assertEq(eigenPod.sumOfPartialWithdrawalsClaimedGwei(), partialWithdrawalAmountGwei, "Incorrect partial withdrawal amount"); + assertEq(vw.amountToSendGwei, partialWithdrawalAmountGwei, "Amount to send via router is not correct"); + assertEq(vw.sharesDeltaGwei, 0, "Shares delta should be 0"); } - function _getStateRootProof() internal returns (BeaconChainProofs.StateRootProof memory) { - return BeaconChainProofs.StateRootProof(getBeaconStateRoot(), abi.encodePacked(getStateRootProof())); + function _setWithdrawalProofParams() internal { + // Set validator index, beacon state root, balance update proof, and validator fields + beaconStateRoot = getBeaconStateRoot(); + validatorFields = getValidatorFields(); + validatorFieldsProof = abi.encodePacked(getValidatorProof()); + withdrawalToProve = _getWithdrawalProof(); + withdrawalFields = getWithdrawalFields(); } /// @notice this function just generates a valid proof so that we can test other functionalities of the withdrawal flow function _getWithdrawalProof() internal returns (BeaconChainProofs.WithdrawalProof memory) { - { bytes32 blockRoot = getBlockRoot(); bytes32 slotRoot = getSlotRoot(); @@ -516,9 +1009,27 @@ contract EigenPodUnitTests is Test, ProofParsing { } } - function _deployPod() internal { - cheats.startPrank(podOwner); - eigenPodManager.createPod(); - cheats.stopPrank(); + ///@notice Effective balance is > 32 ETH + modifier setWithdrawalCredentialsExcess() { + // Set JSON and params + setJSON("./src/test/test-data/withdrawal_credential_proof_302913.json"); + // Set beacon state root, validatorIndex + beaconStateRoot = getBeaconStateRoot(); + uint40 validatorIndex = uint40(getValidatorIndex()); + validatorFieldsProof = abi.encodePacked(getWithdrawalCredentialProof()); // Validator fields are proven here + validatorFields = getValidatorFields(); + + // Get an oracle timestamp + cheats.warp(GOERLI_GENESIS_TIME + 1 days); + uint64 oracleTimestamp = uint64(block.timestamp); + + eigenPodHarness.verifyWithdrawalCredentials( + oracleTimestamp, + beaconStateRoot, + validatorIndex, + validatorFieldsProof, + validatorFields + ); + _; } } \ No newline at end of file diff --git a/src/test/unit/StrategyManagerUnit.t.sol b/src/test/unit/StrategyManagerUnit.t.sol index 94be5dee4..8b0066a8c 100644 --- a/src/test/unit/StrategyManagerUnit.t.sol +++ b/src/test/unit/StrategyManagerUnit.t.sol @@ -12,7 +12,7 @@ import "src/test/mocks/Reenterer.sol"; import "src/test/utils/EigenLayerUnitTestSetup.sol"; /** - * @notice Unit testing of the StrategyMananger contract, entire withdrawal tests related to the + * @notice Unit testing of the StrategyManager contract, entire withdrawal tests related to the * DelegationManager are not tested here but callable functions by the DelegationManager are mocked and tested here. * Contracts tested: StrategyManager.sol * Contracts not mocked: StrategyBase, PauserRegistry @@ -1117,7 +1117,7 @@ contract StrategyManagerUnitTests_addShares is StrategyManagerUnitTests { assertEq( strategyManager.stakerStrategyListLength(staker), MAX_STAKER_STRATEGY_LIST_LENGTH, - "strategyMananger.stakerStrategyListLength(staker) != MAX_STAKER_STRATEGY_LIST_LENGTH" + "strategyManager.stakerStrategyListLength(staker) != MAX_STAKER_STRATEGY_LIST_LENGTH" ); cheats.prank(staker); diff --git a/src/test/utils/Operators.sol b/src/test/utils/Operators.sol index e25edbd8c..84287def6 100644 --- a/src/test/utils/Operators.sol +++ b/src/test/utils/Operators.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.12; -import "../../contracts/libraries/BN254.sol"; import "forge-std/Test.sol"; import "forge-std/Script.sol"; import "forge-std/StdJson.sol"; @@ -25,41 +24,10 @@ contract Operators is Test { return stdJson.readAddress(operatorConfigJson, string.concat(operatorPrefix(index), "Address")); } - function getOperatorSchnorrSignature(uint256 index) public returns(uint256, BN254.G1Point memory) { - uint256 s = readUint(operatorConfigJson, index, "SField"); - BN254.G1Point memory pubkey = BN254.G1Point({ - X: readUint(operatorConfigJson, index, "RPoint.X"), - Y: readUint(operatorConfigJson, index, "RPoint.Y") - }); - return (s, pubkey); - } - function getOperatorSecretKey(uint256 index) public returns(uint256) { return readUint(operatorConfigJson, index, "SecretKey"); } - function getOperatorPubkeyG1(uint256 index) public returns(BN254.G1Point memory) { - BN254.G1Point memory pubkey = BN254.G1Point({ - X: readUint(operatorConfigJson, index, "PubkeyG1.X"), - Y: readUint(operatorConfigJson, index, "PubkeyG1.Y") - }); - return pubkey; - } - - function getOperatorPubkeyG2(uint256 index) public returns(BN254.G2Point memory) { - BN254.G2Point memory pubkey = BN254.G2Point({ - X: [ - readUint(operatorConfigJson, index, "PubkeyG2.X.A1"), - readUint(operatorConfigJson, index, "PubkeyG2.X.A0") - ], - Y: [ - readUint(operatorConfigJson, index, "PubkeyG2.Y.A1"), - readUint(operatorConfigJson, index, "PubkeyG2.Y.A0") - ] - }); - return pubkey; - } - function readUint(string memory json, uint256 index, string memory key) public returns (uint256) { return stringToUint(stdJson.readString(json, string.concat(operatorPrefix(index), key))); } diff --git a/src/test/utils/ProofParsing.sol b/src/test/utils/ProofParsing.sol index 37e20bdc3..44b7de2de 100644 --- a/src/test/utils/ProofParsing.sol +++ b/src/test/utils/ProofParsing.sol @@ -1,13 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.12; -import "../../contracts/libraries/BN254.sol"; import "forge-std/Test.sol"; -import "forge-std/Script.sol"; import "forge-std/StdJson.sol"; - -contract ProofParsing is Test{ +contract ProofParsing is Test { string internal proofConfigJson; string prefix; @@ -17,12 +14,9 @@ contract ProofParsing is Test{ bytes32[46] validatorProof; bytes32[44] historicalSummaryProof; - - bytes32[7] executionPayloadProof; bytes32[4] timestampProofs; - bytes32 slotRoot; bytes32 executionPayloadRoot; @@ -166,12 +160,12 @@ contract ProofParsing is Test{ } function getWithdrawalCredentialProof() public returns(bytes32[] memory) { - bytes32[] memory withdrawalCredenitalProof = new bytes32[](46); + bytes32[] memory withdrawalCredentialProof = new bytes32[](46); for (uint i = 0; i < 46; i++) { prefix = string.concat(".WithdrawalCredentialProof[", string.concat(vm.toString(i), "]")); - withdrawalCredenitalProof[i] = (stdJson.readBytes32(proofConfigJson, prefix)); + withdrawalCredentialProof[i] = (stdJson.readBytes32(proofConfigJson, prefix)); } - return withdrawalCredenitalProof; + return withdrawalCredentialProof; } function getValidatorFieldsProof() public returns(bytes32[] memory) {