Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ forge build

## Running Tests

Tests use FFI for selector extraction, so the `--ffi` flag is required:

```sh
forge test --ffi -vvv
forge test -vvv
```

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

## Guidelines

- Keep gas efficiency in mind — this library is optimized for minimal overhead.
- Follow existing code style and naming conventions.
- One logical change per PR — avoid bundling unrelated changes.
- 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.
- Include test cases for both success and failure paths.

## Reporting Issues
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ remappings = [
]
optimizer_runs = 1_000_000
via_ir = false
ffi = true
ffi = false

[profile.ci]
optimizer_runs = 1_000_000
Expand Down
15 changes: 8 additions & 7 deletions script/DeployDiamond.s.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

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

// Add DiamondLoupeFacet to the cut list
cut[1] = FacetCut({
facetAddress: address(diamondLoupeFacet),
action: FacetCutAction.Add,
functionSelectors: _getSelectors("DiamondLoupeFacet")
functionSelectors: Selectors.decode(IFacet(address(diamondLoupeFacet)).exportSelectors())
});

// Add ERC165Facet to the cut list
cut[2] = FacetCut({
facetAddress: address(erc165Facet),
action: FacetCutAction.Add,
functionSelectors: _getSelectors("ERC165Facet")
functionSelectors: Selectors.decode(IFacet(address(erc165Facet)).exportSelectors())
});

// Add OwnableFacet to the cut list
cut[3] = FacetCut({
facetAddress: address(ownableFacet),
action: FacetCutAction.Add,
functionSelectors: _getSelectors("OwnableFacet")
functionSelectors: Selectors.decode(IFacet(address(ownableFacet)).exportSelectors())
});

// Deploy the Diamond contract with the facets and initialization args
Expand Down
8 changes: 7 additions & 1 deletion src/facets/DiamondCutFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.20;

import {IDiamondCut} from "@diamond/interfaces/IDiamondCut.sol";
import {IFacet} from "@diamond/interfaces/IFacet.sol";
import {DiamondLib, FacetCut} from "@diamond/libraries/DiamondLib.sol";
import {OwnableLib} from "@diamond/libraries/OwnableLib.sol";

Expand All @@ -13,7 +14,7 @@ import {OwnableLib} from "@diamond/libraries/OwnableLib.sol";
/// @dev Note:
/// Remember to add the loupe functions from DiamondLoupeFacet to the diamond.
/// The loupe functions are required by the EIP2535 Diamonds standard.
contract DiamondCutFacet is IDiamondCut {
contract DiamondCutFacet is IDiamondCut, IFacet {
/// @notice Add/replace/remove any number of functions and optionally execute
/// a function with delegatecall
/// @param _diamondCut Contains the facet addresses and function selectors
Expand All @@ -26,4 +27,9 @@ contract DiamondCutFacet is IDiamondCut {
// Call the diamond cut function from the library
DiamondLib.diamondCut(_diamondCut, _init, _calldata);
}

/// @inheritdoc IFacet
function exportSelectors() external pure returns (bytes memory selectors_) {
selectors_ = abi.encodePacked(this.diamondCut.selector);
}
}
13 changes: 12 additions & 1 deletion src/facets/DiamondLoupeFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.20;

import {IDiamondLoupe} from "@diamond/interfaces/IDiamondLoupe.sol";
import {IFacet} from "@diamond/interfaces/IFacet.sol";
import {DiamondLib, DiamondStorage, Facet} from "@diamond/libraries/DiamondLib.sol";

/// @title DiamondLoupeFacet
Expand All @@ -10,7 +11,7 @@ import {DiamondLib, DiamondStorage, Facet} from "@diamond/libraries/DiamondLib.s
/// @author Modified by David Dada <[email protected]> (https://github.com/dadadave80)
///
/// @dev Implements the IDiamondLoupe interface as defined in EIP-2535
contract DiamondLoupeFacet is IDiamondLoupe {
contract DiamondLoupeFacet is IDiamondLoupe, IFacet {
/// @notice Gets all facet addresses and their function selectors.
/// @return facets_ Facet
function facets() external view returns (Facet[] memory facets_) {
Expand Down Expand Up @@ -44,4 +45,14 @@ contract DiamondLoupeFacet is IDiamondLoupe {
function facetAddress(bytes4 _functionSelector) external view override returns (address) {
return DiamondLib.diamondStorage().selectorToFacetAndPosition[_functionSelector].facetAddress;
}

/// @inheritdoc IFacet
function exportSelectors() external pure returns (bytes memory selectors_) {
selectors_ = abi.encodePacked(
this.facets.selector,
this.facetFunctionSelectors.selector,
this.facetAddresses.selector,
this.facetAddress.selector
);
}
}
8 changes: 7 additions & 1 deletion src/facets/ERC165Facet.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IFacet} from "@diamond/interfaces/IFacet.sol";
import {ERC165Lib} from "@diamond/libraries/ERC165Lib.sol";

/// @title ERC165Facet
/// @notice Diamond facet that implements the ERC-165 standard interface detection
/// @author David Dada <[email protected]> (https://github.com/dadadave80)
/// @dev Delegates to ERC165Lib which hardcodes support for ERC-165, ERC-173, IDiamondCut, and IDiamondLoupe
contract ERC165Facet {
contract ERC165Facet is IFacet {
/// @notice Query if a contract implements an interface
/// @param _interfaceId The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
Expand All @@ -17,4 +18,9 @@ contract ERC165Facet {
function supportsInterface(bytes4 _interfaceId) external view returns (bool) {
return ERC165Lib.supportsInterface(_interfaceId);
}

/// @inheritdoc IFacet
function exportSelectors() external pure returns (bytes memory selectors_) {
selectors_ = abi.encodePacked(this.supportsInterface.selector);
}
}
20 changes: 19 additions & 1 deletion src/facets/OwnableFacet.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IFacet} from "@diamond/interfaces/IFacet.sol";
import {OwnableLib} from "@diamond/libraries/OwnableLib.sol";

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

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERC-8153 SELF-DESCRIPTION */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @inheritdoc IFacet
function exportSelectors() external pure returns (bytes memory selectors_) {
selectors_ = abi.encodePacked(
this.transferOwnership.selector,
this.renounceOwnership.selector,
this.requestOwnershipHandover.selector,
this.cancelOwnershipHandover.selector,
this.completeOwnershipHandover.selector,
this.owner.selector,
this.ownershipHandoverExpiresAt.selector
);
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* MODIFIERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down
24 changes: 24 additions & 0 deletions src/interfaces/IFacet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

import {Selectors} from "@diamond-test/helpers/Selectors.sol";
import {Utils} from "@diamond-test/helpers/Utils.sol";
import {DeployedDiamondState} from "@diamond-test/states/DeployedDiamondState.sol";
import {IDiamondCut} from "@diamond/interfaces/IDiamondCut.sol";
import {IDiamondLoupe} from "@diamond/interfaces/IDiamondLoupe.sol";
import {IFacet} from "@diamond/interfaces/IFacet.sol";
import {Facet} from "@diamond/libraries/DiamondLib.sol";

/// @title DiamondTester
Expand Down Expand Up @@ -36,12 +38,12 @@ contract DiamondTester is DeployedDiamondState {
/* LOUPE — Inspecting the Cut */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @notice Every expected selector is registered under its facet
function testLoupe_SelectorsAreComplete() public {
/// @notice Every selector a facet self-reports (ERC-8153) is registered under that facet
function testLoupe_SelectorsAreComplete() public view {
for (uint256 i; i < facetAddresses.length; ++i) {
bytes4[] memory fromGenSelectors = _getSelectors(facetNames[i]);
for (uint256 j; j < fromGenSelectors.length; ++j) {
assertEq(facetAddresses[i], diamondLoupe.facetAddress(fromGenSelectors[j]));
bytes4[] memory exported = Selectors.decode(IFacet(facetAddresses[i]).exportSelectors());
for (uint256 j; j < exported.length; ++j) {
assertEq(facetAddresses[i], diamondLoupe.facetAddress(exported[j]));
}
}
}
Expand Down
Loading
Loading