Skip to content
6 changes: 6 additions & 0 deletions .changeset/seven-insects-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nomicfoundation/hardhat-utils": patch
"hardhat": patch
---

Show proxy chain information in --gas-stats and --gas-stats-json output
73 changes: 73 additions & 0 deletions packages/example-project/contracts/Proxies.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {console} from "hardhat/console.sol";

// We need to define two different proxies so that their implementation
// storage slots are different, so we can chain them.
abstract contract BaseProxy {
fallback() external payable {
address impl = getImplementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}

function getImplementation() internal view virtual returns (address);

receive() external payable {}
}

contract Proxy is BaseProxy {
address implementation;

constructor(address _impl) {
// console.log("Setting implementation for Proxy:", _impl);
implementation = _impl;
}

function getImplementation() internal view override returns (address) {
// console.log("Getting implementation for Proxy:", implementation);
return implementation;
}
}

contract Proxy2 is BaseProxy {
// `implementation` occupies slot 0 so that when Proxy's code runs via
// delegatecall in Proxy2's storage context, reading slot 0 returns the
// actual implementation address.
address implementation;
address proxy1;

constructor(address _impl, address _proxy1) {
// console.log("Setting implementation for Proxy2:", _impl);
proxy1 = _proxy1;
implementation = _impl;
Comment thread
schaable marked this conversation as resolved.
}

function getImplementation() internal view override returns (address) {
// console.log("Getting implementation for Proxy2:", proxy1);
return proxy1;
}
}

contract Impl1 {
function one() external returns (uint256) {
return 1;
}
}

contract Impl2 {
function two() external returns (uint256) {
return 2;
}
}
49 changes: 49 additions & 0 deletions packages/example-project/test/contracts/Proxies.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Test} from "forge-std/Test.sol";
import "../../contracts/Proxies.sol";

contract ProxiesTest is Test {
function test_ShouldTrackProxiedCallsToImpl1AndImpl2AsSeparate() public {
Impl1 impl1 = new Impl1();
Impl2 impl2 = new Impl2();

Proxy proxy1 = new Proxy(address(impl1));
Proxy proxy2 = new Proxy(address(impl2));

Impl1 i1 = Impl1(address(proxy1));
Impl2 i2 = Impl2(address(proxy2));

// emit log("Calling Proxy -> Impl1");
i1.one();

// emit log("Calling Proxy -> Impl2");
i2.two();

// emit log("Calling Impl1");
impl1.one();
}

function test_ShouldTrackProxiedCallsToImpl1AsSeparateWithDifferentProxyChains()
public
{
// We use the same impl but different proxy chains
Impl1 impl1 = new Impl1();

Proxy proxy1 = new Proxy(address(impl1));
Proxy proxy2 = new Proxy(address(impl1));

// We use a proxy in front of proxy1
Proxy2 proxy11 = new Proxy2(address(impl1), address(proxy1));

Impl1 i1 = Impl1(address(proxy11));
Impl1 i2 = Impl1(address(proxy2));

// emit log("Calling Proxy2 -> Proxy -> Impl1");
i1.one();

// emit log("Calling Proxy1 -> Impl1");
i2.one();
}
}
49 changes: 49 additions & 0 deletions packages/example-project/test/node/Proxies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it } from "node:test";

import { network } from "hardhat";

describe("Proxies", async function () {
const { viem } = await network.connect();

it("Should track the proxied calls to Impl1 and Impl2 as separate, despite using the same proxy", async function () {
const impl1 = await viem.deployContract("Impl1");
const impl2 = await viem.deployContract("Impl2");

const proxy1 = await viem.deployContract("Proxy", [impl1.address]);
const proxy2 = await viem.deployContract("Proxy", [impl2.address]);

const i1 = await viem.getContractAt("Impl1", proxy1.address);
const i2 = await viem.getContractAt("Impl2", proxy2.address);

// console.log("Calling Proxy -> Impl1");
await i1.write.one();

// console.log("Calling Proxy -> Impl2");
await i2.write.two();

// console.log("Calling Impl1");
await impl1.write.one();
});

it("Should track the proxied calls to Impl1 as separate if they use separate proxy chains", async function () {
// We use the same impl but different proxy chains
const impl1 = await viem.deployContract("Impl1");

const proxy1 = await viem.deployContract("Proxy", [impl1.address]);
const proxy2 = await viem.deployContract("Proxy", [impl1.address]);

// We use a proxy in front of Proxy1
const proxy11 = await viem.deployContract("Proxy2", [
impl1.address,
proxy1.address,
]);

const i1 = await viem.getContractAt("Impl1", proxy11.address);
const i2 = await viem.getContractAt("Impl1", proxy2.address);

// console.log("Calling Proxy2 -> Proxy -> Impl1");
await i1.write.one();
// console.log("Calling Proxy1 -> Impl1");
await i2.write.one();
});
});
10 changes: 10 additions & 0 deletions packages/hardhat-utils/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface TableTitle {
export interface TableSectionHeader {
type: "section-header";
text: string;
subtitle?: string;
}

export interface TableHeader {
Expand Down Expand Up @@ -138,6 +139,15 @@ export function formatTable(items: TableItem[]): string {
tableWidth - 2 + (headerActualLength - headerDisplayWidth),
);
lines.push("║ " + paddedHeader + " ║");
if (current.subtitle !== undefined) {
const subtitleText = " " + current.subtitle;
const subtitleDisplayWidth = getStringWidth(subtitleText);
const subtitleActualLength = subtitleText.length;
const paddedSubtitle = subtitleText.padEnd(
tableWidth - 2 + (subtitleActualLength - subtitleDisplayWidth),
);
lines.push("║ " + paddedSubtitle + " ║");
}
inSection = true;
} else if (current.type === "header") {
const currentCellCount = current.cells.length;
Expand Down
4 changes: 4 additions & 0 deletions packages/hardhat-utils/src/internal/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export function getHeadingWidth(items: TableItem[]): number {
if (item.type === "section-header" || item.type === "title") {
headingWidth = Math.max(headingWidth, getStringWidth(item.text) + 2);
}
if (item.type === "section-header" && item.subtitle !== undefined) {
// +4 accounts for "║ " prefix (2) and " " indent (2)
headingWidth = Math.max(headingWidth, getStringWidth(item.subtitle) + 4);
}
}
return headingWidth;
}
Expand Down
52 changes: 52 additions & 0 deletions packages/hardhat-utils/test/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,57 @@ describe("format", () => {
].join("\n"),
);
});

it("Should render section header with subtitle on a second line", () => {
const result = formatTable([
{
type: "section-header",
text: "Contract Name",
subtitle: "(via Proxy)",
},
{ type: "header", cells: ["Function", "Gas"] },
{ type: "row", cells: ["transfer", "25000"] },
]);

assert.equal(
result,
[
"╔══════════════════╗",
"║ Contract Name ║",
"║ (via Proxy) ║",
"╟──────────┬───────╢",
"║ Function │ Gas ║",
"╟──────────┼───────╢",
"║ transfer │ 25000 ║",
"╚══════════╧═══════╝",
].join("\n"),
);
});

it("Should expand table when subtitle is wider than content", () => {
const result = formatTable([
{
type: "section-header",
text: "Impl",
subtitle: "(via Proxy2 → Proxy1)",
},
{ type: "header", cells: ["A", "B"] },
{ type: "row", cells: ["1", "2"] },
]);

assert.equal(
result,
[
"╔═════════════════════════╗",
"║ Impl ║",
"║ (via Proxy2 → Proxy1) ║",
"╟───┬─────────────────────╢",
"║ A │ B ║",
"╟───┼─────────────────────╢",
"║ 1 │ 2 ║",
"╚═══╧═════════════════════╝",
].join("\n"),
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export {
markTestRunStart,
markTestRunDone,
markTestWorkerDone,
} from "./helpers.js";
} from "./helpers/compat.js";
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import {
parseFullyQualifiedName,
} from "../../../utils/contract-names.js";

import { getUserFqn } from "./gas-analytics-manager.js";
import { formatSectionHeader } from "./helpers.js";
import { formatSectionHeader, getUserFqn } from "./helpers/utils.js";

export const FUNCTION_GAS_SNAPSHOTS_FILE = ".gas-snapshot";

Expand Down
Loading
Loading