├── .dockerignore ├── .env.example ├── .github └── workflows │ ├── ci_rust.yaml │ ├── ci_ts.yml │ ├── foundry.yml │ └── rewards-scripts-check.yaml ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── abis ├── ECDSAStakeRegistry.json ├── HelloWorldServiceManager.json ├── IAVSDirectory.json └── IDelegationManager.json ├── assets ├── hello-world-diagram.png └── hello-world-diagramv2.png ├── contracts ├── .env.example ├── .gitignore ├── anvil │ ├── build-state.sh │ ├── deploy-el.sh │ └── deploy-helloworld.sh ├── config │ ├── core │ │ └── 31337.json │ └── hello-world │ │ └── 31337.json ├── foundry.toml ├── mocks │ └── MockStrategy.sol ├── script │ ├── DeployEigenLayerCore.s.sol │ ├── HelloWorldDeployer.s.sol │ ├── SetupDistributions.s.sol │ └── utils │ │ ├── CoreDeploymentParsingLib.sol │ │ ├── HelloWorldDeploymentLib.sol │ │ ├── SetupDistributionsLib.sol │ │ └── UpgradeableProxyLib.sol ├── src │ ├── HelloWorldServiceManager.sol │ └── IHelloWorldServiceManager.sol └── test │ ├── CoreDeploymentLib.t.sol │ ├── ERC20Mock.sol │ ├── HelloWorldServiceManager.t.sol │ ├── SetupPaymentsLib.t.sol │ └── mockData │ ├── config │ ├── core │ │ └── 1337.json │ └── hello-world │ │ └── 1337.json │ ├── deployments │ ├── core │ │ └── 1337.json │ └── hello-world │ │ └── 1337.json │ └── scratch │ ├── 31337.json │ ├── payment_info.json │ ├── payments.json │ └── payments_test.json ├── docs ├── FAQ.md └── nix-setup-guide.md ├── flake.lock ├── flake.nix ├── jest.config.ts ├── operator ├── createNewTasks.ts ├── e2e.test.ts ├── index.ts └── rust │ └── crates │ ├── operator │ ├── Cargo.toml │ └── src │ │ ├── anvil.rs │ │ ├── challenger.rs │ │ ├── lib.rs │ │ ├── spam_tasks.rs │ │ └── start_operator.rs │ └── utils │ ├── Cargo.toml │ └── src │ ├── bindings │ ├── ecdsastakeregistry.rs │ ├── helloworldservicemanager.rs │ └── mod.rs │ └── lib.rs ├── package-lock.json ├── package.json ├── rust-toolchain.toml ├── scripts └── rewards-script-check.sh ├── tsconfig.json └── utils └── abis.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | target/ 3 | 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 2 | OPERATOR_RESPONSE_PERCENTAGE=80 3 | RPC_URL=http://localhost:8545 4 | WS_URL=ws://localhost:8545 5 | -------------------------------------------------------------------------------- /.github/workflows/ci_rust.yaml: -------------------------------------------------------------------------------- 1 | name: Build Rust CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | env: 10 | RUST_VERSION: 1.80 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v4 19 | 20 | - name: Rustup toolchain install 21 | uses: dtolnay/rust-toolchain@stable 22 | with: 23 | toolchain: ${{ env.RUST_VERSION }} 24 | 25 | - name: Caching 26 | uses: Swatinem/rust-cache@v2 27 | 28 | - name: Run cargo build 29 | run: | 30 | cargo build 31 | 32 | - name: Run cargo clippy 33 | run: | 34 | cargo clippy --all-targets --all-features -- -D warnings 35 | 36 | - name: Run cargo fmt 37 | run: | 38 | cargo fmt --all -- --check 39 | 40 | test: 41 | name: Test 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout repository 45 | uses: actions/checkout@v4 46 | 47 | - name: Rustup toolchain install 48 | uses: dtolnay/rust-toolchain@stable 49 | with: 50 | toolchain: ${{ env.RUST_VERSION }} 51 | 52 | - name: Caching 53 | uses: Swatinem/rust-cache@v2 54 | 55 | - name: Install Foundry 56 | uses: foundry-rs/foundry-toolchain@v1 57 | with: 58 | version: v0.3.0 59 | 60 | - name: Run anvil with deployed contracts 61 | run: | 62 | make build-anvil-state-with-deployed-contracts 63 | 64 | - name: Run tests 65 | run: cargo test --workspace 66 | -------------------------------------------------------------------------------- /.github/workflows/ci_ts.yml: -------------------------------------------------------------------------------- 1 | name: Build Typescript CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["**"] 8 | 9 | env: 10 | NODE_VERSION: 20 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Use Node.js 20 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ env.NODE_VERSION }} 23 | 24 | - name: Install packages 25 | run: npm install 26 | 27 | - name: Build project 28 | run: npm run build 29 | -------------------------------------------------------------------------------- /.github/workflows/foundry.yml: -------------------------------------------------------------------------------- 1 | name: Foundry project 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | check-fmt: 13 | defaults: 14 | run: 15 | working-directory: ./contracts 16 | 17 | name: Check contract formatting 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | 24 | - name: Install Foundry 25 | uses: foundry-rs/foundry-toolchain@v1 26 | with: 27 | version: nightly 28 | 29 | - name: Print forge version 30 | run: forge --version 31 | 32 | - name: Check contract formatting 33 | run: forge fmt --check 34 | 35 | test: 36 | defaults: 37 | run: 38 | working-directory: ./contracts 39 | 40 | name: Foundry project 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | submodules: recursive 46 | 47 | - name: Install Foundry 48 | uses: foundry-rs/foundry-toolchain@v1 49 | with: 50 | version: nightly 51 | 52 | - name: Print forge version 53 | run: forge --version 54 | 55 | - name: Run Forge build 56 | run: forge build --optimize --optimizer-runs 200 --via-ir --sizes 57 | 58 | - name: Run Forge tests 59 | run: forge test -vvv 60 | -------------------------------------------------------------------------------- /.github/workflows/rewards-scripts-check.yaml: -------------------------------------------------------------------------------- 1 | name: rewards-scripts-check 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | Test: 11 | name: Rewards Scripts Check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install Foundry 17 | uses: foundry-rs/foundry-toolchain@v1 18 | with: 19 | version: stable 20 | 21 | - name: Install forge dependencies 22 | run: forge install 23 | working-directory: ./contracts 24 | 25 | - name: Copy .env.example to .env 26 | run: cp contracts/.env.example contracts/.env 27 | 28 | - name: Test 29 | run: ./scripts/rewards-script-check.sh 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore node_modules directory 2 | node_modules/ 3 | 4 | # Ignore built output 5 | dist/ 6 | 7 | # Ignore environment-specific files 8 | .env 9 | 10 | # Ignore nixos related environment-specific files 11 | .envrc 12 | .cargo/ 13 | .direnv/ 14 | 15 | # Ignore editor-specific files 16 | .vscode/ 17 | .idea/ 18 | 19 | # Ignore TypeScript build artifacts 20 | dist/**/*.js 21 | dist/*.js.map 22 | dist/*.d.ts 23 | 24 | # Ignore local anvil state 25 | # utils/anvil/*.json 26 | contracts/anvil/state.json 27 | 28 | oldUtils 29 | 30 | target/ 31 | debug/ 32 | 33 | yarn.lock 34 | contracts/deployments/ 35 | 36 | **/.DS_Store 37 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contracts/lib/forge-std"] 2 | path = contracts/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "contracts/lib/eigenlayer-middleware"] 5 | path = contracts/lib/eigenlayer-middleware 6 | url = https://github.com/Layr-Labs/eigenlayer-middleware 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["operator/rust/crates/operator/", "operator/rust/crates/utils/"] 3 | 4 | resolver = "2" 5 | 6 | [workspace.package] 7 | version = "0.0.1-alpha" 8 | edition = "2021" 9 | authors = ["Eigen Layer contributors"] 10 | rust-version = "1.80" 11 | repository = "https://github.com/Layr-Labs/hello-world-avs" 12 | homepage = "" 13 | license-file = "LICENSE" 14 | 15 | [workspace.lints.rust] 16 | missing_debug_implementations = "warn" 17 | missing_docs = "warn" 18 | unreachable_pub = "warn" 19 | unused_must_use = "deny" 20 | rust_2018_idioms = { level = "deny", priority = -1 } 21 | 22 | 23 | [workspace.lints] 24 | rustdoc.all = "warn" 25 | 26 | 27 | [workspace.dependencies] 28 | 29 | #tokio 30 | tokio = { version = "1.37.0", features = [ 31 | "test-util", 32 | "full", 33 | "sync", 34 | "rt-multi-thread", 35 | "macros", 36 | ] } 37 | 38 | serde = "1.0.214" 39 | 40 | hello-world-avs-operator = { path = "operator/rust/crates/operator" } 41 | hello-world-utils = { path = "operator/rust/crates/utils" } 42 | 43 | alloy = { version = "0.13", features = ["full"] } 44 | eigensdk = { version = "=1.0.0-rc.0", features = ["full"] } 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json ./ 6 | COPY tsconfig.json ./ 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | CMD ["npm", "start"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Eigen Labs, Inc. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################# HELP MESSAGE ############################# 2 | # Make sure the help command stays first, so that it's printed by default when `make` is called without arguments 3 | .PHONY: help tests 4 | help: 5 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 6 | 7 | 8 | RUST_BINDINGS_PATH:=operator/rust/crates/utils/src/bindings 9 | 10 | -----------------------------: ## 11 | 12 | ___ANVIL_STATE___: ## 13 | build-anvil-state-with-deployed-contracts: ## builds anvil state with deployed contracts and generates a state 14 | @chmod +x ./contracts/anvil/build-state.sh 15 | ./contracts/anvil/build-state.sh 16 | 17 | ___CONTRACTS___: ## 18 | 19 | build-contracts: ## builds all contracts 20 | cd contracts && forge build 21 | 22 | deploy-eigenlayer-contracts: 23 | @chmod +x ./contracts/anvil/deploy-el.sh 24 | ./contracts/anvil/deploy-el.sh 25 | 26 | deploy-helloworld-contracts: 27 | @chmod +x ./contracts/anvil/deploy-helloworld.sh 28 | ./contracts/anvil/deploy-helloworld.sh 29 | 30 | generate-bindings: 31 | cd contracts && forge build --force --skip test --skip script 32 | rm -rf ${RUST_BINDINGS_PATH} 33 | forge bind --alloy --skip-build --overwrite --module \ 34 | --root contracts/ \ 35 | --bindings-path ${RUST_BINDINGS_PATH} \ 36 | --select '^ECDSAStakeRegistry$$' --select '^HelloWorldServiceManager$$' 37 | 38 | __CLI__: ## 39 | 40 | send-fund: ## sends fund to the operator saved in tests/keys/test.ecdsa.key.json 41 | cast send 0x860B6912C2d0337ef05bbC89b0C2CB6CbAEAB4A5 --value 10ether \ 42 | --private-key 43 | 44 | -----------------------------: ## 45 | # We pipe all zapper logs through https://github.com/maoueh/zap-pretty so make sure to install it 46 | # TODO: piping to zap-pretty only works when zapper environment is set to production, unsure why 47 | ____OFFCHAIN_SOFTWARE___: 48 | start-operator: ## start operator (part of quickstart) 49 | tsc && node dist/index.js 50 | 51 | spam-tasks: ## start tasks spamming (part of quickstart) 52 | tsc && node dist/createNewTasks.js 53 | 54 | -----------------------------: ## 55 | _____HELPER_____: ## 56 | tests-contract: ## runs all forge tests 57 | cd contracts && forge test 58 | 59 | ___RUST_OFFCHAIN_SOFTWARE___: 60 | start-rust-operator: ## start operator (part of quickstart) 61 | cargo run --bin start_operator 62 | 63 | spam-rust-tasks: ## start tasks spamming (part of quickstart) 64 | cargo run --bin spam_tasks 65 | 66 | start-rust-challenger: ## start challenger (part of quickstart) 67 | cargo run --bin challenger 68 | 69 | __REWARDS__: ## 70 | 71 | TOKEN_ADDRESS=$(shell jq -r '.addresses.token' contracts/deployments/hello-world/31337.json) 72 | 73 | create-avs-distributions-root: 74 | npm run create-distributions-root 75 | 76 | claim-distributions: 77 | npm run claim-distributions 78 | 79 | create-operator-directed-distributions-root: 80 | npm run create-operator-directed-distributions-root 81 | 82 | get-deployed-token-address: 83 | @echo "Deployed token Address: $(TOKEN_ADDRESS)" 84 | 85 | claimer-account-token-balance: 86 | cast balance --erc20 $(TOKEN_ADDRESS) 0x0000000000000000000000000000000000000001 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hello World AVS 2 | 3 | Welcome to the Hello World AVS. This project shows you the simplest functionality you can expect from an AVS. It will give you a concrete understanding of the basic components. For new users, please find [this video walkthrough](https://drive.google.com/file/d/1P6uA6kYWCbpeorTjADuoTlQ-q8uqwPZf/view?usp=sharing) of the hello world AVS repository. 4 | 5 | ## Architecture 6 | 7 | ![hello-world-png](./assets/hello-world-diagramv2.png) 8 | 9 | ### AVS User Flow 10 | 11 | 1) AVS consumer requests a "Hello World" message to be generated and signed. 12 | 2) HelloWorld contract receives the request and emits a NewTaskCreated event for the request. 13 | 3) All Operators who are registered to the AVS and has staked, delegated assets takes this request. Operator generates the requested message, hashes it, and signs the hash with their private key. 14 | 4) Each Operator submits their signed hash back to the HelloWorld AVS contract. 15 | 5) If the Operator is registered to the AVS and has the minimum needed stake, the submission is accepted. 16 | 17 | That's it. This simple flow highlights some of the core mechanics of how AVSs work. 18 | 19 | ### Slashing 20 | 21 | > [!WARNING] 22 | > This example does not use the new operator-sets workflow. Please refer to [ELIP-002](https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-002.md) for more details. 23 | > For an example of the new workflow, check out the Incredible Squaring examples ([Go version here](https://github.com/Layr-Labs/incredible-squaring-avs), [Rust version here](https://github.com/Layr-Labs/incredible-squaring-avs-rs)). 24 | 25 | The example includes a simple slashing condition: "a task MUST be responded by enough operators before N blocks have passed since the task creation". You can modify the `OPERATOR_RESPONSE_PERCENTAGE` value in the `.env` file to adjust the chance of an operator responding to a task. 26 | In case this condition isn't satisfied by some operator, anyone can permissionlessly slash them via calling `HelloWorldServiceManager.slashOperator`. 27 | 28 | For the [Rust example](#quick-start-rust), we have a `challenger` that listens for new tasks and checks whether the operators have responded. If not, `challenger` is authorized to slash the operator. 29 | 30 | ## Local Devnet Deployment 31 | 32 | The following instructions explain how to manually deploy the AVS from scratch including EigenLayer and AVS specific contracts using Foundry (forge) to a local anvil chain, and start Typescript Operator application and tasks. 33 | 34 | ## Development Environment 35 | 36 | This section describes the tooling required for local development. 37 | 38 | ### Non-Nix Environment 39 | 40 | Install dependencies: 41 | 42 | - [Node](https://nodejs.org/en/download/) 43 | - [Typescript](https://www.typescriptlang.org/download) 44 | - [ts-node](https://www.npmjs.com/package/ts-node) 45 | - [tcs](https://www.npmjs.com/package/tcs#installation) 46 | - [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 47 | - [Foundry](https://getfoundry.sh/) 48 | - [ethers](https://www.npmjs.com/package/ethers) 49 | 50 | ### Nix Environment 51 | 52 | On [Nix](https://nixos.org/) platforms, if you already have the proper Nix configuration, you can build the project's artifacts inside a `nix develop` shell 53 | 54 | ``` sh 55 | nix develop 56 | ``` 57 | 58 | Otherwise, please refer to [installed and configured](./docs/nix-setup-guide.md) section. 59 | 60 | ## Quick start (TypeScript) 61 | 62 | ### Start Anvil Chain 63 | 64 | In terminal window #1, execute the following commands: 65 | 66 | ```sh 67 | 68 | # Install npm packages 69 | npm install 70 | 71 | # Start local anvil chain 72 | npm run start:anvil 73 | ``` 74 | 75 | ### Deploy Contracts and Start Operator 76 | 77 | Open a separate terminal window #2, execute the following commands 78 | 79 | ```sh 80 | # Setup .env file 81 | cp .env.example .env 82 | cp contracts/.env.example contracts/.env 83 | 84 | # Updates dependencies if necessary and builds the contracts 85 | npm run build:forge 86 | 87 | # Deploy the EigenLayer contracts 88 | npm run deploy:core 89 | 90 | # Deploy the Hello World AVS contracts 91 | npm run deploy:hello-world 92 | 93 | # (Optional) Update ABIs 94 | npm run extract:abis 95 | 96 | # Start the Operator application 97 | npm run start:operator 98 | ``` 99 | 100 | ### Create Hello-World-AVS Tasks 101 | 102 | Open a separate terminal window #3, execute the following commands 103 | 104 | ```sh 105 | # Start the createNewTasks application 106 | npm run start:traffic 107 | ``` 108 | 109 | ### Create and Claim Distribution 110 | 111 | In a terminal, start a new instance of anvil and deploy the core and avs contracts 112 | 113 | ```sh 114 | # Start anvil 115 | npm run start:anvil 116 | # Deploy the EigenLayer contracts 117 | npm run deploy:core 118 | 119 | # Deploy the Hello World AVS contracts 120 | npm run deploy:hello-world 121 | 122 | ``` 123 | 124 | In another terminal, run: 125 | 126 | ```sh 127 | # Create distribution roots 128 | npm run create-distributions-root 129 | 130 | # Claim created distribution 131 | npm run claim-distributions 132 | ``` 133 | 134 | To run operator directed rewards distribution, run: 135 | 136 | ```sh 137 | #Create distribution roots 138 | npm run create-operator-directed-distributions-root 139 | 140 | # Claim created rewards distribution 141 | npm run claim-distributions 142 | ``` 143 | 144 | ## Quick start (Rust) 145 | 146 | For Rust example, we have a simple operator that monitors new tasks and responds to them, a spammer that generates random tasks and a challeger that listens for new tasks and checks the operators response, [if found that operator did not respond to the task](#slashing), it will slash the operator. 147 | 148 | ### Anvil Deployment 149 | 150 | 1. Start Anvil Chain 151 | 152 | In terminal window #1, execute the following commands: 153 | 154 | ```sh 155 | # Start local anvil chain 156 | anvil 157 | ``` 158 | 159 | 2. Deploy Contracts 160 | 161 | Open a separate terminal window #2, execute the following commands 162 | 163 | ```sh 164 | # Setup .env file 165 | cp .env.example .env 166 | cp contracts/.env.example contracts/.env 167 | 168 | # Builds the contracts 169 | make build-contracts 170 | 171 | # Deploy the EigenLayer contracts 172 | make deploy-eigenlayer-contracts 173 | 174 | # Deploy the Hello World AVS contracts 175 | make deploy-helloworld-contracts 176 | ``` 177 | 178 | 3. Start Challenge Manager 179 | 180 | In terminal window #2, execute the following command 181 | 182 | ```sh 183 | # Start the Challenge Manager 184 | make start-rust-challenger 185 | ``` 186 | 187 | 4. Start Rust Operator 188 | 189 | In terminal window #3, execute the following command 190 | 191 | ```sh 192 | # Start the Operator 193 | make start-rust-operator 194 | ``` 195 | 196 | 5. Spam Tasks 197 | 198 | Open a separate terminal window #4, execute the following command 199 | 200 | ```sh 201 | # Start sending tasks 202 | make spam-rust-tasks 203 | ``` 204 | 205 | ### Testing 206 | 207 | 1. Build anvil state with deployed contracts 208 | 209 | ```sh 210 | # Build contracts 211 | make build-contracts 212 | 213 | # Starts anvil in the background with the --dump-state flag, builds and deploys the 214 | # contracts, and generates a state.json file for use in tests. 215 | make build-anvil-state-with-deployed-contracts 216 | ``` 217 | 218 | 2. Run tests 219 | 220 | ```sh 221 | cargo test --workspace 222 | ``` 223 | 224 | ## Help and Support 225 | 226 | For help and support deploying and modifying this repo for your AVS, please: 227 | 228 | 1. Open a ticket via the intercom link at [support.eigenlayer.xyz](https://support.eigenlayer.xyz). 229 | 2. Include the necessary troubleshooting information for your environment: 230 | * Local anvil testing: 231 | * Redeploy your local test using `--revert-strings debug` flag via the following commands and retest: `npm run deploy:core-debug && npm run deploy:hello-world-debug` 232 | * Include the full stacktrace from your error as a .txt file attachment. 233 | * Create a minimal repo that demonstrates the behavior (fork or otherwise) 234 | * Steps require to reproduce issue (compile and cause the error) 235 | * Holesky testing: 236 | * Ensure contracts are verified on Holesky. Eg `forge verify-contract --chain-id 17000 --num-of-optimizations 200 src/YourContract.sol:YourContract YOUR_CONTRACT_ADDRESS` 237 | * Send us your transaction hash where your contract is failing. We will use Tenderly to debug (adjust gas limit) and/or cast to re-run the transaction (eg `cast call --trace "trace_replayTransaction(0xTransactionHash)"`). 238 | 239 | ## Contact Us 240 | 241 | If you're planning to build an AVS and would like to speak with a member of the EigenLayer DevRel team to discuss your ideas or architecture, please fill out this form and we'll be in touch shortly: [EigenLayer AVS Intro Call](https://share.hsforms.com/1BksFoaPjSk2l3pQ5J4EVCAein6l) 242 | 243 | ## Disclaimers 244 | 245 | - This repo is meant currently intended for _local anvil development testing_. Holesky deployment support will be added shortly. 246 | - Users who wish to build an AVS for Production purposes will want to migrate from the `ECDSAServiceManagerBase` implementation in `HelloWorldServiceManager.sol` to a BLS style architecture using [RegistryCoordinator](https://github.com/Layr-Labs/eigenlayer-middleware/blob/dev/docs/RegistryCoordinator.md). 247 | 248 | ## Appendix (Future Capabilities In Progress) 249 | 250 | ### Adding a New Strategy 251 | 252 | ### Potential Enhancements to the AVS (for learning purposes) 253 | 254 | The architecture can be further enhanced via: 255 | 256 | - the nature of the request is more sophisticated than generating a constant string 257 | - the operators might need to coordinate with each other 258 | - the type of signature is different based on the constraints of the service 259 | - the type and amount of security used to secure the AVS 260 | -------------------------------------------------------------------------------- /abis/HelloWorldServiceManager.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "constructor", 4 | "inputs": [ 5 | { 6 | "name": "_avsDirectory", 7 | "type": "address", 8 | "internalType": "address" 9 | }, 10 | { 11 | "name": "_stakeRegistry", 12 | "type": "address", 13 | "internalType": "address" 14 | }, 15 | { 16 | "name": "_rewardsCoordinator", 17 | "type": "address", 18 | "internalType": "address" 19 | }, 20 | { 21 | "name": "_delegationManager", 22 | "type": "address", 23 | "internalType": "address" 24 | }, 25 | { 26 | "name": "_allocationManager", 27 | "type": "address", 28 | "internalType": "address" 29 | } 30 | ], 31 | "stateMutability": "nonpayable" 32 | }, 33 | { 34 | "type": "function", 35 | "name": "addPendingAdmin", 36 | "inputs": [ 37 | { 38 | "name": "admin", 39 | "type": "address", 40 | "internalType": "address" 41 | } 42 | ], 43 | "outputs": [], 44 | "stateMutability": "nonpayable" 45 | }, 46 | { 47 | "type": "function", 48 | "name": "allTaskHashes", 49 | "inputs": [ 50 | { 51 | "name": "", 52 | "type": "uint32", 53 | "internalType": "uint32" 54 | } 55 | ], 56 | "outputs": [ 57 | { 58 | "name": "", 59 | "type": "bytes32", 60 | "internalType": "bytes32" 61 | } 62 | ], 63 | "stateMutability": "view" 64 | }, 65 | { 66 | "type": "function", 67 | "name": "allTaskResponses", 68 | "inputs": [ 69 | { 70 | "name": "", 71 | "type": "address", 72 | "internalType": "address" 73 | }, 74 | { 75 | "name": "", 76 | "type": "uint32", 77 | "internalType": "uint32" 78 | } 79 | ], 80 | "outputs": [ 81 | { 82 | "name": "", 83 | "type": "bytes", 84 | "internalType": "bytes" 85 | } 86 | ], 87 | "stateMutability": "view" 88 | }, 89 | { 90 | "type": "function", 91 | "name": "allocationManager", 92 | "inputs": [], 93 | "outputs": [ 94 | { 95 | "name": "", 96 | "type": "address", 97 | "internalType": "address" 98 | } 99 | ], 100 | "stateMutability": "view" 101 | }, 102 | { 103 | "type": "function", 104 | "name": "avsDirectory", 105 | "inputs": [], 106 | "outputs": [ 107 | { 108 | "name": "", 109 | "type": "address", 110 | "internalType": "address" 111 | } 112 | ], 113 | "stateMutability": "view" 114 | }, 115 | { 116 | "type": "function", 117 | "name": "createAVSRewardsSubmission", 118 | "inputs": [ 119 | { 120 | "name": "rewardsSubmissions", 121 | "type": "tuple[]", 122 | "internalType": "struct IRewardsCoordinatorTypes.RewardsSubmission[]", 123 | "components": [ 124 | { 125 | "name": "strategiesAndMultipliers", 126 | "type": "tuple[]", 127 | "internalType": "struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]", 128 | "components": [ 129 | { 130 | "name": "strategy", 131 | "type": "address", 132 | "internalType": "contract IStrategy" 133 | }, 134 | { 135 | "name": "multiplier", 136 | "type": "uint96", 137 | "internalType": "uint96" 138 | } 139 | ] 140 | }, 141 | { 142 | "name": "token", 143 | "type": "address", 144 | "internalType": "contract IERC20" 145 | }, 146 | { 147 | "name": "amount", 148 | "type": "uint256", 149 | "internalType": "uint256" 150 | }, 151 | { 152 | "name": "startTimestamp", 153 | "type": "uint32", 154 | "internalType": "uint32" 155 | }, 156 | { 157 | "name": "duration", 158 | "type": "uint32", 159 | "internalType": "uint32" 160 | } 161 | ] 162 | } 163 | ], 164 | "outputs": [], 165 | "stateMutability": "nonpayable" 166 | }, 167 | { 168 | "type": "function", 169 | "name": "createNewTask", 170 | "inputs": [ 171 | { 172 | "name": "name", 173 | "type": "string", 174 | "internalType": "string" 175 | } 176 | ], 177 | "outputs": [ 178 | { 179 | "name": "", 180 | "type": "tuple", 181 | "internalType": "struct IHelloWorldServiceManager.Task", 182 | "components": [ 183 | { 184 | "name": "name", 185 | "type": "string", 186 | "internalType": "string" 187 | }, 188 | { 189 | "name": "taskCreatedBlock", 190 | "type": "uint32", 191 | "internalType": "uint32" 192 | } 193 | ] 194 | } 195 | ], 196 | "stateMutability": "nonpayable" 197 | }, 198 | { 199 | "type": "function", 200 | "name": "createOperatorDirectedAVSRewardsSubmission", 201 | "inputs": [ 202 | { 203 | "name": "operatorDirectedRewardsSubmissions", 204 | "type": "tuple[]", 205 | "internalType": "struct IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[]", 206 | "components": [ 207 | { 208 | "name": "strategiesAndMultipliers", 209 | "type": "tuple[]", 210 | "internalType": "struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]", 211 | "components": [ 212 | { 213 | "name": "strategy", 214 | "type": "address", 215 | "internalType": "contract IStrategy" 216 | }, 217 | { 218 | "name": "multiplier", 219 | "type": "uint96", 220 | "internalType": "uint96" 221 | } 222 | ] 223 | }, 224 | { 225 | "name": "token", 226 | "type": "address", 227 | "internalType": "contract IERC20" 228 | }, 229 | { 230 | "name": "operatorRewards", 231 | "type": "tuple[]", 232 | "internalType": "struct IRewardsCoordinatorTypes.OperatorReward[]", 233 | "components": [ 234 | { 235 | "name": "operator", 236 | "type": "address", 237 | "internalType": "address" 238 | }, 239 | { 240 | "name": "amount", 241 | "type": "uint256", 242 | "internalType": "uint256" 243 | } 244 | ] 245 | }, 246 | { 247 | "name": "startTimestamp", 248 | "type": "uint32", 249 | "internalType": "uint32" 250 | }, 251 | { 252 | "name": "duration", 253 | "type": "uint32", 254 | "internalType": "uint32" 255 | }, 256 | { 257 | "name": "description", 258 | "type": "string", 259 | "internalType": "string" 260 | } 261 | ] 262 | } 263 | ], 264 | "outputs": [], 265 | "stateMutability": "nonpayable" 266 | }, 267 | { 268 | "type": "function", 269 | "name": "deregisterOperatorFromAVS", 270 | "inputs": [ 271 | { 272 | "name": "operator", 273 | "type": "address", 274 | "internalType": "address" 275 | } 276 | ], 277 | "outputs": [], 278 | "stateMutability": "nonpayable" 279 | }, 280 | { 281 | "type": "function", 282 | "name": "deregisterOperatorFromOperatorSets", 283 | "inputs": [ 284 | { 285 | "name": "operator", 286 | "type": "address", 287 | "internalType": "address" 288 | }, 289 | { 290 | "name": "operatorSetIds", 291 | "type": "uint32[]", 292 | "internalType": "uint32[]" 293 | } 294 | ], 295 | "outputs": [], 296 | "stateMutability": "nonpayable" 297 | }, 298 | { 299 | "type": "function", 300 | "name": "getOperatorRestakedStrategies", 301 | "inputs": [ 302 | { 303 | "name": "_operator", 304 | "type": "address", 305 | "internalType": "address" 306 | } 307 | ], 308 | "outputs": [ 309 | { 310 | "name": "", 311 | "type": "address[]", 312 | "internalType": "address[]" 313 | } 314 | ], 315 | "stateMutability": "view" 316 | }, 317 | { 318 | "type": "function", 319 | "name": "getRestakeableStrategies", 320 | "inputs": [], 321 | "outputs": [ 322 | { 323 | "name": "", 324 | "type": "address[]", 325 | "internalType": "address[]" 326 | } 327 | ], 328 | "stateMutability": "view" 329 | }, 330 | { 331 | "type": "function", 332 | "name": "initialize", 333 | "inputs": [ 334 | { 335 | "name": "initialOwner", 336 | "type": "address", 337 | "internalType": "address" 338 | }, 339 | { 340 | "name": "_rewardsInitiator", 341 | "type": "address", 342 | "internalType": "address" 343 | } 344 | ], 345 | "outputs": [], 346 | "stateMutability": "nonpayable" 347 | }, 348 | { 349 | "type": "function", 350 | "name": "latestTaskNum", 351 | "inputs": [], 352 | "outputs": [ 353 | { 354 | "name": "", 355 | "type": "uint32", 356 | "internalType": "uint32" 357 | } 358 | ], 359 | "stateMutability": "view" 360 | }, 361 | { 362 | "type": "function", 363 | "name": "owner", 364 | "inputs": [], 365 | "outputs": [ 366 | { 367 | "name": "", 368 | "type": "address", 369 | "internalType": "address" 370 | } 371 | ], 372 | "stateMutability": "view" 373 | }, 374 | { 375 | "type": "function", 376 | "name": "registerOperatorToAVS", 377 | "inputs": [ 378 | { 379 | "name": "operator", 380 | "type": "address", 381 | "internalType": "address" 382 | }, 383 | { 384 | "name": "operatorSignature", 385 | "type": "tuple", 386 | "internalType": "struct ISignatureUtils.SignatureWithSaltAndExpiry", 387 | "components": [ 388 | { 389 | "name": "signature", 390 | "type": "bytes", 391 | "internalType": "bytes" 392 | }, 393 | { 394 | "name": "salt", 395 | "type": "bytes32", 396 | "internalType": "bytes32" 397 | }, 398 | { 399 | "name": "expiry", 400 | "type": "uint256", 401 | "internalType": "uint256" 402 | } 403 | ] 404 | } 405 | ], 406 | "outputs": [], 407 | "stateMutability": "nonpayable" 408 | }, 409 | { 410 | "type": "function", 411 | "name": "removeAdmin", 412 | "inputs": [ 413 | { 414 | "name": "admin", 415 | "type": "address", 416 | "internalType": "address" 417 | } 418 | ], 419 | "outputs": [], 420 | "stateMutability": "nonpayable" 421 | }, 422 | { 423 | "type": "function", 424 | "name": "removeAppointee", 425 | "inputs": [ 426 | { 427 | "name": "appointee", 428 | "type": "address", 429 | "internalType": "address" 430 | }, 431 | { 432 | "name": "target", 433 | "type": "address", 434 | "internalType": "address" 435 | }, 436 | { 437 | "name": "selector", 438 | "type": "bytes4", 439 | "internalType": "bytes4" 440 | } 441 | ], 442 | "outputs": [], 443 | "stateMutability": "nonpayable" 444 | }, 445 | { 446 | "type": "function", 447 | "name": "removePendingAdmin", 448 | "inputs": [ 449 | { 450 | "name": "pendingAdmin", 451 | "type": "address", 452 | "internalType": "address" 453 | } 454 | ], 455 | "outputs": [], 456 | "stateMutability": "nonpayable" 457 | }, 458 | { 459 | "type": "function", 460 | "name": "renounceOwnership", 461 | "inputs": [], 462 | "outputs": [], 463 | "stateMutability": "nonpayable" 464 | }, 465 | { 466 | "type": "function", 467 | "name": "respondToTask", 468 | "inputs": [ 469 | { 470 | "name": "task", 471 | "type": "tuple", 472 | "internalType": "struct IHelloWorldServiceManager.Task", 473 | "components": [ 474 | { 475 | "name": "name", 476 | "type": "string", 477 | "internalType": "string" 478 | }, 479 | { 480 | "name": "taskCreatedBlock", 481 | "type": "uint32", 482 | "internalType": "uint32" 483 | } 484 | ] 485 | }, 486 | { 487 | "name": "referenceTaskIndex", 488 | "type": "uint32", 489 | "internalType": "uint32" 490 | }, 491 | { 492 | "name": "signature", 493 | "type": "bytes", 494 | "internalType": "bytes" 495 | } 496 | ], 497 | "outputs": [], 498 | "stateMutability": "nonpayable" 499 | }, 500 | { 501 | "type": "function", 502 | "name": "rewardsInitiator", 503 | "inputs": [], 504 | "outputs": [ 505 | { 506 | "name": "", 507 | "type": "address", 508 | "internalType": "address" 509 | } 510 | ], 511 | "stateMutability": "view" 512 | }, 513 | { 514 | "type": "function", 515 | "name": "setAVSRegistrar", 516 | "inputs": [ 517 | { 518 | "name": "registrar", 519 | "type": "address", 520 | "internalType": "contract IAVSRegistrar" 521 | } 522 | ], 523 | "outputs": [], 524 | "stateMutability": "nonpayable" 525 | }, 526 | { 527 | "type": "function", 528 | "name": "setAppointee", 529 | "inputs": [ 530 | { 531 | "name": "appointee", 532 | "type": "address", 533 | "internalType": "address" 534 | }, 535 | { 536 | "name": "target", 537 | "type": "address", 538 | "internalType": "address" 539 | }, 540 | { 541 | "name": "selector", 542 | "type": "bytes4", 543 | "internalType": "bytes4" 544 | } 545 | ], 546 | "outputs": [], 547 | "stateMutability": "nonpayable" 548 | }, 549 | { 550 | "type": "function", 551 | "name": "setClaimerFor", 552 | "inputs": [ 553 | { 554 | "name": "claimer", 555 | "type": "address", 556 | "internalType": "address" 557 | } 558 | ], 559 | "outputs": [], 560 | "stateMutability": "nonpayable" 561 | }, 562 | { 563 | "type": "function", 564 | "name": "setRewardsInitiator", 565 | "inputs": [ 566 | { 567 | "name": "newRewardsInitiator", 568 | "type": "address", 569 | "internalType": "address" 570 | } 571 | ], 572 | "outputs": [], 573 | "stateMutability": "nonpayable" 574 | }, 575 | { 576 | "type": "function", 577 | "name": "stakeRegistry", 578 | "inputs": [], 579 | "outputs": [ 580 | { 581 | "name": "", 582 | "type": "address", 583 | "internalType": "address" 584 | } 585 | ], 586 | "stateMutability": "view" 587 | }, 588 | { 589 | "type": "function", 590 | "name": "transferOwnership", 591 | "inputs": [ 592 | { 593 | "name": "newOwner", 594 | "type": "address", 595 | "internalType": "address" 596 | } 597 | ], 598 | "outputs": [], 599 | "stateMutability": "nonpayable" 600 | }, 601 | { 602 | "type": "function", 603 | "name": "updateAVSMetadataURI", 604 | "inputs": [ 605 | { 606 | "name": "_metadataURI", 607 | "type": "string", 608 | "internalType": "string" 609 | } 610 | ], 611 | "outputs": [], 612 | "stateMutability": "nonpayable" 613 | }, 614 | { 615 | "type": "event", 616 | "name": "Initialized", 617 | "inputs": [ 618 | { 619 | "name": "version", 620 | "type": "uint8", 621 | "indexed": false, 622 | "internalType": "uint8" 623 | } 624 | ], 625 | "anonymous": false 626 | }, 627 | { 628 | "type": "event", 629 | "name": "NewTaskCreated", 630 | "inputs": [ 631 | { 632 | "name": "taskIndex", 633 | "type": "uint32", 634 | "indexed": true, 635 | "internalType": "uint32" 636 | }, 637 | { 638 | "name": "task", 639 | "type": "tuple", 640 | "indexed": false, 641 | "internalType": "struct IHelloWorldServiceManager.Task", 642 | "components": [ 643 | { 644 | "name": "name", 645 | "type": "string", 646 | "internalType": "string" 647 | }, 648 | { 649 | "name": "taskCreatedBlock", 650 | "type": "uint32", 651 | "internalType": "uint32" 652 | } 653 | ] 654 | } 655 | ], 656 | "anonymous": false 657 | }, 658 | { 659 | "type": "event", 660 | "name": "OwnershipTransferred", 661 | "inputs": [ 662 | { 663 | "name": "previousOwner", 664 | "type": "address", 665 | "indexed": true, 666 | "internalType": "address" 667 | }, 668 | { 669 | "name": "newOwner", 670 | "type": "address", 671 | "indexed": true, 672 | "internalType": "address" 673 | } 674 | ], 675 | "anonymous": false 676 | }, 677 | { 678 | "type": "event", 679 | "name": "RewardsInitiatorUpdated", 680 | "inputs": [ 681 | { 682 | "name": "prevRewardsInitiator", 683 | "type": "address", 684 | "indexed": false, 685 | "internalType": "address" 686 | }, 687 | { 688 | "name": "newRewardsInitiator", 689 | "type": "address", 690 | "indexed": false, 691 | "internalType": "address" 692 | } 693 | ], 694 | "anonymous": false 695 | }, 696 | { 697 | "type": "event", 698 | "name": "TaskResponded", 699 | "inputs": [ 700 | { 701 | "name": "taskIndex", 702 | "type": "uint32", 703 | "indexed": true, 704 | "internalType": "uint32" 705 | }, 706 | { 707 | "name": "task", 708 | "type": "tuple", 709 | "indexed": false, 710 | "internalType": "struct IHelloWorldServiceManager.Task", 711 | "components": [ 712 | { 713 | "name": "name", 714 | "type": "string", 715 | "internalType": "string" 716 | }, 717 | { 718 | "name": "taskCreatedBlock", 719 | "type": "uint32", 720 | "internalType": "uint32" 721 | } 722 | ] 723 | }, 724 | { 725 | "name": "operator", 726 | "type": "address", 727 | "indexed": false, 728 | "internalType": "address" 729 | } 730 | ], 731 | "anonymous": false 732 | }, 733 | { 734 | "type": "error", 735 | "name": "DelayPeriodNotPassed", 736 | "inputs": [] 737 | }, 738 | { 739 | "type": "error", 740 | "name": "OnlyRegistryCoordinator", 741 | "inputs": [] 742 | }, 743 | { 744 | "type": "error", 745 | "name": "OnlyRewardsInitiator", 746 | "inputs": [] 747 | }, 748 | { 749 | "type": "error", 750 | "name": "OnlyStakeRegistry", 751 | "inputs": [] 752 | } 753 | ] -------------------------------------------------------------------------------- /abis/IAVSDirectory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "function", 4 | "name": "OPERATOR_AVS_REGISTRATION_TYPEHASH", 5 | "inputs": [], 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "bytes32", 10 | "internalType": "bytes32" 11 | } 12 | ], 13 | "stateMutability": "view" 14 | }, 15 | { 16 | "type": "function", 17 | "name": "OPERATOR_SET_REGISTRATION_TYPEHASH", 18 | "inputs": [], 19 | "outputs": [ 20 | { 21 | "name": "", 22 | "type": "bytes32", 23 | "internalType": "bytes32" 24 | } 25 | ], 26 | "stateMutability": "view" 27 | }, 28 | { 29 | "type": "function", 30 | "name": "calculateOperatorAVSRegistrationDigestHash", 31 | "inputs": [ 32 | { 33 | "name": "operator", 34 | "type": "address", 35 | "internalType": "address" 36 | }, 37 | { 38 | "name": "avs", 39 | "type": "address", 40 | "internalType": "address" 41 | }, 42 | { 43 | "name": "salt", 44 | "type": "bytes32", 45 | "internalType": "bytes32" 46 | }, 47 | { 48 | "name": "expiry", 49 | "type": "uint256", 50 | "internalType": "uint256" 51 | } 52 | ], 53 | "outputs": [ 54 | { 55 | "name": "", 56 | "type": "bytes32", 57 | "internalType": "bytes32" 58 | } 59 | ], 60 | "stateMutability": "view" 61 | }, 62 | { 63 | "type": "function", 64 | "name": "cancelSalt", 65 | "inputs": [ 66 | { 67 | "name": "salt", 68 | "type": "bytes32", 69 | "internalType": "bytes32" 70 | } 71 | ], 72 | "outputs": [], 73 | "stateMutability": "nonpayable" 74 | }, 75 | { 76 | "type": "function", 77 | "name": "deregisterOperatorFromAVS", 78 | "inputs": [ 79 | { 80 | "name": "operator", 81 | "type": "address", 82 | "internalType": "address" 83 | } 84 | ], 85 | "outputs": [], 86 | "stateMutability": "nonpayable" 87 | }, 88 | { 89 | "type": "function", 90 | "name": "initialize", 91 | "inputs": [ 92 | { 93 | "name": "initialOwner", 94 | "type": "address", 95 | "internalType": "address" 96 | }, 97 | { 98 | "name": "initialPausedStatus", 99 | "type": "uint256", 100 | "internalType": "uint256" 101 | } 102 | ], 103 | "outputs": [], 104 | "stateMutability": "nonpayable" 105 | }, 106 | { 107 | "type": "function", 108 | "name": "operatorSaltIsSpent", 109 | "inputs": [ 110 | { 111 | "name": "operator", 112 | "type": "address", 113 | "internalType": "address" 114 | }, 115 | { 116 | "name": "salt", 117 | "type": "bytes32", 118 | "internalType": "bytes32" 119 | } 120 | ], 121 | "outputs": [ 122 | { 123 | "name": "", 124 | "type": "bool", 125 | "internalType": "bool" 126 | } 127 | ], 128 | "stateMutability": "view" 129 | }, 130 | { 131 | "type": "function", 132 | "name": "registerOperatorToAVS", 133 | "inputs": [ 134 | { 135 | "name": "operator", 136 | "type": "address", 137 | "internalType": "address" 138 | }, 139 | { 140 | "name": "operatorSignature", 141 | "type": "tuple", 142 | "internalType": "struct ISignatureUtils.SignatureWithSaltAndExpiry", 143 | "components": [ 144 | { 145 | "name": "signature", 146 | "type": "bytes", 147 | "internalType": "bytes" 148 | }, 149 | { 150 | "name": "salt", 151 | "type": "bytes32", 152 | "internalType": "bytes32" 153 | }, 154 | { 155 | "name": "expiry", 156 | "type": "uint256", 157 | "internalType": "uint256" 158 | } 159 | ] 160 | } 161 | ], 162 | "outputs": [], 163 | "stateMutability": "nonpayable" 164 | }, 165 | { 166 | "type": "function", 167 | "name": "updateAVSMetadataURI", 168 | "inputs": [ 169 | { 170 | "name": "metadataURI", 171 | "type": "string", 172 | "internalType": "string" 173 | } 174 | ], 175 | "outputs": [], 176 | "stateMutability": "nonpayable" 177 | }, 178 | { 179 | "type": "event", 180 | "name": "AVSMetadataURIUpdated", 181 | "inputs": [ 182 | { 183 | "name": "avs", 184 | "type": "address", 185 | "indexed": true, 186 | "internalType": "address" 187 | }, 188 | { 189 | "name": "metadataURI", 190 | "type": "string", 191 | "indexed": false, 192 | "internalType": "string" 193 | } 194 | ], 195 | "anonymous": false 196 | }, 197 | { 198 | "type": "event", 199 | "name": "OperatorAVSRegistrationStatusUpdated", 200 | "inputs": [ 201 | { 202 | "name": "operator", 203 | "type": "address", 204 | "indexed": true, 205 | "internalType": "address" 206 | }, 207 | { 208 | "name": "avs", 209 | "type": "address", 210 | "indexed": true, 211 | "internalType": "address" 212 | }, 213 | { 214 | "name": "status", 215 | "type": "uint8", 216 | "indexed": false, 217 | "internalType": "enum IAVSDirectoryTypes.OperatorAVSRegistrationStatus" 218 | } 219 | ], 220 | "anonymous": false 221 | }, 222 | { 223 | "type": "error", 224 | "name": "InvalidSignature", 225 | "inputs": [] 226 | }, 227 | { 228 | "type": "error", 229 | "name": "OperatorAlreadyRegisteredToAVS", 230 | "inputs": [] 231 | }, 232 | { 233 | "type": "error", 234 | "name": "OperatorNotRegisteredToAVS", 235 | "inputs": [] 236 | }, 237 | { 238 | "type": "error", 239 | "name": "OperatorNotRegisteredToEigenLayer", 240 | "inputs": [] 241 | }, 242 | { 243 | "type": "error", 244 | "name": "SaltSpent", 245 | "inputs": [] 246 | }, 247 | { 248 | "type": "error", 249 | "name": "SignatureExpired", 250 | "inputs": [] 251 | } 252 | ] -------------------------------------------------------------------------------- /assets/hello-world-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Layr-Labs/hello-world-avs/a9e440507308b9360709f1b5f88481a9eb4e949f/assets/hello-world-diagram.png -------------------------------------------------------------------------------- /assets/hello-world-diagramv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Layr-Labs/hello-world-avs/a9e440507308b9360709f1b5f88481a9eb4e949f/assets/hello-world-diagramv2.png -------------------------------------------------------------------------------- /contracts/.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 2 | HOLESKY_PRIVATE_KEY= 3 | HOLESKY_RPC_URL= 4 | ETHERSCAN_API_KEY= 5 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | broadcast/* 7 | 8 | # Docs 9 | docs/ 10 | 11 | # Dotenv file 12 | .env 13 | 14 | 15 | # DS_store files 16 | .DS_Store 17 | lib/.DS_Store 18 | 19 | -------------------------------------------------------------------------------- /contracts/anvil/build-state.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | STATE_FILE="contracts/anvil/state.json" 6 | 7 | mkdir -p "$(dirname "$STATE_FILE")" 8 | 9 | echo "Starting Anvil with state dump in background" 10 | anvil --dump-state "$STATE_FILE" --port 8545 --base-fee 0 --gas-price 0 > /dev/null 2>&1 & 11 | ANVIL_PID=$! 12 | 13 | sleep 3 14 | 15 | cp .env.example .env 16 | cp contracts/.env.example contracts/.env 17 | 18 | echo "Building contracts" 19 | make build-contracts > /dev/null 2>&1 20 | 21 | echo "Deploying EigenLayer contracts." 22 | make deploy-eigenlayer-contracts > /dev/null 2>&1 23 | 24 | echo "Deploying HelloWorld contracts." 25 | make deploy-helloworld-contracts > /dev/null 2>&1 26 | 27 | echo "Killed Anvil" 28 | kill $ANVIL_PID || true 29 | -------------------------------------------------------------------------------- /contracts/anvil/deploy-el.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | RPC_URL=http://localhost:8545 6 | PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 7 | 8 | # cd to the directory of this script so that this can be run from anywhere 9 | parent_path=$( 10 | cd "$(dirname "${BASH_SOURCE[0]}")" 11 | pwd -P 12 | ) 13 | cd "$parent_path" 14 | 15 | cd ../ 16 | 17 | forge script script/DeployEigenLayerCore.s.sol --rpc-url http://localhost:8545 --broadcast 18 | -------------------------------------------------------------------------------- /contracts/anvil/deploy-helloworld.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | RPC_URL=http://localhost:8545 6 | PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 7 | 8 | # cd to the directory of this script so that this can be run from anywhere 9 | parent_path=$( 10 | cd "$(dirname "${BASH_SOURCE[0]}")" 11 | pwd -P 12 | ) 13 | cd "$parent_path" 14 | 15 | cd ../ 16 | 17 | forge script script/HelloWorldDeployer.s.sol --rpc-url http://localhost:8545 --broadcast 18 | -------------------------------------------------------------------------------- /contracts/config/core/31337.json: -------------------------------------------------------------------------------- 1 | { 2 | "strategyManager": { 3 | "initPausedStatus": 0, 4 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 5 | "initialStrategyWhitelister": "0x959922be3caee4b8cd9a407cc3ac1c251c2007b1" 6 | }, 7 | "delegationManager": { 8 | "initPausedStatus": 0, 9 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 10 | "minWithdrawalDelayBlocks": 50400 11 | }, 12 | "eigenPodManager": { 13 | "initPausedStatus": 0, 14 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 15 | }, 16 | "allocationManager": { 17 | "initPausedStatus": 0, 18 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 19 | "deallocationDelay": 0, 20 | "allocationConfigurationDelay": 0 21 | }, 22 | "strategyFactory": { 23 | "initPausedStatus": 0, 24 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 25 | }, 26 | "avsDirectory": { 27 | "initPausedStatus": 0, 28 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 29 | }, 30 | "rewardsCoordinator": { 31 | "initPausedStatus": 0, 32 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 33 | "rewardsUpdater": "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955", 34 | "activationDelay": 0, 35 | "defaultSplitBips": 1000, 36 | "calculationIntervalSeconds": 86400, 37 | "maxRewardsDuration": 864000, 38 | "maxRetroactiveLength": 432000, 39 | "maxFutureLength": 86400, 40 | "genesisRewardsTimestamp": 1672531200 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/config/hello-world/31337.json: -------------------------------------------------------------------------------- 1 | { 2 | "addresses": { 3 | "rewardsOwner": "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f", 4 | "rewardsInitiator": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" 5 | }, 6 | "keys": { 7 | "rewardsOwner": "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97", 8 | "rewardsInitiator": "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | fs_permissions = [{ access = "read-write", path = "./" }] 6 | solc = "0.8.27" 7 | optimizer = true 8 | via_ir = true 9 | 10 | remappings = [ 11 | "@eigenlayer/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/src/", 12 | "@eigenlayer-scripts/=lib/eigenlayer-middleware/lib/eigenlayer-contracts/script/", 13 | "@eigenlayer-middleware/=lib/eigenlayer-middleware/", 14 | "@openzeppelin/=lib/eigenlayer-middleware/lib/openzeppelin-contracts/", 15 | "@openzeppelin-upgrades/=lib/eigenlayer-middleware/lib/openzeppelin-contracts-upgradeable/", 16 | "forge-std/=lib/forge-std/src/", 17 | ] 18 | 19 | # Ignore warnings from middleware 20 | ignored_warnings_from = ["lib/eigenlayer-middleware"] 21 | 22 | [rpc_endpoints] 23 | mainnet = "${MAINNET_RPC_URL}" 24 | holesky = "${HOLESKY_RPC_URL}" 25 | sepolia = "${SEPOLIA_RPC_URL}" 26 | anvil = "${ANVIL_RPC_URL}" 27 | 28 | [etherscan] 29 | mainnet = { key = "${ETHERSCAN_API_KEY}" } 30 | sepolia = { key = "${ETHERSCAN_API_KEY}" } 31 | holesky = { key = "${ETHERSCAN_API_KEY}" } 32 | 33 | [fmt] 34 | bracket_spacing = false 35 | int_types = "long" 36 | line_length = 100 37 | multiline_func_header = "params_first" 38 | number_underscore = "thousands" 39 | quote_style = "double" 40 | tab_width = 4 41 | -------------------------------------------------------------------------------- /contracts/mocks/MockStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@eigenlayer/contracts/interfaces/IStrategy.sol"; 6 | 7 | contract MockStrategy is IStrategy { 8 | IERC20 public override underlyingToken; 9 | uint256 public override totalShares; 10 | mapping(address => uint256) public userShares; 11 | uint256 public constant EXCHANGE_RATE = 1e18; // 1:1 exchange rate for simplicity 12 | 13 | constructor(IERC20 _underlyingToken) { 14 | underlyingToken = _underlyingToken; 15 | emit StrategyTokenSet(_underlyingToken, 18); // Assuming 18 decimals for simplicity 16 | } 17 | 18 | function deposit(IERC20 token, uint256 amount) external override returns (uint256) { 19 | require(token == underlyingToken, "Invalid token"); 20 | uint256 newShares = amount; 21 | totalShares += newShares; 22 | userShares[msg.sender] += newShares; 23 | emit ExchangeRateEmitted(EXCHANGE_RATE); 24 | return newShares; 25 | } 26 | 27 | function withdraw(address recipient, IERC20 token, uint256 amountShares) external override { 28 | require(token == underlyingToken, "Invalid token"); 29 | require(userShares[msg.sender] >= amountShares, "Insufficient shares"); 30 | userShares[msg.sender] -= amountShares; 31 | totalShares -= amountShares; 32 | underlyingToken.transfer(recipient, amountShares); 33 | } 34 | 35 | function sharesToUnderlying(uint256 amountShares) external pure override returns (uint256) { 36 | return amountShares; 37 | } 38 | 39 | function underlyingToShares(uint256 amountUnderlying) external pure override returns (uint256) { 40 | return amountUnderlying; 41 | } 42 | 43 | function userUnderlying(address user) external view override returns (uint256) { 44 | return userShares[user]; 45 | } 46 | 47 | function shares(address user) external view override returns (uint256) { 48 | return userShares[user]; 49 | } 50 | 51 | function sharesToUnderlyingView(uint256 amountShares) external pure override returns (uint256) { 52 | return amountShares; 53 | } 54 | 55 | function underlyingToSharesView(uint256 amountUnderlying) external pure override returns (uint256) { 56 | return amountUnderlying; 57 | } 58 | 59 | function userUnderlyingView(address user) external view override returns (uint256) { 60 | return userShares[user]; 61 | } 62 | 63 | function explanation() external pure override returns (string memory) { 64 | return "Mock Strategy for testing purposes"; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/script/DeployEigenLayerCore.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.12; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | 6 | import {CoreDeployLib, CoreDeploymentParsingLib} from "./utils/CoreDeploymentParsingLib.sol"; 7 | import {UpgradeableProxyLib} from "./utils/UpgradeableProxyLib.sol"; 8 | 9 | import {IRewardsCoordinator} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; 10 | import {StrategyManager} from "@eigenlayer/contracts/core/StrategyManager.sol"; 11 | 12 | import "forge-std/Test.sol"; 13 | 14 | contract DeployEigenLayerCore is Script, Test { 15 | using CoreDeployLib for *; 16 | using UpgradeableProxyLib for address; 17 | 18 | address internal deployer; 19 | address internal proxyAdmin; 20 | CoreDeployLib.DeploymentData internal deploymentData; 21 | CoreDeployLib.DeploymentConfigData internal configData; 22 | 23 | function setUp() public virtual { 24 | deployer = vm.rememberKey(vm.envUint("PRIVATE_KEY")); 25 | vm.label(deployer, "Deployer"); 26 | } 27 | 28 | function run() external { 29 | vm.startBroadcast(deployer); 30 | //set the rewards updater to the deployer address for payment flow 31 | configData = 32 | CoreDeploymentParsingLib.readDeploymentConfigValues("config/core/", block.chainid); 33 | configData.rewardsCoordinator.rewardsUpdater = deployer; 34 | proxyAdmin = UpgradeableProxyLib.deployProxyAdmin(); 35 | deploymentData = CoreDeployLib.deployContracts(proxyAdmin, configData); 36 | 37 | // TODO: the deployer lib should probably do this 38 | StrategyManager(deploymentData.strategyManager).setStrategyWhitelister( 39 | deploymentData.strategyFactory 40 | ); 41 | vm.stopBroadcast(); 42 | string memory deploymentPath = "deployments/core/"; 43 | CoreDeploymentParsingLib.writeDeploymentJson(deploymentPath, block.chainid, deploymentData); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/script/HelloWorldDeployer.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {console2} from "forge-std/Test.sol"; 6 | import {HelloWorldDeploymentLib} from "./utils/HelloWorldDeploymentLib.sol"; 7 | import {CoreDeployLib, CoreDeploymentParsingLib} from "./utils/CoreDeploymentParsingLib.sol"; 8 | import {UpgradeableProxyLib} from "./utils/UpgradeableProxyLib.sol"; 9 | import {StrategyBase} from "@eigenlayer/contracts/strategies/StrategyBase.sol"; 10 | import {ERC20Mock} from "../test/ERC20Mock.sol"; 11 | import {TransparentUpgradeableProxy} from 12 | "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 13 | import {StrategyFactory} from "@eigenlayer/contracts/strategies/StrategyFactory.sol"; 14 | import {StrategyManager} from "@eigenlayer/contracts/core/StrategyManager.sol"; 15 | import {IRewardsCoordinator} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; 16 | 17 | import { 18 | IECDSAStakeRegistryTypes, 19 | IStrategy 20 | } from "@eigenlayer-middleware/src/interfaces/IECDSAStakeRegistry.sol"; 21 | 22 | import "forge-std/Test.sol"; 23 | 24 | contract HelloWorldDeployer is Script, Test { 25 | using CoreDeployLib for *; 26 | using UpgradeableProxyLib for address; 27 | 28 | address private deployer; 29 | address proxyAdmin; 30 | address rewardsOwner; 31 | address rewardsInitiator; 32 | IStrategy helloWorldStrategy; 33 | CoreDeployLib.DeploymentData coreDeployment; 34 | HelloWorldDeploymentLib.DeploymentData helloWorldDeployment; 35 | HelloWorldDeploymentLib.DeploymentConfigData helloWorldConfig; 36 | IECDSAStakeRegistryTypes.Quorum internal quorum; 37 | ERC20Mock token; 38 | 39 | function setUp() public virtual { 40 | deployer = vm.rememberKey(vm.envUint("PRIVATE_KEY")); 41 | vm.label(deployer, "Deployer"); 42 | 43 | helloWorldConfig = 44 | HelloWorldDeploymentLib.readDeploymentConfigValues("config/hello-world/", block.chainid); 45 | 46 | coreDeployment = 47 | CoreDeploymentParsingLib.readDeploymentJson("deployments/core/", block.chainid); 48 | } 49 | 50 | function run() external { 51 | vm.startBroadcast(deployer); 52 | rewardsOwner = helloWorldConfig.rewardsOwner; 53 | rewardsInitiator = helloWorldConfig.rewardsInitiator; 54 | 55 | token = new ERC20Mock(); 56 | // NOTE: if this fails, it's because the initialStrategyWhitelister is not set to be the StrategyFactory 57 | helloWorldStrategy = 58 | IStrategy(StrategyFactory(coreDeployment.strategyFactory).deployNewStrategy(token)); 59 | 60 | quorum.strategies.push( 61 | IECDSAStakeRegistryTypes.StrategyParams({ 62 | strategy: helloWorldStrategy, 63 | multiplier: 10_000 64 | }) 65 | ); 66 | 67 | token.mint(deployer, 2000); 68 | token.increaseAllowance(address(coreDeployment.strategyManager), 1000); 69 | StrategyManager(coreDeployment.strategyManager).depositIntoStrategy( 70 | helloWorldStrategy, token, 1000 71 | ); 72 | 73 | proxyAdmin = UpgradeableProxyLib.deployProxyAdmin(); 74 | 75 | helloWorldDeployment = HelloWorldDeploymentLib.deployContracts( 76 | proxyAdmin, coreDeployment, quorum, rewardsInitiator, rewardsOwner 77 | ); 78 | 79 | helloWorldDeployment.strategy = address(helloWorldStrategy); 80 | helloWorldDeployment.token = address(token); 81 | 82 | vm.stopBroadcast(); 83 | verifyDeployment(); 84 | HelloWorldDeploymentLib.writeDeploymentJson(helloWorldDeployment); 85 | } 86 | 87 | function verifyDeployment() internal view { 88 | require( 89 | helloWorldDeployment.stakeRegistry != address(0), "StakeRegistry address cannot be zero" 90 | ); 91 | require( 92 | helloWorldDeployment.helloWorldServiceManager != address(0), 93 | "HelloWorldServiceManager address cannot be zero" 94 | ); 95 | require(helloWorldDeployment.strategy != address(0), "Strategy address cannot be zero"); 96 | require(proxyAdmin != address(0), "ProxyAdmin address cannot be zero"); 97 | require( 98 | coreDeployment.delegationManager != address(0), 99 | "DelegationManager address cannot be zero" 100 | ); 101 | require(coreDeployment.avsDirectory != address(0), "AVSDirectory address cannot be zero"); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /contracts/script/SetupDistributions.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {HelloWorldDeploymentLib} from "./utils/HelloWorldDeploymentLib.sol"; 6 | import {CoreDeployLib, CoreDeploymentParsingLib} from "./utils/CoreDeploymentParsingLib.sol"; 7 | import {SetupDistributionsLib} from "./utils/SetupDistributionsLib.sol"; 8 | import {IRewardsCoordinator} from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; 9 | import {RewardsCoordinator} from "@eigenlayer/contracts/core/RewardsCoordinator.sol"; 10 | import {IStrategy} from "@eigenlayer/contracts/interfaces/IStrategy.sol"; 11 | import {ERC20Mock} from "../test/ERC20Mock.sol"; 12 | 13 | import "forge-std/Test.sol"; 14 | 15 | contract SetupDistributions is Script, Test { 16 | struct PaymentInfo { 17 | address recipient; 18 | uint32 numPayments; 19 | uint32 amountPerPayment; 20 | uint32 duration; 21 | uint32 startTimestamp; 22 | uint32 endTimestamp; 23 | uint256 indexToProve; 24 | } 25 | 26 | address private deployer; 27 | CoreDeployLib.DeploymentData coreDeployment; 28 | CoreDeployLib.DeploymentConfigData coreConfig; 29 | 30 | HelloWorldDeploymentLib.DeploymentData helloWorldDeployment; 31 | HelloWorldDeploymentLib.DeploymentConfigData helloWorldConfig; 32 | 33 | RewardsCoordinator rewardsCoordinator; 34 | string internal constant paymentInfofilePath = "test/mockData/scratch/payment_info.json"; 35 | string internal constant filePath = "test/mockData/scratch/payments.json"; 36 | 37 | uint32 constant CALCULATION_INTERVAL_SECONDS = 1 days; 38 | uint256 constant NUM_TOKEN_EARNINGS = 1; 39 | //duration MUST be a multiple of CALCULATION_INTERVAL_SECONDS . 40 | //https://github.com/Layr-Labs/eigenlayer-contracts/blob/865e723a6b5c634cf45cce1817dec0ea95f0e03b/src/contracts/core/RewardsCoordinator.sol#L439 41 | uint32 constant DURATION = 172_800; 42 | uint32 constant REWARDS_END_TIMESTAMP_GAP = 1 days; 43 | uint256 constant NUM_EARNERS = 8; 44 | 45 | uint32 numPayments = 8; 46 | uint32 indexToProve = 0; 47 | uint32 amountPerPayment = 100; 48 | 49 | address recipient = address(1); 50 | IRewardsCoordinator.EarnerTreeMerkleLeaf[] public earnerLeaves; 51 | address[] public earners; 52 | uint32 startTimestamp; 53 | uint32 endTimestamp; 54 | uint256 cumumlativePaymentMultiplier; 55 | address nonceSender = 0x998abeb3E57409262aE5b751f60747921B33613E; 56 | 57 | address operator1 = address(1); 58 | address operator2 = address(2); 59 | 60 | function setUp() public { 61 | deployer = vm.rememberKey(vm.envUint("PRIVATE_KEY")); 62 | vm.label(deployer, "Deployer"); 63 | 64 | coreDeployment = 65 | CoreDeploymentParsingLib.readDeploymentJson("deployments/core/", block.chainid); 66 | coreConfig = 67 | CoreDeploymentParsingLib.readDeploymentConfigValues("config/core/", block.chainid); 68 | helloWorldDeployment = 69 | HelloWorldDeploymentLib.readDeploymentJson("deployments/hello-world/", block.chainid); 70 | helloWorldConfig = 71 | HelloWorldDeploymentLib.readDeploymentConfigValues("config/hello-world/", block.chainid); 72 | 73 | rewardsCoordinator = RewardsCoordinator(coreDeployment.rewardsCoordinator); 74 | 75 | // TODO: Get the filePath from config 76 | } 77 | 78 | function run() external { 79 | vm.startBroadcast(helloWorldConfig.rewardsInitiatorKey); 80 | 81 | // Go back 4 days 82 | uint256 targetStartTimestamp = block.timestamp - 4 days; 83 | // Start Timestamp must be a multiple of CALCULATION_INTERVAL_SECONDS 84 | uint32 diff = (uint32(targetStartTimestamp) % CALCULATION_INTERVAL_SECONDS); 85 | startTimestamp = uint32(targetStartTimestamp) - diff; 86 | 87 | endTimestamp = uint32(block.timestamp) - REWARDS_END_TIMESTAMP_GAP; 88 | emit log_named_uint("startTimestamp", startTimestamp); 89 | emit log_named_uint("endTimestamp", endTimestamp); 90 | emit log_named_uint("block.timestamp", block.timestamp); 91 | emit log_named_uint("MAX_RETROACTIVE_LENGTH", rewardsCoordinator.MAX_RETROACTIVE_LENGTH()); 92 | if (endTimestamp > block.timestamp) { 93 | revert("RewardsEndTimestampNotElapsed. Please wait to generate new payments."); 94 | } 95 | 96 | // sets a multiplier based on block number such that cumulativeEarnings increase accordingly for multiple runs of this script in the same session 97 | uint256 nonce = rewardsCoordinator.getDistributionRootsLength(); 98 | amountPerPayment = uint32(amountPerPayment * (nonce + 1)); 99 | 100 | createAVSRewardsSubmissions(numPayments, amountPerPayment, startTimestamp); 101 | vm.stopBroadcast(); 102 | vm.startBroadcast(deployer); 103 | earners = _getEarners(deployer); 104 | submitPaymentRoot(earners, endTimestamp, numPayments, amountPerPayment); 105 | vm.stopBroadcast(); 106 | } 107 | 108 | function runOperatorDirected() external { 109 | vm.startBroadcast(helloWorldConfig.rewardsInitiatorKey); 110 | 111 | // Go back 4 days 112 | uint256 targetStartTimestamp = block.timestamp - 4 days; 113 | // Start Timestamp must be a multiple of CALCULATION_INTERVAL_SECONDS 114 | uint32 diff = (uint32(targetStartTimestamp) % CALCULATION_INTERVAL_SECONDS); 115 | startTimestamp = uint32(targetStartTimestamp) - diff; 116 | 117 | endTimestamp = uint32(block.timestamp) - REWARDS_END_TIMESTAMP_GAP; 118 | emit log_named_uint("startTimestamp", startTimestamp); 119 | emit log_named_uint("endTimestamp", endTimestamp); 120 | emit log_named_uint("block.timestamp", block.timestamp); 121 | emit log_named_uint("MAX_RETROACTIVE_LENGTH", rewardsCoordinator.MAX_RETROACTIVE_LENGTH()); 122 | if (endTimestamp > block.timestamp) { 123 | revert("RewardsEndTimestampNotElapsed. Please wait to generate new payments."); 124 | } 125 | 126 | // sets a multiplier based on block number such that cumulativeEarnings increase accordingly for multiple runs of this script in the same session 127 | uint256 nonce = rewardsCoordinator.getDistributionRootsLength(); 128 | amountPerPayment = uint32(amountPerPayment * (nonce + 1)); 129 | 130 | createOperatorDirectedAVSRewardsSubmissions( 131 | numPayments, amountPerPayment, startTimestamp, DURATION 132 | ); 133 | vm.stopBroadcast(); 134 | vm.startBroadcast(deployer); 135 | earners = _getEarners(deployer); 136 | submitPaymentRoot(earners, endTimestamp, numPayments, amountPerPayment); 137 | vm.stopBroadcast(); 138 | } 139 | 140 | function executeProcessClaim() public { 141 | uint256 nonce = rewardsCoordinator.getDistributionRootsLength(); 142 | amountPerPayment = uint32(amountPerPayment * nonce); 143 | 144 | vm.startBroadcast(deployer); 145 | earnerLeaves = 146 | _getEarnerLeaves(_getEarners(deployer), amountPerPayment, helloWorldDeployment.strategy); 147 | processClaim( 148 | filePath, indexToProve, recipient, earnerLeaves[indexToProve], amountPerPayment 149 | ); 150 | vm.stopBroadcast(); 151 | } 152 | 153 | function createAVSRewardsSubmissions( 154 | uint256 _numPayments, 155 | uint256 _amountPerPayment, 156 | uint32 _startTimestamp 157 | ) public { 158 | ERC20Mock(helloWorldDeployment.token).mint( 159 | helloWorldConfig.rewardsInitiator, _amountPerPayment * _numPayments 160 | ); 161 | ERC20Mock(helloWorldDeployment.token).increaseAllowance( 162 | helloWorldDeployment.helloWorldServiceManager, _amountPerPayment * _numPayments 163 | ); 164 | uint32 duration = rewardsCoordinator.MAX_REWARDS_DURATION(); 165 | SetupDistributionsLib.createAVSRewardsSubmissions( 166 | helloWorldDeployment.helloWorldServiceManager, 167 | helloWorldDeployment.strategy, 168 | _numPayments, 169 | _amountPerPayment, 170 | duration, 171 | _startTimestamp 172 | ); 173 | } 174 | 175 | function createOperatorDirectedAVSRewardsSubmissions( 176 | uint256 _numPayments, 177 | uint256 _amountPerPayment, 178 | uint32 _startTimestamp, 179 | uint32 duration 180 | ) public { 181 | ERC20Mock(helloWorldDeployment.token).mint( 182 | helloWorldConfig.rewardsInitiator, _amountPerPayment * _numPayments 183 | ); 184 | ERC20Mock(helloWorldDeployment.token).increaseAllowance( 185 | helloWorldDeployment.helloWorldServiceManager, _amountPerPayment * _numPayments 186 | ); 187 | address[] memory operators = new address[](2); 188 | operators[0] = operator1; 189 | operators[1] = operator2; 190 | 191 | SetupDistributionsLib.createOperatorDirectedAVSRewardsSubmissions( 192 | helloWorldDeployment.helloWorldServiceManager, 193 | operators, 194 | helloWorldDeployment.strategy, 195 | _numPayments, 196 | _amountPerPayment, 197 | duration, 198 | _startTimestamp 199 | ); 200 | } 201 | 202 | function processClaim( 203 | string memory _filePath, 204 | uint256 _indexToProve, 205 | address _recipient, 206 | IRewardsCoordinator.EarnerTreeMerkleLeaf memory _earnerLeaf, 207 | uint32 _amountPerPayment 208 | ) public { 209 | SetupDistributionsLib.processClaim( 210 | IRewardsCoordinator(coreDeployment.rewardsCoordinator), 211 | _filePath, 212 | _indexToProve, 213 | _recipient, 214 | _earnerLeaf, 215 | NUM_TOKEN_EARNINGS, 216 | helloWorldDeployment.strategy, 217 | _amountPerPayment 218 | ); 219 | } 220 | 221 | function submitPaymentRoot( 222 | address[] memory _earners, 223 | uint32 _endTimestamp, 224 | uint32 _numPayments, 225 | uint32 _amountPerPayment 226 | ) public { 227 | emit log_named_uint("cumumlativePaymentMultiplier", cumumlativePaymentMultiplier); 228 | bytes32[] memory tokenLeaves = SetupDistributionsLib.createTokenLeaves( 229 | IRewardsCoordinator(coreDeployment.rewardsCoordinator), 230 | NUM_TOKEN_EARNINGS, 231 | _amountPerPayment, 232 | helloWorldDeployment.strategy 233 | ); 234 | IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory _earnerLeaves = 235 | SetupDistributionsLib.createEarnerLeaves(_earners, tokenLeaves); 236 | emit log_named_uint("Earner Leaves Length", _earnerLeaves.length); 237 | emit log_named_uint("numPayments", _numPayments); 238 | 239 | SetupDistributionsLib.submitRoot( 240 | IRewardsCoordinator(coreDeployment.rewardsCoordinator), 241 | tokenLeaves, 242 | _earnerLeaves, 243 | _endTimestamp, 244 | _numPayments, 245 | NUM_TOKEN_EARNINGS, 246 | filePath 247 | ); 248 | } 249 | 250 | function _getEarnerLeaves( 251 | address[] memory _earners, 252 | uint32 _amountPerPayment, 253 | address _strategy 254 | ) internal view returns (IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory) { 255 | bytes32[] memory tokenLeaves = SetupDistributionsLib.createTokenLeaves( 256 | IRewardsCoordinator(coreDeployment.rewardsCoordinator), 257 | NUM_TOKEN_EARNINGS, 258 | _amountPerPayment, 259 | _strategy 260 | ); 261 | 262 | IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory _earnerLeaves = 263 | SetupDistributionsLib.createEarnerLeaves(_earners, tokenLeaves); 264 | 265 | return _earnerLeaves; 266 | } 267 | 268 | function _getEarners( 269 | address _deployer 270 | ) internal pure returns (address[] memory) { 271 | address[] memory _earners = new address[](NUM_EARNERS); 272 | for (uint256 i = 0; i < _earners.length; i++) { 273 | _earners[i] = _deployer; 274 | } 275 | return _earners; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /contracts/script/utils/CoreDeploymentParsingLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 5 | import {TransparentUpgradeableProxy} from 6 | "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 7 | import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; 8 | import {console2} from "forge-std/Test.sol"; 9 | import {Vm} from "forge-std/Vm.sol"; 10 | import {stdJson} from "forge-std/StdJson.sol"; 11 | import {DelegationManager} from "@eigenlayer/contracts/core/DelegationManager.sol"; 12 | import {StrategyManager} from "@eigenlayer/contracts/core/StrategyManager.sol"; 13 | import {AVSDirectory} from "@eigenlayer/contracts/core/AVSDirectory.sol"; 14 | import {EigenPodManager} from "@eigenlayer/contracts/pods/EigenPodManager.sol"; 15 | import {RewardsCoordinator} from "@eigenlayer/contracts/core/RewardsCoordinator.sol"; 16 | import {StrategyBase} from "@eigenlayer/contracts/strategies/StrategyBase.sol"; 17 | import {EigenPod} from "@eigenlayer/contracts/pods/EigenPod.sol"; 18 | import {IETHPOSDeposit} from "@eigenlayer/contracts/interfaces/IETHPOSDeposit.sol"; 19 | import {StrategyBaseTVLLimits} from "@eigenlayer/contracts/strategies/StrategyBaseTVLLimits.sol"; 20 | import {PauserRegistry} from "@eigenlayer/contracts/permissions/PauserRegistry.sol"; 21 | import {IStrategy} from "@eigenlayer/contracts/interfaces/IStrategy.sol"; 22 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 23 | import {ISignatureUtilsMixin} from "@eigenlayer/contracts/interfaces/ISignatureUtilsMixin.sol"; 24 | import {IDelegationManager} from "@eigenlayer/contracts/interfaces/IDelegationManager.sol"; 25 | import {IBeacon} from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; 26 | import {IStrategyManager} from "@eigenlayer/contracts/interfaces/IStrategyManager.sol"; 27 | import {ISlasher} from "@eigenlayer-middleware/src/interfaces/ISlasher.sol"; 28 | import {IEigenPodManager} from "@eigenlayer/contracts/interfaces/IEigenPodManager.sol"; 29 | import {IAVSDirectory} from "@eigenlayer/contracts/interfaces/IAVSDirectory.sol"; 30 | import {IPauserRegistry} from "@eigenlayer/contracts/interfaces/IPauserRegistry.sol"; 31 | import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; 32 | import {StrategyFactory} from "@eigenlayer/contracts/strategies/StrategyFactory.sol"; 33 | 34 | import {UpgradeableProxyLib} from "./UpgradeableProxyLib.sol"; 35 | import {CoreDeployLib} from "@eigenlayer-middleware/test/utils/CoreDeployLib.sol"; 36 | 37 | library CoreDeploymentParsingLib { 38 | using stdJson for *; 39 | using Strings for *; 40 | using UpgradeableProxyLib for address; 41 | 42 | Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); 43 | 44 | function readDeploymentConfigValues( 45 | string memory directoryPath, 46 | string memory fileName 47 | ) internal view returns (CoreDeployLib.DeploymentConfigData memory) { 48 | string memory pathToFile = string.concat(directoryPath, fileName); 49 | 50 | require(vm.exists(pathToFile), "CoreDeployment: Deployment config file does not exist"); 51 | 52 | string memory json = vm.readFile(pathToFile); 53 | 54 | CoreDeployLib.DeploymentConfigData memory data; 55 | 56 | // StrategyManager start 57 | data.strategyManager.initPausedStatus = json.readUint(".strategyManager.initPausedStatus"); 58 | data.strategyManager.initialOwner = json.readAddress(".strategyManager.initialOwner"); 59 | data.strategyManager.initialStrategyWhitelister = 60 | json.readAddress(".strategyManager.initialStrategyWhitelister"); 61 | // StrategyManager config end 62 | 63 | // DelegationManager config start 64 | data.delegationManager.initPausedStatus = 65 | json.readUint(".delegationManager.initPausedStatus"); 66 | data.delegationManager.initialOwner = json.readAddress(".delegationManager.initialOwner"); 67 | data.delegationManager.minWithdrawalDelayBlocks = 68 | uint32(json.readUint(".delegationManager.minWithdrawalDelayBlocks")); 69 | // DelegationManager config end 70 | 71 | // EigenPodManager config start 72 | data.eigenPodManager.initPausedStatus = json.readUint(".eigenPodManager.initPausedStatus"); 73 | data.eigenPodManager.initialOwner = json.readAddress(".eigenPodManager.initialOwner"); 74 | // EigenPodManager config end 75 | 76 | // AllocationManager config start 77 | data.allocationManager.initPausedStatus = 78 | json.readUint(".allocationManager.initPausedStatus"); 79 | data.allocationManager.initialOwner = json.readAddress(".allocationManager.initialOwner"); 80 | data.allocationManager.deallocationDelay = 81 | uint32(json.readUint(".allocationManager.deallocationDelay")); 82 | data.allocationManager.allocationConfigurationDelay = 83 | uint32(json.readUint(".allocationManager.allocationConfigurationDelay")); 84 | // AllocationManager config end 85 | 86 | // StrategyFactory config start 87 | data.strategyFactory.initPausedStatus = json.readUint(".strategyFactory.initPausedStatus"); 88 | data.strategyFactory.initialOwner = json.readAddress(".strategyFactory.initialOwner"); 89 | // StrategyFactory config end 90 | 91 | // AVSDirectory config start 92 | data.avsDirectory.initPausedStatus = json.readUint(".avsDirectory.initPausedStatus"); 93 | data.avsDirectory.initialOwner = json.readAddress(".avsDirectory.initialOwner"); 94 | // AVSDirectory config end 95 | 96 | // RewardsCoordinator config start 97 | data.rewardsCoordinator.initPausedStatus = 98 | json.readUint(".rewardsCoordinator.initPausedStatus"); 99 | data.rewardsCoordinator.initialOwner = json.readAddress(".rewardsCoordinator.initialOwner"); 100 | data.rewardsCoordinator.rewardsUpdater = 101 | json.readAddress(".rewardsCoordinator.rewardsUpdater"); 102 | 103 | data.rewardsCoordinator.activationDelay = 104 | uint32(json.readUint(".rewardsCoordinator.activationDelay")); 105 | data.rewardsCoordinator.defaultSplitBips = 106 | uint16(json.readUint(".rewardsCoordinator.defaultSplitBips")); 107 | data.rewardsCoordinator.calculationIntervalSeconds = 108 | uint32(json.readUint(".rewardsCoordinator.calculationIntervalSeconds")); 109 | data.rewardsCoordinator.maxRewardsDuration = 110 | uint32(json.readUint(".rewardsCoordinator.maxRewardsDuration")); 111 | data.rewardsCoordinator.maxRetroactiveLength = 112 | uint32(json.readUint(".rewardsCoordinator.maxRetroactiveLength")); 113 | data.rewardsCoordinator.maxFutureLength = 114 | uint32(json.readUint(".rewardsCoordinator.maxFutureLength")); 115 | data.rewardsCoordinator.genesisRewardsTimestamp = 116 | uint32(json.readUint(".rewardsCoordinator.genesisRewardsTimestamp")); 117 | // RewardsCoordinator config end 118 | 119 | data.ethPOSDeposit.ethPOSDepositAddress = address(1); 120 | 121 | return data; 122 | } 123 | 124 | function readDeploymentConfigValues( 125 | string memory directoryPath, 126 | uint256 chainId 127 | ) internal view returns (CoreDeployLib.DeploymentConfigData memory) { 128 | return 129 | readDeploymentConfigValues(directoryPath, string.concat(vm.toString(chainId), ".json")); 130 | } 131 | 132 | function readDeploymentJson( 133 | string memory directoryPath, 134 | uint256 chainId 135 | ) internal view returns (CoreDeployLib.DeploymentData memory) { 136 | return readDeploymentJson(directoryPath, string.concat(vm.toString(chainId), ".json")); 137 | } 138 | 139 | function readDeploymentJson( 140 | string memory path, 141 | string memory fileName 142 | ) internal view returns (CoreDeployLib.DeploymentData memory) { 143 | string memory pathToFile = string.concat(path, fileName); 144 | 145 | require(vm.exists(pathToFile), "CoreDeployment: Deployment file does not exist"); 146 | 147 | string memory json = vm.readFile(pathToFile); 148 | 149 | CoreDeployLib.DeploymentData memory data; 150 | data.delegationManager = json.readAddress(".addresses.delegationManager"); 151 | data.avsDirectory = json.readAddress(".addresses.avsDirectory"); 152 | data.strategyManager = json.readAddress(".addresses.strategyManager"); 153 | data.eigenPodManager = json.readAddress(".addresses.eigenPodManager"); 154 | data.allocationManager = json.readAddress(".addresses.allocationManager"); 155 | data.eigenPodBeacon = json.readAddress(".addresses.eigenPodBeacon"); 156 | data.pauserRegistry = json.readAddress(".addresses.pauserRegistry"); 157 | data.strategyFactory = json.readAddress(".addresses.strategyFactory"); 158 | data.strategyBeacon = json.readAddress(".addresses.strategyBeacon"); 159 | data.rewardsCoordinator = json.readAddress(".addresses.rewardsCoordinator"); 160 | data.permissionController = json.readAddress(".addresses.permissionController"); 161 | 162 | return data; 163 | } 164 | 165 | /// TODO: Need to be able to read json from eigenlayer-contracts repo for holesky/mainnet and output the json here 166 | function writeDeploymentJson( 167 | CoreDeployLib.DeploymentData memory data 168 | ) internal { 169 | writeDeploymentJson("deployments/core/", block.chainid, data); 170 | } 171 | 172 | function writeDeploymentJson( 173 | string memory path, 174 | uint256 chainId, 175 | CoreDeployLib.DeploymentData memory data 176 | ) internal { 177 | address proxyAdmin = address(UpgradeableProxyLib.getProxyAdmin(data.strategyManager)); 178 | 179 | string memory deploymentData = _generateDeploymentJson(data, proxyAdmin); 180 | 181 | string memory fileName = string.concat(path, vm.toString(chainId), ".json"); 182 | if (!vm.exists(path)) { 183 | vm.createDir(path, true); 184 | } 185 | 186 | vm.writeFile(fileName, deploymentData); 187 | console2.log("Deployment artifacts written to:", fileName); 188 | } 189 | 190 | function _generateDeploymentJson( 191 | CoreDeployLib.DeploymentData memory data, 192 | address proxyAdmin 193 | ) private view returns (string memory) { 194 | return string.concat( 195 | '{"lastUpdate":{"timestamp":"', 196 | vm.toString(block.timestamp), 197 | '","block_number":"', 198 | vm.toString(block.number), 199 | '"},"addresses":', 200 | _generateContractsJson(data, proxyAdmin), 201 | "}" 202 | ); 203 | } 204 | 205 | function _generateContractsJson( 206 | CoreDeployLib.DeploymentData memory data, 207 | address proxyAdmin 208 | ) private view returns (string memory) { 209 | /// TODO: namespace contracts -> {avs, core} 210 | return string.concat( 211 | '{"proxyAdmin":"', 212 | proxyAdmin.toHexString(), 213 | '","delegationManager":"', 214 | data.delegationManager.toHexString(), 215 | '","delegationManagerImpl":"', 216 | data.delegationManager.getImplementation().toHexString(), 217 | '","avsDirectory":"', 218 | data.avsDirectory.toHexString(), 219 | '","avsDirectoryImpl":"', 220 | data.avsDirectory.getImplementation().toHexString(), 221 | '","strategyManager":"', 222 | data.strategyManager.toHexString(), 223 | '","strategyManagerImpl":"', 224 | data.strategyManager.getImplementation().toHexString(), 225 | '","eigenPodManager":"', 226 | data.eigenPodManager.toHexString(), 227 | '","eigenPodManagerImpl":"', 228 | data.eigenPodManager.getImplementation().toHexString(), 229 | '","allocationManager":"', 230 | data.allocationManager.toHexString(), 231 | '","allocationManagerImpl":"', 232 | data.allocationManager.getImplementation().toHexString(), 233 | '","eigenPodBeacon":"', 234 | data.eigenPodBeacon.toHexString(), 235 | '","pauserRegistry":"', 236 | data.pauserRegistry.toHexString(), 237 | '","pauserRegistryImpl":"', 238 | data.pauserRegistry.getImplementation().toHexString(), 239 | '","strategyFactory":"', 240 | data.strategyFactory.toHexString(), 241 | '","strategyFactoryImpl":"', 242 | data.strategyFactory.getImplementation().toHexString(), 243 | '","strategyBeacon":"', 244 | data.strategyBeacon.toHexString(), 245 | '","rewardsCoordinator":"', 246 | data.rewardsCoordinator.toHexString(), 247 | '","rewardsCoordinatorImpl":"', 248 | data.rewardsCoordinator.getImplementation().toHexString(), 249 | '","permissionController":"', 250 | data.permissionController.toHexString(), 251 | '","permissionControllerImpl":"', 252 | data.permissionController.getImplementation().toHexString(), 253 | '"}' 254 | ); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /contracts/script/utils/HelloWorldDeploymentLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 5 | import {TransparentUpgradeableProxy} from 6 | "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 7 | import {Script} from "forge-std/Script.sol"; 8 | import {console2} from "forge-std/Test.sol"; 9 | import {Vm} from "forge-std/Vm.sol"; 10 | import {stdJson} from "forge-std/StdJson.sol"; 11 | import {ECDSAStakeRegistry} from "@eigenlayer-middleware/src/unaudited/ECDSAStakeRegistry.sol"; 12 | import {HelloWorldServiceManager} from "../../src/HelloWorldServiceManager.sol"; 13 | import {IDelegationManager} from "@eigenlayer/contracts/interfaces/IDelegationManager.sol"; 14 | import {IECDSAStakeRegistryTypes} from 15 | "@eigenlayer-middleware/src/interfaces/IECDSAStakeRegistry.sol"; 16 | import {UpgradeableProxyLib} from "./UpgradeableProxyLib.sol"; 17 | import {CoreDeployLib, CoreDeploymentParsingLib} from "./CoreDeploymentParsingLib.sol"; 18 | import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; 19 | 20 | library HelloWorldDeploymentLib { 21 | using stdJson for *; 22 | using Strings for *; 23 | using UpgradeableProxyLib for address; 24 | 25 | Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); 26 | 27 | struct DeploymentData { 28 | address helloWorldServiceManager; 29 | address stakeRegistry; 30 | address strategy; 31 | address token; 32 | } 33 | 34 | struct DeploymentConfigData { 35 | address rewardsOwner; 36 | address rewardsInitiator; 37 | uint256 rewardsOwnerKey; 38 | uint256 rewardsInitiatorKey; 39 | } 40 | 41 | function deployContracts( 42 | address proxyAdmin, 43 | CoreDeployLib.DeploymentData memory core, 44 | IECDSAStakeRegistryTypes.Quorum memory quorum, 45 | address rewardsInitiator, 46 | address owner 47 | ) internal returns (DeploymentData memory) { 48 | DeploymentData memory result; 49 | 50 | { 51 | // First, deploy upgradeable proxy contracts that will point to the implementations. 52 | result.helloWorldServiceManager = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); 53 | result.stakeRegistry = UpgradeableProxyLib.setUpEmptyProxy(proxyAdmin); 54 | } 55 | deployAndUpgradeStakeRegistryImpl(result, core, quorum); 56 | deployAndUpgradeServiceManagerImpl(result, core, owner, rewardsInitiator); 57 | 58 | return result; 59 | } 60 | 61 | function deployAndUpgradeStakeRegistryImpl( 62 | DeploymentData memory deployment, 63 | CoreDeployLib.DeploymentData memory core, 64 | IECDSAStakeRegistryTypes.Quorum memory quorum 65 | ) private { 66 | address stakeRegistryImpl = 67 | address(new ECDSAStakeRegistry(IDelegationManager(core.delegationManager))); 68 | 69 | bytes memory upgradeCall = abi.encodeCall( 70 | ECDSAStakeRegistry.initialize, (deployment.helloWorldServiceManager, 0, quorum) 71 | ); 72 | UpgradeableProxyLib.upgradeAndCall(deployment.stakeRegistry, stakeRegistryImpl, upgradeCall); 73 | } 74 | 75 | function deployAndUpgradeServiceManagerImpl( 76 | DeploymentData memory deployment, 77 | CoreDeployLib.DeploymentData memory core, 78 | address owner, 79 | address rewardsInitiator 80 | ) private { 81 | address helloWorldServiceManager = deployment.helloWorldServiceManager; 82 | address helloWorldServiceManagerImpl = address( 83 | new HelloWorldServiceManager( 84 | core.avsDirectory, 85 | deployment.stakeRegistry, 86 | core.rewardsCoordinator, 87 | core.delegationManager, 88 | core.allocationManager, 89 | 4 90 | ) 91 | ); 92 | 93 | bytes memory upgradeCall = 94 | abi.encodeCall(HelloWorldServiceManager.initialize, (owner, rewardsInitiator)); 95 | 96 | UpgradeableProxyLib.upgradeAndCall( 97 | helloWorldServiceManager, helloWorldServiceManagerImpl, upgradeCall 98 | ); 99 | } 100 | 101 | function readDeploymentJson( 102 | uint256 chainId 103 | ) internal view returns (DeploymentData memory) { 104 | return readDeploymentJson("deployments/", chainId); 105 | } 106 | 107 | function readDeploymentJson( 108 | string memory directoryPath, 109 | uint256 chainId 110 | ) internal view returns (DeploymentData memory) { 111 | string memory fileName = string.concat(directoryPath, vm.toString(chainId), ".json"); 112 | 113 | require(vm.exists(fileName), "HelloWorldDeployment: Deployment file does not exist"); 114 | 115 | string memory json = vm.readFile(fileName); 116 | 117 | DeploymentData memory data; 118 | /// TODO: 2 Step for reading deployment json. Read to the core and the AVS data 119 | data.helloWorldServiceManager = json.readAddress(".addresses.helloWorldServiceManager"); 120 | data.stakeRegistry = json.readAddress(".addresses.stakeRegistry"); 121 | data.strategy = json.readAddress(".addresses.strategy"); 122 | data.token = json.readAddress(".addresses.token"); 123 | 124 | return data; 125 | } 126 | 127 | /// write to default output path 128 | function writeDeploymentJson( 129 | DeploymentData memory data 130 | ) internal { 131 | writeDeploymentJson("deployments/hello-world/", block.chainid, data); 132 | } 133 | 134 | function writeDeploymentJson( 135 | string memory outputPath, 136 | uint256 chainId, 137 | DeploymentData memory data 138 | ) internal { 139 | address proxyAdmin = 140 | address(UpgradeableProxyLib.getProxyAdmin(data.helloWorldServiceManager)); 141 | 142 | string memory deploymentData = _generateDeploymentJson(data, proxyAdmin); 143 | 144 | string memory fileName = string.concat(outputPath, vm.toString(chainId), ".json"); 145 | if (!vm.exists(outputPath)) { 146 | vm.createDir(outputPath, true); 147 | } 148 | 149 | vm.writeFile(fileName, deploymentData); 150 | console2.log("Deployment artifacts written to:", fileName); 151 | } 152 | 153 | function readDeploymentConfigValues( 154 | string memory directoryPath, 155 | string memory fileName 156 | ) internal view returns (DeploymentConfigData memory) { 157 | string memory pathToFile = string.concat(directoryPath, fileName); 158 | 159 | require( 160 | vm.exists(pathToFile), "HelloWorldDeployment: Deployment Config file does not exist" 161 | ); 162 | 163 | string memory json = vm.readFile(pathToFile); 164 | 165 | DeploymentConfigData memory data; 166 | data.rewardsOwner = json.readAddress(".addresses.rewardsOwner"); 167 | data.rewardsInitiator = json.readAddress(".addresses.rewardsInitiator"); 168 | data.rewardsOwnerKey = json.readUint(".keys.rewardsOwner"); 169 | data.rewardsInitiatorKey = json.readUint(".keys.rewardsInitiator"); 170 | return data; 171 | } 172 | 173 | function readDeploymentConfigValues( 174 | string memory directoryPath, 175 | uint256 chainId 176 | ) internal view returns (DeploymentConfigData memory) { 177 | return 178 | readDeploymentConfigValues(directoryPath, string.concat(vm.toString(chainId), ".json")); 179 | } 180 | 181 | function _generateDeploymentJson( 182 | DeploymentData memory data, 183 | address proxyAdmin 184 | ) private view returns (string memory) { 185 | return string.concat( 186 | '{"lastUpdate":{"timestamp":"', 187 | vm.toString(block.timestamp), 188 | '","block_number":"', 189 | vm.toString(block.number), 190 | '"},"addresses":', 191 | _generateContractsJson(data, proxyAdmin), 192 | "}" 193 | ); 194 | } 195 | 196 | function _generateContractsJson( 197 | DeploymentData memory data, 198 | address proxyAdmin 199 | ) private view returns (string memory) { 200 | return string.concat( 201 | '{"proxyAdmin":"', 202 | proxyAdmin.toHexString(), 203 | '","helloWorldServiceManager":"', 204 | data.helloWorldServiceManager.toHexString(), 205 | '","helloWorldServiceManagerImpl":"', 206 | data.helloWorldServiceManager.getImplementation().toHexString(), 207 | '","stakeRegistry":"', 208 | data.stakeRegistry.toHexString(), 209 | '","stakeRegistryImpl":"', 210 | data.stakeRegistry.getImplementation().toHexString(), 211 | '","strategy":"', 212 | data.strategy.toHexString(), 213 | '","token":"', 214 | data.token.toHexString(), 215 | '"}' 216 | ); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /contracts/script/utils/SetupDistributionsLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import { 5 | IRewardsCoordinator, 6 | IRewardsCoordinatorTypes 7 | } from "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; 8 | import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategyManager.sol"; 9 | import {ECDSAServiceManagerBase} from 10 | "@eigenlayer-middleware/src/unaudited/ECDSAServiceManagerBase.sol"; 11 | import {Vm} from "forge-std/Vm.sol"; 12 | import {console} from "forge-std/console.sol"; 13 | 14 | library SetupDistributionsLib { 15 | Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); 16 | 17 | struct PaymentLeaves { 18 | bytes32[] leaves; 19 | bytes32[] tokenLeaves; 20 | } 21 | 22 | function createAVSRewardsSubmissions( 23 | address helloWorldServiceManager, 24 | address strategy, 25 | uint256 numPayments, 26 | uint256 amountPerPayment, 27 | uint32 duration, 28 | uint32 startTimestamp 29 | ) internal { 30 | IRewardsCoordinatorTypes.RewardsSubmission[] memory rewardsSubmissions = 31 | new IRewardsCoordinatorTypes.RewardsSubmission[](numPayments); 32 | for (uint256 i = 0; i < numPayments; i++) { 33 | IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers = 34 | new IRewardsCoordinatorTypes.StrategyAndMultiplier[](1); 35 | strategiesAndMultipliers[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({ 36 | strategy: IStrategy(strategy), 37 | multiplier: 10_000 38 | }); 39 | 40 | IRewardsCoordinatorTypes.RewardsSubmission memory rewardsSubmission = 41 | IRewardsCoordinatorTypes.RewardsSubmission({ 42 | strategiesAndMultipliers: strategiesAndMultipliers, 43 | token: IStrategy(strategy).underlyingToken(), 44 | amount: amountPerPayment, 45 | startTimestamp: startTimestamp, 46 | duration: duration 47 | }); 48 | 49 | rewardsSubmissions[i] = rewardsSubmission; 50 | } 51 | ECDSAServiceManagerBase(helloWorldServiceManager).createAVSRewardsSubmission( 52 | rewardsSubmissions 53 | ); 54 | } 55 | 56 | function createOperatorDirectedAVSRewardsSubmissions( 57 | address helloWorldServiceManager, 58 | address[] memory operators, 59 | address strategy, 60 | uint256 numPayments, 61 | uint256 amountPerPayment, 62 | uint32 duration, 63 | uint32 startTimestamp 64 | ) internal { 65 | uint256 operatorRewardAmount = amountPerPayment / operators.length; 66 | 67 | IRewardsCoordinatorTypes.OperatorReward[] memory operatorRewards = 68 | new IRewardsCoordinatorTypes.OperatorReward[](operators.length); 69 | for (uint256 i = 0; i < operators.length; i++) { 70 | operatorRewards[i] = IRewardsCoordinatorTypes.OperatorReward({ 71 | operator: operators[i], 72 | amount: operatorRewardAmount 73 | }); 74 | } 75 | 76 | IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory rewardsSubmissions = 77 | new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](numPayments); 78 | for (uint256 i = 0; i < numPayments; i++) { 79 | IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers = 80 | new IRewardsCoordinatorTypes.StrategyAndMultiplier[](1); 81 | strategiesAndMultipliers[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({ 82 | strategy: IStrategy(strategy), 83 | multiplier: 10_000 84 | }); 85 | 86 | IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory rewardsSubmission = 87 | IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({ 88 | strategiesAndMultipliers: strategiesAndMultipliers, 89 | token: IStrategy(strategy).underlyingToken(), 90 | operatorRewards: operatorRewards, 91 | startTimestamp: startTimestamp, 92 | duration: duration, 93 | description: "test" 94 | }); 95 | console.log("rrr"); 96 | console.log(startTimestamp); 97 | console.log(duration); 98 | console.log(block.timestamp); 99 | 100 | rewardsSubmissions[i] = rewardsSubmission; 101 | } 102 | ECDSAServiceManagerBase(helloWorldServiceManager).createOperatorDirectedAVSRewardsSubmission( 103 | rewardsSubmissions 104 | ); 105 | } 106 | 107 | function processClaim( 108 | IRewardsCoordinator rewardsCoordinator, 109 | string memory filePath, 110 | uint256 indexToProve, 111 | address recipient, 112 | IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf memory earnerLeaf, 113 | uint256 NUM_TOKEN_EARNINGS, 114 | address strategy, 115 | uint32 amountPerPayment 116 | ) internal { 117 | PaymentLeaves memory paymentLeaves = parseLeavesFromJson(filePath); 118 | 119 | bytes memory proof = generateMerkleProof(paymentLeaves.leaves, indexToProve); 120 | //we only have one token leaf 121 | bytes memory tokenProof = generateMerkleProof(paymentLeaves.tokenLeaves, 0); 122 | 123 | uint32[] memory tokenIndices = new uint32[](NUM_TOKEN_EARNINGS); 124 | bytes[] memory tokenProofs = new bytes[](NUM_TOKEN_EARNINGS); 125 | tokenProofs[0] = tokenProof; 126 | 127 | IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[] memory tokenLeaves = 128 | new IRewardsCoordinatorTypes.TokenTreeMerkleLeaf[](NUM_TOKEN_EARNINGS); 129 | tokenLeaves[0] = defaultTokenLeaf(amountPerPayment, strategy); 130 | 131 | // this workflow assumes a new root submitted for every payment claimed. So we get the latest rood index to process a claim for 132 | uint256 rootIndex = rewardsCoordinator.getDistributionRootsLength() - 1; 133 | 134 | IRewardsCoordinatorTypes.RewardsMerkleClaim memory claim = IRewardsCoordinatorTypes 135 | .RewardsMerkleClaim({ 136 | rootIndex: uint32(rootIndex), 137 | earnerIndex: uint32(indexToProve), 138 | earnerTreeProof: proof, 139 | earnerLeaf: earnerLeaf, 140 | tokenIndices: tokenIndices, 141 | tokenTreeProofs: tokenProofs, 142 | tokenLeaves: tokenLeaves 143 | }); 144 | 145 | rewardsCoordinator.processClaim(claim, recipient); 146 | } 147 | 148 | function submitRoot( 149 | IRewardsCoordinator rewardsCoordinator, 150 | bytes32[] memory tokenLeaves, 151 | IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf[] memory earnerLeaves, 152 | uint32 rewardsCalculationEndTimestamp, 153 | uint256 NUM_PAYMENTS, 154 | uint256 NUM_TOKEN_EARNINGS, 155 | string memory filePath 156 | ) internal { 157 | bytes32 paymentRoot = createPaymentRoot( 158 | rewardsCoordinator, 159 | tokenLeaves, 160 | earnerLeaves, 161 | NUM_PAYMENTS, 162 | NUM_TOKEN_EARNINGS, 163 | filePath 164 | ); 165 | rewardsCoordinator.submitRoot(paymentRoot, rewardsCalculationEndTimestamp); 166 | } 167 | 168 | function createPaymentRoot( 169 | IRewardsCoordinator rewardsCoordinator, 170 | bytes32[] memory tokenLeaves, 171 | IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf[] memory earnerLeaves, 172 | uint256 NUM_PAYMENTS, 173 | uint256 NUM_TOKEN_EARNINGS, 174 | string memory filePath 175 | ) internal returns (bytes32) { 176 | require( 177 | earnerLeaves.length == NUM_PAYMENTS, "Number of earners must match number of payments" 178 | ); 179 | bytes32[] memory leaves = new bytes32[](NUM_PAYMENTS); 180 | 181 | require( 182 | tokenLeaves.length == NUM_TOKEN_EARNINGS, 183 | "Number of token leaves must match number of token earnings" 184 | ); 185 | for (uint256 i = 0; i < NUM_PAYMENTS; i++) { 186 | leaves[i] = rewardsCoordinator.calculateEarnerLeafHash(earnerLeaves[i]); 187 | } 188 | 189 | writeLeavesToJson(leaves, tokenLeaves, filePath); 190 | return (merkleizeKeccak(leaves)); 191 | } 192 | 193 | function createEarnerLeaves( 194 | address[] calldata earners, 195 | bytes32[] memory tokenLeaves 196 | ) public pure returns (IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf[] memory) { 197 | IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf[] memory leaves = 198 | new IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf[](earners.length); 199 | for (uint256 i = 0; i < earners.length; i++) { 200 | leaves[i] = IRewardsCoordinatorTypes.EarnerTreeMerkleLeaf({ 201 | earner: earners[i], 202 | earnerTokenRoot: createTokenRoot(tokenLeaves) 203 | }); 204 | } 205 | return leaves; 206 | } 207 | 208 | function createTokenRoot( 209 | bytes32[] memory tokenLeaves 210 | ) public pure returns (bytes32) { 211 | return merkleizeKeccak(tokenLeaves); 212 | } 213 | 214 | function createTokenLeaves( 215 | IRewardsCoordinator rewardsCoordinator, 216 | uint256 NUM_TOKEN_EARNINGS, 217 | uint256 TOKEN_EARNINGS, 218 | address strategy 219 | ) internal view returns (bytes32[] memory) { 220 | bytes32[] memory leaves = new bytes32[](NUM_TOKEN_EARNINGS); 221 | for (uint256 i = 0; i < NUM_TOKEN_EARNINGS; i++) { 222 | IRewardsCoordinatorTypes.TokenTreeMerkleLeaf memory leaf = 223 | defaultTokenLeaf(TOKEN_EARNINGS, strategy); 224 | leaves[i] = rewardsCoordinator.calculateTokenLeafHash(leaf); 225 | } 226 | return leaves; 227 | } 228 | 229 | function defaultTokenLeaf( 230 | uint256 TOKEN_EARNINGS, 231 | address strategy 232 | ) internal view returns (IRewardsCoordinatorTypes.TokenTreeMerkleLeaf memory) { 233 | IRewardsCoordinatorTypes.TokenTreeMerkleLeaf memory leaf = IRewardsCoordinatorTypes 234 | .TokenTreeMerkleLeaf({ 235 | token: IStrategy(strategy).underlyingToken(), 236 | cumulativeEarnings: TOKEN_EARNINGS 237 | }); 238 | return leaf; 239 | } 240 | 241 | function writeLeavesToJson( 242 | bytes32[] memory leaves, 243 | bytes32[] memory tokenLeaves, 244 | string memory filePath 245 | ) internal { 246 | string memory parent_object = "parent_object"; 247 | vm.serializeBytes32(parent_object, "leaves", leaves); 248 | string memory finalJson = vm.serializeBytes32(parent_object, "tokenLeaves", tokenLeaves); 249 | vm.writeJson(finalJson, filePath); 250 | } 251 | 252 | function parseLeavesFromJson( 253 | string memory filePath 254 | ) internal view returns (PaymentLeaves memory) { 255 | string memory json = vm.readFile(filePath); 256 | bytes memory data = vm.parseJson(json); 257 | return abi.decode(data, (PaymentLeaves)); 258 | } 259 | 260 | function generateMerkleProof( 261 | bytes32[] memory leaves, 262 | uint256 index 263 | ) internal pure returns (bytes memory) { 264 | require(leaves.length > 0, "Leaves array cannot be empty"); 265 | require(index < leaves.length, "Index out of bounds"); 266 | 267 | leaves = padLeaves(leaves); 268 | 269 | uint256 n = leaves.length; 270 | uint256 depth = 0; 271 | while ((1 << depth) < n) { 272 | depth++; 273 | } 274 | 275 | bytes32[] memory proof = new bytes32[](depth); 276 | uint256 proofIndex = 0; 277 | 278 | for (uint256 i = 0; i < depth; i++) { 279 | uint256 levelSize = (n + 1) / 2; 280 | uint256 siblingIndex = (index % 2 == 0) ? index + 1 : index - 1; 281 | 282 | if (siblingIndex < n) { 283 | proof[proofIndex] = leaves[siblingIndex]; 284 | proofIndex++; 285 | } 286 | 287 | for (uint256 j = 0; j < levelSize; j++) { 288 | if (2 * j + 1 < n) { 289 | leaves[j] = keccak256(abi.encodePacked(leaves[2 * j], leaves[2 * j + 1])); 290 | } else { 291 | leaves[j] = leaves[2 * j]; 292 | } 293 | } 294 | 295 | n = levelSize; 296 | index /= 2; 297 | } 298 | 299 | return abi.encodePacked(proof); 300 | } 301 | 302 | /** 303 | * @notice this function returns the merkle root of a tree created from a set of leaves using keccak256 as its hash function 304 | * @param leaves the leaves of the merkle tree 305 | * @return The computed Merkle root of the tree. 306 | * @dev This pads to the next power of 2. very inefficient! just for POC 307 | */ 308 | function merkleizeKeccak( 309 | bytes32[] memory leaves 310 | ) internal pure returns (bytes32) { 311 | // uint256 paddedLength = 2; 312 | // while(paddedLength < leaves.length) { 313 | // paddedLength <<= 1; 314 | // } 315 | 316 | // bytes32[] memory paddedLeaves = new bytes32[](paddedLength); 317 | // for (uint256 i = 0; i < leaves.length; i++) { 318 | // paddedLeaves[i] = leaves[i]; 319 | // } 320 | leaves = padLeaves(leaves); 321 | 322 | //there are half as many nodes in the layer above the leaves 323 | uint256 numNodesInLayer = leaves.length / 2; 324 | //create a layer to store the internal nodes 325 | bytes32[] memory layer = new bytes32[](numNodesInLayer); 326 | //fill the layer with the pairwise hashes of the leaves 327 | for (uint256 i = 0; i < numNodesInLayer; i++) { 328 | layer[i] = keccak256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); 329 | } 330 | //the next layer above has half as many nodes 331 | numNodesInLayer /= 2; 332 | //while we haven't computed the root 333 | while (numNodesInLayer != 0) { 334 | //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children 335 | for (uint256 i = 0; i < numNodesInLayer; i++) { 336 | layer[i] = keccak256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); 337 | } 338 | //the next layer above has half as many nodes 339 | numNodesInLayer /= 2; 340 | } 341 | //the first node in the layer is the root 342 | return layer[0]; 343 | } 344 | 345 | function padLeaves( 346 | bytes32[] memory leaves 347 | ) internal pure returns (bytes32[] memory) { 348 | uint256 paddedLength = 2; 349 | while (paddedLength < leaves.length) { 350 | paddedLength <<= 1; 351 | } 352 | 353 | bytes32[] memory paddedLeaves = new bytes32[](paddedLength); 354 | for (uint256 i = 0; i < leaves.length; i++) { 355 | paddedLeaves[i] = leaves[i]; 356 | } 357 | return paddedLeaves; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /contracts/script/utils/UpgradeableProxyLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 6 | import { 7 | ITransparentUpgradeableProxy, 8 | TransparentUpgradeableProxy 9 | } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 10 | import {EmptyContract} from "@eigenlayer/test/mocks/EmptyContract.sol"; 11 | 12 | library UpgradeableProxyLib { 13 | bytes32 internal constant IMPLEMENTATION_SLOT = 14 | 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 15 | bytes32 internal constant ADMIN_SLOT = 16 | 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; 17 | 18 | Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); 19 | 20 | function deployProxyAdmin() internal returns (address) { 21 | return address(new ProxyAdmin()); 22 | } 23 | 24 | function setUpEmptyProxy( 25 | address admin 26 | ) internal returns (address) { 27 | address emptyContract = address(new EmptyContract()); 28 | return address(new TransparentUpgradeableProxy(emptyContract, admin, "")); 29 | } 30 | 31 | function upgrade(address proxy, address impl) internal { 32 | ProxyAdmin admin = getProxyAdmin(proxy); 33 | admin.upgrade(ITransparentUpgradeableProxy(payable(proxy)), impl); 34 | } 35 | 36 | function upgradeAndCall(address proxy, address impl, bytes memory initData) internal { 37 | ProxyAdmin admin = getProxyAdmin(proxy); 38 | admin.upgradeAndCall(ITransparentUpgradeableProxy(payable(proxy)), impl, initData); 39 | } 40 | 41 | function getImplementation( 42 | address proxy 43 | ) internal view returns (address) { 44 | bytes32 value = vm.load(proxy, IMPLEMENTATION_SLOT); 45 | return address(uint160(uint256(value))); 46 | } 47 | 48 | function getProxyAdmin( 49 | address proxy 50 | ) internal view returns (ProxyAdmin) { 51 | bytes32 value = vm.load(proxy, ADMIN_SLOT); 52 | return ProxyAdmin(address(uint160(uint256(value)))); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /contracts/src/HelloWorldServiceManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | import {ECDSAServiceManagerBase} from 5 | "@eigenlayer-middleware/src/unaudited/ECDSAServiceManagerBase.sol"; 6 | import {ECDSAStakeRegistry} from "@eigenlayer-middleware/src/unaudited/ECDSAStakeRegistry.sol"; 7 | import {IServiceManager} from "@eigenlayer-middleware/src/interfaces/IServiceManager.sol"; 8 | import {ECDSAUpgradeable} from 9 | "@openzeppelin-upgrades/contracts/utils/cryptography/ECDSAUpgradeable.sol"; 10 | import {IERC1271Upgradeable} from 11 | "@openzeppelin-upgrades/contracts/interfaces/IERC1271Upgradeable.sol"; 12 | import {IHelloWorldServiceManager} from "./IHelloWorldServiceManager.sol"; 13 | import "@openzeppelin/contracts/utils/Strings.sol"; 14 | import "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; 15 | import {IAllocationManager} from "@eigenlayer/contracts/interfaces/IAllocationManager.sol"; 16 | import {TransparentUpgradeableProxy} from 17 | "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; 18 | 19 | /** 20 | * @title Primary entrypoint for procuring services from HelloWorld. 21 | * @author Eigen Labs, Inc. 22 | */ 23 | contract HelloWorldServiceManager is ECDSAServiceManagerBase, IHelloWorldServiceManager { 24 | using ECDSAUpgradeable for bytes32; 25 | 26 | uint32 public latestTaskNum; 27 | 28 | // mapping of task indices to all tasks hashes 29 | // when a task is created, task hash is stored here, 30 | // and responses need to pass the actual task, 31 | // which is hashed onchain and checked against this mapping 32 | mapping(uint32 => bytes32) public allTaskHashes; 33 | 34 | // mapping of task indices to hash of abi.encode(taskResponse, taskResponseMetadata) 35 | mapping(address => mapping(uint32 => bytes)) public allTaskResponses; 36 | 37 | // mapping of task indices to task status (true if task has been responded to, false otherwise) 38 | // TODO: use bitmap? 39 | mapping(uint32 => bool) public taskWasResponded; 40 | 41 | // max interval in blocks for responding to a task 42 | // operators can be penalized if they don't respond in time 43 | uint32 public immutable MAX_RESPONSE_INTERVAL_BLOCKS; 44 | 45 | modifier onlyOperator() { 46 | require( 47 | ECDSAStakeRegistry(stakeRegistry).operatorRegistered(msg.sender), 48 | "Operator must be the caller" 49 | ); 50 | _; 51 | } 52 | 53 | constructor( 54 | address _avsDirectory, 55 | address _stakeRegistry, 56 | address _rewardsCoordinator, 57 | address _delegationManager, 58 | address _allocationManager, 59 | uint32 _maxResponseIntervalBlocks 60 | ) 61 | ECDSAServiceManagerBase( 62 | _avsDirectory, 63 | _stakeRegistry, 64 | _rewardsCoordinator, 65 | _delegationManager, 66 | _allocationManager 67 | ) 68 | { 69 | MAX_RESPONSE_INTERVAL_BLOCKS = _maxResponseIntervalBlocks; 70 | } 71 | 72 | function initialize(address initialOwner, address _rewardsInitiator) external initializer { 73 | __ServiceManagerBase_init(initialOwner, _rewardsInitiator); 74 | } 75 | 76 | // These are just to comply with IServiceManager interface 77 | function addPendingAdmin( 78 | address admin 79 | ) external onlyOwner {} 80 | 81 | function removePendingAdmin( 82 | address pendingAdmin 83 | ) external onlyOwner {} 84 | 85 | function removeAdmin( 86 | address admin 87 | ) external onlyOwner {} 88 | 89 | function setAppointee(address appointee, address target, bytes4 selector) external onlyOwner {} 90 | 91 | function removeAppointee( 92 | address appointee, 93 | address target, 94 | bytes4 selector 95 | ) external onlyOwner {} 96 | 97 | function deregisterOperatorFromOperatorSets( 98 | address operator, 99 | uint32[] memory operatorSetIds 100 | ) external { 101 | // unused 102 | } 103 | 104 | /* FUNCTIONS */ 105 | // NOTE: this function creates new task, assigns it a taskId 106 | function createNewTask( 107 | string memory name 108 | ) external returns (Task memory) { 109 | // create a new task struct 110 | Task memory newTask; 111 | newTask.name = name; 112 | newTask.taskCreatedBlock = uint32(block.number); 113 | 114 | // store hash of task onchain, emit event, and increase taskNum 115 | allTaskHashes[latestTaskNum] = keccak256(abi.encode(newTask)); 116 | emit NewTaskCreated(latestTaskNum, newTask); 117 | latestTaskNum = latestTaskNum + 1; 118 | 119 | return newTask; 120 | } 121 | 122 | function respondToTask( 123 | Task calldata task, 124 | uint32 referenceTaskIndex, 125 | bytes memory signature 126 | ) external { 127 | // check that the task is valid, hasn't been responded yet, and is being responded in time 128 | require( 129 | keccak256(abi.encode(task)) == allTaskHashes[referenceTaskIndex], 130 | "supplied task does not match the one recorded in the contract" 131 | ); 132 | require( 133 | block.number <= task.taskCreatedBlock + MAX_RESPONSE_INTERVAL_BLOCKS, 134 | "Task response time has already expired" 135 | ); 136 | 137 | // The message that was signed 138 | bytes32 messageHash = keccak256(abi.encodePacked("Hello, ", task.name)); 139 | bytes32 ethSignedMessageHash = messageHash.toEthSignedMessageHash(); 140 | bytes4 magicValue = IERC1271Upgradeable.isValidSignature.selector; 141 | 142 | // Decode the signature data to get operators and their signatures 143 | (address[] memory operators, bytes[] memory signatures, uint32 referenceBlock) = 144 | abi.decode(signature, (address[], bytes[], uint32)); 145 | 146 | // Check that referenceBlock matches task creation block 147 | require( 148 | referenceBlock == task.taskCreatedBlock, 149 | "Reference block must match task creation block" 150 | ); 151 | 152 | // Store each operator's signature 153 | for (uint256 i = 0; i < operators.length; i++) { 154 | // Check that this operator hasn't already responded 155 | require( 156 | allTaskResponses[operators[i]][referenceTaskIndex].length == 0, 157 | "Operator has already responded to the task" 158 | ); 159 | 160 | // Store the operator's signature 161 | allTaskResponses[operators[i]][referenceTaskIndex] = signatures[i]; 162 | 163 | // Emit event for this operator 164 | emit TaskResponded(referenceTaskIndex, task, operators[i]); 165 | } 166 | 167 | taskWasResponded[referenceTaskIndex] = true; 168 | 169 | // Verify all signatures at once 170 | bytes4 isValidSignatureResult = 171 | ECDSAStakeRegistry(stakeRegistry).isValidSignature(ethSignedMessageHash, signature); 172 | 173 | require(magicValue == isValidSignatureResult, "Invalid signature"); 174 | } 175 | 176 | function slashOperator( 177 | Task calldata task, 178 | uint32 referenceTaskIndex, 179 | address operator 180 | ) external { 181 | // check that the task is valid, hasn't been responsed yet 182 | require( 183 | keccak256(abi.encode(task)) == allTaskHashes[referenceTaskIndex], 184 | "supplied task does not match the one recorded in the contract" 185 | ); 186 | require(!taskWasResponded[referenceTaskIndex], "Task has already been responded to"); 187 | require( 188 | allTaskResponses[operator][referenceTaskIndex].length == 0, 189 | "Operator has already responded to the task" 190 | ); 191 | require( 192 | block.number > task.taskCreatedBlock + MAX_RESPONSE_INTERVAL_BLOCKS, 193 | "Task response time has not expired yet" 194 | ); 195 | // check operator was registered when task was created 196 | uint256 operatorWeight = ECDSAStakeRegistry(stakeRegistry).getOperatorWeightAtBlock( 197 | operator, task.taskCreatedBlock 198 | ); 199 | require(operatorWeight > 0, "Operator was not registered when task was created"); 200 | 201 | // we update the storage with a sentinel value 202 | allTaskResponses[operator][referenceTaskIndex] = "slashed"; 203 | 204 | // TODO: slash operator 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /contracts/src/IHelloWorldServiceManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | interface IHelloWorldServiceManager { 5 | event NewTaskCreated(uint32 indexed taskIndex, Task task); 6 | 7 | event TaskResponded(uint32 indexed taskIndex, Task task, address operator); 8 | 9 | struct Task { 10 | string name; 11 | uint32 taskCreatedBlock; 12 | } 13 | 14 | function latestTaskNum() external view returns (uint32); 15 | 16 | function allTaskHashes( 17 | uint32 taskIndex 18 | ) external view returns (bytes32); 19 | 20 | function allTaskResponses( 21 | address operator, 22 | uint32 taskIndex 23 | ) external view returns (bytes memory); 24 | 25 | function createNewTask( 26 | string memory name 27 | ) external returns (Task memory); 28 | 29 | function respondToTask( 30 | Task calldata task, 31 | uint32 referenceTaskIndex, 32 | bytes calldata signature 33 | ) external; 34 | 35 | function slashOperator( 36 | Task calldata task, 37 | uint32 referenceTaskIndex, 38 | address operator 39 | ) external; 40 | } 41 | -------------------------------------------------------------------------------- /contracts/test/CoreDeploymentLib.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {console2} from "forge-std/console2.sol"; 6 | import { 7 | CoreDeployLib, CoreDeploymentParsingLib 8 | } from "../script/utils/CoreDeploymentParsingLib.sol"; 9 | import {UpgradeableProxyLib} from "../script/utils/UpgradeableProxyLib.sol"; 10 | 11 | contract CoreDeploymentParsingLibTest is Test { 12 | using UpgradeableProxyLib for address; 13 | 14 | address proxyAdmin; 15 | CoreDeployLib.DeploymentData deploymentData; 16 | CoreDeployLib.DeploymentConfigData configData; 17 | 18 | function setUp() public { 19 | proxyAdmin = UpgradeableProxyLib.deployProxyAdmin(); 20 | } 21 | 22 | /// won't test specific functionality/values. Testing behavior of the library 23 | function test_ReadConfig() public view { 24 | CoreDeploymentParsingLib.readDeploymentConfigValues("test/mockData/config/core/", 1337); 25 | } 26 | 27 | /// forge-config: default.allow_internal_expect_revert = true 28 | function test_ReadConfig_Reverts() public { 29 | vm.expectRevert(); 30 | /// Incorrect path 31 | CoreDeploymentParsingLib.readDeploymentConfigValues("test/mockData/deployments/core/", 1337); 32 | } 33 | 34 | function test_ReadDeployment() public view { 35 | CoreDeploymentParsingLib.readDeploymentJson("test/mockData/deployments/core/", 1337); 36 | } 37 | 38 | /// forge-config: default.allow_internal_expect_revert = true 39 | function test_ReadDeployment_Reverts() public { 40 | vm.expectRevert(); 41 | /// Incorrect path 42 | CoreDeploymentParsingLib.readDeploymentJson("test/mockData/config/core/", 1337); 43 | } 44 | 45 | function test_DeployContracts() public { 46 | configData = 47 | CoreDeploymentParsingLib.readDeploymentConfigValues("test/mockData/config/core/", 1337); 48 | deploymentData = CoreDeployLib.deployContracts(proxyAdmin, configData); 49 | 50 | assertTrue(deploymentData.delegationManager != address(0), "DelegationManager not deployed"); 51 | assertTrue(deploymentData.avsDirectory != address(0), "AVSDirectory not deployed"); 52 | assertTrue(deploymentData.strategyManager != address(0), "StrategyManager not deployed"); 53 | } 54 | 55 | function test_WriteDeploymentJson() public { 56 | configData = 57 | CoreDeploymentParsingLib.readDeploymentConfigValues("test/mockData/config/core/", 1337); 58 | deploymentData = CoreDeployLib.deployContracts(proxyAdmin, configData); 59 | 60 | string memory scratchPath = "test/mockData/scratch/test_WriteDeploymentJson/"; 61 | CoreDeploymentParsingLib.writeDeploymentJson(scratchPath, block.chainid, deploymentData); 62 | 63 | string memory fileName = string.concat(scratchPath, vm.toString(block.chainid), ".json"); 64 | assertTrue(vm.exists(fileName), "Deployment file not created"); 65 | 66 | vm.removeFile(fileName); 67 | } 68 | 69 | function test_WriteAndReadDeploymentJson() public { 70 | configData = 71 | CoreDeploymentParsingLib.readDeploymentConfigValues("test/mockData/config/core/", 1337); 72 | deploymentData = CoreDeployLib.deployContracts(proxyAdmin, configData); 73 | 74 | string memory scratchPath = "test/mockData/scratch/test_WriteAndReadDeploymentJson/"; 75 | 76 | CoreDeploymentParsingLib.writeDeploymentJson(scratchPath, block.chainid, deploymentData); 77 | 78 | string memory fileName = string.concat(vm.toString(block.chainid), ".json"); 79 | 80 | CoreDeploymentParsingLib.readDeploymentJson(scratchPath, fileName); 81 | 82 | vm.removeFile(string.concat(scratchPath, fileName)); 83 | } 84 | 85 | function test_ReadConfigFromM2DeploymentData() public { 86 | /// TODO: Deployment json is missing the strategy factory 87 | vm.skip(true); 88 | // Path to the M2 deployment data JSON file 89 | string memory m2DeploymentDataPath = 90 | "lib/eigenlayer-middleware/lib/eigenlayer-contracts/script/output/devnet/"; 91 | string memory m2DeploymentFilename = "M2_from_scratch_deployment_data.json"; 92 | 93 | CoreDeploymentParsingLib.readDeploymentJson(m2DeploymentDataPath, m2DeploymentFilename); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /contracts/test/ERC20Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.12; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract ERC20Mock is ERC20 { 7 | constructor() ERC20("", "") {} 8 | 9 | function mint(address account, uint256 amount) public { 10 | _mint(account, amount); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/test/SetupPaymentsLib.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../script/utils/SetupDistributionsLib.sol"; 6 | import "../script/utils/CoreDeploymentParsingLib.sol"; 7 | import "../script/utils/HelloWorldDeploymentLib.sol"; 8 | import "@eigenlayer/contracts/interfaces/IRewardsCoordinator.sol"; 9 | import "../src/IHelloWorldServiceManager.sol"; 10 | import "@eigenlayer/contracts/interfaces/IStrategy.sol"; 11 | import "@eigenlayer/contracts/libraries/Merkle.sol"; 12 | import "../script/DeployEigenLayerCore.s.sol"; 13 | import "../script/HelloWorldDeployer.s.sol"; 14 | import {StrategyFactory} from "@eigenlayer/contracts/strategies/StrategyFactory.sol"; 15 | import {HelloWorldTaskManagerSetup} from "test/HelloWorldServiceManager.t.sol"; 16 | import {ECDSAServiceManagerBase} from 17 | "@eigenlayer-middleware/src/unaudited/ECDSAServiceManagerBase.sol"; 18 | import { 19 | IECDSAStakeRegistryTypes, 20 | IStrategy 21 | } from "@eigenlayer-middleware/src/interfaces/IECDSAStakeRegistry.sol"; 22 | import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; 23 | 24 | contract TestConstants { 25 | uint256 constant NUM_PAYMENTS = 8; 26 | uint256 constant NUM_TOKEN_EARNINGS = 1; 27 | uint256 constant TOKEN_EARNINGS = 100; 28 | 29 | address RECIPIENT = address(1); 30 | address EARNER = address(2); 31 | uint256 INDEX_TO_PROVE = 0; 32 | uint256 NUM_EARNERS = 4; 33 | } 34 | 35 | contract SetupDistributionsLibTest is Test, TestConstants, HelloWorldTaskManagerSetup { 36 | using SetupDistributionsLib for *; 37 | 38 | Vm cheats = Vm(VM_ADDRESS); 39 | 40 | IRewardsCoordinator public rewardsCoordinator; 41 | IHelloWorldServiceManager public helloWorldServiceManager; 42 | IStrategy public strategy; 43 | 44 | address rewardsInitiator = address(1); 45 | address rewardsOwner = address(2); 46 | 47 | function setUp() public virtual override { 48 | proxyAdmin = UpgradeableProxyLib.deployProxyAdmin(); 49 | coreConfigData = 50 | CoreDeploymentParsingLib.readDeploymentConfigValues("test/mockData/config/core/", 1337); 51 | coreDeployment = CoreDeployLib.deployContracts(proxyAdmin, coreConfigData); 52 | 53 | vm.prank(coreConfigData.strategyManager.initialOwner); 54 | StrategyManager(coreDeployment.strategyManager).setStrategyWhitelister( 55 | coreDeployment.strategyFactory 56 | ); 57 | 58 | mockToken = new ERC20Mock(); 59 | 60 | strategy = addStrategy(address(mockToken)); // Similar function to HW_SM test using strategy factory 61 | quorum.strategies.push( 62 | IECDSAStakeRegistryTypes.StrategyParams({strategy: strategy, multiplier: 10_000}) 63 | ); 64 | 65 | helloWorldDeployment = HelloWorldDeploymentLib.deployContracts( 66 | proxyAdmin, coreDeployment, quorum, rewardsInitiator, rewardsOwner 67 | ); 68 | labelContracts(coreDeployment, helloWorldDeployment); 69 | 70 | cheats.prank(rewardsOwner); 71 | ECDSAServiceManagerBase(helloWorldDeployment.helloWorldServiceManager).setRewardsInitiator( 72 | rewardsInitiator 73 | ); 74 | 75 | rewardsCoordinator = IRewardsCoordinator(coreDeployment.rewardsCoordinator); 76 | 77 | mockToken.mint(address(this), 100_000); 78 | mockToken.mint(address(rewardsCoordinator), 100_000); 79 | mockToken.mint(rewardsInitiator, 100_000); 80 | } 81 | 82 | function testSubmitRoot() public { 83 | address[] memory earners = new address[](NUM_EARNERS); 84 | for (uint256 i = 0; i < earners.length; i++) { 85 | earners[i] = address(1); 86 | } 87 | uint32 endTimestamp = rewardsCoordinator.currRewardsCalculationEndTimestamp() + 1 weeks; 88 | cheats.warp(endTimestamp + 1); 89 | 90 | bytes32[] memory tokenLeaves = SetupDistributionsLib.createTokenLeaves( 91 | rewardsCoordinator, NUM_TOKEN_EARNINGS, TOKEN_EARNINGS, address(strategy) 92 | ); 93 | IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory earnerLeaves = 94 | SetupDistributionsLib.createEarnerLeaves(earners, tokenLeaves); 95 | 96 | string memory filePath = "testSubmitRoot.json"; 97 | 98 | cheats.startPrank(rewardsCoordinator.rewardsUpdater()); 99 | SetupDistributionsLib.submitRoot( 100 | rewardsCoordinator, tokenLeaves, earnerLeaves, endTimestamp, NUM_EARNERS, 1, filePath 101 | ); 102 | cheats.stopPrank(); 103 | vm.removeFile(filePath); 104 | } 105 | 106 | function testWriteLeavesToJson() public { 107 | bytes32[] memory leaves = new bytes32[](2); 108 | leaves[0] = bytes32(uint256(1)); 109 | leaves[1] = bytes32(uint256(2)); 110 | 111 | bytes32[] memory tokenLeaves = new bytes32[](2); 112 | tokenLeaves[0] = bytes32(uint256(3)); 113 | tokenLeaves[1] = bytes32(uint256(4)); 114 | 115 | string memory filePath = "testWriteLeavesToJson.json"; 116 | 117 | SetupDistributionsLib.writeLeavesToJson(leaves, tokenLeaves, filePath); 118 | 119 | assertTrue(vm.exists(filePath), "JSON file should be created"); 120 | vm.removeFile(filePath); 121 | } 122 | 123 | function testParseLeavesFromJson() public { 124 | string memory filePath = "test_parse_payments.json"; 125 | string memory jsonContent = '{"leaves":["0x1234"], "tokenLeaves":["0x5678"]}'; 126 | vm.writeFile(filePath, jsonContent); 127 | 128 | SetupDistributionsLib.PaymentLeaves memory paymentLeaves = 129 | SetupDistributionsLib.parseLeavesFromJson(filePath); 130 | 131 | assertEq(paymentLeaves.leaves.length, 1, "Incorrect number of leaves"); 132 | assertEq(paymentLeaves.tokenLeaves.length, 1, "Incorrect number of token leaves"); 133 | 134 | vm.removeFile(filePath); 135 | } 136 | 137 | function testGenerateMerkleProof() public view { 138 | SetupDistributionsLib.PaymentLeaves memory paymentLeaves = 139 | SetupDistributionsLib.parseLeavesFromJson("test/mockData/scratch/payments_test.json"); 140 | 141 | bytes32[] memory leaves = paymentLeaves.leaves; 142 | uint256 indexToProve = 0; 143 | 144 | bytes32[] memory proof = new bytes32[](2); 145 | proof[0] = leaves[1]; 146 | proof[1] = keccak256(abi.encodePacked(leaves[2], leaves[3])); 147 | 148 | bytes memory proofBytesConstructed = abi.encodePacked(proof); 149 | bytes memory proofBytesCalculated = 150 | SetupDistributionsLib.generateMerkleProof(leaves, indexToProve); 151 | 152 | require( 153 | keccak256(proofBytesConstructed) == keccak256(proofBytesCalculated), 154 | "Proofs do not match" 155 | ); 156 | 157 | bytes32 root = SetupDistributionsLib.merkleizeKeccak(leaves); 158 | 159 | require( 160 | Merkle.verifyInclusionKeccak( 161 | proofBytesCalculated, root, leaves[indexToProve], indexToProve 162 | ) 163 | ); 164 | } 165 | 166 | function testProcessClaim() public { 167 | emit log_named_address("token address", address(mockToken)); 168 | string memory filePath = "testProcessClaim.json"; 169 | 170 | address[] memory earners = new address[](NUM_EARNERS); 171 | for (uint256 i = 0; i < earners.length; i++) { 172 | earners[i] = address(1); 173 | } 174 | uint32 endTimestamp = rewardsCoordinator.currRewardsCalculationEndTimestamp() + 1 weeks; 175 | cheats.warp(endTimestamp + 1); 176 | 177 | bytes32[] memory tokenLeaves = SetupDistributionsLib.createTokenLeaves( 178 | rewardsCoordinator, NUM_TOKEN_EARNINGS, TOKEN_EARNINGS, address(strategy) 179 | ); 180 | IRewardsCoordinator.EarnerTreeMerkleLeaf[] memory earnerLeaves = 181 | SetupDistributionsLib.createEarnerLeaves(earners, tokenLeaves); 182 | 183 | cheats.startPrank(rewardsCoordinator.rewardsUpdater()); 184 | SetupDistributionsLib.submitRoot( 185 | rewardsCoordinator, tokenLeaves, earnerLeaves, endTimestamp, NUM_EARNERS, 1, filePath 186 | ); 187 | cheats.stopPrank(); 188 | 189 | cheats.warp(block.timestamp + 2 weeks); 190 | 191 | cheats.startPrank(earnerLeaves[INDEX_TO_PROVE].earner, earnerLeaves[INDEX_TO_PROVE].earner); 192 | SetupDistributionsLib.processClaim( 193 | rewardsCoordinator, 194 | filePath, 195 | INDEX_TO_PROVE, 196 | RECIPIENT, 197 | earnerLeaves[INDEX_TO_PROVE], 198 | NUM_TOKEN_EARNINGS, 199 | address(strategy), 200 | uint32(TOKEN_EARNINGS) 201 | ); 202 | 203 | cheats.stopPrank(); 204 | 205 | vm.removeFile(filePath); 206 | } 207 | 208 | function testCreateAVSRewardsSubmissions() public { 209 | uint256 numPayments = 5; 210 | uint256 amountPerPayment = 100; 211 | uint32 duration = rewardsCoordinator.MAX_REWARDS_DURATION(); 212 | uint32 genesisTimestamp = rewardsCoordinator.GENESIS_REWARDS_TIMESTAMP(); 213 | uint32 startTimestamp = genesisTimestamp + 10 days; 214 | cheats.warp(startTimestamp + 1); 215 | 216 | cheats.prank(rewardsInitiator); 217 | mockToken.increaseAllowance( 218 | helloWorldDeployment.helloWorldServiceManager, amountPerPayment * numPayments 219 | ); 220 | 221 | cheats.startPrank(rewardsInitiator); 222 | SetupDistributionsLib.createAVSRewardsSubmissions( 223 | address(helloWorldDeployment.helloWorldServiceManager), 224 | address(strategy), 225 | numPayments, 226 | amountPerPayment, 227 | duration, 228 | startTimestamp 229 | ); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /contracts/test/mockData/config/core/1337.json: -------------------------------------------------------------------------------- 1 | { 2 | "strategyManager": { 3 | "initPausedStatus": 0, 4 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 5 | "initialStrategyWhitelister": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 6 | }, 7 | "delegationManager": { 8 | "initPausedStatus": 0, 9 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 10 | "minWithdrawalDelayBlocks": 50400 11 | }, 12 | "eigenPodManager": { 13 | "initPausedStatus": 0, 14 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 15 | }, 16 | "allocationManager": { 17 | "initPausedStatus": 0, 18 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 19 | "deallocationDelay": 0, 20 | "allocationConfigurationDelay": 0 21 | }, 22 | "strategyFactory": { 23 | "initPausedStatus": 0, 24 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 25 | }, 26 | "avsDirectory": { 27 | "initPausedStatus": 0, 28 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 29 | }, 30 | "rewardsCoordinator": { 31 | "initPausedStatus": 0, 32 | "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 33 | "rewardsUpdater": "0x1234567890123456789012345678901234567890", 34 | "activationDelay": 604800, 35 | "defaultSplitBips": 1000, 36 | "calculationIntervalSeconds": 86400, 37 | "maxRewardsDuration": 864000, 38 | "maxRetroactiveLength": 86400, 39 | "maxFutureLength": 86400, 40 | "genesisRewardsTimestamp": 1672531200 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/test/mockData/config/hello-world/1337.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewardsOwner": "0x", 3 | "rewardsInitiator": "0x" 4 | } 5 | -------------------------------------------------------------------------------- /contracts/test/mockData/deployments/core/1337.json: -------------------------------------------------------------------------------- 1 | { 2 | "lastUpdate": { 3 | "timestamp": "1740693626", 4 | "block_number": "0" 5 | }, 6 | "addresses": { 7 | "proxyAdmin": "0x5fbdb2315678afecb367f032d93f642f64180aa3", 8 | "delegationManager": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", 9 | "delegationManagerImpl": "0xa85233c63b9ee964add6f2cffe00fd84eb32338f", 10 | "avsDirectory": "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9", 11 | "avsDirectoryImpl": "0x4a679253410272dd5232b3ff7cf5dbb88f295319", 12 | "strategyManager": "0x0165878a594ca255338adfa4d48449f69242eb8f", 13 | "strategyManagerImpl": "0x4ed7c70f96b99c776995fb64377f0d4ab3b0e1c1", 14 | "eigenPodManager": "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6", 15 | "eigenPodManagerImpl": "0xa82ff9afd8f496c3d6ac40e2a0f282e47488cfc9", 16 | "allocationManager": "0x610178da211fef7d417bc0e6fed39f05609ad788", 17 | "allocationManagerImpl": "0x322813fd9a801c5507c9de605d63cea4f2ce6c44", 18 | "eigenPodBeacon": "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0", 19 | "pauserRegistry": "0x9a676e781a523b5d0c0e43731313a708cb607508", 20 | "pauserRegistryImpl": "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82", 21 | "strategyFactory": "0x959922be3caee4b8cd9a407cc3ac1c251c2007b1", 22 | "strategyFactoryImpl": "0x95401dc811bb5740090279ba06cfa8fcf6113778", 23 | "strategyBeacon": "0xf5059a5d33d5853360d16c683c16e67980206f36", 24 | "rewardsCoordinator": "0x68b1d87f95878fe05b998f19b66f4baba5de1aed", 25 | "rewardsCoordinatorImpl": "0x70e0ba845a1a0f2da3359c97e0285013525ffc49", 26 | "permissionController": "0xc6e7df5e7b4f2a278906862b61205850344d4e7d", 27 | "permissionControllerImpl": "0x59b670e9fa9d0a427751af201d676719a970857b" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/test/mockData/deployments/hello-world/1337.json: -------------------------------------------------------------------------------- 1 | { 2 | "lastUpdate": { 3 | "timestamp": "1740693629", 4 | "block_number": "39" 5 | }, 6 | "addresses": { 7 | "proxyAdmin": "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf", 8 | "helloWorldServiceManager": "0x5eb3bc0a489c5a8288765d2336659ebca68fcd00", 9 | "helloWorldServiceManagerImpl": "0x5f3f1dbd7b74c6b46e8c44f98792a1daf8d69154", 10 | "stakeRegistry": "0x809d550fca64d94bd9f66e60752a544199cfac3d", 11 | "stakeRegistryImpl": "0x4c5859f0f772848b2d91f1d83e2fe57935348029", 12 | "strategy": "0x24b3c7704709ed1491473f30393ffc93cfb0fc34", 13 | "token": "0x99bba657f2bbc93c02d617f8ba121cb8fc104acf" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/test/mockData/scratch/31337.json: -------------------------------------------------------------------------------- 1 | {"lastUpdate":{"timestamp":"1","block_number":"1"},"addresses":{"proxyAdmin":"0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f","delegation":"0xf62849f9a0b5bf2913b396098f7c7019b51a820a","delegationManagerImpl":"0xdb25a7b768311de128bbda7b8426c3f9c74f3240","avsDirectory":"0xc7183455a4c133ae270771860664b6b7ec320bb1","avsDirectoryImpl":"0x3381cd18e2fb4db236bf0525938ab6e43db0440f","strategyManager":"0x1d1499e622d69689cdf9004d05ec547d650ff211","strategyManagerImpl":"0x756e0562323adcda4430d6cb456d9151f605290b","eigenPodManager":"0x03a6a84cd762d9707a21605b548aaab891562aab","eigenPodManagerImpl":"0xe8dc788818033232ef9772cb2e6622f1ec8bc840","strategyFactory":"0x13aa49bac059d709dd0a18d6bb63290076a702d7","strategyFactoryImpl":"0x1af7f588a501ea2b5bb3feefa744892aa2cf00e6","strategyBeacon":"0x886d6d1eb8d415b00052828cd6d5b321f072073d","rewardsCoordinator":"0x15cf58144ef33af1e14b5208015d11f9143e27b9"}} -------------------------------------------------------------------------------- /contracts/test/mockData/scratch/payment_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "paymentInfo": { 3 | "recipient": "0x5555666677778888999900001111222233334444", 4 | "numPayments": 8, 5 | "amountPerPayment": "100000000000", 6 | "duration": 2592000, 7 | "startTimestamp": 864000, 8 | "endTimestamp": 1701907200, 9 | "indexToProve": 0 10 | } 11 | } -------------------------------------------------------------------------------- /contracts/test/mockData/scratch/payments.json: -------------------------------------------------------------------------------- 1 | { 2 | "leaves": [ 3 | "0x356fc063496b2cb93d500f88a326bc90c734137e93310643119b81618f40273c", 4 | "0x356fc063496b2cb93d500f88a326bc90c734137e93310643119b81618f40273c", 5 | "0x356fc063496b2cb93d500f88a326bc90c734137e93310643119b81618f40273c", 6 | "0x356fc063496b2cb93d500f88a326bc90c734137e93310643119b81618f40273c", 7 | "0x356fc063496b2cb93d500f88a326bc90c734137e93310643119b81618f40273c", 8 | "0x356fc063496b2cb93d500f88a326bc90c734137e93310643119b81618f40273c", 9 | "0x356fc063496b2cb93d500f88a326bc90c734137e93310643119b81618f40273c", 10 | "0x356fc063496b2cb93d500f88a326bc90c734137e93310643119b81618f40273c" 11 | ], 12 | "tokenLeaves": [ 13 | "0xb85e3af535d0b85802155d225e67e51112a9404dba7a93c4dcfaca8ae5b9be9e" 14 | ] 15 | } -------------------------------------------------------------------------------- /contracts/test/mockData/scratch/payments_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "leaves": [ 3 | "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", 4 | "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", 5 | "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42", 6 | "0x29036a1d92861ffd464a1e285030fad3978a36f953ae33c160e606d2ac746c42" 7 | ], 8 | "tokenLeaves": [ 9 | "0xf5d87050cb923194fe63c7ed2c45cbc913fa6ecf322f3631149c36d9460b3ad6" 10 | ] 11 | } -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | FAQ About The Repo 2 | 3 | The goal is to be a list of random bespoke things that might not be immediately clear and if using an AI tool to ask questions about the repo can be a source for the AI to surface information to the user 4 | 5 | Typescript Notes: 6 | 7 | - We used ts-node in the repo as a dev dependency and dev commands use ts-node to run the typescript files so that there isn't an intermediate build step that the developer must call in the process to compile the typescript files to javascript and then separately use the output javascript. This allows us to directly run the typescript files which get compiled on the fly. We will still use tsc when creating production builds of our code but during development using ts-node has better UX and is more clear for developing 8 | 9 | Solidity Notes: 10 | 11 | - If you're running into low level errors with transactions being executed by ethers.js ie, code: BAD_DATA or returned with No Data related errors, then one solution to get more verbose errors is to compile and deploy your smart contracts with --revert-strings debug. This will insert verbose revert strings to catch before a low level revert would happen in an anvil instance. 12 | -------------------------------------------------------------------------------- /docs/nix-setup-guide.md: -------------------------------------------------------------------------------- 1 | # Nix Setup Guide 2 | 3 | Here you will find instructions on how to install and configure [nix package manager](https://nixos.wiki/wiki/Nix_package_manager) to work with 4 | projects. 5 | 6 | ## Installing / Upgrading 7 | 8 | If you don't have nix installed, follow the instructions on the 9 | [official website](https://nixos.org/download), or simply use one of following 10 | commands in a terminal: 11 | 12 | - For multi-user installation (i.e., system-wide setup, **recommended**) 13 | ```bash 14 | sh <(curl -L https://nixos.org/nix/install) --daemon 15 | ``` 16 | - For single-user installation (i.e., local setup) 17 | ```bash 18 | sh <(curl -L https://nixos.org/nix/install) --no-daemon 19 | ``` 20 | 21 | We use nix `flakes` which require nix version `>= 2.4`, therefore if you already 22 | have nix installed, make sure to 23 | [upgrade](https://nixos.org/manual/nix/stable/installation/upgrading) to a 24 | recent version. 25 | 26 | ## Becoming a Truster User 27 | 28 | If you are on NixOS or have installed nix in multi-user mode, you **must** be 29 | a [trusted user](https://nixos.org/nix/manual/#ssec-multi-user), which is 30 | necessary to enable access to our binary cache. 31 | 32 | On non-NixOS systems, append the following line to the system-wide configuration 33 | `/etc/nix/nix.conf`: 34 | ```txt 35 | trusted-users = USER root 36 | ``` 37 | **Where `USER` is the result of running `whoami` in a terminal.** 38 | 39 | You can also use a group name by prefixing it with `@`. For instance, to add all 40 | members of the `wheel` group: 41 | ```txt 42 | trusted-users = @wheel root 43 | ``` 44 | 45 | On NixOS, add the user/group name to the list under 46 | [`nix.settings.trusted-users`](https://search.nixos.org/options?show=nix.settings.trusted-users). 47 | 48 | ## Setting Up for Flakes 49 | 50 | We use [Nix flakes](https://nixos.wiki/wiki/Flakes). To configure for flakes, follow the instruction on the [Nixos Wiki](https://nixos.wiki/wiki/Flakes) or simply do one of the following depending on your system: 51 | 52 | - On non-NixOS systems, edit one of the following two files (create them if they don't exist): 53 | - `/etc/nix/nix.conf` for system-wide settings on a multi-user installation 54 | - `~/.config/nix/nix.conf` for user-wide settings on a single-user installation 55 | 56 | By appending the following lines: 57 | ```txt 58 | experimental-features = nix-command flakes 59 | ``` 60 | 61 | On NixOS systems, set the following NixOS options: 62 | ```nix 63 | nix.settings.experimental-features = [ "nix-command" "flakes" ]; 64 | ``` 65 | 66 | ## Notes for Apple Users 67 | 68 | Apple Silicon users can run any Intel binary out-of-the-box thanks to Rosetta 69 | emulation, but when working with nix flakes, the `aarch64-darwin` system will be 70 | selected by default. 71 | 72 | However, some projects at cannot be built natively on `aarch64-darwin`. 73 | 74 | Therefore you must specify the `--system` explicitly to target `x86_64-darwin`. 75 | ```bash 76 | nix (develop|build|run|check) --system x86_64-darwin # Will use the x86_64-darwin derivation 77 | nix (develop|build|run|check) # Will use the aarch64-darwin derivation, if available 78 | ``` 79 | 80 | To enable this, you must append the following lines to your `/etc/nix/nix.conf` 81 | or `~/.config/nix/nix.conf`: 82 | ```txt 83 | extra-platforms = x86_64-darwin aarch64-darwin 84 | extra-sandbox-paths = /System/Library/Frameworks /System/Library/PrivateFrameworks /usr/lib /private/tmp /private/var/tmp /usr/bin/env 85 | ``` 86 | 87 | You may need to reload the nix daemon for changes to take effect: 88 | ```bash 89 | sudo launchctl stop org.nixos.nix-daemon 90 | sudo launchctl start org.nixos.nix-daemon 91 | ``` 92 | 93 | ## Development Shell and VSCode 94 | 95 | Now that you have nix installed and configured, you may enter the development 96 | shell: 97 | ``` 98 | nix develop 99 | ``` 100 | If you are on Apple Silicon and a native shell is not available, you will want 101 | to run this instead: 102 | ``` 103 | nix develop --system x86_64-darwin 104 | ``` 105 | 106 | If you are a `VSCode` user, you may also start your development session by 107 | executing the following command: 108 | ``` 109 | nix develop --command code 110 | ``` 111 | 112 | If you are running `nix develop` for the first time, please enter `y` at the 113 | following prompts to create the local nix settings file 114 | `~/.local/share/nix/trusted-settings.json`: 115 | ```txt 116 | > do you want to allow configuration setting 'accept-flake-config' to be set to 'true' (y/N)? 117 | > do you want to permanently mark this value as trusted (y/N)? 118 | ``` 119 | 120 | It is particularly important to accept the `extra-substituters` and 121 | `extra-trusted-public-keys` settings as these will grant access to our binary 122 | cache. 123 | 124 | When `nix develop` is run for the first time, a significant amount of 125 | dependencies will be downloaded, built and installed. This process may take a 126 | couple of hours but is expected to happen only once. However, if you ever 127 | witness that GHC is also being built from scratch, then it is likely that your 128 | binary cache has not been configured properly or is not being considered. 129 | Accepting the configuration settings as outlined above should be sufficient to 130 | avoid this. Nevertheless, if your caches are still broken, you'll want to review 131 | this document carefully to ensure that your nix installation is properly 132 | configured. 133 | 134 | You will know that your caches are broken if you see this message: 135 | ``` 136 | warning: ignoring untrusted substituter 'https://cache.iog.io', you are not a trusted user. 137 | ``` 138 | 139 | If is possible that your settings are correct but the nix daemon is not properly 140 | considering them. In this case, stop the `nix develop` process and restart the 141 | `nix-daemon` as follows: 142 | ```bash 143 | sudo systemctl stop nix-daemon.service 144 | ``` 145 | Once done, you can launch the `nix develop` process again. 146 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1726560853, 9 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "locked": { 23 | "lastModified": 1644229661, 24 | "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", 25 | "owner": "numtide", 26 | "repo": "flake-utils", 27 | "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "numtide", 32 | "repo": "flake-utils", 33 | "type": "github" 34 | } 35 | }, 36 | "foundry": { 37 | "inputs": { 38 | "flake-utils": "flake-utils_2", 39 | "nixpkgs": "nixpkgs" 40 | }, 41 | "locked": { 42 | "lastModified": 1725354688, 43 | "narHash": "sha256-KHHFemVt6C/hbGoMzIq7cpxmjdp+KZVZaqbvx02aliY=", 44 | "owner": "shazow", 45 | "repo": "foundry.nix", 46 | "rev": "671672bd60a0d2e5f6757638fdf27e806df755a4", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "shazow", 51 | "ref": "monthly", 52 | "repo": "foundry.nix", 53 | "type": "github" 54 | } 55 | }, 56 | "nixpkgs": { 57 | "locked": { 58 | "lastModified": 1666753130, 59 | "narHash": "sha256-Wff1dGPFSneXJLI2c0kkdWTgxnQ416KE6X4KnFkgPYQ=", 60 | "owner": "NixOS", 61 | "repo": "nixpkgs", 62 | "rev": "f540aeda6f677354f1e7144ab04352f61aaa0118", 63 | "type": "github" 64 | }, 65 | "original": { 66 | "id": "nixpkgs", 67 | "type": "indirect" 68 | } 69 | }, 70 | "nixpkgs_2": { 71 | "locked": { 72 | "lastModified": 1726755586, 73 | "narHash": "sha256-PmUr/2GQGvFTIJ6/Tvsins7Q43KTMvMFhvG6oaYK+Wk=", 74 | "owner": "NixOs", 75 | "repo": "nixpkgs", 76 | "rev": "c04d5652cfa9742b1d519688f65d1bbccea9eb7e", 77 | "type": "github" 78 | }, 79 | "original": { 80 | "owner": "NixOs", 81 | "ref": "nixos-unstable", 82 | "repo": "nixpkgs", 83 | "type": "github" 84 | } 85 | }, 86 | "nixpkgs_3": { 87 | "locked": { 88 | "lastModified": 1718428119, 89 | "narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=", 90 | "owner": "NixOS", 91 | "repo": "nixpkgs", 92 | "rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5", 93 | "type": "github" 94 | }, 95 | "original": { 96 | "owner": "NixOS", 97 | "ref": "nixpkgs-unstable", 98 | "repo": "nixpkgs", 99 | "type": "github" 100 | } 101 | }, 102 | "root": { 103 | "inputs": { 104 | "flake-utils": "flake-utils", 105 | "foundry": "foundry", 106 | "nixpkgs": "nixpkgs_2", 107 | "rust-overlay": "rust-overlay" 108 | } 109 | }, 110 | "rust-overlay": { 111 | "inputs": { 112 | "nixpkgs": "nixpkgs_3" 113 | }, 114 | "locked": { 115 | "lastModified": 1726972233, 116 | "narHash": "sha256-FlL/bNESOtDQoczRhmPfReNAmLqVg+dAX4HectPOOf0=", 117 | "owner": "oxalica", 118 | "repo": "rust-overlay", 119 | "rev": "36d73192555e569d27579f6c486fea3ab768823c", 120 | "type": "github" 121 | }, 122 | "original": { 123 | "owner": "oxalica", 124 | "repo": "rust-overlay", 125 | "type": "github" 126 | } 127 | }, 128 | "systems": { 129 | "locked": { 130 | "lastModified": 1681028828, 131 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 132 | "owner": "nix-systems", 133 | "repo": "default", 134 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 135 | "type": "github" 136 | }, 137 | "original": { 138 | "owner": "nix-systems", 139 | "repo": "default", 140 | "type": "github" 141 | } 142 | } 143 | }, 144 | "root": "root", 145 | "version": 7 146 | } 147 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "ethereum-rs project"; 3 | inputs = { 4 | nixpkgs.url = "github:NixOs/nixpkgs/nixos-unstable"; 5 | rust-overlay.url = "github:oxalica/rust-overlay"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | foundry.url = "github:shazow/foundry.nix/monthly"; # Use monthly branch for permanent releases 8 | 9 | }; 10 | outputs = { self, nixpkgs, rust-overlay, flake-utils, foundry, ... }@inputs: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | pkgs = import nixpkgs { 14 | inherit system; 15 | overlays = [ rust-overlay.overlays.default foundry.overlay ]; 16 | }; 17 | 18 | toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 19 | cargoTomlContents = builtins.readFile ./Cargo.toml; 20 | version = (builtins.fromTOML cargoTomlContents).package.version; 21 | 22 | ethereumEs = pkgs.rustPlatform.buildRustPackage { 23 | inherit version; 24 | name = "ethereumEs"; 25 | buildInputs = with pkgs; [ openssl ]; 26 | nativeBuildInputs = with pkgs; [ pkg-config openssl.dev ]; 27 | 28 | src = pkgs.lib.cleanSourceWith { src = self; }; 29 | 30 | cargoLock.lockFile = ./Cargo.lock; 31 | 32 | }; 33 | in { 34 | 35 | overlays.default = final: prev: { ethereumEs = ethereumEs; }; 36 | 37 | gitRev = if (builtins.hasAttr "rev" self) then self.rev else "dirty"; 38 | 39 | devShells.default = pkgs.mkShell { 40 | buildInputs = with pkgs; [ 41 | foundry-bin 42 | solc 43 | toolchain 44 | openssl 45 | cargo-insta 46 | pkg-config 47 | eza 48 | rust-analyzer-unwrapped 49 | nodejs_20 50 | nodePackages.typescript 51 | nodePackages.typescript-language-server 52 | watchexec 53 | ]; 54 | shellHook = '' 55 | ## for the IDE to access rust crates source code 56 | export RUST_SRC_PATH="${toolchain}/lib/rustlib/src/rust/library" 57 | 58 | ## do not pollute the global cargo repository 59 | export CARGO_HOME="$(pwd)/.cargo" 60 | export PATH="$CARGO_HOME/bin:$PATH" 61 | 62 | ''; 63 | }; 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testMatch: ['**/*.test.ts'], 7 | verbose: true, 8 | testTimeout: 60000, // 60 seconds 9 | maxWorkers: 1, // Run tests sequentially 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /operator/createNewTasks.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import * as dotenv from "dotenv"; 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | dotenv.config(); 6 | 7 | // Setup env variables 8 | const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); 9 | const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); 10 | /// TODO: Hack 11 | let chainId = 31337; 12 | 13 | const avsDeploymentData = JSON.parse(fs.readFileSync(path.resolve(__dirname, `../contracts/deployments/hello-world/${chainId}.json`), 'utf8')); 14 | const helloWorldServiceManagerAddress = avsDeploymentData.addresses.helloWorldServiceManager; 15 | const helloWorldServiceManagerABI = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../abis/HelloWorldServiceManager.json'), 'utf8')); 16 | // Initialize contract objects from ABIs 17 | const helloWorldServiceManager = new ethers.Contract(helloWorldServiceManagerAddress, helloWorldServiceManagerABI, wallet); 18 | 19 | 20 | // Function to generate random names 21 | function generateRandomName(): string { 22 | const adjectives = ['Quick', 'Lazy', 'Sleepy', 'Noisy', 'Hungry']; 23 | const nouns = ['Fox', 'Dog', 'Cat', 'Mouse', 'Bear']; 24 | const adjective = adjectives[Math.floor(Math.random() * adjectives.length)]; 25 | const noun = nouns[Math.floor(Math.random() * nouns.length)]; 26 | const randomName = `${adjective}${noun}${Math.floor(Math.random() * 1000)}`; 27 | return randomName; 28 | } 29 | 30 | async function createNewTask(taskName: string) { 31 | try { 32 | // Send a transaction to the createNewTask function 33 | const tx = await helloWorldServiceManager.createNewTask(taskName); 34 | 35 | // Wait for the transaction to be mined 36 | const receipt = await tx.wait(); 37 | 38 | console.log(`Transaction successful with hash: ${receipt.hash}`); 39 | } catch (error) { 40 | console.error('Error sending transaction:', error); 41 | } 42 | } 43 | 44 | // Function to create a new task with a random name every 15 seconds 45 | function startCreatingTasks() { 46 | setInterval(() => { 47 | const randomName = generateRandomName(); 48 | console.log(`Creating new task with name: ${randomName}`); 49 | createNewTask(randomName); 50 | }, 24000); 51 | } 52 | 53 | // Start the process 54 | startCreatingTasks(); -------------------------------------------------------------------------------- /operator/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { createAnvil, Anvil } from "@viem/anvil"; 2 | import { describe, beforeAll, afterAll, it, expect } from '@jest/globals'; 3 | import { exec } from 'child_process'; 4 | import fs from 'fs/promises'; 5 | import path from 'path'; 6 | import util from 'util'; 7 | import { ethers } from "ethers"; 8 | import * as dotenv from "dotenv"; 9 | 10 | dotenv.config(); 11 | 12 | const execAsync = util.promisify(exec); 13 | 14 | async function loadJsonFile(filePath: string): Promise { 15 | try { 16 | const content = await fs.readFile(filePath, 'utf-8'); 17 | return JSON.parse(content); 18 | } catch (error) { 19 | console.error(`Error loading file ${filePath}:`, error); 20 | return null; 21 | } 22 | } 23 | 24 | async function loadDeployments(): Promise> { 25 | const coreFilePath = path.join(__dirname, '..', 'contracts', 'deployments', 'core', '31337.json'); 26 | const helloWorldFilePath = path.join(__dirname, '..', 'contracts', 'deployments', 'hello-world', '31337.json'); 27 | 28 | const [coreDeployment, helloWorldDeployment] = await Promise.all([ 29 | loadJsonFile(coreFilePath), 30 | loadJsonFile(helloWorldFilePath) 31 | ]); 32 | 33 | if (!coreDeployment || !helloWorldDeployment) { 34 | console.error('Error loading deployments'); 35 | return {}; 36 | } 37 | 38 | return { 39 | core: coreDeployment, 40 | helloWorld: helloWorldDeployment 41 | }; 42 | } 43 | 44 | describe('Operator Functionality', () => { 45 | let anvil: Anvil; 46 | let deployment: Record; 47 | let provider: ethers.JsonRpcProvider; 48 | let signer: ethers.Wallet; 49 | let delegationManager: ethers.Contract; 50 | let helloWorldServiceManager: ethers.Contract; 51 | let ecdsaRegistryContract: ethers.Contract; 52 | let avsDirectory: ethers.Contract; 53 | 54 | beforeAll(async () => { 55 | anvil = createAnvil(); 56 | await anvil.start(); 57 | await execAsync('npm run deploy:core'); 58 | await execAsync('npm run deploy:hello-world'); 59 | deployment = await loadDeployments(); 60 | 61 | provider = new ethers.JsonRpcProvider(process.env.RPC_URL); 62 | signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); 63 | 64 | const delegationManagerABI = await loadJsonFile(path.join(__dirname, '..', 'abis', 'IDelegationManager.json')); 65 | const ecdsaRegistryABI = await loadJsonFile(path.join(__dirname, '..', 'abis', 'ECDSAStakeRegistry.json')); 66 | const helloWorldServiceManagerABI = await loadJsonFile(path.join(__dirname, '..', 'abis', 'HelloWorldServiceManager.json')); 67 | const avsDirectoryABI = await loadJsonFile(path.join(__dirname, '..', 'abis', 'IAVSDirectory.json')); 68 | 69 | delegationManager = new ethers.Contract(deployment.core.addresses.delegationManager, delegationManagerABI, signer); 70 | helloWorldServiceManager = new ethers.Contract(deployment.helloWorld.addresses.helloWorldServiceManager, helloWorldServiceManagerABI, signer); 71 | ecdsaRegistryContract = new ethers.Contract(deployment.helloWorld.addresses.stakeRegistry, ecdsaRegistryABI, signer); 72 | avsDirectory = new ethers.Contract(deployment.core.addresses.avsDirectory, avsDirectoryABI, signer); 73 | }); 74 | 75 | it('should register as an operator', async () => { 76 | const tx = await delegationManager.registerAsOperator( 77 | "0x0000000000000000000000000000000000000000", 78 | 0, 79 | "" 80 | ); 81 | await tx.wait(); 82 | 83 | const isOperator = await delegationManager.isOperator(signer.address); 84 | expect(isOperator).toBe(true); 85 | }); 86 | 87 | it('should register operator to AVS', async () => { 88 | const salt = ethers.hexlify(ethers.randomBytes(32)); 89 | const expiry = Math.floor(Date.now() / 1000) + 3600; 90 | 91 | const operatorDigestHash = await avsDirectory.calculateOperatorAVSRegistrationDigestHash( 92 | signer.address, 93 | await helloWorldServiceManager.getAddress(), 94 | salt, 95 | expiry 96 | ); 97 | 98 | const operatorSigningKey = new ethers.SigningKey(process.env.PRIVATE_KEY!); 99 | const operatorSignedDigestHash = operatorSigningKey.sign(operatorDigestHash); 100 | const operatorSignature = ethers.Signature.from(operatorSignedDigestHash).serialized; 101 | 102 | const tx = await ecdsaRegistryContract.registerOperatorWithSignature( 103 | { 104 | signature: operatorSignature, 105 | salt: salt, 106 | expiry: expiry 107 | }, 108 | signer.address 109 | ); 110 | await tx.wait(); 111 | 112 | const isRegistered = await ecdsaRegistryContract.operatorRegistered(signer.address); 113 | expect(isRegistered).toBe(true); 114 | }); 115 | 116 | it('should create a new task', async () => { 117 | const taskName = "Steven"; 118 | 119 | const tx = await helloWorldServiceManager.createNewTask(taskName); 120 | await tx.wait(); 121 | }); 122 | 123 | it('should sign and respond to a task', async () => { 124 | const taskIndex = 0; 125 | const taskCreatedBlock = await provider.getBlockNumber(); 126 | const taskName = "Steven"; 127 | const message = `Hello, ${taskName}`; 128 | const messageHash = ethers.solidityPackedKeccak256(["string"], [message]); 129 | const messageBytes = ethers.getBytes(messageHash); 130 | const signature = await signer.signMessage(messageBytes); 131 | 132 | const operators = [await signer.getAddress()]; 133 | const signatures = [signature]; 134 | const signedTask = ethers.AbiCoder.defaultAbiCoder().encode( 135 | ["address[]", "bytes[]", "uint32"], 136 | [operators, signatures, ethers.toBigInt(taskCreatedBlock)] 137 | ); 138 | 139 | const tx = await helloWorldServiceManager.respondToTask( 140 | { name: taskName, taskCreatedBlock: taskCreatedBlock }, 141 | taskIndex, 142 | signedTask 143 | ); 144 | await tx.wait(); 145 | }); 146 | 147 | afterAll(async () => { 148 | await anvil.stop(); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /operator/index.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import * as dotenv from "dotenv"; 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | dotenv.config(); 6 | 7 | // Check if the process.env object is empty 8 | if (!Object.keys(process.env).length) { 9 | throw new Error("process.env object is empty"); 10 | } 11 | 12 | // Setup env variables 13 | const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); 14 | const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); 15 | /// TODO: Hack 16 | let chainId = 31337; 17 | 18 | const avsDeploymentData = JSON.parse(fs.readFileSync(path.resolve(__dirname, `../contracts/deployments/hello-world/${chainId}.json`), 'utf8')); 19 | // Load core deployment data 20 | const coreDeploymentData = JSON.parse(fs.readFileSync(path.resolve(__dirname, `../contracts/deployments/core/${chainId}.json`), 'utf8')); 21 | 22 | 23 | const delegationManagerAddress = coreDeploymentData.addresses.delegationManager; // todo: reminder to fix the naming of this contract in the deployment file, change to delegationManager 24 | const avsDirectoryAddress = coreDeploymentData.addresses.avsDirectory; 25 | const helloWorldServiceManagerAddress = avsDeploymentData.addresses.helloWorldServiceManager; 26 | const ecdsaStakeRegistryAddress = avsDeploymentData.addresses.stakeRegistry; 27 | 28 | 29 | 30 | // Load ABIs 31 | const delegationManagerABI = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../abis/IDelegationManager.json'), 'utf8')); 32 | const ecdsaRegistryABI = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../abis/ECDSAStakeRegistry.json'), 'utf8')); 33 | const helloWorldServiceManagerABI = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../abis/HelloWorldServiceManager.json'), 'utf8')); 34 | const avsDirectoryABI = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../abis/IAVSDirectory.json'), 'utf8')); 35 | 36 | // Initialize contract objects from ABIs 37 | const delegationManager = new ethers.Contract(delegationManagerAddress, delegationManagerABI, wallet); 38 | const helloWorldServiceManager = new ethers.Contract(helloWorldServiceManagerAddress, helloWorldServiceManagerABI, wallet); 39 | const ecdsaRegistryContract = new ethers.Contract(ecdsaStakeRegistryAddress, ecdsaRegistryABI, wallet); 40 | const avsDirectory = new ethers.Contract(avsDirectoryAddress, avsDirectoryABI, wallet); 41 | 42 | 43 | const signAndRespondToTask = async (taskIndex: number, taskCreatedBlock: number, taskName: string) => { 44 | const message = `Hello, ${taskName}`; 45 | const messageHash = ethers.solidityPackedKeccak256(["string"], [message]); 46 | const messageBytes = ethers.getBytes(messageHash); 47 | const signature = await wallet.signMessage(messageBytes); 48 | 49 | console.log(`Signing and responding to task ${taskIndex}`); 50 | 51 | const operators = [await wallet.getAddress()]; 52 | const signatures = [signature]; 53 | const signedTask = ethers.AbiCoder.defaultAbiCoder().encode( 54 | ["address[]", "bytes[]", "uint32"], 55 | [operators, signatures, taskCreatedBlock] 56 | ); 57 | 58 | const tx = await helloWorldServiceManager.respondToTask( 59 | { name: taskName, taskCreatedBlock: taskCreatedBlock }, 60 | taskIndex, 61 | signedTask 62 | ); 63 | await tx.wait(); 64 | console.log(`Responded to task.`); 65 | }; 66 | 67 | const registerOperator = async () => { 68 | 69 | // Registers as an Operator in EigenLayer. 70 | try { 71 | const tx1 = await delegationManager.registerAsOperator( 72 | "0x0000000000000000000000000000000000000000", // initDelegationApprover 73 | 0, // allocationDelay 74 | "", // metadataURI 75 | ); 76 | await tx1.wait(); 77 | console.log("Operator registered to Core EigenLayer contracts"); 78 | } catch (error) { 79 | console.error("Error in registering as operator:", error); 80 | } 81 | 82 | const salt = ethers.hexlify(ethers.randomBytes(32)); 83 | const expiry = Math.floor(Date.now() / 1000) + 3600; // Example expiry, 1 hour from now 84 | 85 | // Define the output structure 86 | let operatorSignatureWithSaltAndExpiry = { 87 | signature: "", 88 | salt: salt, 89 | expiry: expiry 90 | }; 91 | 92 | // Calculate the digest hash, which is a unique value representing the operator, avs, unique value (salt) and expiration date. 93 | const operatorDigestHash = await avsDirectory.calculateOperatorAVSRegistrationDigestHash( 94 | wallet.address, 95 | await helloWorldServiceManager.getAddress(), 96 | salt, 97 | expiry 98 | ); 99 | console.log(operatorDigestHash); 100 | 101 | // Sign the digest hash with the operator's private key 102 | console.log("Signing digest hash with operator's private key"); 103 | const operatorSigningKey = new ethers.SigningKey(process.env.PRIVATE_KEY!); 104 | const operatorSignedDigestHash = operatorSigningKey.sign(operatorDigestHash); 105 | 106 | // Encode the signature in the required format 107 | operatorSignatureWithSaltAndExpiry.signature = ethers.Signature.from(operatorSignedDigestHash).serialized; 108 | 109 | console.log("Registering Operator to AVS Registry contract"); 110 | 111 | 112 | // Register Operator to AVS 113 | // Per release here: https://github.com/Layr-Labs/eigenlayer-middleware/blob/v0.2.1-mainnet-rewards/src/unaudited/ECDSAStakeRegistry.sol#L49 114 | const tx2 = await ecdsaRegistryContract.registerOperatorWithSignature( 115 | operatorSignatureWithSaltAndExpiry, 116 | wallet.address 117 | ); 118 | await tx2.wait(); 119 | console.log("Operator registered on AVS successfully"); 120 | }; 121 | 122 | const monitorNewTasks = async () => { 123 | //console.log(`Creating new task "EigenWorld"`); 124 | //await helloWorldServiceManager.createNewTask("EigenWorld"); 125 | 126 | helloWorldServiceManager.on("NewTaskCreated", async (taskIndex: number, task: any) => { 127 | console.log(`New task detected: Hello, ${task.name}`); 128 | await signAndRespondToTask(taskIndex, task.taskCreatedBlock, task.name); 129 | }); 130 | 131 | console.log("Monitoring for new tasks..."); 132 | }; 133 | 134 | const main = async () => { 135 | await registerOperator(); 136 | monitorNewTasks().catch((error) => { 137 | console.error("Error monitoring tasks:", error); 138 | }); 139 | }; 140 | 141 | main().catch((error) => { 142 | console.error("Error in main function:", error); 143 | }); 144 | -------------------------------------------------------------------------------- /operator/rust/crates/operator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-world-avs-operator" 3 | description = "Hello world avs operator start and spam utilities" 4 | 5 | version.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | repository.workspace = true 9 | license-file.workspace = true 10 | 11 | [dependencies] 12 | alloy.workspace = true 13 | serde_json = "1.0.121" 14 | num-bigint = "0.4.4" 15 | reqwest = "0.12.9" 16 | hex = "0.4.3" 17 | futures = "0.3.30" 18 | serde = "1.0.214" 19 | testcontainers = "0.23" 20 | 21 | #eigensdk-rs 22 | eigensdk.workspace = true 23 | 24 | hello-world-utils.workspace = true 25 | 26 | #misc 27 | dotenv = "0.15.0" 28 | rand = "0.9" 29 | chrono = "0.4.38" 30 | tracing = "0.1.40" 31 | futures-util = "0.3" 32 | eyre = "0.6.12" 33 | #tokio 34 | tokio = { workspace = true, features = ["full"] } 35 | [lints] 36 | workspace = true 37 | 38 | 39 | [dev-dependencies] 40 | serial_test = "3.1.1" 41 | 42 | [[bin]] 43 | name = "start_operator" 44 | path = "src/start_operator.rs" 45 | 46 | 47 | [[bin]] 48 | name = "spam_tasks" 49 | path = "src/spam_tasks.rs" 50 | 51 | [[bin]] 52 | name = "challenger" 53 | path = "src/challenger.rs" 54 | -------------------------------------------------------------------------------- /operator/rust/crates/operator/src/anvil.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use testcontainers::{ 3 | core::{IntoContainerPort, WaitFor}, 4 | runners::AsyncRunner, 5 | ContainerAsync, GenericImage, ImageExt, 6 | }; 7 | const ANVIL_IMAGE: &str = "ghcr.io/foundry-rs/foundry"; 8 | const ANVIL_TAG: &str = "latest"; 9 | const ANVIL_STATE_PATH: &str = "contracts/anvil/state.json"; 10 | 11 | fn workspace_dir() -> PathBuf { 12 | let output = std::process::Command::new(env!("CARGO")) 13 | .arg("locate-project") 14 | .arg("--workspace") 15 | .arg("--message-format=plain") 16 | .output() 17 | .unwrap() 18 | .stdout; 19 | let cargo_path = Path::new(std::str::from_utf8(&output).unwrap().trim()); 20 | cargo_path.parent().unwrap().to_path_buf() 21 | } 22 | 23 | /// Start an anvil container for testing, using the dump state file `ANVIL_STATE_PATH` 24 | pub async fn start_anvil_container() -> (ContainerAsync, String, String) { 25 | let relative_path = PathBuf::from(ANVIL_STATE_PATH); 26 | let absolute_path = workspace_dir().join(relative_path); 27 | let absolute_path_str = absolute_path.to_str().unwrap(); 28 | 29 | let container = GenericImage::new(ANVIL_IMAGE, ANVIL_TAG) 30 | .with_entrypoint("anvil") 31 | .with_wait_for(WaitFor::message_on_stdout("Listening on")) 32 | .with_exposed_port(8545.tcp()) 33 | .with_mount(testcontainers::core::Mount::bind_mount( 34 | absolute_path_str, 35 | "/state.json", 36 | )) 37 | .with_cmd([ 38 | "--host", 39 | "0.0.0.0", 40 | "--load-state", 41 | "/state.json", 42 | "--base-fee", 43 | "0", 44 | "--gas-price", 45 | "0", 46 | "--port", 47 | "8545", 48 | ]) 49 | .start() 50 | .await 51 | .unwrap(); 52 | 53 | let port = container 54 | .ports() 55 | .await 56 | .unwrap() 57 | .map_to_host_port_ipv4(8545.tcp()) 58 | .unwrap(); 59 | 60 | let http_endpoint = format!("http://localhost:{port}"); 61 | let ws_endpoint = format!("ws://localhost:{port}"); 62 | 63 | (container, http_endpoint, ws_endpoint) 64 | } 65 | -------------------------------------------------------------------------------- /operator/rust/crates/operator/src/challenger.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | use std::{collections::HashMap, env, str::FromStr, sync::LazyLock}; 3 | 4 | use alloy::{ 5 | eips::BlockNumberOrTag, 6 | primitives::Address, 7 | providers::Provider, 8 | rpc::types::{Filter, Log}, 9 | signers::local::PrivateKeySigner, 10 | sol_types::SolEvent, 11 | }; 12 | use dotenv::dotenv; 13 | use eigensdk::{ 14 | common::{get_provider, get_signer, get_ws_provider}, 15 | logging::{get_logger, init_logger, log_level::LogLevel}, 16 | }; 17 | use eyre::{Ok, Result}; 18 | use futures::StreamExt; 19 | use hello_world_utils::{ 20 | get_hello_world_service_manager, 21 | helloworldservicemanager::{ 22 | HelloWorldServiceManager::{self}, 23 | IHelloWorldServiceManager::Task, 24 | }, 25 | }; 26 | use tokio::signal::{self}; 27 | 28 | static RPC_URL: LazyLock = 29 | LazyLock::new(|| env::var("RPC_URL").expect("failed to retrieve RPC URL")); 30 | 31 | static WS_URL: LazyLock = 32 | LazyLock::new(|| env::var("WS_URL").expect("failed to retrieve WS URL")); 33 | 34 | static KEY: LazyLock = 35 | LazyLock::new(|| env::var("PRIVATE_KEY").expect("failed to retrieve private key")); 36 | 37 | /// Challenger struct 38 | #[derive(Debug)] 39 | pub struct Challenger { 40 | service_manager_address: Address, 41 | rpc_url: String, 42 | ws_url: String, 43 | tasks: HashMap, 44 | max_response_interval_blocks: u32, 45 | operator_address: Address, 46 | } 47 | 48 | /// Challenger implementation 49 | impl Challenger { 50 | /// Create a new challenger 51 | pub async fn new(rpc_url: String, ws_url: String, private_key: String) -> Result { 52 | let signer = PrivateKeySigner::from_str(&private_key)?; 53 | let operator_address = signer.address(); 54 | 55 | let service_manager_address = get_hello_world_service_manager().unwrap(); 56 | 57 | let pr = get_provider(&rpc_url); 58 | let service_manager_contract = HelloWorldServiceManager::new(service_manager_address, &pr); 59 | let max_response_interval_blocks = service_manager_contract 60 | .MAX_RESPONSE_INTERVAL_BLOCKS() 61 | .call() 62 | .await? 63 | ._0; 64 | 65 | Ok(Self { 66 | service_manager_address, 67 | rpc_url, 68 | ws_url, 69 | tasks: HashMap::new(), 70 | max_response_interval_blocks, 71 | operator_address, 72 | }) 73 | } 74 | 75 | pub async fn start_challenger(&mut self) -> Result<()> { 76 | get_logger().info("Challenger started: monitoring tasks", ""); 77 | 78 | let ws_provider = get_ws_provider(&self.ws_url).await?; 79 | 80 | // Subscribe to NewTaskCreated events 81 | let new_task_filter = Filter::new() 82 | .address(self.service_manager_address) 83 | .event_signature(HelloWorldServiceManager::NewTaskCreated::SIGNATURE_HASH) 84 | .from_block(BlockNumberOrTag::Latest); 85 | let mut new_task_stream = ws_provider 86 | .subscribe_logs(&new_task_filter) 87 | .await? 88 | .into_stream(); 89 | 90 | // Subscribe to TaskResponded events 91 | let responded_filter = Filter::new() 92 | .address(self.service_manager_address) 93 | .event_signature(HelloWorldServiceManager::TaskResponded::SIGNATURE_HASH) 94 | .from_block(BlockNumberOrTag::Latest); 95 | let mut responded_stream = ws_provider 96 | .subscribe_logs(&responded_filter) 97 | .await? 98 | .into_stream(); 99 | 100 | // Subscribe to new block events 101 | let mut block_stream = ws_provider.subscribe_blocks().await?.into_stream(); 102 | 103 | loop { 104 | tokio::select! { 105 | Some(log) = new_task_stream.next() => { 106 | let decode = log.log_decode::().ok(); 107 | if let Some(decoded) = decode { 108 | self.handle_task_creation(decoded); 109 | } 110 | }, 111 | Some(log) = responded_stream.next() => { 112 | let decode = log.log_decode::().ok(); 113 | if let Some(decoded) = decode { 114 | self.handle_task_response(decoded); 115 | } 116 | }, 117 | Some(block) = block_stream.next() => { 118 | self.check_tasks_timeout(block.number).await?; 119 | }, 120 | _ = signal::ctrl_c() => { 121 | get_logger().info("Received Ctrl+C, shutting down...", ""); 122 | break; 123 | } 124 | } 125 | } 126 | 127 | Ok(()) 128 | } 129 | 130 | /// Handle a new task creation and handle the time that an operator has to respond to the task 131 | fn handle_task_creation(&mut self, decoded: Log) { 132 | let event = decoded.data().clone(); 133 | let task_index = event.taskIndex; 134 | get_logger().info( 135 | &format!( 136 | "New task received: {} at block {}", 137 | task_index, event.task.taskCreatedBlock 138 | ), 139 | "", 140 | ); 141 | 142 | // Save the task and create a cancellation channel 143 | self.tasks.insert(task_index, event.task.clone()); 144 | } 145 | 146 | /// Handle a task response and cancel the timeout timer 147 | fn handle_task_response(&mut self, decoded: Log) { 148 | let event = decoded.data(); 149 | let task_index = event.taskIndex; 150 | 151 | get_logger().info(&format!("Task {} responded", task_index), ""); 152 | self.tasks.remove(&task_index); 153 | } 154 | 155 | /// Handle the expiration of a task and slash the operator if so. 156 | /// This function is called when a new block is received, iterates over all tasks and checks if they have expired 157 | /// If so, it slashes the operator and removes the task from the list. 158 | async fn check_tasks_timeout(&mut self, current_block: u64) -> Result<()> { 159 | for (task_index, task) in self.tasks.clone() { 160 | let task_created_block = task.taskCreatedBlock; 161 | let expiration_block = (task_created_block + self.max_response_interval_blocks) as u64; 162 | 163 | if current_block > expiration_block { 164 | get_logger().info( 165 | &format!( 166 | "Task {} expired at {}, operator didn't respond", 167 | task_index, expiration_block 168 | ), 169 | "", 170 | ); 171 | self.slash_operator(task.clone(), task_index).await?; 172 | self.tasks.remove(&task_index); 173 | } 174 | } 175 | 176 | Ok(()) 177 | } 178 | 179 | /// Execute the slashing of an operator 180 | async fn slash_operator(&self, task: Task, task_index: u32) -> Result<()> { 181 | let pr = get_signer(&KEY.to_string(), &self.rpc_url); 182 | let hello_world_contract = HelloWorldServiceManager::new(self.service_manager_address, &pr); 183 | 184 | get_logger().info( 185 | &format!( 186 | "Slashing operator {} in task {}", 187 | self.operator_address, task_index 188 | ), 189 | "", 190 | ); 191 | 192 | let tx_result = hello_world_contract 193 | .slashOperator(task, task_index, self.operator_address) 194 | .send() 195 | .await?; 196 | 197 | get_logger().info( 198 | &format!("Slashing transaction sent: {}", tx_result.tx_hash()), 199 | "", 200 | ); 201 | 202 | Ok(()) 203 | } 204 | } 205 | 206 | #[tokio::main] 207 | pub async fn main() -> Result<()> { 208 | dotenv().ok(); 209 | init_logger(LogLevel::Info); 210 | 211 | let mut challenger = Challenger::new(RPC_URL.to_string(), WS_URL.to_string(), KEY.to_string()) 212 | .await 213 | .unwrap(); 214 | 215 | challenger.start_challenger().await?; 216 | 217 | Ok(()) 218 | } 219 | -------------------------------------------------------------------------------- /operator/rust/crates/operator/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Start creating tasks and respond appropriately 2 | //! testing utils 3 | /// Challenger struct for monitoring task completions and performing slashing 4 | pub mod challenger; 5 | /// Create createNewTask at regular intervals with random task names 6 | pub mod spam_tasks; 7 | /// Register Operator and monitor for NewTaskCreated event 8 | pub mod start_operator; 9 | 10 | /// Anvil container for testing 11 | #[cfg(test)] 12 | pub mod anvil; 13 | 14 | #[cfg(test)] 15 | mod tests { 16 | use crate::anvil::start_anvil_container; 17 | use crate::spam_tasks::create_new_task; 18 | use crate::start_operator::register_operator; 19 | 20 | use alloy::network::EthereumWallet; 21 | use alloy::primitives::Address; 22 | use alloy::providers::ProviderBuilder; 23 | use alloy::signers::local::PrivateKeySigner; 24 | use dotenv::dotenv; 25 | use eigensdk::common::get_provider; 26 | use eigensdk::logging::init_logger; 27 | 28 | use eigensdk::utils::slashing::core::delegationmanager::DelegationManager; 29 | use hello_world_utils::helloworldservicemanager::HelloWorldServiceManager::{ 30 | self, latestTaskNumReturn, 31 | }; 32 | use hello_world_utils::{ 33 | get_anvil_eigenlayer_deployment_data, get_anvil_hello_world_deployment_data, 34 | }; 35 | use reqwest::Url; 36 | use serial_test::serial; 37 | use std::env; 38 | use std::str::FromStr; 39 | use std::sync::LazyLock; 40 | 41 | static KEY: LazyLock = 42 | LazyLock::new(|| env::var("PRIVATE_KEY").expect("failed to retrieve private key")); 43 | 44 | #[tokio::test] 45 | #[serial] 46 | async fn test_register_operator() { 47 | let (_container, anvil_http, _) = start_anvil_container().await; 48 | 49 | dotenv().ok(); 50 | init_logger(eigensdk::logging::log_level::LogLevel::Info); 51 | let private_key = &KEY.clone(); 52 | register_operator(&anvil_http, private_key).await.unwrap(); 53 | 54 | let signer = PrivateKeySigner::from_str(private_key).unwrap(); 55 | let wallet = EthereumWallet::from(signer.clone()); 56 | let pr = ProviderBuilder::new() 57 | .wallet(wallet) 58 | .on_http(Url::from_str(&anvil_http).unwrap()); 59 | let el_data = get_anvil_eigenlayer_deployment_data().unwrap(); 60 | let delegation_manager_address: Address = 61 | el_data.addresses.delegation_manager.parse().unwrap(); 62 | let contract_delegation_manager = DelegationManager::new(delegation_manager_address, &pr); 63 | 64 | let is_operator = contract_delegation_manager 65 | .isOperator(signer.address()) 66 | .call() 67 | .await 68 | .unwrap() 69 | ._0; 70 | 71 | assert!(is_operator); 72 | } 73 | 74 | #[tokio::test] 75 | #[serial] 76 | async fn test_spam_tasks() { 77 | let (_container, anvil_http, _) = start_anvil_container().await; 78 | 79 | dotenv().ok(); 80 | init_logger(eigensdk::logging::log_level::LogLevel::Info); 81 | 82 | let hw_data = get_anvil_hello_world_deployment_data().unwrap(); 83 | let hello_world_contract_address: Address = hw_data 84 | .addresses 85 | .hello_world_service_manager 86 | .parse() 87 | .unwrap(); 88 | let provider = &get_provider(&anvil_http); 89 | let hello_world_contract = 90 | HelloWorldServiceManager::new(hello_world_contract_address, provider); 91 | 92 | let latest_task_num = hello_world_contract.latestTaskNum().call().await.unwrap(); 93 | 94 | let latestTaskNumReturn { _0: task_num } = latest_task_num; 95 | let _ = create_new_task(&anvil_http, "HelloEigen").await; 96 | 97 | let latest_task_num_after_creating_task = 98 | hello_world_contract.latestTaskNum().call().await.unwrap(); 99 | let latestTaskNumReturn { 100 | _0: task_num_after_task, 101 | } = latest_task_num_after_creating_task; 102 | 103 | assert_eq!(task_num + 1, task_num_after_task); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /operator/rust/crates/operator/src/spam_tasks.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | use alloy::primitives::Address; 3 | use dotenv::dotenv; 4 | use eigensdk::common::get_signer; 5 | use eigensdk::logging::{get_logger, init_logger, log_level::LogLevel}; 6 | use eyre::Result; 7 | use hello_world_utils::get_anvil_hello_world_deployment_data; 8 | use hello_world_utils::helloworldservicemanager::HelloWorldServiceManager; 9 | use rand::Rng; 10 | use std::env; 11 | use std::sync::LazyLock; 12 | use tokio::time::{self, Duration}; 13 | 14 | static RPC_URL: LazyLock = 15 | LazyLock::new(|| env::var("RPC_URL").expect("failed to retrieve RPC URL")); 16 | 17 | static KEY: LazyLock = 18 | LazyLock::new(|| env::var("PRIVATE_KEY").expect("failed to retrieve private key")); 19 | 20 | /// Generate random task names from the given adjectives and nouns 21 | fn generate_random_name() -> String { 22 | let adjectives = ["Quick", "Lazy", "Sleepy", "Noisy", "Hungry"]; 23 | let nouns = ["Fox", "Dog", "Cat", "Mouse", "Bear"]; 24 | 25 | let mut rng = rand::rng(); 26 | 27 | let adjective = adjectives[rng.random_range(0..adjectives.len())]; 28 | let noun = nouns[rng.random_range(0..nouns.len())]; 29 | let number: u16 = rng.random_range(0..1000); 30 | 31 | format!("{}{}{}", adjective, noun, number) 32 | } 33 | 34 | /// Calls CreateNewTask function of the Hello world service manager contract 35 | pub async fn create_new_task(rpc_url: &str, task_name: &str) -> Result<()> { 36 | let hw_data = get_anvil_hello_world_deployment_data()?; 37 | let hello_world_contract_address: Address = 38 | hw_data.addresses.hello_world_service_manager.parse()?; 39 | let pr = get_signer(&KEY.clone(), rpc_url); 40 | let hello_world_contract = HelloWorldServiceManager::new(hello_world_contract_address, pr); 41 | 42 | let tx = hello_world_contract 43 | .createNewTask(task_name.to_string()) 44 | .send() 45 | .await? 46 | .get_receipt() 47 | .await?; 48 | 49 | println!( 50 | "Transaction successfull with tx : {:?}", 51 | tx.transaction_hash 52 | ); 53 | 54 | Ok(()) 55 | } 56 | 57 | /// Start creating tasks at every 15 seconds 58 | async fn start_creating_tasks() { 59 | let mut interval = time::interval(Duration::from_secs(6)); 60 | init_logger(LogLevel::Info); 61 | loop { 62 | interval.tick().await; 63 | let random_name = generate_random_name(); 64 | get_logger().info( 65 | &format!("Creating new task with name: {random_name}"), 66 | "start_creating_tasks", 67 | ); 68 | let _ = create_new_task(&RPC_URL, &random_name).await; 69 | } 70 | } 71 | 72 | #[allow(dead_code)] 73 | #[tokio::main] 74 | async fn main() { 75 | dotenv().ok(); 76 | start_creating_tasks().await; 77 | } 78 | -------------------------------------------------------------------------------- /operator/rust/crates/operator/src/start_operator.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | use alloy::dyn_abi::DynSolValue; 3 | use alloy::{ 4 | primitives::{eip191_hash_message, keccak256, Address, FixedBytes, U256}, 5 | providers::Provider, 6 | rpc::types::{BlockNumberOrTag, Filter}, 7 | signers::{local::PrivateKeySigner, SignerSync}, 8 | sol_types::{SolEvent, SolValue}, 9 | }; 10 | use chrono::Utc; 11 | use dotenv::dotenv; 12 | use eigensdk::client_elcontracts::{ 13 | reader::ELChainReader, 14 | writer::{ELChainWriter, Operator}, 15 | }; 16 | use eigensdk::common::{get_provider, get_signer, get_ws_provider}; 17 | use eigensdk::logging::{get_logger, init_logger, log_level::LogLevel}; 18 | use eyre::Result; 19 | use futures::StreamExt; 20 | use hello_world_utils::ecdsastakeregistry::ECDSAStakeRegistry; 21 | use hello_world_utils::{ 22 | ecdsastakeregistry::ISignatureUtilsMixinTypes::SignatureWithSaltAndExpiry, 23 | helloworldservicemanager::{HelloWorldServiceManager, IHelloWorldServiceManager::Task}, 24 | }; 25 | use hello_world_utils::{ 26 | get_anvil_eigenlayer_deployment_data, get_hello_world_service_manager, 27 | get_stake_registry_address, 28 | }; 29 | use rand::{Rng, TryRngCore}; 30 | use std::sync::LazyLock; 31 | use std::{env, str::FromStr}; 32 | 33 | static RPC_URL: LazyLock = 34 | LazyLock::new(|| env::var("RPC_URL").expect("failed to retrieve RPC URL")); 35 | 36 | static WS_URL: LazyLock = 37 | LazyLock::new(|| env::var("WS_URL").expect("failed to retrieve WS URL")); 38 | 39 | static KEY: LazyLock = 40 | LazyLock::new(|| env::var("PRIVATE_KEY").expect("failed to retrieve private key")); 41 | 42 | static OPERATOR_RESPONSE_PERCENTAGE: LazyLock = LazyLock::new(|| { 43 | env::var("OPERATOR_RESPONSE_PERCENTAGE") 44 | .expect("failed to retrieve operator response percentage") 45 | .parse::() 46 | .expect("failed to parse operator response percentage") 47 | }); 48 | 49 | async fn sign_and_respond_to_task( 50 | rpc_url: &str, 51 | private_key: &str, 52 | task_index: u32, 53 | task_created_block: u32, 54 | name: String, 55 | ) -> Result<()> { 56 | let pr = get_signer(private_key, rpc_url); 57 | let signer = PrivateKeySigner::from_str(private_key)?; 58 | 59 | let message = format!("Hello, {}", name); 60 | let m_hash = eip191_hash_message(keccak256(message.abi_encode_packed())); 61 | let operators: Vec = vec![DynSolValue::Address(signer.address())]; 62 | let signature: Vec = 63 | vec![DynSolValue::Bytes(signer.sign_hash_sync(&m_hash)?.into())]; 64 | let current_block = U256::from(get_provider(rpc_url).get_block_number().await?); 65 | let signature_data = DynSolValue::Tuple(vec![ 66 | DynSolValue::Array(operators.clone()), 67 | DynSolValue::Array(signature.clone()), 68 | DynSolValue::Uint(current_block, 32), 69 | ]) 70 | .abi_encode_params(); 71 | 72 | get_logger().info( 73 | &format!("Signing and responding to task: {task_index:?}"), 74 | "", 75 | ); 76 | let hello_world_contract_address: Address = get_hello_world_service_manager()?; 77 | let hello_world_contract = HelloWorldServiceManager::new(hello_world_contract_address, &pr); 78 | 79 | let task = Task { 80 | name, 81 | taskCreatedBlock: task_created_block, 82 | }; 83 | let response_hash = hello_world_contract 84 | .respondToTask(task, task_index, signature_data.into()) 85 | .gas(500000) 86 | .send() 87 | .await? 88 | .get_receipt() 89 | .await? 90 | .transaction_hash; 91 | get_logger().info( 92 | &format!("Responded to task with tx hash {}", response_hash), 93 | "", 94 | ); 95 | Ok(()) 96 | } 97 | 98 | /// Monitor new tasks 99 | async fn monitor_new_tasks(rpc_url: &str, private_key: &str) -> Result<()> { 100 | let hello_world_contract_address: Address = get_hello_world_service_manager()?; 101 | 102 | let ws_provider = get_ws_provider(&WS_URL).await?; 103 | 104 | // Subscribe to NewTaskCreated events 105 | let filter = Filter::new() 106 | .address(hello_world_contract_address) 107 | .event_signature(HelloWorldServiceManager::NewTaskCreated::SIGNATURE_HASH) 108 | .from_block(BlockNumberOrTag::Latest); 109 | let mut new_task_stream = ws_provider.subscribe_logs(&filter).await?.into_stream(); 110 | 111 | // Process tasks when a new event is detected 112 | while let Some(log) = new_task_stream.next().await { 113 | if let Ok(decoded) = log.log_decode::() { 114 | let HelloWorldServiceManager::NewTaskCreated { taskIndex, task } = decoded.inner.data; 115 | get_logger().info( 116 | &format!( 117 | "New task {} detected at block {}", 118 | taskIndex, task.taskCreatedBlock 119 | ), 120 | "", 121 | ); 122 | 123 | // There is a `OPERATOR_RESPONSE_PERCENTAGE` chance that the operator will respond to the task. 124 | // If the operator does not respond, the operator will be slashed. 125 | let should_respond = rand::rng().random_bool(*OPERATOR_RESPONSE_PERCENTAGE / 100.0); 126 | 127 | if should_respond { 128 | sign_and_respond_to_task( 129 | rpc_url, 130 | private_key, 131 | taskIndex, 132 | task.taskCreatedBlock, 133 | task.name, 134 | ) 135 | .await?; 136 | } else { 137 | get_logger().info( 138 | &format!("Operator did not respond to task {}", taskIndex), 139 | "", 140 | ); 141 | } 142 | } 143 | } 144 | 145 | Ok(()) 146 | } 147 | 148 | #[allow(dead_code)] 149 | /// Monitor new tasks using polling 150 | async fn monitor_new_tasks_polling(rpc_url: &str, private_key: &str) -> Result<()> { 151 | let pr = get_signer(private_key, rpc_url); 152 | let hello_world_contract_address: Address = get_hello_world_service_manager()?; 153 | let mut latest_processed_block = pr.get_block_number().await?; 154 | 155 | loop { 156 | let current_block = pr.get_block_number().await?; 157 | get_logger().info( 158 | &format!( 159 | "Monitoring for new tasks from block {} to {}", 160 | latest_processed_block, current_block 161 | ), 162 | "", 163 | ); 164 | 165 | let filter = Filter::new() 166 | .address(hello_world_contract_address) 167 | .from_block(BlockNumberOrTag::Number(latest_processed_block)) 168 | .to_block(BlockNumberOrTag::Number(current_block)); 169 | 170 | let logs = pr.get_logs(&filter).await?; 171 | 172 | for log in logs { 173 | if let Some(&HelloWorldServiceManager::NewTaskCreated::SIGNATURE_HASH) = log.topic0() { 174 | let HelloWorldServiceManager::NewTaskCreated { taskIndex, task } = log 175 | .log_decode() 176 | .expect("Failed to decode log new task created") 177 | .inner 178 | .data; 179 | get_logger().info( 180 | &format!("New task {} detected: Hello, {}", taskIndex, task.name), 181 | "", 182 | ); 183 | 184 | // There is a `OPERATOR_RESPONSE_PERCENTAGE` chance that the operator will respond to the task. 185 | // If the operator does not respond, the operator will be slashed. 186 | let should_respond = rand::rng().random_bool(*OPERATOR_RESPONSE_PERCENTAGE / 100.0); 187 | 188 | if should_respond { 189 | let _ = sign_and_respond_to_task( 190 | rpc_url, 191 | private_key, 192 | taskIndex, 193 | task.taskCreatedBlock, 194 | task.name, 195 | ) 196 | .await; 197 | } else { 198 | get_logger().info( 199 | &format!("Operator did not respond to task {}", taskIndex), 200 | "", 201 | ); 202 | } 203 | } 204 | } 205 | 206 | tokio::time::sleep(tokio::time::Duration::from_secs(12)).await; 207 | latest_processed_block = current_block + 1; 208 | } 209 | } 210 | 211 | pub async fn register_operator(rpc_url: &str, private_key: &str) -> Result<()> { 212 | let pr = get_signer(private_key, rpc_url); 213 | let signer = PrivateKeySigner::from_str(private_key)?; 214 | 215 | let el_data = get_anvil_eigenlayer_deployment_data()?; 216 | let delegation_manager_address: Address = el_data.addresses.delegation_manager.parse()?; 217 | let avs_directory_address: Address = el_data.addresses.avs_directory.parse()?; 218 | 219 | let elcontracts_reader_instance = ELChainReader::new( 220 | get_logger().clone(), 221 | None, 222 | delegation_manager_address, 223 | Address::ZERO, 224 | avs_directory_address, 225 | None, 226 | rpc_url.to_string(), 227 | ); 228 | let elcontracts_writer_instance = ELChainWriter::new( 229 | Address::ZERO, 230 | Address::ZERO, 231 | None, 232 | None, 233 | Address::ZERO, 234 | elcontracts_reader_instance.clone(), 235 | rpc_url.to_string(), 236 | private_key.to_string(), 237 | ); 238 | 239 | let operator = Operator { 240 | address: signer.address(), 241 | delegation_approver_address: Address::ZERO, 242 | staker_opt_out_window_blocks: Some(0), 243 | metadata_url: Default::default(), 244 | allocation_delay: Some(0), 245 | _deprecated_earnings_receiver_address: None, 246 | }; 247 | 248 | let is_registered = elcontracts_reader_instance 249 | .is_operator_registered(signer.address()) 250 | .await 251 | .unwrap(); 252 | get_logger().info(&format!("is registered {}", is_registered), ""); 253 | let tx_hash = elcontracts_writer_instance 254 | .register_as_operator(operator) 255 | .await?; 256 | let receipt = pr.get_transaction_receipt(tx_hash).await?; 257 | if !receipt.is_some_and(|r| r.inner.is_success()) { 258 | get_logger().error("Operator registration failed", ""); 259 | return Err(eyre::eyre!("Operator registration failed")); 260 | } 261 | get_logger().info( 262 | &format!( 263 | "Operator registered on EL successfully tx_hash {:?}", 264 | tx_hash 265 | ), 266 | "", 267 | ); 268 | let mut salt = [0u8; 32]; 269 | rand::rngs::OsRng.try_fill_bytes(&mut salt).unwrap(); 270 | 271 | let salt = FixedBytes::from_slice(&salt); 272 | let now = Utc::now().timestamp(); 273 | let expiry: U256 = U256::from(now + 3600); 274 | 275 | let hello_world_contract_address: Address = get_hello_world_service_manager()?; 276 | let digest_hash = elcontracts_reader_instance 277 | .calculate_operator_avs_registration_digest_hash( 278 | signer.address(), 279 | hello_world_contract_address, 280 | salt, 281 | expiry, 282 | ) 283 | .await?; 284 | 285 | let signature = signer.sign_hash_sync(&digest_hash)?; 286 | let operator_signature = SignatureWithSaltAndExpiry { 287 | signature: signature.as_bytes().into(), 288 | salt, 289 | expiry, 290 | }; 291 | let stake_registry_address = get_stake_registry_address()?; 292 | let contract_ecdsa_stake_registry = ECDSAStakeRegistry::new(stake_registry_address, &pr); 293 | let registeroperator_details_call = contract_ecdsa_stake_registry 294 | .registerOperatorWithSignature(operator_signature, signer.clone().address()) 295 | .gas(500000); 296 | let register_hello_world_hash = registeroperator_details_call 297 | .send() 298 | .await? 299 | .get_receipt() 300 | .await? 301 | .transaction_hash; 302 | 303 | get_logger().info( 304 | &format!( 305 | "Operator registered on AVS successfully :{} , tx_hash :{}", 306 | signer.address(), 307 | register_hello_world_hash 308 | ), 309 | "", 310 | ); 311 | 312 | Ok(()) 313 | } 314 | 315 | #[tokio::main] 316 | pub async fn main() { 317 | use tokio::signal; 318 | dotenv().ok(); 319 | init_logger(LogLevel::Info); 320 | let rpc_url = &RPC_URL; 321 | if let Err(e) = register_operator(rpc_url, &KEY).await { 322 | eprintln!("Failed to register operator: {:?}", e); 323 | return; 324 | } 325 | 326 | // Start the task monitoring as a separate async task to keep the process running 327 | tokio::spawn(async { 328 | if let Err(e) = monitor_new_tasks(rpc_url, &KEY).await { 329 | eprintln!("Failed to monitor new tasks: {:?}", e); 330 | } 331 | }); 332 | 333 | // Wait for a Ctrl+C signal to gracefully shut down 334 | let _ = signal::ctrl_c().await; 335 | get_logger().info("Received Ctrl+C, shutting down...", ""); 336 | } 337 | -------------------------------------------------------------------------------- /operator/rust/crates/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-world-utils" 3 | description = "Hello world avs operator start and spam utilities" 4 | 5 | version.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | repository.workspace = true 9 | license-file.workspace = true 10 | 11 | [dependencies] 12 | alloy.workspace = true 13 | serde.workspace = true 14 | serde_json = "1.0.121" 15 | eyre = "0.6.12" 16 | -------------------------------------------------------------------------------- /operator/rust/crates/utils/src/bindings/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports, clippy::all, rustdoc::all)] 2 | //! This module contains the sol! generated bindings for solidity contracts. 3 | //! This is autogenerated code. 4 | //! Do not manually edit these files. 5 | //! These files may be overwritten by the codegen system at any time. 6 | pub mod r#ecdsastakeregistry; 7 | pub mod r#helloworldservicemanager; 8 | -------------------------------------------------------------------------------- /operator/rust/crates/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(doctest))] 2 | mod bindings; 3 | pub use bindings::*; 4 | 5 | use std::path::Path; 6 | 7 | use alloy::primitives::Address; 8 | use serde::Deserialize; 9 | 10 | #[derive(Deserialize, Debug)] 11 | pub struct HelloWorldData { 12 | #[serde(rename = "lastUpdate")] 13 | pub last_update: LastUpdate, 14 | pub addresses: HelloWorldAddresses, 15 | } 16 | 17 | #[derive(Deserialize, Debug)] 18 | pub struct LastUpdate { 19 | pub timestamp: String, 20 | pub block_number: String, 21 | } 22 | 23 | #[derive(Deserialize, Debug)] 24 | pub struct HelloWorldAddresses { 25 | #[serde(rename = "proxyAdmin")] 26 | pub proxy_admin: String, 27 | #[serde(rename = "helloWorldServiceManager")] 28 | pub hello_world_service_manager: String, 29 | #[serde(rename = "helloWorldServiceManagerImpl")] 30 | pub hello_world_service_manager_impl: String, 31 | #[serde(rename = "stakeRegistry")] 32 | pub stake_registry: String, 33 | #[serde(rename = "stakeRegistryImpl")] 34 | pub stake_registry_impl: String, 35 | pub strategy: String, 36 | pub token: String, 37 | } 38 | 39 | #[derive(Deserialize, Debug)] 40 | pub struct EigenLayerData { 41 | #[serde(rename = "lastUpdate")] 42 | pub last_update: LastUpdate, 43 | pub addresses: EigenLayerAddresses, 44 | } 45 | 46 | #[derive(Deserialize, Debug)] 47 | pub struct EigenLayerAddresses { 48 | #[serde(rename = "proxyAdmin")] 49 | pub proxy_admin: String, 50 | #[serde(rename = "delegationManager")] 51 | pub delegation_manager: String, 52 | #[serde(rename = "delegationManagerImpl")] 53 | pub delegation_manager_impl: String, 54 | #[serde(rename = "avsDirectory")] 55 | pub avs_directory: String, 56 | #[serde(rename = "avsDirectoryImpl")] 57 | pub avs_directory_impl: String, 58 | #[serde(rename = "strategyManager")] 59 | pub strategy_manager: String, 60 | #[serde(rename = "strategyManagerImpl")] 61 | pub strategy_manager_impl: String, 62 | #[serde(rename = "eigenPodManager")] 63 | pub eigen_pod_manager: String, 64 | #[serde(rename = "eigenPodManagerImpl")] 65 | pub eigen_pod_manager_impl: String, 66 | #[serde(rename = "strategyFactory")] 67 | pub strategy_factory: String, 68 | #[serde(rename = "strategyFactoryImpl")] 69 | pub strategy_factory_impl: String, 70 | #[serde(rename = "strategyBeacon")] 71 | pub strategy_beacon: String, 72 | } 73 | 74 | pub fn get_anvil_eigenlayer_deployment_data() -> eyre::Result { 75 | let file_path = Path::new(&env!("CARGO_MANIFEST_DIR").to_string()) 76 | .join("../../../../contracts/deployments/core/31337.json"); 77 | let data = std::fs::read_to_string(file_path)?; 78 | let el_parsed: EigenLayerData = serde_json::from_str(&data)?; 79 | Ok(el_parsed) 80 | } 81 | 82 | pub fn get_anvil_hello_world_deployment_data() -> eyre::Result { 83 | let file_path = Path::new(&env!("CARGO_MANIFEST_DIR").to_string()) 84 | .join("../../../../contracts/deployments/hello-world/31337.json"); 85 | let data = std::fs::read_to_string(file_path)?; 86 | let parsed_data: HelloWorldData = serde_json::from_str(&data)?; 87 | Ok(parsed_data) 88 | } 89 | 90 | pub fn get_hello_world_service_manager() -> eyre::Result
{ 91 | let data = get_anvil_hello_world_deployment_data()?; 92 | let hello_world_contract_address: Address = 93 | data.addresses.hello_world_service_manager.parse()?; 94 | Ok(hello_world_contract_address) 95 | } 96 | 97 | pub fn get_stake_registry_address() -> eyre::Result
{ 98 | let data = get_anvil_hello_world_deployment_data()?; 99 | let stake_registry_address: Address = data.addresses.stake_registry.parse()?; 100 | Ok(stake_registry_address) 101 | } 102 | 103 | pub fn get_delegation_manager_address() -> eyre::Result
{ 104 | let data = get_anvil_eigenlayer_deployment_data()?; 105 | let delegation_manager_address: Address = data.addresses.delegation_manager.parse()?; 106 | Ok(delegation_manager_address) 107 | } 108 | 109 | pub fn get_avs_directory_address() -> eyre::Result
{ 110 | let data = get_anvil_eigenlayer_deployment_data()?; 111 | let avs_directory_address: Address = data.addresses.avs_directory.parse()?; 112 | Ok(avs_directory_address) 113 | } 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world-avs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start:operator": "ts-node operator/index.ts", 7 | "start:traffic": "ts-node operator/createNewTasks.ts", 8 | "start:anvil": "anvil", 9 | "deploy:core": "cd contracts && forge script script/DeployEigenLayerCore.s.sol --rpc-url http://localhost:8545 --broadcast --optimize --optimizer-runs 200 --via-ir", 10 | "deploy:hello-world": "cd contracts && forge script script/HelloWorldDeployer.s.sol --rpc-url http://localhost:8545 --broadcast --optimize --optimizer-runs 200 --via-ir", 11 | "deploy:core-debug": "cd contracts && forge script script/DeployEigenLayerCore.s.sol --rpc-url http://localhost:8545 --broadcast --revert-strings debug --optimize --optimizer-runs 200 --via-ir", 12 | "deploy:hello-world-debug": "cd contracts && forge script script/HelloWorldDeployer.s.sol --rpc-url http://localhost:8545 --broadcast --revert-strings debug", 13 | "create-distributions-root": "cd contracts && forge script script/SetupDistributions.s.sol --rpc-url http://localhost:8545 --broadcast -v --sender 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", 14 | "claim-distributions": "cd contracts && forge script script/SetupDistributions.s.sol --rpc-url http://localhost:8545 --broadcast --sig \"executeProcessClaim()\" -v --sender 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", 15 | "create-operator-directed-distributions-root": "cd contracts && forge script script/SetupDistributions.s.sol --rpc-url http://localhost:8545 --broadcast --sig \"runOperatorDirected()\" -v --sender 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", 16 | "build": "tsc", 17 | "build:forge": "cd contracts && forge build", 18 | "extract:abis": "node utils/abis.js", 19 | "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest" 20 | }, 21 | "dependencies": { 22 | "dotenv": "^10.0.0", 23 | "ethers": "^6.13.2" 24 | }, 25 | "devDependencies": { 26 | "@types/jest": "^29.5.13", 27 | "@types/node": "^20.12.12", 28 | "@viem/anvil": "^0.0.10", 29 | "jest": "^29.7.0", 30 | "ts-jest": "^29.2.5", 31 | "ts-node": "^10.9.2", 32 | "typescript": "^5.4.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = [ 4 | "cargo", 5 | "clippy", 6 | "rust-analyzer", 7 | "rust-src", 8 | "rust-std", 9 | "rustc", 10 | "rustfmt", 11 | ] 12 | targets = [ "wasm32-unknown-unknown" ] 13 | profile = "minimal" 14 | -------------------------------------------------------------------------------- /scripts/rewards-script-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This flags makes the script exit if any command has a non-zero exit status, or 4 | # if tries to use a non defined variable 5 | set -e -o nounset 6 | 7 | # Execute anvil in background 8 | anvil -q & 9 | 10 | # Deploy contracts 11 | make deploy-eigenlayer-contracts 12 | make deploy-helloworld-contracts 13 | 14 | # Check that at first, claimer balance in token is zero 15 | initialBalance=$(make claimer-account-token-balance | tail -n1 | tr -d '[:space:]') 16 | 17 | echo "Initial balance: '$initialBalance'" 18 | 19 | if [ "$initialBalance" -ne 0 ]; then 20 | echo "claimer balance in token should be zero" 21 | exit 2 22 | fi 23 | 24 | # Create and claim normal distribution root 25 | echo "Creating distribution root:" 26 | make create-avs-distributions-root 27 | 28 | echo "Claiming distribution root:" 29 | make claim-distributions 30 | 31 | # Check that after claim, claimer balance in token is 100 32 | balanceAfterClaim=$(make claimer-account-token-balance | tail -n1 | tr -d '[:space:]') 33 | 34 | echo "Balance after first claim: '$balanceAfterClaim'" 35 | 36 | if [ "$balanceAfterClaim" -ne 100 ]; then 37 | echo "After first claim, claimer balance in token should be 100" 38 | exit 3 39 | fi 40 | 41 | # Create and claim operator directed distribution root 42 | echo "Creating operator directed distribution root:" 43 | make create-operator-directed-distributions-root 44 | 45 | echo "Claiming distribution root:" 46 | make claim-distributions 47 | 48 | # Check that after another claim, claimer balance in token is 200 49 | balanceAfterClaim=$(make claimer-account-token-balance | tail -n1 | tr -d '[:space:]') 50 | 51 | echo "Balance after second claim: '$balanceAfterClaim'" 52 | 53 | if [ "$balanceAfterClaim" -ne 200 ]; then 54 | echo "After second claim, claimer balance in token should be 200" 55 | exit 3 56 | fi 57 | 58 | # Kill anvil executing in background 59 | kill $(pgrep anvil) 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "contracts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /utils/abis.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const abiDir = 'abis'; 5 | const contractsDir = 'contracts'; 6 | const artifactsDir = path.join(contractsDir, 'out'); 7 | 8 | const contractsToExtract = [ 9 | 'IAVSDirectory', 10 | 'IDelegationManager', 11 | 'ECDSAStakeRegistry', 12 | 'HelloWorldServiceManager' 13 | ]; 14 | 15 | if (!fs.existsSync(abiDir)) { 16 | fs.mkdirSync(abiDir); 17 | } 18 | 19 | function checkArtifactsDirectory() { 20 | if (!fs.existsSync(artifactsDir)) { 21 | console.error(`The artifacts directory '${artifactsDir}' does not exist.`); 22 | console.log('Please compile your contracts first using "forge build"'); 23 | process.exit(1); 24 | } 25 | 26 | const files = fs.readdirSync(artifactsDir); 27 | if (files.length === 0) { 28 | console.error(`The artifacts directory '${artifactsDir}' is empty.`); 29 | console.log('Please compile your contracts first using "forge build" or confirm the path is correct.'); 30 | process.exit(1); 31 | } 32 | } 33 | 34 | function extractAbi(contractName) { 35 | const outputPath = path.join(artifactsDir, `${contractName}.sol`, `${contractName}.json`); 36 | const abiOutputPath = path.join(abiDir, `${contractName}.json`); 37 | 38 | try { 39 | const contractData = JSON.parse(fs.readFileSync(outputPath, 'utf8')); 40 | const abi = JSON.stringify(contractData.abi, null, 2); 41 | fs.writeFileSync(abiOutputPath, abi); 42 | console.log(`Extracted ABI for ${contractName}`); 43 | } catch (error) { 44 | console.error(`Error extracting ABI for ${contractName}:`, error.message); 45 | } 46 | } 47 | 48 | checkArtifactsDirectory(); 49 | 50 | for (const contractName of contractsToExtract) { 51 | extractAbi(contractName); 52 | } 53 | 54 | console.log('ABI extraction complete. Check the "abis" directory for the output.'); --------------------------------------------------------------------------------