Skip to content

Commit 58de958

Browse files
authored
Merge pull request #29 from dadadave80/feat/erc-8153-export-selectors
feat: adopt ERC-8153 exportSelectors for on-chain facet self-description
2 parents a765d38 + 696ad3d commit 58de958

16 files changed

Lines changed: 330 additions & 65 deletions

CONTRIBUTING.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ forge build
1818

1919
## Running Tests
2020

21-
Tests use FFI for selector extraction, so the `--ffi` flag is required:
22-
2321
```sh
24-
forge test --ffi -vvv
22+
forge test -vvv
2523
```
2624

2725
## Making Changes
@@ -34,14 +32,15 @@ forge test --ffi -vvv
3432
- `src/initializers/` — Initializer contracts
3533
3. Add or update tests in `test/` to cover your changes.
3634
4. Run `forge fmt` to format your code.
37-
5. Ensure all tests pass with `forge test --ffi`.
35+
5. Ensure all tests pass with `forge test`.
3836
6. Open a pull request against `main`.
3937

4038
## Guidelines
4139

4240
- Keep gas efficiency in mind — this library is optimized for minimal overhead.
4341
- Follow existing code style and naming conventions.
4442
- One logical change per PR — avoid bundling unrelated changes.
43+
- Selectors are self-reported on-chain via ERC-8153 `exportSelectors()`. When you add an external/public function to a facet, you MUST also add its selector to that facet's `exportSelectors()`, extend the expected set in `test/ExportSelectorsTester.t.sol`, and exercise it through the diamond with a routed-call test. A function omitted from `exportSelectors()` will not be cut in or routed — and no test will fail unless you add one.
4544
- Include test cases for both success and failure paths.
4645

4746
## Reporting Issues

foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ remappings = [
99
]
1010
optimizer_runs = 1_000_000
1111
via_ir = false
12-
ffi = true
12+
ffi = false
1313

1414
[profile.ci]
1515
optimizer_runs = 1_000_000

script/DeployDiamond.s.sol

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.20;
33

4-
import {GetSelectors} from "@diamond-test/helpers/GetSelectors.sol";
4+
import {Selectors} from "@diamond-test/helpers/Selectors.sol";
55
import {MockDiamond} from "@diamond-test/mocks/MockDiamond.sol";
66
import {DiamondCutFacet} from "@diamond/facets/DiamondCutFacet.sol";
77
import {DiamondLoupeFacet} from "@diamond/facets/DiamondLoupeFacet.sol";
@@ -11,6 +11,7 @@ import {DiamondInit} from "@diamond/initializers/DiamondInit.sol";
1111
import {ERC165Init} from "@diamond/initializers/ERC165Init.sol";
1212
import {MultiInit} from "@diamond/initializers/MultiInit.sol";
1313
import {OwnableInit} from "@diamond/initializers/OwnableInit.sol";
14+
import {IFacet} from "@diamond/interfaces/IFacet.sol";
1415
import {ContextLib} from "@diamond/libraries/ContextLib.sol";
1516
import {FacetCut, FacetCutAction} from "@diamond/libraries/DiamondLib.sol";
1617
import {Script} from "forge-std/Script.sol";
@@ -20,7 +21,7 @@ import {Script} from "forge-std/Script.sol";
2021
/// @author David Dada
2122
///
2223
/// @dev Uses Foundry's `Script` and a helper contract to deploy and wire up DiamondCutFacet, DiamondLoupeFacet, and OwnableFacet
23-
contract DeployDiamond is Script, GetSelectors {
24+
contract DeployDiamond is Script {
2425
/// @notice Executes the deployment of the Diamond contract with the initial facets and ERC165 interface setup
2526
/// @dev Broadcasts transactions using Foundry's scripting environment (`vm.startBroadcast()` and `vm.stopBroadcast()`).
2627
/// Deploys three core facets, sets up DiamondArgs, encodes an initializer call, and constructs the Diamond.
@@ -39,32 +40,32 @@ contract DeployDiamond is Script, GetSelectors {
3940
// Create an array of FacetCut entries for standard facets
4041
FacetCut[] memory cut = new FacetCut[](4);
4142

42-
// Add DiamondCutFacet to the cut list
43+
// Add DiamondCutFacet to the cut list (selectors read on-chain via ERC-8153 exportSelectors)
4344
cut[0] = FacetCut({
4445
facetAddress: address(diamondCutFacet),
4546
action: FacetCutAction.Add,
46-
functionSelectors: _getSelectors("DiamondCutFacet")
47+
functionSelectors: Selectors.decode(IFacet(address(diamondCutFacet)).exportSelectors())
4748
});
4849

4950
// Add DiamondLoupeFacet to the cut list
5051
cut[1] = FacetCut({
5152
facetAddress: address(diamondLoupeFacet),
5253
action: FacetCutAction.Add,
53-
functionSelectors: _getSelectors("DiamondLoupeFacet")
54+
functionSelectors: Selectors.decode(IFacet(address(diamondLoupeFacet)).exportSelectors())
5455
});
5556

5657
// Add ERC165Facet to the cut list
5758
cut[2] = FacetCut({
5859
facetAddress: address(erc165Facet),
5960
action: FacetCutAction.Add,
60-
functionSelectors: _getSelectors("ERC165Facet")
61+
functionSelectors: Selectors.decode(IFacet(address(erc165Facet)).exportSelectors())
6162
});
6263

6364
// Add OwnableFacet to the cut list
6465
cut[3] = FacetCut({
6566
facetAddress: address(ownableFacet),
6667
action: FacetCutAction.Add,
67-
functionSelectors: _getSelectors("OwnableFacet")
68+
functionSelectors: Selectors.decode(IFacet(address(ownableFacet)).exportSelectors())
6869
});
6970

7071
// Deploy the Diamond contract with the facets and initialization args

src/facets/DiamondCutFacet.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.20;
33

44
import {IDiamondCut} from "@diamond/interfaces/IDiamondCut.sol";
5+
import {IFacet} from "@diamond/interfaces/IFacet.sol";
56
import {DiamondLib, FacetCut} from "@diamond/libraries/DiamondLib.sol";
67
import {OwnableLib} from "@diamond/libraries/OwnableLib.sol";
78

@@ -13,7 +14,7 @@ import {OwnableLib} from "@diamond/libraries/OwnableLib.sol";
1314
/// @dev Note:
1415
/// Remember to add the loupe functions from DiamondLoupeFacet to the diamond.
1516
/// The loupe functions are required by the EIP2535 Diamonds standard.
16-
contract DiamondCutFacet is IDiamondCut {
17+
contract DiamondCutFacet is IDiamondCut, IFacet {
1718
/// @notice Add/replace/remove any number of functions and optionally execute
1819
/// a function with delegatecall
1920
/// @param _diamondCut Contains the facet addresses and function selectors
@@ -26,4 +27,9 @@ contract DiamondCutFacet is IDiamondCut {
2627
// Call the diamond cut function from the library
2728
DiamondLib.diamondCut(_diamondCut, _init, _calldata);
2829
}
30+
31+
/// @inheritdoc IFacet
32+
function exportSelectors() external pure returns (bytes memory selectors_) {
33+
selectors_ = abi.encodePacked(this.diamondCut.selector);
34+
}
2935
}

src/facets/DiamondLoupeFacet.sol

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.20;
33

44
import {IDiamondLoupe} from "@diamond/interfaces/IDiamondLoupe.sol";
5+
import {IFacet} from "@diamond/interfaces/IFacet.sol";
56
import {DiamondLib, DiamondStorage, Facet} from "@diamond/libraries/DiamondLib.sol";
67

78
/// @title DiamondLoupeFacet
@@ -10,7 +11,7 @@ import {DiamondLib, DiamondStorage, Facet} from "@diamond/libraries/DiamondLib.s
1011
/// @author Modified by David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80)
1112
///
1213
/// @dev Implements the IDiamondLoupe interface as defined in EIP-2535
13-
contract DiamondLoupeFacet is IDiamondLoupe {
14+
contract DiamondLoupeFacet is IDiamondLoupe, IFacet {
1415
/// @notice Gets all facet addresses and their function selectors.
1516
/// @return facets_ Facet
1617
function facets() external view returns (Facet[] memory facets_) {
@@ -44,4 +45,14 @@ contract DiamondLoupeFacet is IDiamondLoupe {
4445
function facetAddress(bytes4 _functionSelector) external view override returns (address) {
4546
return DiamondLib.diamondStorage().selectorToFacetAndPosition[_functionSelector].facetAddress;
4647
}
48+
49+
/// @inheritdoc IFacet
50+
function exportSelectors() external pure returns (bytes memory selectors_) {
51+
selectors_ = abi.encodePacked(
52+
this.facets.selector,
53+
this.facetFunctionSelectors.selector,
54+
this.facetAddresses.selector,
55+
this.facetAddress.selector
56+
);
57+
}
4758
}

src/facets/ERC165Facet.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.20;
33

4+
import {IFacet} from "@diamond/interfaces/IFacet.sol";
45
import {ERC165Lib} from "@diamond/libraries/ERC165Lib.sol";
56

67
/// @title ERC165Facet
78
/// @notice Diamond facet that implements the ERC-165 standard interface detection
89
/// @author David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80)
910
/// @dev Delegates to ERC165Lib which hardcodes support for ERC-165, ERC-173, IDiamondCut, and IDiamondLoupe
10-
contract ERC165Facet {
11+
contract ERC165Facet is IFacet {
1112
/// @notice Query if a contract implements an interface
1213
/// @param _interfaceId The interface identifier, as specified in ERC-165
1314
/// @dev Interface identification is specified in ERC-165. This function
@@ -17,4 +18,9 @@ contract ERC165Facet {
1718
function supportsInterface(bytes4 _interfaceId) external view returns (bool) {
1819
return ERC165Lib.supportsInterface(_interfaceId);
1920
}
21+
22+
/// @inheritdoc IFacet
23+
function exportSelectors() external pure returns (bytes memory selectors_) {
24+
selectors_ = abi.encodePacked(this.supportsInterface.selector);
25+
}
2026
}

src/facets/OwnableFacet.sol

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.20;
33

4+
import {IFacet} from "@diamond/interfaces/IFacet.sol";
45
import {OwnableLib} from "@diamond/libraries/OwnableLib.sol";
56

67
/// @title OwnableRolesFacet
@@ -15,7 +16,7 @@ import {OwnableLib} from "@diamond/libraries/OwnableLib.sol";
1516
/// While the ownable portion follows
1617
/// [EIP-173](https://eips.ethereum.org/EIPS/eip-173) for compatibility,
1718
/// the nomenclature for the 2-step ownership handover may be unique to this codebase.
18-
contract OwnableFacet {
19+
contract OwnableFacet is IFacet {
1920
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
2021
/* PUBLIC UPDATE FUNCTIONS */
2122
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -61,6 +62,23 @@ contract OwnableFacet {
6162
return OwnableLib.ownershipHandoverExpiresAt(_pendingOwner);
6263
}
6364

65+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
66+
/* ERC-8153 SELF-DESCRIPTION */
67+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
68+
69+
/// @inheritdoc IFacet
70+
function exportSelectors() external pure returns (bytes memory selectors_) {
71+
selectors_ = abi.encodePacked(
72+
this.transferOwnership.selector,
73+
this.renounceOwnership.selector,
74+
this.requestOwnershipHandover.selector,
75+
this.cancelOwnershipHandover.selector,
76+
this.completeOwnershipHandover.selector,
77+
this.owner.selector,
78+
this.ownershipHandoverExpiresAt.selector
79+
);
80+
}
81+
6482
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
6583
/* MODIFIERS */
6684
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

src/interfaces/IFacet.sol

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
/// @title IFacet
5+
/// @notice ERC-8153 facet self-description interface.
6+
/// @author David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80)
7+
///
8+
/// @dev Every facet reports its own function selectors on-chain, so upgraders and
9+
/// deployment tooling no longer depend on off-chain selector extraction.
10+
/// See https://eips.ethereum.org/EIPS/eip-8153.
11+
///
12+
/// @custom:security Selectors are SELF-REPORTED by the facet. Only build `FacetCut`s from the
13+
/// `exportSelectors()` of facets whose bytecode you trust (e.g. facets you deployed
14+
/// yourself). A malicious facet can claim selectors it does not implement — including
15+
/// `diamondCut` or ownership selectors — to hijack routing. `Add` cuts fail closed on
16+
/// selector collision, but `Replace` cuts and cuts on a fresh diamond do not.
17+
interface IFacet {
18+
/// @notice Returns this facet's externally-callable function selectors, ABI-packed.
19+
/// @dev The returned length is always a multiple of 4. `exportSelectors` MUST exclude
20+
/// itself: it is called directly on the facet address, never routed through the
21+
/// diamond, and every facet declares it (routing it would collide across facets).
22+
/// @return selectors_ The facet's selectors packed as consecutive 4-byte values.
23+
function exportSelectors() external pure returns (bytes memory selectors_);
24+
}

test/DiamondTester.t.sol

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.20;
33

4+
import {Selectors} from "@diamond-test/helpers/Selectors.sol";
45
import {Utils} from "@diamond-test/helpers/Utils.sol";
56
import {DeployedDiamondState} from "@diamond-test/states/DeployedDiamondState.sol";
67
import {IDiamondCut} from "@diamond/interfaces/IDiamondCut.sol";
78
import {IDiamondLoupe} from "@diamond/interfaces/IDiamondLoupe.sol";
9+
import {IFacet} from "@diamond/interfaces/IFacet.sol";
810
import {Facet} from "@diamond/libraries/DiamondLib.sol";
911

1012
/// @title DiamondTester
@@ -36,12 +38,12 @@ contract DiamondTester is DeployedDiamondState {
3638
/* LOUPE — Inspecting the Cut */
3739
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
3840

39-
/// @notice Every expected selector is registered under its facet
40-
function testLoupe_SelectorsAreComplete() public {
41+
/// @notice Every selector a facet self-reports (ERC-8153) is registered under that facet
42+
function testLoupe_SelectorsAreComplete() public view {
4143
for (uint256 i; i < facetAddresses.length; ++i) {
42-
bytes4[] memory fromGenSelectors = _getSelectors(facetNames[i]);
43-
for (uint256 j; j < fromGenSelectors.length; ++j) {
44-
assertEq(facetAddresses[i], diamondLoupe.facetAddress(fromGenSelectors[j]));
44+
bytes4[] memory exported = Selectors.decode(IFacet(facetAddresses[i]).exportSelectors());
45+
for (uint256 j; j < exported.length; ++j) {
46+
assertEq(facetAddresses[i], diamondLoupe.facetAddress(exported[j]));
4547
}
4648
}
4749
}

0 commit comments

Comments
 (0)