mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## Contracts upgrade command with simple version tracking This PR aims to take the most minimal changes from #438 to make the upgrade command available. So it adds the `bun cli contracts upgrade` command for deploying a new `DataHavenServiceManager` implementation and upgrading the proxy, and includes a simple version tracking via a `contracts/VERSION` file. ### Contracts **`DataHavenServiceManager.sol`** - Added `_version` storage variable - Added `DATAHAVEN_VERSION()` view function, - Added `updateVersion(string)` function gated by `onlyProxyAdmin` - Added `VersionUpdated` event - The version is set at initialization and updated atomically with proxy upgrades via `upgradeAndCall`. ### CLI **`bun cli contracts upgrade`** works in two modes: _dry-run_ or _execute_. **Dry-run (default)** Deploys the new implementation on-chain (signed by the deployer key), then prints a ready-to-submit JSON payload for the multisig to execute the proxy upgrade. No AVS owner key required. ```bash # Uses version from contracts/VERSION (standard workflow) bun cli contracts upgrade --chain hoodi # Override version for this upgrade only (warns if it differs from contracts/VERSION) bun cli contracts upgrade --chain hoodi --target x.y.z ``` Example output: ```json { "to": "0xProxyAdmin...", "value": "0", "data": "0x...", "description": "Upgrade ServiceManager proxy to 0xNewImpl... and set version to 1.1.0" } ``` **Execute mode (`--execute`)** Deploys the implementation and broadcasts the proxy upgrade + version update in a single atomic `upgradeAndCall` transaction. Requires `AVS_OWNER_PRIVATE_KEY`. Used mostly for testing. ```bash bun cli contracts upgrade --chain anvil --execute ``` --- ### Expected flow - Bump mannually contracts/VERSION (e.g., 1.1.0) - Run bun cli contracts upgrade --chain anvil|hoodi|mainnet
146 lines
5.2 KiB
TypeScript
146 lines
5.2 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,
|
|
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(),
|
|
ProxyAdmin: ethAddressCustom.optional(),
|
|
// Version tag for this set of deployed contracts (optional for backwards compatibility)
|
|
version: z.string().optional(),
|
|
deps: z
|
|
.object({
|
|
eigenlayer: z
|
|
.object({
|
|
release: z.string().optional(),
|
|
gitCommit: z.string().optional()
|
|
})
|
|
.optional(),
|
|
snowbridge: z
|
|
.object({
|
|
release: z.string().optional(),
|
|
gitCommit: z.string().optional()
|
|
})
|
|
.optional()
|
|
})
|
|
.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,
|
|
ProxyAdmin: generated.proxyAdminAbi,
|
|
DeployedStrategies: erc20Abi
|
|
} as const satisfies Record<
|
|
keyof Omit<Deployments, "network" | "version" | "deps" | "RewardsAgent">,
|
|
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;
|
|
};
|