Fix: command cli deploy contracts (#319)

## Summary

This PR fixes several issues with the CLI deploy-contracts command to
properly support local Anvil deployments and improves the overall
contract deployment workflow.

  ### Key fixes:
  - Add support for anvil chain in the CLI deploy contracts command
- Rename PRIVATE_KEY to DEPLOYER_PRIVATE_KEY for consistency and clarity
across the deployment flow
- Fix EigenLayer contract status display for local/anvil chains by
reading addresses from the deployments file instead of config
- Fix runShellCommandWithLogger to properly throw errors on command
failure
  - Correct totalSteps in DeployTestnet.s.sol from 2 to 4

  ### Housekeeping:
- Update .gitignore to ignore the entire broadcast/ folder
(autogenerated Foundry artifacts)
- Streamline contracts/README.md with clearer structure and deployment
instructions
This commit is contained in:
Ahmad Kaouk 2025-11-27 15:06:04 +01:00 committed by GitHub
parent 0618e84268
commit ffd01d8f1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 143 additions and 152 deletions

View file

@ -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/

View file

@ -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/<network>.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/<network>.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/<network>.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.

View file

@ -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);

View file

@ -57,7 +57,7 @@ contract DeployTestnet is DeployBase {
);
currentTestnet = _detectAndValidateNetwork(networkName);
totalSteps = 2; // Reduced steps since we're not deploying EigenLayer
totalSteps = 4;
_executeSharedDeployment();
}

View file

@ -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

View file

@ -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

View file

@ -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."
);
}
};

View file

@ -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<string, string> = {};
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 || ""
}
];

View file

@ -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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--chain <value>", "Target chain (hoodi, holesky, mainnet, anvil)")
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
.option("--private-key <value>", "Private key for deployment", process.env.PRIVATE_KEY || "")
.option(
"--private-key <value>",
"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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--chain <value>", "Target chain (hoodi, holesky, mainnet, anvil)")
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
.option("--private-key <value>", "Private key for deployment", process.env.PRIVATE_KEY || "")
.option(
"--private-key <value>",
"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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--chain <value>", "Target chain (hoodi, holesky, mainnet, anvil)")
.option("--rpc-url <value>", "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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--chain <value>", "Target chain (hoodi, holesky, mainnet, anvil)")
.option("--uri <value>", "New metadata URI (required)")
.option("--reset", "Use if you want to reset the metadata URI")
.option("--rpc-url <value>", "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 <value>", "Target chain (hoodi, holesky, mainnet)")
.option("--chain <value>", "Target chain (hoodi, holesky, mainnet, anvil)")
.option("--rpc-url <value>", "Chain RPC URL (optional, defaults based on chain)")
.option("--private-key <value>", "Private key for deployment", process.env.PRIVATE_KEY || "")
.option(
"--private-key <value>",
"Private key for deployment",
process.env.DEPLOYER_PRIVATE_KEY || ""
)
.option("--skip-verification", "Skip contract verification", false)
.hook("preAction", contractsPreActionHook)
.action(contractsCheck);

View file

@ -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);
}

View file

@ -11,9 +11,10 @@ export const runShellCommandWithLogger = async (
env?: object;
logLevel?: LogLevel;
waitFor?: (...args: unknown[]) => Promise<void>;
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);