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); } 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 new file mode 100644 index 00000000..45c013d3 --- /dev/null +++ b/test/benchmark/Benchmark.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {BaseBenchmark} from "./Base.t.sol"; +import {DiamondLoupeFacet} from "../../src/diamond/DiamondLoupeFacet.sol"; + +contract LoupeGasBenchmarkTest is BaseBenchmark { + /*////////////////////////////////////////////////////////////// + OVERRIDES + //////////////////////////////////////////////////////////////*/ + + function _deployLoupe() internal override returns (address) { + return address(new DiamondLoupeFacet()); + } + + /*////////////////////////////////////////////////////////////// + GAS BENCHMARKS + //////////////////////////////////////////////////////////////*/ + + // Estimated gas: 370_049_638 + 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()); + + DiamondLoupeFacet.Facet[] memory allFacets = abi.decode(data, (DiamondLoupeFacet.Facet[])); + assertEq(allFacets.length, NUM_FACETS + 1); // plus Loupe + } + + // Estimated gas: 5_889_500 + 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); + } + + // Estimated gas: 31_606_629 + 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 + } + + // Estimated gas: 12_672 + 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/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 {} +} 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), "()"))); + } +}