From 5153017cddfbe59bf193b15a4643fb2b4bd5d164 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 3 Nov 2025 10:18:58 +0100 Subject: [PATCH 1/4] feat: add ERC-6909 facet and lib skeleton with NatSpec --- src/token/ERC6909/ERC6909Facet.sol | 90 ++++++++++++++++++++++++++++++ src/token/ERC6909/LibERC6909.sol | 78 ++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 src/token/ERC6909/ERC6909Facet.sol create mode 100644 src/token/ERC6909/LibERC6909.sol diff --git a/src/token/ERC6909/ERC6909Facet.sol b/src/token/ERC6909/ERC6909Facet.sol new file mode 100644 index 0000000..e118cd0 --- /dev/null +++ b/src/token/ERC6909/ERC6909Facet.sol @@ -0,0 +1,90 @@ +// 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 owner balance for id is insufficient. + error ERC6909InsufficientBalance(); + + /// @notice Thrown when spender allowance for id is insufficient. + error ERC6909InsufficientPermission(); + + /// @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) {} + + /// @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 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) {} + + /// @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) {} +} diff --git a/src/token/ERC6909/LibERC6909.sol b/src/token/ERC6909/LibERC6909.sol new file mode 100644 index 0000000..e569929 --- /dev/null +++ b/src/token/ERC6909/LibERC6909.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/// @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. +/// This library is intended to be used by custom facets to integrate with ERC-6909 functionality. +library LibERC6909 { + /// @notice Thrown when owner balance for id is insufficient. + error ERC6909InsufficientBalance(); + + /// @notice Thrown when spender allowance for id is insufficient. + error ERC6909InsufficientPermission(); + + /// @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 internal 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 Mints `_amount` of token id `_id` to `_to`. + /// @param _to The address of the receiver. + /// @param _id The id of the token. + /// @param _amount The amount of the token. + function mint(address _to, uint256 _id, uint256 _amount) internal {} + + /// @notice Burns `_amount` of token id `_id` from `_from`. + /// @param _from The address of the sender. + /// @param _id The id of the token. + /// @param _amount The amount of the token. + function burn(address _from, uint256 _id, uint256 _amount) internal {} + + /// @notice Transfers `_amount` of token id `_id` from `_from` to `_to`. + /// @param _by The address initiating the transfer. + /// @param _from The address of the sender. + /// @param _to The address of the receiver. + /// @param _id The id of the token. + /// @param _amount The amount of the token. + function transfer(address _by, address _from, address _to, uint256 _id, uint256 _amount) internal {} + + /// @notice Approves an amount of an id to a spender. + /// @param _owner The token owner. + /// @param _spender The address of the spender. + /// @param _id The id of the token. + /// @param _amount The amount of the token. + function approve(address _owner, address _spender, uint256 _id, uint256 _amount) internal {} + + /// @notice Sets or removes a spender as an operator for the caller. + /// @param _owner The address of the owner. + /// @param _spender The address of the spender. + /// @param _approved The approval status. + function setOperator(address _owner, address _spender, bool _approved) internal {} +} From 199c3e184b08316b25c93c4398d7b2304c457d29 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 3 Nov 2025 11:02:05 +0100 Subject: [PATCH 2/4] feat: implement LibERC6909 functions --- src/token/ERC6909/LibERC6909.sol | 54 +++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/token/ERC6909/LibERC6909.sol b/src/token/ERC6909/LibERC6909.sol index e569929..d0da679 100644 --- a/src/token/ERC6909/LibERC6909.sol +++ b/src/token/ERC6909/LibERC6909.sol @@ -5,6 +5,7 @@ pragma solidity >=0.8.30; /// @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. /// This library is intended to be used by custom facets to integrate with ERC-6909 functionality. +/// @dev Adapted from: https://github.com/Vectorized/solady/blob/main/src/tokens/ERC6909.sol library LibERC6909 { /// @notice Thrown when owner balance for id is insufficient. error ERC6909InsufficientBalance(); @@ -47,32 +48,75 @@ library LibERC6909 { /// @param _to The address of the receiver. /// @param _id The id of the token. /// @param _amount The amount of the token. - function mint(address _to, uint256 _id, uint256 _amount) internal {} + function mint(address _to, uint256 _id, uint256 _amount) internal { + ERC6909Storage storage s = getStorage(); + + s.balanceOf[_to][_id] += _amount; + + emit Transfer(msg.sender, address(0), _to, _id, _amount); + } /// @notice Burns `_amount` of token id `_id` from `_from`. /// @param _from The address of the sender. /// @param _id The id of the token. /// @param _amount The amount of the token. - function burn(address _from, uint256 _id, uint256 _amount) internal {} + function burn(address _from, uint256 _id, uint256 _amount) internal { + ERC6909Storage storage s = getStorage(); + + s.balanceOf[_from][_id] -= _amount; + + emit Transfer(msg.sender, _from, address(0), _id, _amount); + } /// @notice Transfers `_amount` of token id `_id` from `_from` to `_to`. + /// @dev Allowance is not deducted if it is `type(uint256).max` + /// @dev Allowance is not deducted if `_by` is an operator for `_from`. /// @param _by The address initiating the transfer. /// @param _from The address of the sender. /// @param _to The address of the receiver. /// @param _id The id of the token. /// @param _amount The amount of the token. - function transfer(address _by, address _from, address _to, uint256 _id, uint256 _amount) internal {} + function transfer(address _by, address _from, address _to, uint256 _id, uint256 _amount) internal { + ERC6909Storage storage s = getStorage(); + + if (_by != address(0) && !s.isOperator[_from][_by]) { + uint256 allowed = s.allowance[_from][_by][_id]; + if (allowed != type(uint256).max) { + if (_amount > allowed) revert ERC6909InsufficientPermission(); + s.allowance[_from][_by][_id] = allowed - _amount; + } + } + + if (_amount > s.balanceOf[_from][_id]) revert ERC6909InsufficientBalance(); + + s.balanceOf[_from][_id] -= _amount; + s.balanceOf[_to][_id] += _amount; + + emit Transfer(_by, _from, _to, _id, _amount); + } /// @notice Approves an amount of an id to a spender. /// @param _owner The token owner. /// @param _spender The address of the spender. /// @param _id The id of the token. /// @param _amount The amount of the token. - function approve(address _owner, address _spender, uint256 _id, uint256 _amount) internal {} + function approve(address _owner, address _spender, uint256 _id, uint256 _amount) internal { + ERC6909Storage storage s = getStorage(); + + s.allowance[_owner][_spender][_id] = _amount; + + emit Approval(_owner, _spender, _id, _amount); + } /// @notice Sets or removes a spender as an operator for the caller. /// @param _owner The address of the owner. /// @param _spender The address of the spender. /// @param _approved The approval status. - function setOperator(address _owner, address _spender, bool _approved) internal {} + function setOperator(address _owner, address _spender, bool _approved) internal { + ERC6909Storage storage s = getStorage(); + + s.isOperator[_owner][_spender] = _approved; + + emit OperatorSet(_owner, _spender, _approved); + } } From 3be87cb6c824eedd1944a083e0f66db1f4c43ce6 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 3 Nov 2025 11:44:24 +0100 Subject: [PATCH 3/4] feat: implement ERC6909Facet functions --- src/token/ERC6909/ERC6909Facet.sol | 65 ++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/src/token/ERC6909/ERC6909Facet.sol b/src/token/ERC6909/ERC6909Facet.sol index e118cd0..d1f3602 100644 --- a/src/token/ERC6909/ERC6909Facet.sol +++ b/src/token/ERC6909/ERC6909Facet.sol @@ -3,13 +3,8 @@ 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. +/// @dev Adapted from: https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC6909.sol contract ERC6909Facet { - /// @notice Thrown when owner balance for id is insufficient. - error ERC6909InsufficientBalance(); - - /// @notice Thrown when spender allowance for id is insufficient. - error ERC6909InsufficientPermission(); - /// @notice Emitted when a transfer occurs. event Transfer( address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount @@ -45,27 +40,42 @@ contract ERC6909Facet { /// @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) {} + 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) {} + 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) {} + 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) {} + function transfer(address _receiver, uint256 _id, uint256 _amount) external returns (bool) { + ERC6909Storage storage s = getStorage(); + + s.balanceOf[msg.sender][_id] -= _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. @@ -73,18 +83,47 @@ contract ERC6909Facet { /// @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) {} + function transferFrom(address _sender, address _receiver, uint256 _id, uint256 _amount) external returns (bool) { + 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; + } + + s.balanceOf[_sender][_id] -= _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) {} + function approve(address _spender, uint256 _id, uint256 _amount) external returns (bool) { + 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) {} + function setOperator(address _spender, bool _approved) external returns (bool) { + ERC6909Storage storage s = getStorage(); + + s.isOperator[msg.sender][_spender] = _approved; + + emit OperatorSet(msg.sender, _spender, _approved); + + return true; + } } From 681c035ab740bf2781c90a10523f251875f2fa20 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Mon, 3 Nov 2025 11:45:06 +0100 Subject: [PATCH 4/4] refactor: align style with ERC6909Facet, rely on checked arithmetic --- src/token/ERC6909/LibERC6909.sol | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/token/ERC6909/LibERC6909.sol b/src/token/ERC6909/LibERC6909.sol index d0da679..0e78180 100644 --- a/src/token/ERC6909/LibERC6909.sol +++ b/src/token/ERC6909/LibERC6909.sol @@ -5,14 +5,8 @@ pragma solidity >=0.8.30; /// @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. /// This library is intended to be used by custom facets to integrate with ERC-6909 functionality. -/// @dev Adapted from: https://github.com/Vectorized/solady/blob/main/src/tokens/ERC6909.sol +/// @dev Adapted from: https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC6909.sol library LibERC6909 { - /// @notice Thrown when owner balance for id is insufficient. - error ERC6909InsufficientBalance(); - - /// @notice Thrown when spender allowance for id is insufficient. - error ERC6909InsufficientPermission(); - /// @notice Emitted when a transfer occurs. event Transfer( address _caller, address indexed _sender, address indexed _receiver, uint256 indexed _id, uint256 _amount @@ -81,14 +75,9 @@ library LibERC6909 { if (_by != address(0) && !s.isOperator[_from][_by]) { uint256 allowed = s.allowance[_from][_by][_id]; - if (allowed != type(uint256).max) { - if (_amount > allowed) revert ERC6909InsufficientPermission(); - s.allowance[_from][_by][_id] = allowed - _amount; - } + if (allowed != type(uint256).max) s.allowance[_from][_by][_id] = allowed - _amount; } - if (_amount > s.balanceOf[_from][_id]) revert ERC6909InsufficientBalance(); - s.balanceOf[_from][_id] -= _amount; s.balanceOf[_to][_id] += _amount;