mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-23 17:28:23 +00:00
## Summary
- Add multi-environment deployment support (stagenet, testnet, mainnet)
to CLI and contracts
- Configure stagenet and testnet runtimes with correct genesis hashes
and Snowbridge Agent IDs
- Add CLI commands for BEEFY checkpoint updates and rewards origin
computation
- Add ETH validator strategies (native beacon chain ETH + LSTs) to all
config files
## Changes
### Runtime Configuration
**Stagenet Runtime:**
- Set `StagenetGenesisHash` to DataHaven stagenet genesis hash
- Configure `RewardsAgentOrigin` with computed Snowbridge Agent ID
- Add tests verifying rewards account derivation and agent ID
computation
**Testnet Runtime:**
- Set `TestnetGenesisHash` to DataHaven testnet genesis hash
- Configure `RewardsAgentOrigin` with computed Snowbridge Agent ID
- Add tests verifying rewards account derivation and agent ID
computation
The Rewards Agent ID is computed following Snowbridge's location
description pattern:
```
blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis), "AccountKey20", rewards_account))
```
### CLI Enhancements
- All contracts subcommands (`status`, `deploy`, `verify`,
`update-metadata`) now accept `--environment` option
- Config and deployment files use environment-prefixed naming (e.g.,
`stagenet-hoodi.json`, `testnet-hoodi.json`)
- New `update-beefy-checkpoint` command that:
- Connects to a live DataHaven chain via WebSocket RPC
- Fetches all BEEFY data at the same finalized block for consistency
- Uses parallel queries with `Promise.all` for better performance
- Computes authority hashes (keccak256 of Ethereum addresses derived
from BEEFY public keys)
- Uses Snowbridge's quorum formula `n - floor((n-1)/3)` for strictly >
2/3 majority
- New `update-rewards-origin` command that computes the Snowbridge Agent
ID for the rewards pallet
- Centralized validation via `contractsPreActionHook` for all contract
commands
- Environment validation against allowlist (`stagenet`, `testnet`,
`mainnet`)
### Contract Changes
- Network validation uses explicit allowlist instead of suffix matching
- Added `initialValidatorSetId` and `nextValidatorSetId` fields to
`SnowbridgeConfig` struct
- `DeployBase.s.sol` now uses config values for validator set IDs
instead of hardcoded 0/1
- `DeployParams.s.sol` loads validator set IDs from config with
backwards compatibility
### Validator Strategies
Added ETH-equivalent strategies to allow validators to stake using
native ETH or LSTs:
**All Networks:**
- `0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0` - Native beacon chain ETH
(virtual strategy)
**Hoodi Testnet:**
- `0xf8a1a66130d614c7360e868576d5e59203475fe0` - stETH
- `0x24579aD4fe83aC53546E5c2D3dF5F85D6383420d` - WETH
**Ethereum Mainnet:**
- `0x93c4b944D05dfe6df7645A86cd2206016c51564D` - stETH
- `0x1BeE69b7dFFfA4E2d53C2a2Df135C388AD25dCD2` - rETH
- `0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc` - cbETH
### Config Files
- `stagenet-hoodi.json` - Hoodi testnet with stagenet EigenLayer
addresses
- `testnet-hoodi.json` - Hoodi testnet with testnet EigenLayer addresses
- `mainnet-ethereum.json` - Ethereum mainnet with mainnet EigenLayer
addresses
- Removed `hoodi.json` (replaced by environment-prefixed files)
## Usage
```bash
# Deploy to stagenet on Hoodi
bun cli contracts deploy --chain hoodi --environment stagenet
# Update BEEFY checkpoint from live chain
bun cli contracts update-beefy-checkpoint \
--chain hoodi \
--environment stagenet \
--rpc-url wss://services.datahaven-dev.network/stagenet
# Compute rewards origin for a chain
bun cli contracts update-rewards-origin \
--chain hoodi \
--environment stagenet \
--rpc-url wss://services.datahaven-dev.network/stagenet
# Check deployment status
bun cli contracts status --chain hoodi --environment stagenet
```
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
121 lines
4.6 KiB
TypeScript
121 lines
4.6 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 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,
|
|
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>;
|
|
|
|
/**
|
|
* Parses the deployments file for a given network
|
|
* @param networkId - The network identifier (e.g., "anvil", "hoodi", "stagenet-hoodi")
|
|
* This can include an environment prefix like "stagenet-" or "testnet-"
|
|
*/
|
|
export const parseDeploymentsFile = async (networkId = "anvil"): Promise<Deployments> => {
|
|
const deploymentsPath = `../contracts/deployments/${networkId}.json`;
|
|
const deploymentsFile = Bun.file(deploymentsPath);
|
|
if (!(await deploymentsFile.exists())) {
|
|
logger.error(`File ${deploymentsPath} does not exist`);
|
|
throw new Error(`Error reading ${networkId} 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 ${networkId} deployments file.`);
|
|
return parsedDeployments;
|
|
} catch (error) {
|
|
logger.error(`Failed to parse ${networkId} deployments file:`, error);
|
|
throw new Error(`Invalid ${networkId} deployments 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,
|
|
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">, 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
|
|
});
|
|
};
|
|
|
|
export const getAbi = async (contract: string) => {
|
|
const contractInstance = await getContractInstance(contract as ContractName);
|
|
return contractInstance.abi;
|
|
};
|