diff --git a/contracts/.gitignore b/contracts/.gitignore index 58010582..56cb5437 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -2,10 +2,8 @@ cache/ out/ -# Ignores development broadcast logs -!/broadcast -/broadcast/*/3*/ -/broadcast/**/dry-run/ +# Ignores development broadcast logs (autogenerated by forge script --broadcast) +broadcast/ # Docs docs/ diff --git a/contracts/README.md b/contracts/README.md index d3d03221..cc7db4fe 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,145 +1,71 @@ -# DataHaven AVS Smart Contracts 📜 +# DataHaven AVS Smart Contracts -This directory contains the smart contracts for the DataHaven Actively Validated Service (AVS) built on EigenLayer. - -## Overview - -DataHaven is an EVM-compatible Substrate blockchain secured by EigenLayer. These contracts implement the AVS Service Manager, middleware, and associated utilities that integrate with EigenLayer's operator registration, slashing, and rewards infrastructure. +Implements the Actively Validated Service (AVS) logic for DataHaven, secured by EigenLayer. These contracts manage operator registration, handle cross-chain rewards via Snowbridge, and enforce slashing with a veto period. ## Project Structure ``` contracts/ -├── src/ # Smart contract source code +├── src/ │ ├── DataHavenServiceManager.sol # Core AVS service manager -│ ├── RewardsRegistry.sol # Validator performance & rewards tracking -│ ├── VetoableSlasher.sol # Slashing with veto period +│ ├── middleware/ # RewardsRegistry, VetoableSlasher, Snowbridge helpers │ ├── interfaces/ # Contract interfaces -│ ├── libraries/ # Utility libraries -│ └── middleware/ # EigenLayer middleware integration -├── script/ # Deployment & setup scripts -│ └── deploy/ # Environment-specific deployment -├── test/ # Foundry test suites -└── foundry.toml # Foundry configuration +│ └── libraries/ # Utility libraries +├── script/ # Deployment & setup scripts +├── lib/ # External dependencies (EigenLayer, Snowbridge, OpenZeppelin) +└── test/ # Foundry test suites ``` -### Key Contracts +## Key Components -- **DataHavenServiceManager**: Manages operator lifecycle, registration, and deregistration with EigenLayer -- **RewardsRegistry**: Tracks validator performance metrics and handles reward distribution via Snowbridge -- **VetoableSlasher**: Implements slashing mechanism with dispute resolution veto period -- **Middleware**: Integration layer with EigenLayer's core contracts (based on [eigenlayer-middleware](https://github.com/Layr-Labs/eigenlayer-middleware)) +- **DataHavenServiceManager** (`src/DataHavenServiceManager.sol`): Core contract for operator lifecycle; inherits `ServiceManagerBase`. +- **RewardsRegistry** (`src/middleware/RewardsRegistry.sol`): Tracks validator performance and distributes rewards via Snowbridge. +- **VetoableSlasher** (`src/middleware/VetoableSlasher.sol`): Handles slashing requests with a dispute resolution veto window. -## Prerequisites +## Development -- [Foundry](https://book.getfoundry.sh/getting-started/installation) - -## Build - -To build the contracts: +Requires [Foundry](https://book.getfoundry.sh). ```bash -cd contracts +# Build and Test forge build -``` - -This will compile all contracts and generate artifacts in the `out` directory. - -## Test - -Run the test suite with: - -```bash forge test -``` -For more verbose output including logs: - -```bash -forge test -vv -``` - -For maximum verbosity including stack traces: - -```bash -forge test -vvvv -``` - -Run specific test contracts: - -```bash -forge test --match-contract RewardsRegistry -``` - -Run specific test functions: - -```bash -forge test --match-test test_newRewardsMessage -``` - -Exclude specific tests: - -```bash -forge test --no-match-test test_newRewardsMessage_OnlyRewardsAgent -``` - -## Deployment - -### Local Deployment - -1. In a separate terminal, start a local Anvil instance: - -```bash -anvil -``` - -2. Deploy to local Anvil: - -```bash -forge script script/deploy/DeployLocal.s.sol --rpc-url anvil --broadcast -``` - -### Network Deployment - -To deploy to a network configured in `foundry.toml`: - -```bash -forge script script/deploy/DeployLocal.s.sol --rpc-url $NETWORK_RPC_URL --private-key $PRIVATE_KEY --broadcast -``` - -Replace `$NETWORK_RPC_URL` with the RPC endpoint and `$PRIVATE_KEY` with your deployer's private key. - -Or using a network from `foundry.toml`: - -```bash -forge script script/deploy/DeployLocal.s.sol --rpc-url mainnet --private-key $PRIVATE_KEY --broadcast +# Regenerate TS bindings (after contract changes) +cd ../test && bun generate:wagmi ``` ## Configuration -The deployment configuration can be modified in: +Deployment parameters (EigenLayer addresses, initial validators, owners) are defined in `contracts/config/.json`. +- **Do not edit** `Config.sol` or `DeployParams.s.sol` directly; they only load the JSON. +- Ensure `contracts/config/hoodi.json` (or `holesky.json`) matches your target environment before deploying. -- `script/deploy/Config.sol`: Environment-specific configuration -- `script/deploy/DeployParams.s.sol`: Deployment parameters +## Deployment -## Code Generation - -After making changes to contracts, regenerate TypeScript bindings for the test framework: +Two deployment paths exist: **Local** (Anvil) and **Testnet** (Hoodi/Holesky). Both install the **DataHaven AVS contracts** (ServiceManager, RewardsRegistry, VetoableSlasher) and **Snowbridge** (BeefyClient, Gateway, Agent). They differ in EigenLayer setup: +### Local (Anvil) +**`DeployLocal.s.sol`** bootstraps a full EigenLayer core deployment (DelegationManager, StrategyManager, AVSDirectory, etc.) alongside DataHaven AVS and Snowbridge. ```bash -cd ../test -bun generate:wagmi +anvil +forge script script/deploy/DeployLocal.s.sol --rpc-url anvil --broadcast ``` -This generates type-safe contract interfaces used by the E2E test suite. +### Testnet (Hoodi / Holesky) +**`DeployTestnet.s.sol`** references existing EigenLayer contracts (addresses from `contracts/config/.json`) and only deploys DataHaven AVS + Snowbridge. +```bash +NETWORK=hoodi forge script script/deploy/DeployTestnet.s.sol \ + --rpc-url hoodi \ + --private-key $PRIVATE_KEY \ + --broadcast +``` +Supported networks: `hoodi`, `holesky` (no mainnet config yet). Artifacts → `contracts/deployments/.json`. -## Integration with DataHaven +## How It Works +1. **Registration**: Validators register with EigenLayer via `DataHavenServiceManager`. +2. **Performance Tracking**: DataHaven computes reward points and sends a Merkle root to `RewardsRegistry` on Ethereum via Snowbridge. +3. **Rewards Claims**: Validators claim rewards on Ethereum from `RewardsRegistry` using Merkle proofs. +4. **Slashing**: Misbehavior triggers `VetoableSlasher` (subject to veto period). -These contracts integrate with the DataHaven Substrate node through: - -1. **Operator Registration**: Validators register on-chain via `DataHavenServiceManager` -2. **Performance Tracking**: Node submits validator metrics to `RewardsRegistry` -3. **Cross-chain Rewards**: Rewards distributed from Ethereum to DataHaven via Snowbridge -4. **Slashing**: Misbehavior triggers slashing through `VetoableSlasher` with veto period - -For full network integration testing, see the [test directory](../test/README.md). +See `test/README.md` for full network integration tests. diff --git a/contracts/script/deploy/DeployBase.s.sol b/contracts/script/deploy/DeployBase.s.sol index 894f66c9..96d4b307 100644 --- a/contracts/script/deploy/DeployBase.s.sol +++ b/contracts/script/deploy/DeployBase.s.sol @@ -114,7 +114,6 @@ abstract contract DeployBase is Script, DeployParams, Accounts { _logProgress(); // Deploy Snowbridge (same for both modes) - Logging.logHeader("SNOWBRIDGE DEPLOYMENT"); ( BeefyClient beefyClient, AgentExecutor agentExecutor, @@ -170,6 +169,8 @@ abstract contract DeployBase is Script, DeployParams, Accounts { function _deploySnowbridge( SnowbridgeConfig memory config ) internal returns (BeefyClient, AgentExecutor, IGatewayV2, address payable) { + Logging.logHeader("SNOWBRIDGE DEPLOYMENT"); + Logging.logSection("Deploying Snowbridge Core Components"); BeefyClient beefyClient = _deployBeefyClient(config); diff --git a/contracts/script/deploy/DeployTestnet.s.sol b/contracts/script/deploy/DeployTestnet.s.sol index 8236988d..153e5727 100644 --- a/contracts/script/deploy/DeployTestnet.s.sol +++ b/contracts/script/deploy/DeployTestnet.s.sol @@ -57,7 +57,7 @@ contract DeployTestnet is DeployBase { ); currentTestnet = _detectAndValidateNetwork(networkName); - totalSteps = 2; // Reduced steps since we're not deploying EigenLayer + totalSteps = 4; _executeSharedDeployment(); } diff --git a/test/cli/handlers/contracts/.env.example b/test/cli/handlers/contracts/.env.example index 0939183e..d33611b8 100644 --- a/test/cli/handlers/contracts/.env.example +++ b/test/cli/handlers/contracts/.env.example @@ -2,7 +2,7 @@ # Copy this file to .env and fill in your values # Private key for contract deployment (REQUIRED) -PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000 +DEPLOYER_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000 # AVS Owner private key (for post-deployment configuration) AVS_OWNER_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000 diff --git a/test/cli/handlers/contracts/README.md b/test/cli/handlers/contracts/README.md index 1055c21d..2ea44b4a 100644 --- a/test/cli/handlers/contracts/README.md +++ b/test/cli/handlers/contracts/README.md @@ -29,9 +29,9 @@ cd test && cp cli/handlers/contracts/.env.example .env Edit `.env` with your values: ```bash # Required: Private key with deployment funds -PRIVATE_KEY=0x... +DEPLOYER_PRIVATE_KEY=0x... -# Required: AVS owner private key (can be same as PRIVATE_KEY) +# Required: AVS owner private key (can be same as DEPLOYER_PRIVATE_KEY) AVS_OWNER_PRIVATE_KEY=0x... # Optional: For contract verification diff --git a/test/cli/handlers/contracts/deploy.ts b/test/cli/handlers/contracts/deploy.ts index abddeb11..d16daa79 100644 --- a/test/cli/handlers/contracts/deploy.ts +++ b/test/cli/handlers/contracts/deploy.ts @@ -91,17 +91,19 @@ export const contractsPreActionHook = async (thisCommand: any) => { const privateKey = thisCommand.getOptionValue("privateKey"); if (!chain) { - logger.error("❌ Chain is required. Use --chain option (hoodi, holesky, mainnet)"); + logger.error("❌ Chain is required. Use --chain option (hoodi, holesky, mainnet, anvil)"); process.exit(1); } - const supportedChains = ["hoodi", "holesky", "mainnet"]; + const supportedChains = ["hoodi", "holesky", "mainnet", "anvil"]; if (!supportedChains.includes(chain)) { logger.error(`❌ Unsupported chain: ${chain}. Supported chains: ${supportedChains.join(", ")}`); process.exit(1); } - if (!privateKey) { - logger.warn("⚠️ Private key not provided. Will use PRIVATE_KEY environment variable"); + if (!privateKey && !process.env.DEPLOYER_PRIVATE_KEY) { + logger.warn( + "⚠️ Private key not provided. Will use DEPLOYER_PRIVATE_KEY environment variable if set, or default Anvil key." + ); } }; diff --git a/test/cli/handlers/contracts/status.ts b/test/cli/handlers/contracts/status.ts index 684057d9..c984ec6f 100644 --- a/test/cli/handlers/contracts/status.ts +++ b/test/cli/handlers/contracts/status.ts @@ -25,7 +25,8 @@ export const showDeploymentPlanAndStatus = async (chain: string) => { await showEigenLayerContractStatus( config, deploymentParams.chainId.toString(), - deploymentParams.rpcUrl + deploymentParams.rpcUrl, + chain ); printDivider(); @@ -109,27 +110,69 @@ const showDatahavenContractStatus = async (chain: string, rpcUrl: string) => { /** * Shows the status of EigenLayer contracts (verification only) */ -const showEigenLayerContractStatus = async (config: any, chainId: string, rpcUrl: string) => { +const showEigenLayerContractStatus = async ( + config: any, + chainId: string, + rpcUrl: string, + chain: string +) => { try { + // For local/anvil deployments, read addresses from deployments file + // For testnet/mainnet, use addresses from config file + let eigenLayerAddresses: Record = {}; + const isLocal = chain === "anvil" || chain === "local"; + + if (isLocal) { + try { + const deploymentsPath = `../contracts/deployments/${chain === "local" ? "anvil" : chain}.json`; + const deploymentsFile = Bun.file(deploymentsPath); + if (await deploymentsFile.exists()) { + const deployments = await deploymentsFile.json(); + eigenLayerAddresses = { + DelegationManager: deployments.DelegationManager, + StrategyManager: deployments.StrategyManager, + EigenPodManager: deployments.EigenPodManager, + AVSDirectory: deployments.AVSDirectory, + RewardsCoordinator: deployments.RewardsCoordinator, + AllocationManager: deployments.AllocationManager, + PermissionController: deployments.PermissionController + }; + } + } catch (error) { + logger.debug(`Could not read deployments file for EigenLayer contracts: ${error}`); + } + } + const contracts = [ { name: "DelegationManager", - address: config.eigenLayer.delegationManager + address: eigenLayerAddresses.DelegationManager || config.eigenLayer?.delegationManager || "" + }, + { + name: "StrategyManager", + address: eigenLayerAddresses.StrategyManager || config.eigenLayer?.strategyManager || "" + }, + { + name: "EigenPodManager", + address: eigenLayerAddresses.EigenPodManager || config.eigenLayer?.eigenPodManager || "" + }, + { + name: "AVSDirectory", + address: eigenLayerAddresses.AVSDirectory || config.eigenLayer?.avsDirectory || "" }, - { name: "StrategyManager", address: config.eigenLayer.strategyManager }, - { name: "EigenPodManager", address: config.eigenLayer.eigenPodManager }, - { name: "AVSDirectory", address: config.eigenLayer.avsDirectory }, { name: "RewardsCoordinator", - address: config.eigenLayer.rewardsCoordinator + address: + eigenLayerAddresses.RewardsCoordinator || config.eigenLayer?.rewardsCoordinator || "" }, { name: "AllocationManager", - address: config.eigenLayer.allocationManager + address: eigenLayerAddresses.AllocationManager || config.eigenLayer?.allocationManager || "" }, { name: "PermissionController", - address: config.eigenLayer.permissionController + address: + eigenLayerAddresses.PermissionController || config.eigenLayer?.permissionController || "" } ]; diff --git a/test/cli/index.ts b/test/cli/index.ts index 4b81b001..399e5cb3 100644 --- a/test/cli/index.ts +++ b/test/cli/index.ts @@ -198,7 +198,7 @@ const contractsCommand = program - update-metadata: Update the metadata URI of an existing AVS contract Common options: - --chain: Target chain (required: hoodi, holesky, mainnet) + --chain: Target chain (required: hoodi, holesky, mainnet, anvil) --rpc-url: Chain RPC URL (optional, defaults based on chain) --private-key: Private key for deployment --skip-verification: Skip contract verification @@ -210,9 +210,13 @@ const contractsCommand = program contractsCommand .command("status") .description("Show deployment plan, configuration, and status") - .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--chain ", "Target chain (hoodi, holesky, mainnet, anvil)") .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") - .option("--private-key ", "Private key for deployment", process.env.PRIVATE_KEY || "") + .option( + "--private-key ", + "Private key for deployment", + process.env.DEPLOYER_PRIVATE_KEY || "" + ) .option("--skip-verification", "Skip contract verification", false) .hook("preAction", contractsPreActionHook) .action(contractsCheck); @@ -221,9 +225,13 @@ contractsCommand contractsCommand .command("deploy") .description("Deploy DataHaven AVS contracts to specified chain") - .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--chain ", "Target chain (hoodi, holesky, mainnet, anvil)") .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") - .option("--private-key ", "Private key for deployment", process.env.PRIVATE_KEY || "") + .option( + "--private-key ", + "Private key for deployment", + process.env.DEPLOYER_PRIVATE_KEY || "" + ) .option("--skip-verification", "Skip contract verification", false) .hook("preAction", contractsPreActionHook) .action(contractsDeploy); @@ -232,7 +240,7 @@ contractsCommand contractsCommand .command("verify") .description("Verify deployed contracts on block explorer") - .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--chain ", "Target chain (hoodi, holesky, mainnet, anvil)") .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") .option("--skip-verification", "Skip contract verification", false) .hook("preAction", contractsPreActionHook) @@ -242,7 +250,7 @@ contractsCommand contractsCommand .command("update-metadata") .description("Update AVS metadata URI for the DataHaven Service Manager") - .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--chain ", "Target chain (hoodi, holesky, mainnet, anvil)") .option("--uri ", "New metadata URI (required)") .option("--reset", "Use if you want to reset the metadata URI") .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") @@ -270,9 +278,13 @@ contractsCommand // Default Contracts command (runs check) contractsCommand .description("Show deployment plan, configuration, and status") - .option("--chain ", "Target chain (hoodi, holesky, mainnet)") + .option("--chain ", "Target chain (hoodi, holesky, mainnet, anvil)") .option("--rpc-url ", "Chain RPC URL (optional, defaults based on chain)") - .option("--private-key ", "Private key for deployment", process.env.PRIVATE_KEY || "") + .option( + "--private-key ", + "Private key for deployment", + process.env.DEPLOYER_PRIVATE_KEY || "" + ) .option("--skip-verification", "Skip contract verification", false) .hook("preAction", contractsPreActionHook) .action(contractsCheck); diff --git a/test/scripts/deploy-contracts.ts b/test/scripts/deploy-contracts.ts index 7ceca773..83935da7 100644 --- a/test/scripts/deploy-contracts.ts +++ b/test/scripts/deploy-contracts.ts @@ -54,7 +54,7 @@ export const constructDeployCommand = (options: ContractDeploymentOptions): stri const { chain, rpcUrl, verified, blockscoutBackendUrl } = options; const deploymentScript = - !chain || chain === "anvil" || chain === "local" + !chain || chain === "anvil" ? "script/deploy/DeployLocal.s.sol" : "script/deploy/DeployTestnet.s.sol"; @@ -83,12 +83,16 @@ export const constructDeployCommand = (options: ContractDeploymentOptions): stri export const executeDeployment = async ( deployCommand: string, parameterCollection?: ParameterCollection, - chain?: string + chain?: string, + privateKey?: string ) => { logger.info("⌛️ Deploying contracts (this might take a few minutes)..."); // Using custom shell command to improve logging with forge's stdoutput - await runShellCommandWithLogger(deployCommand, { cwd: "../contracts" }); + await runShellCommandWithLogger(deployCommand, { + cwd: "../contracts", + env: privateKey ? { DEPLOYER_PRIVATE_KEY: privateKey } : undefined + }); // After deployment, read the: // - Gateway address @@ -198,7 +202,7 @@ export const deployContracts = async (options: { // Construct and execute deployment const deployCommand = constructDeployCommand(deploymentOptions); - await executeDeployment(deployCommand); + await executeDeployment(deployCommand, undefined, options.chain, options.privateKey); logger.success(`DataHaven contracts deployed successfully to ${options.chain}`); }; @@ -251,5 +255,5 @@ if (import.meta.main) { await buildContracts(); const deployCommand = constructDeployCommand(options); - await executeDeployment(deployCommand); + await executeDeployment(deployCommand, undefined, undefined, options.privateKey); } diff --git a/test/utils/shell.ts b/test/utils/shell.ts index 4d316a49..d0d450e3 100644 --- a/test/utils/shell.ts +++ b/test/utils/shell.ts @@ -11,9 +11,10 @@ export const runShellCommandWithLogger = async ( env?: object; logLevel?: LogLevel; waitFor?: (...args: unknown[]) => Promise; + throwOnError?: boolean; } ) => { - const { cwd = ".", env = {}, logLevel = "info" as LogLevel } = options || {}; + const { cwd = ".", env = {}, logLevel = "info" as LogLevel, throwOnError = true } = options || {}; try { if (!existsSync(cwd)) { @@ -92,6 +93,10 @@ export const runShellCommandWithLogger = async ( trimmedStderr.includes("\n") ? `>_ \n${trimmedStderr}` : `>_ ${trimmedStderr}` ); } + + if (throwOnError) { + throw new Error(`Command failed with exit code ${exitCode}`); + } } } catch (err) { logger.error("❌ Error running shell command:", command, "in", cwd);