Skip to content

Conversation

@lumoswiz
Copy link
Contributor

@lumoswiz lumoswiz commented Nov 3, 2025

Summary

Add ERC6909Facet and LibERC6909 implementations adhering to ERC-6909.

A minimal styling favouring panic via arithmetic checks in lieu of custom errors was used here. Please see additional notes below for alternative styles.

Testing to be done over the next few days.

Changes Made

Added:

  • ERC6909Facet.sol
  • LibERC6909.sol

Checklist

Before submitting this PR, please ensure:

  • Code follows the Solidity feature ban - No inheritance, constructors, modifiers, public/private variables, external library functions, using for directives, or selfdestruct

  • Code follows Design Principles - Readable, uses diamond storage, favors composition over inheritance

  • Code matches the codebase style - Consistent formatting, documentation, and patterns (e.g. ERC20Facet.sol)

  • Code is formatted with forge fmt

  • Tests are included - All new functionality has comprehensive tests

  • All tests pass - Run forge test and ensure everything works

  • Documentation updated - If applicable, update relevant documentation

Make sure to follow the CONTRIBUTING.md guidelines.

Additional Notes

There are multiple ERC-6909 implementations available, with slightly different styles. See links below:

I have opted for the minimal Uniswap V4/Solmate implementation, however, I am happy to adapt to whatever styling you prefer. Please let me know.

@github-actions
Copy link

github-actions bot commented Nov 3, 2025

Coverage Report

Coverage

Metric Coverage Details
Lines 47% 667/1431 lines
Functions 58% 143/246 functions
Branches 25% 63/253 branches

Last updated: Mon, 03 Nov 2025 10:53:47 GMT for commit 681c035

@github-actions
Copy link

github-actions bot commented Nov 3, 2025

Gas Report

No gas usage changes detected between main and feat/erc6909.

All functions maintain the same gas costs. ✅

Last updated: Mon, 03 Nov 2025 10:54:08 GMT for commit 681c035

@mudgen
Copy link
Contributor

mudgen commented Nov 3, 2025

Great! Will look at this. I am glad to have an ERC-6909 implementation.

@lumoswiz
Copy link
Contributor Author

lumoswiz commented Nov 5, 2025

Forgot to add, after I have finalised the styling and testing of the base ERC-6909, I'll add the extensions (token supply, metadata and content URI).

@mudgen
Copy link
Contributor

mudgen commented Nov 8, 2025

@lumoswiz Just want you to know, I am happy this pull request was submitted, because I want Compose to provide this ERC functionality. I and/or another reviewer will get around to this pull request.

Copy link
Contributor

@mudgen mudgen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great!


if (_by != address(0) && !s.isOperator[_from][_by]) {
uint256 allowed = s.allowance[_from][_by][_id];
if (allowed != type(uint256).max) s.allowance[_from][_by][_id] = allowed - _amount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use braces here, for example:

if (allowed != type(uint256).max)) {
    s.allowance[_from][_by][_id] = allowed - _amount;
}

ERC6909Storage storage s = getStorage();
if (msg.sender != _sender && !s.isOperator[_sender][msg.sender]) {
uint256 allowed = s.allowance[_sender][msg.sender][_id];
if (allowed != type(uint256).max) s.allowance[_sender][msg.sender][_id] = allowed - _amount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use braces here please.

@mudgen mudgen marked this pull request as ready for review November 16, 2025 20:55
Copilot AI review requested due to automatic review settings November 16, 2025 20:55
Copilot finished reviewing on behalf of mudgen November 16, 2025 20:58
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds an ERC-6909 multi-token standard implementation using the diamond storage pattern. The implementation follows a minimal style that favors arithmetic panics over custom errors, adapted from the Uniswap V4/Solmate implementation.

Key changes:

  • Added LibERC6909.sol providing internal functions (mint, burn, transfer, approve, setOperator) for ERC-6909 token operations
  • Added ERC6909Facet.sol exposing the public ERC-6909 interface (balanceOf, allowance, isOperator, transfer, transferFrom, approve, setOperator)

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/token/ERC6909/LibERC6909.sol Library containing ERC-6909 internal functions and diamond storage layout for multi-token operations
src/token/ERC6909/ERC6909Facet.sol Facet implementing the public ERC-6909 interface using diamond storage pattern

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


if (_by != address(0) && !s.isOperator[_from][_by]) {
uint256 allowed = s.allowance[_from][_by][_id];
if (allowed != type(uint256).max) s.allowance[_from][_by][_id] = allowed - _amount;
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one-line if statement violates the project's style guide (STYLE.md section 5), which requires all if statements to use brackets and a newline. The statement should be reformatted as:\nsolidity\nif (allowed != type(uint256).max) {\n s.allowance[_from][_by][_id] = allowed - _amount;\n}\n

Suggested change
if (allowed != type(uint256).max) s.allowance[_from][_by][_id] = allowed - _amount;
if (allowed != type(uint256).max) {
s.allowance[_from][_by][_id] = allowed - _amount;
}

Copilot uses AI. Check for mistakes.
ERC6909Storage storage s = getStorage();
if (msg.sender != _sender && !s.isOperator[_sender][msg.sender]) {
uint256 allowed = s.allowance[_sender][msg.sender][_id];
if (allowed != type(uint256).max) s.allowance[_sender][msg.sender][_id] = allowed - _amount;
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one-line if statement violates the project's style guide (STYLE.md section 5), which requires all if statements to use brackets and a newline. The statement should be reformatted as:\nsolidity\nif (allowed != type(uint256).max) {\n s.allowance[_sender][msg.sender][_id] = allowed - _amount;\n}\n

Suggested change
if (allowed != type(uint256).max) s.allowance[_sender][msg.sender][_id] = allowed - _amount;
if (allowed != type(uint256).max) {
s.allowance[_sender][msg.sender][_id] = allowed - _amount;
}

Copilot uses AI. Check for mistakes.

/// @title LibERC6909 — ERC-6909 Library
/// @notice Provides internal functions and storage layout for ERC-6909 minimal multi-token logic.
/// @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions.
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment claims to use 'ERC-6093 for error conventions', but the implementation does not define or use any custom errors (it relies on arithmetic panics instead). Either remove the ERC-6093 reference from the documentation or add appropriate custom errors as done in other token implementations (see LibERC20.sol for reference).

Suggested change
/// @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions.
/// @dev Uses ERC-8042 for storage location standardization.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +37
/// @notice Emitted when a transfer occurs.
event Transfer(
address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount
);

/// @notice Emitted when an operator is set.
event OperatorSet(address indexed _owner, address indexed _spender, bool _approved);

/// @notice Emitted when an approval occurs.
event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _amount);

/// @dev Storage position determined by the keccak256 hash of the diamond storage identifier.
bytes32 constant STORAGE_POSITION = keccak256("compose.erc6909");

/// @custom:storage-location erc8042:compose.erc6909
struct ERC6909Storage {
mapping(address owner => mapping(uint256 id => uint256 amount)) balanceOf;
mapping(address owner => mapping(address spender => mapping(uint256 id => uint256 amount))) allowance;
mapping(address owner => mapping(address spender => bool)) isOperator;
}

/// @notice Returns a pointer to the ERC-6909 storage struct.
/// @dev Uses inline assembly to access the storage slot defined by STORAGE_POSITION.
/// @return s The ERC6909Storage struct in storage.
function getStorage() internal pure returns (ERC6909Storage storage s) {
bytes32 position = STORAGE_POSITION;
assembly {
s.slot := position
}
}
Copy link

Copilot AI Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] There is significant code duplication between ERC6909Facet.sol and LibERC6909.sol. Both files define the same events (Transfer, OperatorSet, Approval), storage struct (ERC6909Storage), storage position constant (STORAGE_POSITION), and getStorage() function. According to the 'No Imports in Facets' rule (STYLE.md section 1), this duplication is intentional for self-containment. However, consider whether LibERC6909 functions could be used by the facet to reduce this duplication, or document why both implementations need to be completely standalone.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@mudgen mudgen Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I suggest you read the documentation for the project. Specifically the design of compose here: https://compose.diamonds/docs/design/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants