-
Notifications
You must be signed in to change notification settings - Fork 17
feat: initialize ENSLabelHealer contract #2054
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 15 commits
9651df3
fcb99b6
9e5b820
8d137de
e18c8b8
aff4a88
5d9550b
b9c9b1f
dfdbd19
f98ca02
78375e0
4d4b6e3
e1cbfec
eb76aab
ceb6fd5
1677910
60d86c4
0e21ac8
4fd3ddc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| name: "Contracts: CI" | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - "contracts/**" | ||
| pull_request: | ||
| paths: | ||
| - "contracts/**" | ||
|
|
||
| jobs: | ||
| discover: | ||
| name: "Discover contracts" | ||
| runs-on: blacksmith-4vcpu-ubuntu-2204 | ||
| outputs: | ||
| contracts: ${{ steps.find.outputs.contracts }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Find Foundry projects | ||
| id: find | ||
| run: | | ||
| dirs=$(find contracts -name "foundry.toml" -not -path "*/lib/*" -exec dirname {} \; | sort | jq -R -s -c 'split("\n") | map(select(length > 0))') | ||
| echo "contracts=$dirs" >> $GITHUB_OUTPUT | ||
|
|
||
| ci: | ||
| name: "CI (${{ matrix.contract }})" | ||
| needs: discover | ||
| runs-on: blacksmith-4vcpu-ubuntu-2204 | ||
| strategy: | ||
| matrix: | ||
| contract: ${{ fromJson(needs.discover.outputs.contracts) }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: recursive | ||
|
|
||
| - name: Sync and init submodules | ||
| run: | | ||
| git submodule sync --recursive | ||
| git submodule update --init --recursive | ||
|
|
||
| - name: Install Foundry | ||
| uses: foundry-rs/foundry-toolchain@v1 | ||
|
|
||
| - name: Format | ||
| working-directory: ${{ matrix.contract }} | ||
| run: forge fmt --check | ||
|
|
||
| - name: Build | ||
| working-directory: ${{ matrix.contract }} | ||
| run: forge build --sizes | ||
|
|
||
| - name: Test | ||
| working-directory: ${{ matrix.contract }} | ||
| run: forge test -vvv | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| [submodule "contracts/ens-label-healer/lib/openzeppelin-contracts-upgradeable"] | ||
| path = contracts/ens-label-healer/lib/openzeppelin-contracts-upgradeable | ||
| url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable | ||
| [submodule "contracts/ens-label-healer/lib/forge-std"] | ||
| path = contracts/ens-label-healer/lib/forge-std | ||
| url = https://github.com/foundry-rs/forge-std | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| [submodule "contracts/ens-label-healer/lib/openzeppelin-contracts"] | ||
| path = contracts/ens-label-healer/lib/openzeppelin-contracts | ||
| url = https://github.com/OpenZeppelin/openzeppelin-contracts | ||
|
sevenzing marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,9 @@ | ||||||||||||||||
| # ENSNode Contracts | ||||||||||||||||
|
|
||||||||||||||||
| Solidity smart contracts used in the ENSNode ecosystem. Each contract lives in its own subdirectory as a standalone [Foundry](https://book.getfoundry.sh/) project. | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
|
|
||||||||||||||||
| ## Contracts | ||||||||||||||||
|
|
||||||||||||||||
| | Directory | Contract | Purpose | | ||||||||||||||||
| | ------------------- | ---------------- | ------------------------------------------------------------- | | ||||||||||||||||
| | `ens-label-healer/` | `ENSLabelHealer` | Permissioned on-chain label emitter for unresolved ENS labels | | ||||||||||||||||
|
sevenzing marked this conversation as resolved.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Feedback:
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Address that receives DEFAULT_ADMIN_ROLE on the proxy (required). | ||
| ADMIN_ADDRESS= | ||
|
|
||
| # Address that receives SUBMITTER_ROLE during deployment (optional). | ||
| # If omitted, grant it manually after deployment via grantRole. | ||
| SUBMITTER_ADDRESS= | ||
|
|
||
| # Private key of the deployer account (required for deployment). | ||
| # Must hold enough ETH to cover gas on the target network. | ||
| DEPLOYER_PRIVATE_KEY= | ||
|
|
||
| # RPC URLs for each network. | ||
| SEPOLIA_RPC_URL= | ||
|
sevenzing marked this conversation as resolved.
|
||
| MAINNET_RPC_URL= | ||
|
sevenzing marked this conversation as resolved.
|
||
|
|
||
| # Etherscan key for source verification. | ||
| ETHERSCAN_API_KEY= | ||
|
sevenzing marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| ENSLabelHealerFuzzTest:testFuzz_submitBatch_emitsOneEventPerInput(string[8],uint8) (runs: 10000, μ: 42523, ~: 41433) | ||
| ENSLabelHealerFuzzTest:testFuzz_submit_emitsForAnyLabel(string) (runs: 10000, μ: 25399, ~: 25224) | ||
| ENSLabelHealerInvariantTest:invariant_noLabelHealedWhilePaused() (runs: 500, calls: 50000, reverts: 0) | ||
| ENSLabelHealerTest:test_initialize_assignsAdminRole() (gas: 15001) | ||
| ENSLabelHealerTest:test_submitBatch_emitsForEveryItem() (gas: 34182) | ||
| ENSLabelHealerTest:test_submitBatch_revertsWhenPaused() (gas: 48705) | ||
| ENSLabelHealerTest:test_submit_allowsDuplicates() (gas: 31011) | ||
| ENSLabelHealerTest:test_submit_allowsEmptyLabel() (gas: 23473) | ||
| ENSLabelHealerTest:test_submit_emitsLabelHealed() (gas: 24080) | ||
| ENSLabelHealerTest:test_submit_revertsForNonSubmitter() (gas: 18582) | ||
| ENSLabelHealerTest:test_submit_revertsWhenPaused() (gas: 48112) | ||
| ENSLabelHealerTest:test_submit_succeedsAfterUnpause() (gas: 38140) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Foundry build artifacts | ||
| /out | ||
| /cache | ||
|
|
||
| # Deployment broadcast logs (contain tx hashes, gas costs, etc.) | ||
| # Commit these deliberately when you want to record a deployment. | ||
| /broadcast | ||
| .env |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| # ENSLabelHealer | ||
|
|
||
| Permissioned on-chain label emitter for unresolved ENS labels. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please see related feedback for how we document this contract, introduce its purpose, and document it's role and responsibilities within a larger set of systems. |
||
|
|
||
| Some ENS registry contracts emit events containing only a labelhash, without the plaintext label. This applies to both the original ENS Registry and ENS Registry With Fallback flows. `ENSLabelHealer` lets trusted submitters publish labels on-chain via `LabelHealed(string label)`. | ||
|
sevenzing marked this conversation as resolved.
|
||
|
|
||
| ## Prerequisites | ||
|
|
||
| Install [Foundry](https://book.getfoundry.sh/getting-started/installation): | ||
|
|
||
| After cloning the repo, pull the submodules: | ||
|
|
||
| ```bash | ||
| git submodule update --init --recursive contracts/ | ||
| ``` | ||
|
|
||
| ## Environment | ||
|
|
||
| Copy `.env.example` and fill in the values: | ||
|
|
||
| ```bash | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| ## Development | ||
|
|
||
| All commands run from `contracts/ens-label-healer/`. | ||
|
|
||
| ### Build | ||
|
|
||
| ```bash | ||
| forge build | ||
| ``` | ||
|
|
||
| ### Test | ||
|
|
||
| ```bash | ||
| # Run all tests (unit + fuzz + invariant) | ||
| forge test -vvv | ||
|
|
||
| # Run a single test file | ||
| forge test --match-path test/ENSLabelHealer.t.sol -vvv | ||
|
|
||
| # Run a single test by name | ||
| forge test --match-test test_submit_emitsLabelHealed -vvv | ||
| ``` | ||
|
|
||
| ### Format | ||
|
|
||
| ```bash | ||
| # Check formatting (used in CI) | ||
| forge fmt --check | ||
|
|
||
| # Auto-fix formatting | ||
| forge fmt | ||
| ``` | ||
|
|
||
| ### Gas snapshot | ||
|
|
||
| ```bash | ||
| forge snapshot | ||
| ``` | ||
|
|
||
| Commit the `.gas-snapshot` file to track gas changes across PRs. | ||
|
|
||
| ## Deployment | ||
|
|
||
| The deploy script (`script/Deploy.s.sol`) deploys `ENSLabelHealer` behind a UUPS proxy. It reads `ADMIN_ADDRESS` from the environment. | ||
|
|
||
| The grant script (`script/Grant.s.sol`) grants `SUBMITTER_ROLE` on an existing proxy. | ||
|
|
||
| ### Local devnet (Anvil) | ||
|
|
||
| Start a local chain in a separate terminal: | ||
|
|
||
| ```bash | ||
| anvil | ||
| ``` | ||
|
|
||
| Deploy using Anvil's pre-funded account 0: | ||
|
|
||
| ```bash | ||
| ADMIN_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \ | ||
| forge script script/Deploy.s.sol \ | ||
| --rpc-url http://localhost:8545 \ | ||
| --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ | ||
| --broadcast | ||
| ``` | ||
|
|
||
| Grant submitter access: | ||
|
|
||
| ```bash | ||
| PROXY_ADDRESS=<proxy address from deployment> \ | ||
| SUBMITTER_ADDRESS=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \ | ||
| forge script script/Grant.s.sol \ | ||
| --rpc-url http://localhost:8545 \ | ||
| --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ | ||
| --broadcast | ||
| ``` | ||
|
|
||
| Note the proxy address printed by deployment for manual testing. | ||
|
|
||
| #### Manual testing with cast | ||
|
|
||
| ```bash | ||
| export PROXY=<proxy address from above> | ||
| export SUBMITTER_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | ||
|
|
||
| # Submit a label | ||
| cast send $PROXY "submit(string)" "vitalik" \ | ||
| --private-key $SUBMITTER_KEY \ | ||
| --rpc-url http://localhost:8545 | ||
|
|
||
| # Read the emitted event | ||
| cast logs --rpc-url http://localhost:8545 \ | ||
| --address $PROXY \ | ||
| "LabelHealed(string)" | ||
| ``` | ||
|
|
||
| ### Sepolia testnet | ||
|
|
||
| **Current deployment:** | ||
|
|
||
| | | | | ||
| | -------- | ------------------------------------------------------------------------------------------------------- | | ||
| | Proxy | `0x1d7412e45265e935C5b083a8f278183175E8ce3d` | | ||
| | Explorer | [sepolia.etherscan.io](https://sepolia.etherscan.io/address/0x1d7412e45265e935C5b083a8f278183175E8ce3d) | | ||
| | Sourcify | [repo.sourcify.dev](https://repo.sourcify.dev/11155111/0x1d7412e45265e935C5b083a8f278183175E8ce3d) | | ||
|
|
||
|
|
||
| ```bash | ||
| source .env && forge script script/Deploy.s.sol \ | ||
| --rpc-url sepolia \ | ||
| --private-key $DEPLOYER_PRIVATE_KEY \ | ||
| --broadcast \ | ||
| --verify | ||
| ``` | ||
|
|
||
| Then grant `SUBMITTER_ROLE`. Requires `PROXY_ADDRESS` and `SUBMITTER_ADDRESS` ENV: | ||
|
|
||
| ```bash | ||
| forge script script/Grant.s.sol \ | ||
| --rpc-url sepolia \ | ||
| --private-key $DEPLOYER_PRIVATE_KEY \ | ||
| --broadcast | ||
| ``` | ||
|
|
||
| ### Mainnet | ||
|
|
||
| Always do a dry-run first (drop `--broadcast`) to simulate the deployment and review expected transactions: | ||
|
|
||
| ```bash | ||
| source .env && forge script script/Deploy.s.sol \ | ||
| --rpc-url mainnet \ | ||
| --private-key $DEPLOYER_PRIVATE_KEY | ||
| ``` | ||
|
|
||
| Then broadcast: | ||
|
|
||
| ```bash | ||
| source .env && forge script script/Deploy.s.sol \ | ||
| --rpc-url mainnet \ | ||
| --private-key $DEPLOYER_PRIVATE_KEY \ | ||
| --broadcast \ | ||
| --verify \ | ||
| --etherscan-api-key $ETHERSCAN_API_KEY | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| [profile.default] | ||
| src = "src" | ||
| out = "out" | ||
| libs = ["lib"] | ||
| solc = "0.8.24" | ||
| optimizer = true | ||
| optimizer_runs = 200 | ||
|
|
||
| [rpc_endpoints] | ||
| mainnet = "${MAINNET_RPC_URL}" | ||
| sepolia = "${SEPOLIA_RPC_URL}" | ||
|
|
||
| [etherscan] | ||
| mainnet = { key = "${ETHERSCAN_API_KEY}" } | ||
| sepolia = { key = "${ETHERSCAN_API_KEY}" } | ||
|
|
||
| [profile.default.fuzz] | ||
| runs = 10000 | ||
|
|
||
| [profile.default.invariant] | ||
| runs = 500 | ||
| depth = 100 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ | ||
| @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ | ||
| forge-std/=lib/forge-std/src/ | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| import "forge-std/Script.sol"; | ||
| import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; | ||
| import "../src/ENSLabelHealer.sol"; | ||
|
|
||
| /// @notice Deploys ENSLabelHealer behind an ERC-1967 / UUPS proxy. | ||
| /// | ||
| /// Required environment variables: | ||
| /// ADMIN_ADDRESS — address to assign DEFAULT_ADMIN_ROLE on the proxy. | ||
| /// | ||
| /// Usage: | ||
| /// forge script script/Deploy.s.sol \ | ||
| /// --rpc-url sepolia \ | ||
| /// --broadcast \ | ||
| /// --verify | ||
| contract Deploy is Script { | ||
| function run() external { | ||
| address admin = vm.envAddress("ADMIN_ADDRESS"); | ||
|
|
||
| vm.startBroadcast(); | ||
|
|
||
| ENSLabelHealer impl = new ENSLabelHealer(); | ||
| bytes memory initData = abi.encodeCall(ENSLabelHealer.initialize, (admin)); | ||
| ENSLabelHealer proxy = ENSLabelHealer(address(new ERC1967Proxy(address(impl), initData))); | ||
|
|
||
| vm.stopBroadcast(); | ||
|
|
||
| console.log("Implementation:", address(impl)); | ||
| console.log("Proxy: ", address(proxy)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.24; | ||
|
|
||
| import "forge-std/Script.sol"; | ||
| import "../src/ENSLabelHealer.sol"; | ||
|
sevenzing marked this conversation as resolved.
|
||
|
|
||
| /// @notice Grants SUBMITTER_ROLE on an existing ENSLabelHealer proxy. | ||
| /// | ||
| /// Required environment variables: | ||
| /// PROXY_ADDRESS — deployed ENSLabelHealer proxy address. | ||
| /// SUBMITTER_ADDRESS — address to receive SUBMITTER_ROLE. | ||
| /// | ||
| /// Usage: | ||
| /// forge script script/Grant.s.sol \ | ||
| /// --rpc-url sepolia \ | ||
| /// --broadcast | ||
| contract Grant is Script { | ||
|
sevenzing marked this conversation as resolved.
|
||
| function run() external { | ||
|
sevenzing marked this conversation as resolved.
|
||
| address proxyAddress = vm.envAddress("PROXY_ADDRESS"); | ||
| address submitter = vm.envAddress("SUBMITTER_ADDRESS"); | ||
|
|
||
| ENSLabelHealer proxy = ENSLabelHealer(proxyAddress); | ||
|
|
||
| vm.startBroadcast(); | ||
| proxy.grantRole(proxy.SUBMITTER_ROLE(), submitter); | ||
| vm.stopBroadcast(); | ||
|
|
||
| console.log("Proxy: ", proxyAddress); | ||
| console.log("Submitter: ", submitter); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.