-
Notifications
You must be signed in to change notification settings - Fork 61
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
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
6706f12
feat: add ERC-6909 facet and lib skeleton with NatSpec
lumoswiz 32da46f
feat: implement LibERC6909 functions
lumoswiz 909fa55
feat: implement ERC6909Facet functions
lumoswiz cee5374
refactor: align style with ERC6909Facet, rely on checked arithmetic
lumoswiz 24020b7
style: apply review formatting and drop redundant comment
lumoswiz 3fc7216
chore: standardise ERC6909 folder structure to support extensibility
lumoswiz 6a45727
test: add LibERC6909 harness
lumoswiz df293a6
fix: add missing return statements to getters
lumoswiz f741e53
test: add LibERC6909 mint unit tests
lumoswiz 9bd5502
test: add LibERC6909 burn unit tests
lumoswiz ed2ead2
test: add LibERC6909 approve unit tests
lumoswiz 0ecc745
test: add LibERC6909 setOperator unit tests
lumoswiz 7ebefbc
test: add LibERC6909 transfer unit tests
lumoswiz 34744f2
test: add ERC6909FacetHarness and ERC6909Facet unit tests
lumoswiz a41a18d
test: fix failing ERC6909Facet unit tests
lumoswiz 7ba1fa7
chore: forge fmt
lumoswiz 506b458
chore: forge fmt (after foundryup)
lumoswiz 86b3be0
fix: exempt owner from allowance checks in LibERC6909.transfer & upda…
lumoswiz 11a19ef
refactor: use OZ-like custom errors for ERC6909Facet and fix unit tests
lumoswiz 07eb959
refactor: use OZ-like custom errors for LibERC6909 and fix unit tests
lumoswiz b6f2ac8
feat: add IERC6909
lumoswiz 82481d0
refactor: move zero address checks before storage loads in ERC6909Fac…
lumoswiz 50e8e21
Update test/token/ERC6909/ERC6909/harnesses/ERC6909FacetHarness.sol
mudgen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity >=0.8.30; | ||
|
|
||
| /// @title ERC-6909 Minimal Multi-Token Interface | ||
| /// @notice Interface for ERC-6909 multi-token contracts with custom errors. | ||
| interface IERC6909 { | ||
| /// @notice Thrown when the sender has insufficient balance. | ||
| error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); | ||
|
|
||
| /// @notice Thrown when the spender has insufficient allowance. | ||
| error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); | ||
|
|
||
| /// @notice Thrown when the approver address is invalid. | ||
| error ERC6909InvalidApprover(address _approver); | ||
|
|
||
| /// @notice Thrown when the receiver address is invalid. | ||
| error ERC6909InvalidReceiver(address _receiver); | ||
|
|
||
| /// @notice Thrown when the sender address is invalid. | ||
| error ERC6909InvalidSender(address _sender); | ||
|
|
||
| /// @notice Thrown when the spender address is invalid. | ||
| error ERC6909InvalidSpender(address _spender); | ||
|
|
||
| /// @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); | ||
|
|
||
| /// @notice Owner balance of an id. | ||
| /// @param _owner The address of the owner. | ||
| /// @param _id The id of the token. | ||
| /// @return The balance of the token. | ||
| function balanceOf(address _owner, uint256 _id) external view returns (uint256); | ||
|
|
||
| /// @notice Spender allowance of an id. | ||
| /// @param _owner The address of the owner. | ||
| /// @param _spender The address of the spender. | ||
| /// @param _id The id of the token. | ||
| /// @return The allowance of the token. | ||
| function allowance(address _owner, address _spender, uint256 _id) external view returns (uint256); | ||
|
|
||
| /// @notice Checks if a spender is approved by an owner as an operator. | ||
| /// @param _owner The address of the owner. | ||
| /// @param _spender The address of the spender. | ||
| /// @return The approval status. | ||
| function isOperator(address _owner, address _spender) external view returns (bool); | ||
|
|
||
| /// @notice Approves an amount of an id to a spender. | ||
| /// @param _spender The address of the spender. | ||
| /// @param _id The id of the token. | ||
| /// @param _amount The amount of the token. | ||
| /// @return Whether the approval succeeded. | ||
| function approve(address _spender, uint256 _id, uint256 _amount) external returns (bool); | ||
|
|
||
| /// @notice Sets or removes a spender as an operator for the caller. | ||
| /// @param _spender The address of the spender. | ||
| /// @param _approved The approval status. | ||
| /// @return Whether the operator update succeeded. | ||
| function setOperator(address _spender, bool _approved) external returns (bool); | ||
|
|
||
| /// @notice Transfers an amount of an id from the caller to a receiver. | ||
| /// @param _receiver The address of the receiver. | ||
| /// @param _id The id of the token. | ||
| /// @param _amount The amount of the token. | ||
| /// @return Whether the transfer succeeded. | ||
| function transfer(address _receiver, uint256 _id, uint256 _amount) external returns (bool); | ||
|
|
||
| /// @notice Transfers an amount of an id from a sender to a receiver. | ||
| /// @param _sender The address of the sender. | ||
| /// @param _receiver The address of the receiver. | ||
| /// @param _id The id of the token. | ||
| /// @param _amount The amount of the token. | ||
| /// @return Whether the transfer succeeded. | ||
| function transferFrom(address _sender, address _receiver, uint256 _id, uint256 _amount) external returns (bool); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity >=0.8.30; | ||
|
|
||
| /// @title ERC-6909 Minimal Multi-Token Interface | ||
| /// @notice A complete, dependency-free ERC-6909 implementation using the diamond storage pattern. | ||
| contract ERC6909Facet { | ||
| /// @notice Thrown when the sender has insufficient balance. | ||
| error ERC6909InsufficientBalance(address _sender, uint256 _balance, uint256 _needed, uint256 _id); | ||
|
|
||
| /// @notice Thrown when the spender has insufficient allowance. | ||
| error ERC6909InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed, uint256 _id); | ||
|
|
||
| /// @notice Thrown when the receiver address is invalid. | ||
| error ERC6909InvalidReceiver(address _receiver); | ||
|
|
||
| /// @notice Thrown when the sender address is invalid. | ||
| error ERC6909InvalidSender(address _sender); | ||
|
|
||
| /// @notice Thrown when the spender address is invalid. | ||
| error ERC6909InvalidSpender(address _spender); | ||
|
|
||
| /// @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 | ||
| } | ||
| } | ||
|
|
||
| /// @notice Owner balance of an id. | ||
| /// @param _owner The address of the owner. | ||
| /// @param _id The id of the token. | ||
| /// @return The balance of the token. | ||
| function balanceOf(address _owner, uint256 _id) external view returns (uint256) { | ||
| return getStorage().balanceOf[_owner][_id]; | ||
| } | ||
|
|
||
| /// @notice Spender allowance of an id. | ||
| /// @param _owner The address of the owner. | ||
| /// @param _spender The address of the spender. | ||
| /// @param _id The id of the token. | ||
| /// @return The allowance of the token. | ||
| function allowance(address _owner, address _spender, uint256 _id) external view returns (uint256) { | ||
| return getStorage().allowance[_owner][_spender][_id]; | ||
| } | ||
|
|
||
| /// @notice Checks if a spender is approved by an owner as an operator. | ||
| /// @param _owner The address of the owner. | ||
| /// @param _spender The address of the spender. | ||
| /// @return The approval status. | ||
| function isOperator(address _owner, address _spender) external view returns (bool) { | ||
| return getStorage().isOperator[_owner][_spender]; | ||
| } | ||
|
|
||
| /// @notice Transfers an amount of an id from the caller to a receiver. | ||
| /// @param _receiver The address of the receiver. | ||
| /// @param _id The id of the token. | ||
| /// @param _amount The amount of the token. | ||
| /// @return Whether the transfer succeeded. | ||
| function transfer(address _receiver, uint256 _id, uint256 _amount) external returns (bool) { | ||
| if (_receiver == address(0)) { | ||
| revert ERC6909InvalidReceiver(address(0)); | ||
| } | ||
|
|
||
| ERC6909Storage storage s = getStorage(); | ||
|
|
||
| uint256 fromBalance = s.balanceOf[msg.sender][_id]; | ||
|
|
||
| if (fromBalance < _amount) { | ||
| revert ERC6909InsufficientBalance(msg.sender, fromBalance, _amount, _id); | ||
| } | ||
|
|
||
| unchecked { | ||
| s.balanceOf[msg.sender][_id] = fromBalance - _amount; | ||
| } | ||
|
|
||
| s.balanceOf[_receiver][_id] += _amount; | ||
|
|
||
| emit Transfer(msg.sender, msg.sender, _receiver, _id, _amount); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /// @notice Transfers an amount of an id from a sender to a receiver. | ||
| /// @param _sender The address of the sender. | ||
| /// @param _receiver The address of the receiver. | ||
| /// @param _id The id of the token. | ||
| /// @param _amount The amount of the token. | ||
| /// @return Whether the transfer succeeded. | ||
| function transferFrom(address _sender, address _receiver, uint256 _id, uint256 _amount) external returns (bool) { | ||
| if (_sender == address(0)) { | ||
| revert ERC6909InvalidSender(address(0)); | ||
| } | ||
|
|
||
| if (_receiver == address(0)) { | ||
| revert ERC6909InvalidReceiver(address(0)); | ||
| } | ||
|
|
||
| ERC6909Storage storage s = getStorage(); | ||
|
|
||
| if (msg.sender != _sender && !s.isOperator[_sender][msg.sender]) { | ||
| uint256 currentAllowance = s.allowance[_sender][msg.sender][_id]; | ||
| if (currentAllowance < type(uint256).max) { | ||
| if (currentAllowance < _amount) { | ||
| revert ERC6909InsufficientAllowance(msg.sender, currentAllowance, _amount, _id); | ||
| } | ||
| unchecked { | ||
| s.allowance[_sender][msg.sender][_id] = currentAllowance - _amount; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| uint256 fromBalance = s.balanceOf[_sender][_id]; | ||
| if (fromBalance < _amount) { | ||
| revert ERC6909InsufficientBalance(_sender, fromBalance, _amount, _id); | ||
| } | ||
| unchecked { | ||
| s.balanceOf[_sender][_id] = fromBalance - _amount; | ||
| } | ||
|
|
||
| s.balanceOf[_receiver][_id] += _amount; | ||
|
|
||
| emit Transfer(msg.sender, _sender, _receiver, _id, _amount); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /// @notice Approves an amount of an id to a spender. | ||
| /// @param _spender The address of the spender. | ||
| /// @param _id The id of the token. | ||
| /// @param _amount The amount of the token. | ||
| /// @return Whether the approval succeeded. | ||
| function approve(address _spender, uint256 _id, uint256 _amount) external returns (bool) { | ||
| if (_spender == address(0)) { | ||
| revert ERC6909InvalidSpender(address(0)); | ||
| } | ||
|
|
||
| ERC6909Storage storage s = getStorage(); | ||
|
|
||
| s.allowance[msg.sender][_spender][_id] = _amount; | ||
|
|
||
| emit Approval(msg.sender, _spender, _id, _amount); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /// @notice Sets or removes a spender as an operator for the caller. | ||
| /// @param _spender The address of the spender. | ||
| /// @param _approved The approval status. | ||
| /// @return Whether the operator update succeeded. | ||
| function setOperator(address _spender, bool _approved) external returns (bool) { | ||
| if (_spender == address(0)) { | ||
| revert ERC6909InvalidSpender(address(0)); | ||
| } | ||
|
|
||
| ERC6909Storage storage s = getStorage(); | ||
|
|
||
| s.isOperator[msg.sender][_spender] = _approved; | ||
|
|
||
| emit OperatorSet(msg.sender, _spender, _approved); | ||
|
|
||
| return true; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.