-
Notifications
You must be signed in to change notification settings - Fork 52
Add ERC-6909 implementations #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Coverage Report
Last updated: Mon, 03 Nov 2025 10:53:47 GMT for commit |
Gas ReportNo gas usage changes detected between All functions maintain the same gas costs. ✅ Last updated: Mon, 03 Nov 2025 10:54:08 GMT for commit |
|
Great! Will look at this. I am glad to have an ERC-6909 implementation. |
|
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). |
|
@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. |
mudgen
left a comment
There was a problem hiding this 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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use braces here please.
There was a problem hiding this 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.solproviding internal functions (mint, burn, transfer, approve, setOperator) for ERC-6909 token operations - Added
ERC6909Facet.solexposing 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; |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
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
| if (allowed != type(uint256).max) s.allowance[_from][_by][_id] = allowed - _amount; | |
| 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; |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
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
| 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; | |
| } |
|
|
||
| /// @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. |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
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).
| /// @dev Uses ERC-8042 for storage location standardization and ERC-6093 for error conventions. | |
| /// @dev Uses ERC-8042 for storage location standardization. |
| /// @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 | ||
| } | ||
| } |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/
Summary
Add
ERC6909FacetandLibERC6909implementations 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.solLibERC6909.solChecklist
Before submitting this PR, please ensure:
Code follows the Solidity feature ban - No inheritance, constructors, modifiers, public/private variables, external library functions,
using fordirectives, orselfdestructCode 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 fmtTests are included - All new functionality has comprehensive tests
All tests pass - Run
forge testand ensure everything worksDocumentation 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.