Skip to content
Open
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
129 changes: 129 additions & 0 deletions src/token/ERC6909/ERC6909Facet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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.
/// @dev Adapted from: https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC6909.sol
contract ERC6909Facet {
/// @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
}
}
Comment on lines +8 to +37
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/


/// @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) {
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.
/// @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) {
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.

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.
}

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) {
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) {
ERC6909Storage storage s = getStorage();

s.isOperator[msg.sender][_spender] = _approved;

emit OperatorSet(msg.sender, _spender, _approved);

return true;
}
}
111 changes: 111 additions & 0 deletions src/token/ERC6909/LibERC6909.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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.
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.
/// This library is intended to be used by custom facets to integrate with ERC-6909 functionality.
/// @dev Adapted from: https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC6909.sol
library LibERC6909 {
/// @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 {
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 {
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 {
ERC6909Storage storage s = getStorage();

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;
}

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.
}

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 {
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 {
ERC6909Storage storage s = getStorage();

s.isOperator[_owner][_spender] = _approved;

emit OperatorSet(_owner, _spender, _approved);
}
}