mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
## Summary This PR introduces support for deploying Datahaven contracts to different chains (hoodi, holesky, mainnet), as well as a new cli command to manage this deployment separately from the regular deployment, while maintaining compatibility with it. #### New CLI command - **`bun cli contracts deploy`** - Deploy contracts to supported chains (Hoodi, Holesky, Mainnet) - **`bun cli contracts status`** - Check deployment configuration and status - **`bun cli contracts verify`** - Verify contracts on block explorers - Commands need the chain parameter: `--chain <hoodi | holesky | mainnet>` - Right now only `hoodi` and `holesky` are supported ### Deployment #### Hoodi & Holesky Network Support - Added **DeployBase.s.sol** as common ground for **DeployTestnet.s.sol** (also new) and **DeployLocal.s.sol** (existing). - **Hoodi configuration** (`contracts/config/hoodi.json`) with deployed EigenLayer contract addresses to reference. - **Holesky configuration** (`contracts/config/hoodi.json`) with deployed EigenLayer contract addresses to reference. #### Contracts being deployed - **DataHaven**: ServiceManager, VetoableSlasher, RewardsRegistry - **Snowbridge**: BeefyClient, AgentExecutor, Gateway, RewardsAgent - **EigenLayer**: References existing deployed contracts (not re-deployed) #### Deployment files When the deployment is done, a new file under `contracts/deployments` is generated with the addresses of the deployed contracts, for each chain (it will be overriden per chain if run multiple times). So we would have one `anvil.json`, `hoodi.json`, `holesky.json`, etc, with the addresses of the deployed contracts for reference and for later verification. #### Todo - [x] Test compatibility with existing `bun cli launch` and `bun cli deploy` commands #### For follow-up PRs - Fix verification issue with `foundry verify-contracts` when specifying the `chain` or `chain-id` parameter, needed for hoodi (https://github.com/foundry-rs/foundry/issues/7466). - Add `redeploy` feature to only override implementation contract and leave the proxy address untouched ## Usage Examples ```bash # Deploy to Hoodi network bun cli contracts deploy --chain hoodi # Check deployment status bun cli contracts status --chain hoodi # Verify contracts on block explorer bun cli contracts verify --chain hoodi ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added deployment and configuration support for new networks "hoodi" and "holesky", including new configuration and deployment files. * Introduced a CLI tool for managing contract deployments, status checks, and verification across supported chains. * Added example environment configuration and comprehensive deployment documentation. * Enabled contract verification and status reporting via the CLI with support for block explorer integration. * **Improvements** * Refactored deployment scripts for modularity, supporting both local and testnet environments. * Centralized and extended configuration loading to support additional contract addresses and network parameters. * Enhanced deployment utilities and typings to support multi-network deployments. * **Bug Fixes** * Improved input validation and error handling in CLI commands and deployment scripts. * Added explicit handling for zero address in operator strategy retrieval. * **Chores** * Updated documentation and configuration templates for easier onboarding and deployment management. * Improved logging and output formatting for deployment and verification processes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
148 lines
5.7 KiB
TypeScript
148 lines
5.7 KiB
TypeScript
import * as generated from "contract-bindings";
|
|
import invariant from "tiny-invariant";
|
|
import { type Abi, erc20Abi, getContract, isAddress } from "viem";
|
|
import { z } from "zod";
|
|
import { logger } from "./logger";
|
|
import { createDefaultClient, type ViemClientInterface } from "./viem";
|
|
|
|
const ethAddressRegex = /^0x[a-fA-F0-9]{40}$/;
|
|
const ethAddress = z.string().regex(ethAddressRegex, "Invalid Ethereum address");
|
|
const ethAddressCustom = z.custom<`0x${string}`>(
|
|
(val) => typeof val === "string" && ethAddressRegex.test(val),
|
|
{ message: "Invalid Ethereum address" }
|
|
);
|
|
const ethBytes32Regex = /^0x[a-fA-F0-9]{64}$/;
|
|
const ethBytes32 = z.string().regex(ethBytes32Regex, "Invalid Ethereum bytes32");
|
|
const ethBytes4Regex = /^0x[a-fA-F0-9]{8}$/;
|
|
const ethBytes4 = z.string().regex(ethBytes4Regex, "Invalid Ethereum bytes4");
|
|
|
|
const DeployedStrategySchema = z.object({
|
|
address: ethAddress,
|
|
underlyingToken: ethAddress,
|
|
tokenCreator: ethAddress
|
|
});
|
|
|
|
const DeploymentsSchema = z.object({
|
|
network: z.string(),
|
|
BeefyClient: ethAddressCustom,
|
|
AgentExecutor: ethAddressCustom,
|
|
Gateway: ethAddressCustom,
|
|
ServiceManager: ethAddressCustom,
|
|
ServiceManagerImplementation: ethAddressCustom,
|
|
VetoableSlasher: ethAddressCustom,
|
|
RewardsRegistry: ethAddressCustom,
|
|
RewardsAgent: ethAddressCustom,
|
|
DelegationManager: ethAddressCustom,
|
|
StrategyManager: ethAddressCustom,
|
|
AVSDirectory: ethAddressCustom,
|
|
EigenPodManager: ethAddressCustom.optional(),
|
|
EigenPodBeacon: ethAddressCustom.optional(),
|
|
RewardsCoordinator: ethAddressCustom,
|
|
AllocationManager: ethAddressCustom,
|
|
PermissionController: ethAddressCustom,
|
|
ETHPOSDeposit: ethAddressCustom.optional(),
|
|
BaseStrategyImplementation: ethAddressCustom.optional(),
|
|
DeployedStrategies: z.array(DeployedStrategySchema).optional()
|
|
});
|
|
|
|
export type Deployments = z.infer<typeof DeploymentsSchema>;
|
|
|
|
const RewardsInfoSchema = z.object({
|
|
RewardsAgent: ethAddressCustom,
|
|
RewardsAgentOrigin: ethBytes32,
|
|
updateRewardsMerkleRootSelector: ethBytes4
|
|
});
|
|
|
|
export type RewardsInfo = z.infer<typeof RewardsInfoSchema>;
|
|
|
|
export const parseDeploymentsFile = async (network = "anvil"): Promise<Deployments> => {
|
|
const deploymentsPath = `../contracts/deployments/${network}.json`;
|
|
const deploymentsFile = Bun.file(deploymentsPath);
|
|
if (!(await deploymentsFile.exists())) {
|
|
logger.error(`File ${deploymentsPath} does not exist`);
|
|
throw new Error(`Error reading ${network} deployments file`);
|
|
}
|
|
const deploymentsJson = await deploymentsFile.json();
|
|
logger.info(`Deployments: ${JSON.stringify(deploymentsJson, null, 2)}`);
|
|
try {
|
|
const parsedDeployments = DeploymentsSchema.parse(deploymentsJson);
|
|
logger.debug(`Successfully parsed ${network} deployments file.`);
|
|
return parsedDeployments;
|
|
} catch (error) {
|
|
logger.error(`Failed to parse ${network} deployments file:`, error);
|
|
throw new Error(`Invalid ${network} deployments file format`);
|
|
}
|
|
};
|
|
|
|
export const parseRewardsInfoFile = async (network = "anvil"): Promise<RewardsInfo> => {
|
|
const rewardsInfoPath = `../contracts/deployments/${network}-rewards-info.json`;
|
|
const rewardsInfoFile = Bun.file(rewardsInfoPath);
|
|
if (!(await rewardsInfoFile.exists())) {
|
|
logger.error(`File ${rewardsInfoPath} does not exist`);
|
|
throw new Error(`Error reading ${network} rewards info file`);
|
|
}
|
|
const rewardsInfoJson = await rewardsInfoFile.json();
|
|
try {
|
|
const parsedRewardsInfo = RewardsInfoSchema.parse(rewardsInfoJson);
|
|
logger.debug(`Successfully parsed ${network} rewards info file.`);
|
|
return parsedRewardsInfo;
|
|
} catch (error) {
|
|
logger.error(`Failed to parse ${network} rewards info file:`, error);
|
|
throw new Error(`Invalid ${network} rewards info file format`);
|
|
}
|
|
};
|
|
|
|
// Add to this if we add any new contracts
|
|
const abiMap = {
|
|
BeefyClient: generated.beefyClientAbi,
|
|
AgentExecutor: generated.agentExecutorAbi,
|
|
Gateway: generated.gatewayAbi,
|
|
ServiceManager: generated.dataHavenServiceManagerAbi,
|
|
ServiceManagerImplementation: generated.dataHavenServiceManagerAbi,
|
|
VetoableSlasher: generated.vetoableSlasherAbi,
|
|
RewardsRegistry: generated.rewardsRegistryAbi,
|
|
RewardsAgent: generated.agentAbi,
|
|
DelegationManager: generated.delegationManagerAbi,
|
|
StrategyManager: generated.strategyManagerAbi,
|
|
AVSDirectory: generated.avsDirectoryAbi,
|
|
EigenPodManager: generated.eigenPodManagerAbi,
|
|
EigenPodBeacon: generated.eigenPodAbi,
|
|
RewardsCoordinator: generated.rewardsCoordinatorAbi,
|
|
AllocationManager: generated.allocationManagerAbi,
|
|
PermissionController: generated.permissionControllerAbi,
|
|
ETHPOSDeposit: generated.iethposDepositAbi,
|
|
BaseStrategyImplementation: generated.strategyBaseTvlLimitsAbi,
|
|
DeployedStrategies: erc20Abi
|
|
} as const satisfies Record<keyof Omit<Deployments, "network" | "RewardsAgentOrigin">, Abi>;
|
|
|
|
type ContractName = keyof typeof abiMap;
|
|
type AbiFor<C extends ContractName> = (typeof abiMap)[C];
|
|
export type ContractInstance<C extends ContractName> = Awaited<
|
|
ReturnType<typeof getContractInstance<C>>
|
|
>;
|
|
|
|
// TODO: make this work with DeployedStrategies
|
|
export const getContractInstance = async <C extends ContractName>(
|
|
contract: C,
|
|
viemClient?: ViemClientInterface,
|
|
network = "anvil"
|
|
) => {
|
|
const deployments = await parseDeploymentsFile(network);
|
|
const contractAddress = deployments[contract];
|
|
logger.debug(`Contract ${contract} deployed to ${contractAddress}`);
|
|
|
|
const client = viemClient ?? (await createDefaultClient());
|
|
invariant(
|
|
typeof contractAddress === "string" && isAddress(contractAddress),
|
|
`Contract address for ${contract} is not a valid address`
|
|
);
|
|
|
|
const abi: AbiFor<C> = abiMap[contract];
|
|
invariant(abi, `ABI for contract ${contract} not found`);
|
|
|
|
return getContract({
|
|
address: contractAddress,
|
|
abi,
|
|
client
|
|
});
|
|
};
|