Skip to content
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,43 @@ versionController.addSubDeveloper(subDeveloperAddress);
// Same process as key developer, but limited to their key dev's contract types
```

**Upload via CLI Script**

The `uploadBytecode.ts` script provides a convenient CLI for uploading bytecode.
It validates developer access before sending a transaction and supports loading bytecode from Hardhat/Foundry artifacts, custom JSON files, or raw hex files.

```bash
# Initial release (creates version 1.0.0) from a Hardhat compilation artifact
npx hardhat run scripts/cli/uploadBytecode.ts --network ethereum -- \
--contract-type Comet \
--release-type initial \
--source-url "https://github.com/compound-finance/comet/releases/v1.0.0" \
--bytecode-file artifacts/contracts/Comet.sol/Comet.json

# Patch release (creates next patch under 1.0.x)
npx hardhat run scripts/cli/uploadBytecode.ts --network ethereum -- \
--contract-type Comet \
--release-type patch --major 1 --minor 0 \
--source-url "https://github.com/compound-finance/comet/releases/v1.0.1" \
--bytecode-file artifacts/contracts/Comet.sol/Comet.json

# Alternative version (creates 1.0.0-gas-optimized)
npx hardhat run scripts/cli/uploadBytecode.ts --network ethereum -- \
--contract-type Comet \
--release-type alternative --major 1 --minor 0 --patch 0 --alternative gas-optimized \
--source-url "https://github.com/compound-finance/comet/releases/v1.0.0-gas-optimized" \
--bytecode-file artifacts/contracts/CometGasOptimized.sol/CometGasOptimized.json

# Load bytecode from a custom JSON file with a specific key
npx hardhat run scripts/cli/uploadBytecode.ts --network ethereum -- \
--contract-type Comet \
--release-type initial \
--source-url "https://github.com/..." \
--bytecode-file bytecodes/contracts.json --json-key CometInitCode
```

Run `npx hardhat run scripts/cli/uploadBytecode.ts -- --help` for the full list of flags.

### Phase 3: Audit & Verification 🔍

**Step 1: Auditor Review Process**
Expand Down Expand Up @@ -382,6 +419,58 @@ versionController.verifyAudit(
// At least one signature is sufficient to allow deployment of the bytecode
```

**Sign & Verify via CLI Scripts**

The `signAuditReport.ts` script lets an auditor generate an EIP-712 signature off-chain.
It validates the signer has `AUDITOR_ROLE` and retrieves bytecode data from the contract before signing.

```bash
# Auditor signs an audit report for Comet v1.0.0
npx hardhat run scripts/cli/signAuditReport.ts --network ethereum -- \
--contract-type Comet \
--major 1 --minor 0 --patch 0 \
--audit-report-url "https://audits.firm.com/comet-v1.0.0-report.pdf"

# Sign for an alternative version
npx hardhat run scripts/cli/signAuditReport.ts --network ethereum -- \
--contract-type Comet \
--major 1 --minor 0 --patch 0 --alternative gas-optimized \
--audit-report-url "https://audits.firm.com/comet-gas-optimized-report.pdf"
```

The script outputs the signature, bytecode version hash, and init code hash.
A developer then submits the signature on-chain using `submitAuditReport.ts`.

**Submit Audit Report via CLI Script**

The `submitAuditReport.ts` script lets a developer submit an auditor-signed audit report on-chain
via `verifyBytecode()`. It recovers the auditor address from the EIP-712 signature, validates both
developer access and auditor role before sending the transaction.

```bash
# Developer submits audit report for Comet v1.0.0
npx hardhat run scripts/cli/submitAuditReport.ts --network ethereum -- \
--contract-type Comet \
--major 1 --minor 0 --patch 0 \
--audit-report-url "https://audits.firm.com/comet-v1.0.0-report.pdf" \
--signature 0xabc123...def456

# Submit for an alternative version
npx hardhat run scripts/cli/submitAuditReport.ts --network ethereum -- \
--contract-type Comet \
--major 1 --minor 0 --patch 0 --alternative gas-optimized \
--audit-report-url "https://audits.firm.com/comet-gas-optimized-report.pdf" \
--signature 0xabc123...def456

# With explicit VersionController address
npx hardhat run scripts/cli/submitAuditReport.ts --network ethereum -- \
--version-controller 0x1234...abcd \
--contract-type Comet \
--major 1 --minor 0 --patch 0 \
--audit-report-url "https://audits.firm.com/report.pdf" \
--signature 0xabc123...def456
```

### Phase 4: Cross-chain Distribution 🌐

**Step 1: L1 to L2 Transmission**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"function getVerifiedBytecode(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _version) view returns (bytes)",
"function grantRole(bytes32 role, address account)",
"function hasRole(bytes32 role, address account) view returns (bool)",
"function initialize(address _governor, address _guardian)",
"function initialize(address _initialAdmin, address _guardian)",
"function isBytecodeUploaded(bytes32) view returns (bool)",
"function isBytecodeVerified(tuple(bytes32 contractType, tuple(tuple(uint64 major, uint64 minor, uint64 patch) version, string alternative) version) _version) view returns (bool)",
"function isDeveloper(address _account) view returns (bool)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1538,7 +1538,7 @@
"inputs": [
{
"internalType": "address",
"name": "_governor",
"name": "_initialAdmin",
"type": "address"
},
{
Expand Down
6 changes: 3 additions & 3 deletions contracts/VersionController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ contract VersionController is
_disableInitializers();
}

function initialize(address _governor, address _guardian) external initializer {
if (_governor == address(0) || _guardian == address(0)) revert ZeroAddress();
function initialize(address _initialAdmin, address _guardian) external initializer {
if (_initialAdmin == address(0) || _guardian == address(0)) revert ZeroAddress();
__AccessControlEnumerable_init();
__UUPSUpgradeable_init();
__EIP712_init("VersionController", "1");
_grantRole(DEFAULT_ADMIN_ROLE, _governor);
_grantRole(DEFAULT_ADMIN_ROLE, _initialAdmin);
_grantRole(GUARDIAN_ROLE, _guardian);
}

Expand Down
2 changes: 1 addition & 1 deletion docs/VersionController.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ constructor() public
### initialize

```solidity
function initialize(address _governor, address _guardian) external
function initialize(address _initialAdmin, address _guardian) external
```

### checkDeveloper
Expand Down
247 changes: 247 additions & 0 deletions scripts/cli/signAuditReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/**
* Audit Report Signing Script
*
* Generates an EIP-712 signature for an audit report on a specific bytecode version.
* The auditor signs off-chain; a developer then submits the signature on-chain via
* `verifyBytecode()` on the VersionController contract.
*
* Usage:
* # Sign audit report for version 1.0.0
* npx hardhat run scripts/cli/signAuditReport.ts --network ethereum -- \
* --contract-type Comet \
* --major 1 --minor 0 --patch 0 \
* --audit-report-url "https://audits.firm.com/comet-v1.0.0-report.pdf"
*
* # Sign audit report for an alternative version
* npx hardhat run scripts/cli/signAuditReport.ts --network ethereum -- \
* --contract-type Comet \
* --major 1 --minor 0 --patch 0 --alternative gas-optimized \
* --audit-report-url "https://audits.firm.com/comet-v1.0.0-gas-optimized-report.pdf"
*
* # With explicit VersionController address
* npx hardhat run scripts/cli/signAuditReport.ts --network ethereum -- \
* --version-controller 0x1234...abcd \
* --contract-type Comet \
* --major 1 --minor 0 --patch 0 \
* --audit-report-url "https://audits.firm.com/report.pdf"
*/

/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access,
@typescript-eslint/no-non-null-assertion, @typescript-eslint/restrict-template-expressions,
@typescript-eslint/use-unknown-in-catch-callback-variable */

import { ethers } from "hardhat";
import fs from "fs";
import path from "path";
import { signAuditReport, formatVersion, SignAuditReportParams } from "../utils/signAuditReport";

const HELP_TEXT = `
Audit Report Signing Script — generates EIP-712 signature for bytecode audit reports

The auditor signs off-chain. A developer then submits the signature on-chain
via verifyBytecode() on the VersionController contract.

Required flags:
--contract-type <name> Contract type name (e.g., "Comet", "CometExt")
--major <n> Major version number
--minor <n> Minor version number
--patch <n> Patch version number
--audit-report-url <url> URL to the audit report

Optional flags:
--alternative <label> Alternative version label (default: "")
--version-controller <addr> VersionController address (default: from deployments/)
--help Show this help message

Examples:
# Sign audit for Comet v1.0.0
npx hardhat run scripts/cli/signAuditReport.ts --network ethereum -- \\
--contract-type Comet --major 1 --minor 0 --patch 0 \\
--audit-report-url "https://audits.firm.com/comet-v1.0.0-report.pdf"

# Sign audit for an alternative version
npx hardhat run scripts/cli/signAuditReport.ts --network ethereum -- \\
--contract-type Comet --major 1 --minor 0 --patch 0 --alternative gas-optimized \\
--audit-report-url "https://audits.firm.com/comet-gas-optimized-report.pdf"
`;

interface CliArgs {
contractType?: string;
major?: string;
minor?: string;
patch?: string;
alternative?: string;
auditReportURL?: string;
versionController?: string;
help?: boolean;
}

/**
* Parse CLI arguments. Unknown flags are silently ignored (e.g., --network consumed by Hardhat).
*/
function parseCliArgs(): CliArgs {
const args = process.argv.slice(2);
const result: CliArgs = {};

for (let i = 0; i < args.length; i++) {
const arg = args[i];
const next = args[i + 1];

switch (arg) {
case "--help":
case "-h":
result.help = true;
break;
case "--contract-type":
result.contractType = next;
i++;
break;
case "--major":
result.major = next;
i++;
break;
case "--minor":
result.minor = next;
i++;
break;
case "--patch":
result.patch = next;
i++;
break;
case "--alternative":
result.alternative = next;
i++;
break;
case "--audit-report-url":
result.auditReportURL = next;
i++;
break;
case "--version-controller":
result.versionController = next;
i++;
break;
}
}

return result;
}

/**
* Resolve VersionController address from deployment artifacts or CLI override.
*/
function loadVersionControllerAddress(networkName: string, override?: string): string {
if (override) return override;

const deploymentFile = path.join(process.cwd(), "deployments", networkName, "VersionController.json");
if (fs.existsSync(deploymentFile)) {
const deployment = JSON.parse(fs.readFileSync(deploymentFile, "utf8"));
return deployment.address as string;
}

throw new Error(
`VersionController address not found for network "${networkName}".\n` +
`Either deploy first or provide --version-controller <address>`
);
}

async function main() {
const cliArgs = parseCliArgs();

if (cliArgs.help) {
console.log(HELP_TEXT);
return;
}

// Validate required flags
const missing: string[] = [];
if (!cliArgs.contractType) missing.push("--contract-type");
if (cliArgs.major === undefined) missing.push("--major");
if (cliArgs.minor === undefined) missing.push("--minor");
if (cliArgs.patch === undefined) missing.push("--patch");
if (!cliArgs.auditReportURL) missing.push("--audit-report-url");

if (missing.length > 0) {
console.error(`Missing required flags: ${missing.join(", ")}`);
console.error("Run with --help for usage information.");
process.exit(1);
}

console.log("AUDIT REPORT SIGNING");
console.log("═".repeat(60));

// Network and signer info
const networkInfo = await ethers.provider.getNetwork();
const [signer] = await ethers.getSigners();

console.log(`Network: ${networkInfo.name} (Chain ID: ${networkInfo.chainId})`);
console.log(`Auditor: ${signer.address}`);
console.log("");

// Resolve VersionController address
const vcAddress = loadVersionControllerAddress(networkInfo.name, cliArgs.versionController);
console.log(`VersionController: ${vcAddress}`);
console.log("");

// Build params
const params: SignAuditReportParams = {
contractType: cliArgs.contractType!,
major: Number(cliArgs.major),
minor: Number(cliArgs.minor),
patch: Number(cliArgs.patch),
alternative: cliArgs.alternative ?? "",
auditReportURL: cliArgs.auditReportURL!
};

// Pre-flight summary
console.log("Signing Summary:");
console.log("─".repeat(40));
console.log(` Contract Type: ${params.contractType}`);
console.log(` Version: ${formatVersion(params)}`);
console.log(` Audit Report URL: ${params.auditReportURL}`);
console.log("");

// Get contract instance and sign
const versionController = await ethers.getContractAt("VersionController", vcAddress);

console.log("Validating auditor role...");
console.log("Retrieving on-chain bytecode data...");
const result = await signAuditReport(versionController, signer, params);

console.log("");
console.log("Signature generated successfully!");
console.log("═".repeat(60));
console.log("");
console.log("Signature Output:");
console.log("─".repeat(40));
console.log(` Signature: ${result.signature}`);
console.log("");
console.log("Metadata:");
console.log(` Auditor: ${result.auditorAddress}`);
console.log(` Contract Type: ${result.contractType} (${result.contractTypeBytes32})`);
console.log(` Version: ${result.version}`);
console.log(` Bytecode Version Hash: ${result.bytecodeVersionHash}`);
console.log(` Init Code Hash: ${result.initCodeHash}`);
console.log(` Audit Report URL: ${result.auditReportURL}`);
console.log("");
console.log("Next step: A developer submits this signature on-chain:");
console.log(" versionController.verifyBytecode(bytecodeVersion, auditReportURL, signature)");
console.log("");
console.log("═".repeat(60));
}

// Handle unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
process.exit(1);
});

if (require.main === module) {
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("\nSigning failed:", error);
process.exit(1);
});
}

export default main;
Loading