From edc3e308e8985c15114c6da5d28590943cb92e99 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 2 Nov 2025 19:33:32 +0100 Subject: [PATCH 1/7] feat: add minimal diamond contract with test setup for benchmarking --- test/benchmark/Benchmark.t.sol | 35 ++++++++++++++++++++++++++++++ test/benchmark/MinimalDiamond.sol | 36 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/benchmark/Benchmark.t.sol create mode 100644 test/benchmark/MinimalDiamond.sol diff --git a/test/benchmark/Benchmark.t.sol b/test/benchmark/Benchmark.t.sol new file mode 100644 index 00000000..5e72b49d --- /dev/null +++ b/test/benchmark/Benchmark.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {MinimalDiamond} from "./MinimalDiamond.sol"; +import {LibDiamond} from "../../src/diamond/LibDiamond.sol"; +import {DiamondLoupeFacet} from "../../src/diamond/DiamondLoupeFacet.sol"; + +contract LoupeGasBenchmarkTest is Test { + MinimalDiamond internal diamond; + DiamondLoupeFacet internal loupe; + + function setUp() public { + loupe = new DiamondLoupeFacet(); + diamond = new MinimalDiamond(); + + bytes4[] memory loupeSelectors = new bytes4[](4); + loupeSelectors[0] = DiamondLoupeFacet.facets.selector; + loupeSelectors[1] = DiamondLoupeFacet.facetFunctionSelectors.selector; + loupeSelectors[2] = DiamondLoupeFacet.facetAddresses.selector; + loupeSelectors[3] = DiamondLoupeFacet.facetAddress.selector; + + LibDiamond.FacetCut[] memory dc = new LibDiamond.FacetCut[](1); + + dc[0] = LibDiamond.FacetCut({ + facetAddress: address(loupe), + action: LibDiamond.FacetCutAction.Add, + functionSelectors: loupeSelectors + }); + + MinimalDiamond.DiamondArgs memory args = MinimalDiamond.DiamondArgs({init: address(0), initCalldata: ""}); + + diamond.initialize(dc, args); + } +} diff --git a/test/benchmark/MinimalDiamond.sol b/test/benchmark/MinimalDiamond.sol new file mode 100644 index 00000000..25535581 --- /dev/null +++ b/test/benchmark/MinimalDiamond.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {LibDiamond} from "../../src/diamond/LibDiamond.sol"; + +// Adapted from: https://github.com/mudgen/diamond-1-hardhat/blob/main/contracts/Diamond.sol + +contract MinimalDiamond { + error FunctionNotFound(bytes4 selector); + + struct DiamondArgs { + address init; + bytes initCalldata; + } + + function initialize(LibDiamond.FacetCut[] calldata _diamondCut, DiamondArgs calldata _args) public payable { + LibDiamond.diamondCut(_diamondCut, _args.init, _args.initCalldata); + } + + fallback() external payable { + LibDiamond.DiamondStorage storage s = LibDiamond.getStorage(); + address facet = s.facetAndPosition[msg.sig].facet; + if (facet == address(0)) revert FunctionNotFound(msg.sig); + + assembly { + calldatacopy(0, 0, calldatasize()) + let ok := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch ok + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + receive() external payable {} +} From e55bd28cf56fe009231faf5811b19135d39864ad Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 2 Nov 2025 19:42:45 +0100 Subject: [PATCH 2/7] test: add test utils for setting FacetAndPosition storage --- test/benchmark/Benchmark.t.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/benchmark/Benchmark.t.sol b/test/benchmark/Benchmark.t.sol index 5e72b49d..5ad4b051 100644 --- a/test/benchmark/Benchmark.t.sol +++ b/test/benchmark/Benchmark.t.sol @@ -10,6 +10,8 @@ contract LoupeGasBenchmarkTest is Test { MinimalDiamond internal diamond; DiamondLoupeFacet internal loupe; + bytes32 internal constant DIAMOND_STORAGE_POSITION = keccak256("compose.diamond"); + function setUp() public { loupe = new DiamondLoupeFacet(); diamond = new MinimalDiamond(); @@ -32,4 +34,16 @@ contract LoupeGasBenchmarkTest is Test { diamond.initialize(dc, args); } + + function _facetAndPositionsSlot(bytes4 selector) internal pure returns (bytes32) { + return keccak256(abi.encode(selector, DIAMOND_STORAGE_POSITION)); + } + + function _packFacetAndPosition(address facet, uint16 position) internal pure returns (bytes32) { + return bytes32((uint256(uint160(facet))) | (uint256(position) << 160)); + } + + function _storeFacetAndPosition(address account, bytes4 selector, address facet, uint16 position) internal { + vm.store(account, _facetAndPositionsSlot(selector), _packFacetAndPosition(facet, position)); + } } From 6a481804e86185c203930dd3edf0e9361f4e0cf8 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 2 Nov 2025 20:29:00 +0100 Subject: [PATCH 3/7] test: add test utils for setting packed selectors --- test/benchmark/Benchmark.t.sol | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/benchmark/Benchmark.t.sol b/test/benchmark/Benchmark.t.sol index 5ad4b051..bb18bab9 100644 --- a/test/benchmark/Benchmark.t.sol +++ b/test/benchmark/Benchmark.t.sol @@ -35,6 +35,10 @@ contract LoupeGasBenchmarkTest is Test { diamond.initialize(dc, args); } + /*////////////////////////////////////////////////////////////// + STORAGE HELPERS + //////////////////////////////////////////////////////////////*/ + function _facetAndPositionsSlot(bytes4 selector) internal pure returns (bytes32) { return keccak256(abi.encode(selector, DIAMOND_STORAGE_POSITION)); } @@ -46,4 +50,28 @@ contract LoupeGasBenchmarkTest is Test { function _storeFacetAndPosition(address account, bytes4 selector, address facet, uint16 position) internal { vm.store(account, _facetAndPositionsSlot(selector), _packFacetAndPosition(facet, position)); } + + function _selectorsLengthSlot() internal pure returns (bytes32) { + return bytes32(uint256(DIAMOND_STORAGE_POSITION) + 1); + } + + function _selectorsDataBase() internal pure returns (bytes32) { + return keccak256(abi.encode(uint256(DIAMOND_STORAGE_POSITION) + 1)); + } + + function _storeSelectorAtIndex(address account, bytes4 selector, uint256 index) internal { + bytes32 base = _selectorsDataBase(); + uint256 packedWordIndex = index / 8; + uint256 laneIndex = index % 8; + bytes32 packedWordSlot = bytes32(uint256(base) + packedWordIndex); + + bytes32 oldPackedWord = vm.load(account, packedWordSlot); + + uint256 laneShiftBits = laneIndex * 32; + uint256 clearLaneMask = ~(uint256(0xffffffff) << laneShiftBits); + uint256 laneInsertBits = (uint256(uint32(selector)) << laneShiftBits); + uint256 newPackedWord = (uint256(oldPackedWord) & clearLaneMask) | laneInsertBits; + + vm.store(account, packedWordSlot, bytes32(newPackedWord)); + } } From 5ecaad19755712bee1e18f31868c0d6fa6660749 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 2 Nov 2025 20:41:54 +0100 Subject: [PATCH 4/7] test: build complex diamond helpers --- test/benchmark/Benchmark.t.sol | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/benchmark/Benchmark.t.sol b/test/benchmark/Benchmark.t.sol index bb18bab9..a23fcb2b 100644 --- a/test/benchmark/Benchmark.t.sol +++ b/test/benchmark/Benchmark.t.sol @@ -11,6 +11,8 @@ contract LoupeGasBenchmarkTest is Test { DiamondLoupeFacet internal loupe; bytes32 internal constant DIAMOND_STORAGE_POSITION = keccak256("compose.diamond"); + uint256 internal constant NUM_FACETS = 200; + uint256 internal constant SELECTORS_PER_FACET = 10; function setUp() public { loupe = new DiamondLoupeFacet(); @@ -33,6 +35,8 @@ contract LoupeGasBenchmarkTest is Test { MinimalDiamond.DiamondArgs memory args = MinimalDiamond.DiamondArgs({init: address(0), initCalldata: ""}); diamond.initialize(dc, args); + + _buildDiamond(address(diamond), NUM_FACETS, SELECTORS_PER_FACET); } /*////////////////////////////////////////////////////////////// @@ -74,4 +78,37 @@ contract LoupeGasBenchmarkTest is Test { vm.store(account, packedWordSlot, bytes32(newPackedWord)); } + + /*////////////////////////////////////////////////////////////// + DATA HELPERS + //////////////////////////////////////////////////////////////*/ + + function _buildDiamond(address account, uint256 nFacets, uint256 perFacet) internal { + uint256 total = nFacets * perFacet; + vm.store(account, _selectorsLengthSlot(), bytes32(total)); + + uint256 globalIndex = 4; + for (uint256 f = 0; f < nFacets; f++) { + address facet = _facetAddr(f); + for (uint16 j = 0; j < perFacet; j++) { + bytes4 selector = _selectorFor(f, j); + + _storeSelectorAtIndex(account, selector, globalIndex); + + _storeFacetAndPosition(account, selector, facet, j); + + unchecked { + ++globalIndex; + } + } + } + } + + function _facetAddr(uint256 f) internal returns (address) { + return makeAddr(string.concat("facet ", vm.toString(f))); + } + + function _selectorFor(uint256 f, uint16 j) internal pure returns (bytes4) { + return bytes4(keccak256(abi.encodePacked("fn_f_", vm.toString(f), "_idx_", vm.toString(j), "()"))); + } } From c3fbaddc99bee987c784423bcabbc328dc01c5ae Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 2 Nov 2025 21:00:42 +0100 Subject: [PATCH 5/7] test: add initial benchmarks --- test/benchmark/Benchmark.t.sol | 48 +++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/test/benchmark/Benchmark.t.sol b/test/benchmark/Benchmark.t.sol index a23fcb2b..3a53cd1e 100644 --- a/test/benchmark/Benchmark.t.sol +++ b/test/benchmark/Benchmark.t.sol @@ -13,12 +13,14 @@ contract LoupeGasBenchmarkTest is Test { bytes32 internal constant DIAMOND_STORAGE_POSITION = keccak256("compose.diamond"); uint256 internal constant NUM_FACETS = 200; uint256 internal constant SELECTORS_PER_FACET = 10; + uint256 internal constant NUM_LOUPE_SELECTORS = 4; + uint256 internal constant TOTAL_SELECTORS = NUM_FACETS * SELECTORS_PER_FACET + NUM_LOUPE_SELECTORS; function setUp() public { loupe = new DiamondLoupeFacet(); diamond = new MinimalDiamond(); - bytes4[] memory loupeSelectors = new bytes4[](4); + bytes4[] memory loupeSelectors = new bytes4[](NUM_LOUPE_SELECTORS); loupeSelectors[0] = DiamondLoupeFacet.facets.selector; loupeSelectors[1] = DiamondLoupeFacet.facetFunctionSelectors.selector; loupeSelectors[2] = DiamondLoupeFacet.facetAddresses.selector; @@ -39,6 +41,50 @@ contract LoupeGasBenchmarkTest is Test { _buildDiamond(address(diamond), NUM_FACETS, SELECTORS_PER_FACET); } + /*////////////////////////////////////////////////////////////// + GAS BENCHMARKS + //////////////////////////////////////////////////////////////*/ + + // Estimated gas: 370_049_638 + function testGas_Loupe_Facets() external { + uint256 startGas = gasleft(); + (bool success, bytes memory data) = address(diamond).call(abi.encodeCall(DiamondLoupeFacet.facets, ())); + emit log_uint(startGas - gasleft()); + + DiamondLoupeFacet.Facet[] memory allFacets = abi.decode(data, (DiamondLoupeFacet.Facet[])); + assertEq(allFacets.length, NUM_FACETS + 1); // plus Loupe + } + + // Estimated gas: 5_889_482 + function testGas_Loupe_FacetFunctionSelectors() external { + uint256 startGas = gasleft(); + (bool success, bytes memory data) = + address(diamond).call(abi.encodeCall(DiamondLoupeFacet.facetFunctionSelectors, (address(loupe)))); + emit log_uint(startGas - gasleft()); + + bytes4[] memory facetSelectors = abi.decode(data, (bytes4[])); + assertEq(facetSelectors.length, NUM_LOUPE_SELECTORS); + } + + // Estimated gas: 31_606_629 + function testGas_Loupe_FacetAddresses() external { + uint256 startGas = gasleft(); + (bool success, bytes memory data) = address(diamond).call(abi.encodeCall(DiamondLoupeFacet.facetAddresses, ())); + emit log_uint(startGas - gasleft()); + + address[] memory allFacets = abi.decode(data, (address[])); + assertEq(allFacets.length, NUM_FACETS + 1); // plus Loupe + } + + // Estimated gas: 12_654 + function testGas_Loupe_FacetAddress() external { + uint256 startGas = gasleft(); + (bool success, bytes memory data) = address(diamond).call( + abi.encodeCall(DiamondLoupeFacet.facetAddress, (DiamondLoupeFacet.facetAddresses.selector)) + ); + emit log_uint(startGas - gasleft()); + } + /*////////////////////////////////////////////////////////////// STORAGE HELPERS //////////////////////////////////////////////////////////////*/ From 28ea33b6caf2930e54df588b595451f670f7440c Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 2 Nov 2025 21:51:09 +0100 Subject: [PATCH 6/7] refactor: abstract base gas loupe benchmarking contract with usage --- test/benchmark/Base.t.sol | 42 +++++++++++ test/benchmark/Benchmark.t.sol | 130 ++++----------------------------- test/benchmark/Optimised.t.sol | 61 ++++++++++++++++ test/benchmark/Utils.sol | 90 +++++++++++++++++++++++ 4 files changed, 207 insertions(+), 116 deletions(-) create mode 100644 test/benchmark/Base.t.sol create mode 100644 test/benchmark/Optimised.t.sol create mode 100644 test/benchmark/Utils.sol diff --git a/test/benchmark/Base.t.sol b/test/benchmark/Base.t.sol new file mode 100644 index 00000000..ede6145a --- /dev/null +++ b/test/benchmark/Base.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Utils} from "./Utils.sol"; + +import {MinimalDiamond} from "./MinimalDiamond.sol"; +import {LibDiamond} from "../../src/diamond/LibDiamond.sol"; +import {DiamondLoupeFacet} from "../../src/diamond/DiamondLoupeFacet.sol"; + +abstract contract BaseBenchmark is Utils { + MinimalDiamond internal diamond; + address internal loupe; + + function setUp() public { + diamond = new MinimalDiamond(); + loupe = _deployLoupe(); + + // Initialize minimal diamond with DiamondLoupeFacet address and selectors. + bytes4[] memory loupeSelectors = new bytes4[](NUM_LOUPE_SELECTORS); + loupeSelectors[0] = SELECTOR_FACETS; + loupeSelectors[1] = SELECTOR_FACET_FUNCTION_SELECTORS; + loupeSelectors[2] = SELECTOR_FACET_ADDRESSES; + loupeSelectors[3] = SELECTOR_FACET_ADDRESS; + + LibDiamond.FacetCut[] memory dc = new LibDiamond.FacetCut[](1); + + dc[0] = LibDiamond.FacetCut({ + facetAddress: loupe, + action: LibDiamond.FacetCutAction.Add, + functionSelectors: loupeSelectors + }); + + MinimalDiamond.DiamondArgs memory args = MinimalDiamond.DiamondArgs({init: address(0), initCalldata: ""}); + + diamond.initialize(dc, args); + + // Initiatlise complex storage for minimal diamond + _buildDiamond(address(diamond), NUM_FACETS, SELECTORS_PER_FACET); + } + + function _deployLoupe() internal virtual returns (address); +} diff --git a/test/benchmark/Benchmark.t.sol b/test/benchmark/Benchmark.t.sol index 3a53cd1e..45c013d3 100644 --- a/test/benchmark/Benchmark.t.sol +++ b/test/benchmark/Benchmark.t.sol @@ -1,44 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.30; -import {Test} from "forge-std/Test.sol"; -import {MinimalDiamond} from "./MinimalDiamond.sol"; -import {LibDiamond} from "../../src/diamond/LibDiamond.sol"; +import {BaseBenchmark} from "./Base.t.sol"; import {DiamondLoupeFacet} from "../../src/diamond/DiamondLoupeFacet.sol"; -contract LoupeGasBenchmarkTest is Test { - MinimalDiamond internal diamond; - DiamondLoupeFacet internal loupe; - - bytes32 internal constant DIAMOND_STORAGE_POSITION = keccak256("compose.diamond"); - uint256 internal constant NUM_FACETS = 200; - uint256 internal constant SELECTORS_PER_FACET = 10; - uint256 internal constant NUM_LOUPE_SELECTORS = 4; - uint256 internal constant TOTAL_SELECTORS = NUM_FACETS * SELECTORS_PER_FACET + NUM_LOUPE_SELECTORS; - - function setUp() public { - loupe = new DiamondLoupeFacet(); - diamond = new MinimalDiamond(); - - bytes4[] memory loupeSelectors = new bytes4[](NUM_LOUPE_SELECTORS); - loupeSelectors[0] = DiamondLoupeFacet.facets.selector; - loupeSelectors[1] = DiamondLoupeFacet.facetFunctionSelectors.selector; - loupeSelectors[2] = DiamondLoupeFacet.facetAddresses.selector; - loupeSelectors[3] = DiamondLoupeFacet.facetAddress.selector; - - LibDiamond.FacetCut[] memory dc = new LibDiamond.FacetCut[](1); - - dc[0] = LibDiamond.FacetCut({ - facetAddress: address(loupe), - action: LibDiamond.FacetCutAction.Add, - functionSelectors: loupeSelectors - }); - - MinimalDiamond.DiamondArgs memory args = MinimalDiamond.DiamondArgs({init: address(0), initCalldata: ""}); - - diamond.initialize(dc, args); +contract LoupeGasBenchmarkTest is BaseBenchmark { + /*////////////////////////////////////////////////////////////// + OVERRIDES + //////////////////////////////////////////////////////////////*/ - _buildDiamond(address(diamond), NUM_FACETS, SELECTORS_PER_FACET); + function _deployLoupe() internal override returns (address) { + return address(new DiamondLoupeFacet()); } /*////////////////////////////////////////////////////////////// @@ -48,18 +20,18 @@ contract LoupeGasBenchmarkTest is Test { // Estimated gas: 370_049_638 function testGas_Loupe_Facets() external { uint256 startGas = gasleft(); - (bool success, bytes memory data) = address(diamond).call(abi.encodeCall(DiamondLoupeFacet.facets, ())); + (bool success, bytes memory data) = address(diamond).call(abi.encodeWithSelector(SELECTOR_FACETS)); emit log_uint(startGas - gasleft()); DiamondLoupeFacet.Facet[] memory allFacets = abi.decode(data, (DiamondLoupeFacet.Facet[])); assertEq(allFacets.length, NUM_FACETS + 1); // plus Loupe } - // Estimated gas: 5_889_482 + // Estimated gas: 5_889_500 function testGas_Loupe_FacetFunctionSelectors() external { uint256 startGas = gasleft(); (bool success, bytes memory data) = - address(diamond).call(abi.encodeCall(DiamondLoupeFacet.facetFunctionSelectors, (address(loupe)))); + address(diamond).call(abi.encodeWithSelector(SELECTOR_FACET_FUNCTION_SELECTORS, loupe)); emit log_uint(startGas - gasleft()); bytes4[] memory facetSelectors = abi.decode(data, (bytes4[])); @@ -69,92 +41,18 @@ contract LoupeGasBenchmarkTest is Test { // Estimated gas: 31_606_629 function testGas_Loupe_FacetAddresses() external { uint256 startGas = gasleft(); - (bool success, bytes memory data) = address(diamond).call(abi.encodeCall(DiamondLoupeFacet.facetAddresses, ())); + (bool success, bytes memory data) = address(diamond).call(abi.encodeWithSelector(SELECTOR_FACET_ADDRESSES)); emit log_uint(startGas - gasleft()); address[] memory allFacets = abi.decode(data, (address[])); assertEq(allFacets.length, NUM_FACETS + 1); // plus Loupe } - // Estimated gas: 12_654 + // Estimated gas: 12_672 function testGas_Loupe_FacetAddress() external { uint256 startGas = gasleft(); - (bool success, bytes memory data) = address(diamond).call( - abi.encodeCall(DiamondLoupeFacet.facetAddress, (DiamondLoupeFacet.facetAddresses.selector)) - ); + (bool success, bytes memory data) = + address(diamond).call(abi.encodeWithSelector(SELECTOR_FACET_ADDRESS, SELECTOR_FACET_ADDRESSES)); emit log_uint(startGas - gasleft()); } - - /*////////////////////////////////////////////////////////////// - STORAGE HELPERS - //////////////////////////////////////////////////////////////*/ - - function _facetAndPositionsSlot(bytes4 selector) internal pure returns (bytes32) { - return keccak256(abi.encode(selector, DIAMOND_STORAGE_POSITION)); - } - - function _packFacetAndPosition(address facet, uint16 position) internal pure returns (bytes32) { - return bytes32((uint256(uint160(facet))) | (uint256(position) << 160)); - } - - function _storeFacetAndPosition(address account, bytes4 selector, address facet, uint16 position) internal { - vm.store(account, _facetAndPositionsSlot(selector), _packFacetAndPosition(facet, position)); - } - - function _selectorsLengthSlot() internal pure returns (bytes32) { - return bytes32(uint256(DIAMOND_STORAGE_POSITION) + 1); - } - - function _selectorsDataBase() internal pure returns (bytes32) { - return keccak256(abi.encode(uint256(DIAMOND_STORAGE_POSITION) + 1)); - } - - function _storeSelectorAtIndex(address account, bytes4 selector, uint256 index) internal { - bytes32 base = _selectorsDataBase(); - uint256 packedWordIndex = index / 8; - uint256 laneIndex = index % 8; - bytes32 packedWordSlot = bytes32(uint256(base) + packedWordIndex); - - bytes32 oldPackedWord = vm.load(account, packedWordSlot); - - uint256 laneShiftBits = laneIndex * 32; - uint256 clearLaneMask = ~(uint256(0xffffffff) << laneShiftBits); - uint256 laneInsertBits = (uint256(uint32(selector)) << laneShiftBits); - uint256 newPackedWord = (uint256(oldPackedWord) & clearLaneMask) | laneInsertBits; - - vm.store(account, packedWordSlot, bytes32(newPackedWord)); - } - - /*////////////////////////////////////////////////////////////// - DATA HELPERS - //////////////////////////////////////////////////////////////*/ - - function _buildDiamond(address account, uint256 nFacets, uint256 perFacet) internal { - uint256 total = nFacets * perFacet; - vm.store(account, _selectorsLengthSlot(), bytes32(total)); - - uint256 globalIndex = 4; - for (uint256 f = 0; f < nFacets; f++) { - address facet = _facetAddr(f); - for (uint16 j = 0; j < perFacet; j++) { - bytes4 selector = _selectorFor(f, j); - - _storeSelectorAtIndex(account, selector, globalIndex); - - _storeFacetAndPosition(account, selector, facet, j); - - unchecked { - ++globalIndex; - } - } - } - } - - function _facetAddr(uint256 f) internal returns (address) { - return makeAddr(string.concat("facet ", vm.toString(f))); - } - - function _selectorFor(uint256 f, uint16 j) internal pure returns (bytes4) { - return bytes4(keccak256(abi.encodePacked("fn_f_", vm.toString(f), "_idx_", vm.toString(j), "()"))); - } } diff --git a/test/benchmark/Optimised.t.sol b/test/benchmark/Optimised.t.sol new file mode 100644 index 00000000..58ebc53c --- /dev/null +++ b/test/benchmark/Optimised.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {BaseBenchmark} from "./Base.t.sol"; +import {DiamondLoupeFacet} from "../../src/diamond/DiamondLoupeFacet.sol"; + +/// @dev Please override `_deployLoupe` with the deployment of your optimised contract. +contract OptimisedLoupeBenchmarkTest is BaseBenchmark { + /*////////////////////////////////////////////////////////////// + OVERRIDES + //////////////////////////////////////////////////////////////*/ + + /// @dev Override with a deployment of your optimised diamond loupe contract. + function _deployLoupe() internal override returns (address) { + return address(new DiamondLoupeFacet()); + } + + /*////////////////////////////////////////////////////////////// + GAS BENCHMARKS + //////////////////////////////////////////////////////////////*/ + + struct Facet { + address facet; + bytes4[] functionSelectors; + } + + function testGas_Loupe_Facets() external { + uint256 startGas = gasleft(); + (bool success, bytes memory data) = address(diamond).call(abi.encodeWithSelector(SELECTOR_FACETS)); + emit log_uint(startGas - gasleft()); + + Facet[] memory allFacets = abi.decode(data, (Facet[])); + assertEq(allFacets.length, NUM_FACETS + 1); // plus Loupe + } + + function testGas_Loupe_FacetFunctionSelectors() external { + uint256 startGas = gasleft(); + (bool success, bytes memory data) = + address(diamond).call(abi.encodeWithSelector(SELECTOR_FACET_FUNCTION_SELECTORS, loupe)); + emit log_uint(startGas - gasleft()); + + bytes4[] memory facetSelectors = abi.decode(data, (bytes4[])); + assertEq(facetSelectors.length, NUM_LOUPE_SELECTORS); + } + + function testGas_Loupe_FacetAddresses() external { + uint256 startGas = gasleft(); + (bool success, bytes memory data) = address(diamond).call(abi.encodeWithSelector(SELECTOR_FACET_ADDRESSES)); + emit log_uint(startGas - gasleft()); + + address[] memory allFacets = abi.decode(data, (address[])); + assertEq(allFacets.length, NUM_FACETS + 1); // plus Loupe + } + + function testGas_Loupe_FacetAddress() external { + uint256 startGas = gasleft(); + (bool success, bytes memory data) = + address(diamond).call(abi.encodeWithSelector(SELECTOR_FACET_ADDRESS, SELECTOR_FACET_ADDRESSES)); + emit log_uint(startGas - gasleft()); + } +} diff --git a/test/benchmark/Utils.sol b/test/benchmark/Utils.sol new file mode 100644 index 00000000..5ba52fef --- /dev/null +++ b/test/benchmark/Utils.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; + +contract Utils is Test { + bytes32 internal constant DIAMOND_STORAGE_POSITION = keccak256("compose.diamond"); + bytes4 constant SELECTOR_FACETS = bytes4(keccak256("facets()")); + bytes4 constant SELECTOR_FACET_FUNCTION_SELECTORS = bytes4(keccak256("facetFunctionSelectors(address)")); + bytes4 constant SELECTOR_FACET_ADDRESSES = bytes4(keccak256("facetAddresses()")); + bytes4 constant SELECTOR_FACET_ADDRESS = bytes4(keccak256("facetAddress(bytes4)")); + + uint256 internal constant NUM_FACETS = 200; + uint256 internal constant SELECTORS_PER_FACET = 10; + uint256 internal constant NUM_LOUPE_SELECTORS = 4; + uint256 internal constant TOTAL_SELECTORS = NUM_FACETS * SELECTORS_PER_FACET + NUM_LOUPE_SELECTORS; + + /*////////////////////////////////////////////////////////////// + STORAGE HELPERS + //////////////////////////////////////////////////////////////*/ + + function _facetAndPositionsSlot(bytes4 selector) internal pure returns (bytes32) { + return keccak256(abi.encode(selector, DIAMOND_STORAGE_POSITION)); + } + + function _packFacetAndPosition(address facet, uint16 position) internal pure returns (bytes32) { + return bytes32((uint256(uint160(facet))) | (uint256(position) << 160)); + } + + function _storeFacetAndPosition(address account, bytes4 selector, address facet, uint16 position) internal { + vm.store(account, _facetAndPositionsSlot(selector), _packFacetAndPosition(facet, position)); + } + + function _selectorsLengthSlot() internal pure returns (bytes32) { + return bytes32(uint256(DIAMOND_STORAGE_POSITION) + 1); + } + + function _selectorsDataBase() internal pure returns (bytes32) { + return keccak256(abi.encode(uint256(DIAMOND_STORAGE_POSITION) + 1)); + } + + function _storeSelectorAtIndex(address account, bytes4 selector, uint256 index) internal { + bytes32 base = _selectorsDataBase(); + uint256 packedWordIndex = index / 8; + uint256 laneIndex = index % 8; + bytes32 packedWordSlot = bytes32(uint256(base) + packedWordIndex); + + bytes32 oldPackedWord = vm.load(account, packedWordSlot); + + uint256 laneShiftBits = laneIndex * 32; + uint256 clearLaneMask = ~(uint256(0xffffffff) << laneShiftBits); + uint256 laneInsertBits = (uint256(uint32(selector)) << laneShiftBits); + uint256 newPackedWord = (uint256(oldPackedWord) & clearLaneMask) | laneInsertBits; + + vm.store(account, packedWordSlot, bytes32(newPackedWord)); + } + + /*////////////////////////////////////////////////////////////// + DATA HELPERS + //////////////////////////////////////////////////////////////*/ + + function _buildDiamond(address account, uint256 nFacets, uint256 perFacet) internal { + uint256 total = nFacets * perFacet; + vm.store(account, _selectorsLengthSlot(), bytes32(total)); + + uint256 globalIndex = 4; + for (uint256 f = 0; f < nFacets; f++) { + address facet = _facetAddr(f); + for (uint16 j = 0; j < perFacet; j++) { + bytes4 selector = _selectorFor(f, j); + + _storeSelectorAtIndex(account, selector, globalIndex); + + _storeFacetAndPosition(account, selector, facet, j); + + unchecked { + ++globalIndex; + } + } + } + } + + function _facetAddr(uint256 f) internal returns (address) { + return makeAddr(string.concat("facet ", vm.toString(f))); + } + + function _selectorFor(uint256 f, uint16 j) internal pure returns (bytes4) { + return bytes4(keccak256(abi.encodePacked("fn_f_", vm.toString(f), "_idx_", vm.toString(j), "()"))); + } +} From 11617893549997389bf53369ab2569a390c3fa89 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 2 Nov 2025 21:51:27 +0100 Subject: [PATCH 7/7] chore: forge fmt --- src/token/ERC1155/ERC1155Facet.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/token/ERC1155/ERC1155Facet.sol b/src/token/ERC1155/ERC1155Facet.sol index 91c50523..71591955 100644 --- a/src/token/ERC1155/ERC1155Facet.sol +++ b/src/token/ERC1155/ERC1155Facet.sol @@ -257,9 +257,8 @@ contract ERC1155Facet { emit TransferSingle(msg.sender, _from, _to, _id, _value); if (_to.code.length > 0) { - try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns ( - bytes4 response - ) { + try IERC1155Receiver(_to).onERC1155Received(msg.sender, _from, _id, _value, _data) returns (bytes4 response) + { if (response != IERC1155Receiver.onERC1155Received.selector) { revert ERC1155InvalidReceiver(_to); }