From 097292421c17fd497d9daa31640c568a37906d13 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Tue, 28 Oct 2025 11:05:43 +0800 Subject: [PATCH 01/16] init group conception --- ERCS/eip-0000.md | 130 +++++++++++++++++++++++++++ assets/eip-groups/GroupContainer.sol | 101 +++++++++++++++++++++ assets/eip-groups/IERCGroup.sol | 57 ++++++++++++ assets/eip-groups/README.md | 8 ++ 4 files changed, 296 insertions(+) create mode 100644 ERCS/eip-0000.md create mode 100644 assets/eip-groups/GroupContainer.sol create mode 100644 assets/eip-groups/IERCGroup.sol create mode 100644 assets/eip-groups/README.md diff --git a/ERCS/eip-0000.md b/ERCS/eip-0000.md new file mode 100644 index 00000000000..0df80f02c90 --- /dev/null +++ b/ERCS/eip-0000.md @@ -0,0 +1,130 @@ +--- +title: Groups — Multi-member onchain containers for shared resources +description: A general-purpose standard for onchain groups with membership, invitations, and shared resource metadata. +author: bool) isMember; + mapping(address => bool) pendingInvite; + mapping(bytes32 => string) resources; + } + + uint256 private _nextGroupId; + mapping(uint256 => GroupData) private _groups; + + constructor() { + _nextGroupId = 1; + } + + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == type(IERCGroup).interfaceId; + } + + function createGroup(string calldata metadataURI) external override returns (uint256 groupId) { + groupId = _nextGroupId++; + GroupData storage g = _groups[groupId]; + g.owner = msg.sender; + g.metadataURI = metadataURI; + if (!g.isMember[msg.sender]) { + g.isMember[msg.sender] = true; + g.memberCount = 1; + } + emit GroupCreated(groupId, msg.sender, metadataURI); + } + + function groupOwner(uint256 groupId) public view override returns (address) { + return _groups[groupId].owner; + } + + function isMember(uint256 groupId, address account) public view override returns (bool) { + return _groups[groupId].isMember[account]; + } + + function getMemberCount(uint256 groupId) external view override returns (uint256) { + return _groups[groupId].memberCount; + } + + function inviteMember(uint256 groupId, address account) external override { + GroupData storage g = _groups[groupId]; + require(msg.sender == g.owner, "Only owner can invite"); + require(account != address(0), "Zero address"); + require(!g.isMember[account], "Already member"); + require(!g.pendingInvite[account], "Already invited"); + g.pendingInvite[account] = true; + emit MemberInvited(groupId, msg.sender, account); + } + + function acceptInvite(uint256 groupId) external override { + GroupData storage g = _groups[groupId]; + require(g.pendingInvite[msg.sender], "No invite"); + require(!g.isMember[msg.sender], "Already member"); + g.pendingInvite[msg.sender] = false; + g.isMember[msg.sender] = true; + unchecked { g.memberCount += 1; } + emit MemberJoined(groupId, msg.sender); + } + + function removeMember(uint256 groupId, address account) external override { + GroupData storage g = _groups[groupId]; + require(msg.sender == g.owner, "Only owner can remove"); + require(account != g.owner, "Cannot remove owner"); + require(g.isMember[account], "Not a member"); + g.isMember[account] = false; + unchecked { g.memberCount -= 1; } + emit MemberRemoved(groupId, account, msg.sender); + } + + function setResource(uint256 groupId, bytes32 key, string calldata value) external override { + GroupData storage g = _groups[groupId]; + require(g.isMember[msg.sender] || msg.sender == g.owner, "Only member or owner"); + if (bytes(value).length == 0) { + // delete by setting to empty string (idempotent) + if (bytes(g.resources[key]).length != 0) { + g.resources[key] = ""; + } + } else { + g.resources[key] = value; + } + emit ResourceUpdated(groupId, key, value, msg.sender); + } + + function getResource(uint256 groupId, bytes32 key) external view override returns (string memory) { + return _groups[groupId].resources[key]; + } +} + + diff --git a/assets/eip-groups/IERCGroup.sol b/assets/eip-groups/IERCGroup.sol new file mode 100644 index 00000000000..012d8bb1559 --- /dev/null +++ b/assets/eip-groups/IERCGroup.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.20; + +interface IERC165 { + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +/// @title IERCGroup — Minimal interface for onchain groups +/// @notice A group is a container with an owner, members, and shared resources +interface IERCGroup is IERC165 { + /// @dev Emitted when a new group is created + event GroupCreated(uint256 indexed groupId, address indexed owner, string metadataURI); + + /// @dev Emitted when the owner invites an account + event MemberInvited(uint256 indexed groupId, address indexed inviter, address indexed invitee); + + /// @dev Emitted when an invited account accepts and becomes a member + event MemberJoined(uint256 indexed groupId, address indexed account); + + /// @dev Emitted when a member is removed (cannot remove the owner) + event MemberRemoved(uint256 indexed groupId, address indexed account, address indexed by); + + /// @dev Emitted when a resource key is set/updated/cleared (empty value means delete) + event ResourceUpdated(uint256 indexed groupId, bytes32 indexed key, string value, address indexed by); + + /// @notice Create a new group; caller becomes owner and initial member + /// @param metadataURI Optional offchain metadata (e.g., JSON document) + /// @return groupId Newly created group identifier + function createGroup(string calldata metadataURI) external returns (uint256 groupId); + + /// @notice Returns the owner of a group + function groupOwner(uint256 groupId) external view returns (address); + + /// @notice Returns true if `account` is a member of the group + function isMember(uint256 groupId, address account) external view returns (bool); + + /// @notice Returns current number of members (including owner) + function getMemberCount(uint256 groupId) external view returns (uint256); + + /// @notice Owner invites an account to join the group + function inviteMember(uint256 groupId, address account) external; + + /// @notice Invitee accepts an outstanding invite and becomes a member + function acceptInvite(uint256 groupId) external; + + /// @notice Owner removes a member (owner cannot be removed) + function removeMember(uint256 groupId, address account) external; + + /// @notice Set or clear a resource value for the group + /// @dev Setting to an empty string SHOULD be treated as deletion + function setResource(uint256 groupId, bytes32 key, string calldata value) external; + + /// @notice Read a resource value for the group (empty string if unset) + function getResource(uint256 groupId, bytes32 key) external view returns (string memory); +} + + diff --git a/assets/eip-groups/README.md b/assets/eip-groups/README.md new file mode 100644 index 00000000000..f0137449e00 --- /dev/null +++ b/assets/eip-groups/README.md @@ -0,0 +1,8 @@ +Groups reference implementation assets + +- After creating the Ethereum Magicians discussion, update `discussions-to` in `ERCS/EIP-0000.md` with the live URL. +- Contracts: + - `IERCGroup.sol`: interface + - `GroupContainer.sol`: minimal implementation + + From 4155b35921501e26822b8eece732770c518850d8 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Tue, 28 Oct 2025 11:11:49 +0800 Subject: [PATCH 02/16] update format --- ERCS/eip-0000.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ERCS/eip-0000.md b/ERCS/eip-0000.md index 0df80f02c90..2c82a7c4215 100644 --- a/ERCS/eip-0000.md +++ b/ERCS/eip-0000.md @@ -1,7 +1,8 @@ --- -title: Groups — Multi-member onchain containers for shared resources -description: A general-purpose standard for onchain groups with membership, invitations, and shared resource metadata. -author: Date: Tue, 28 Oct 2025 11:14:54 +0800 Subject: [PATCH 03/16] rename eip-0000.md to erc-0000.md --- ERCS/{eip-0000.md => erc-0000.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ERCS/{eip-0000.md => erc-0000.md} (100%) diff --git a/ERCS/eip-0000.md b/ERCS/erc-0000.md similarity index 100% rename from ERCS/eip-0000.md rename to ERCS/erc-0000.md From 34141fd18cd3cb9b4a6fe94eacf986143f7b74eb Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Wed, 29 Oct 2025 09:35:50 +0800 Subject: [PATCH 04/16] Updated with assigned number --- ERCS/{erc-0000.md => erc-8063.md} | 4 ++-- assets/{eip-groups => erc-8063}/GroupContainer.sol | 0 assets/{eip-groups => erc-8063}/IERCGroup.sol | 0 assets/{eip-groups => erc-8063}/README.md | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename ERCS/{erc-0000.md => erc-8063.md} (99%) rename assets/{eip-groups => erc-8063}/GroupContainer.sol (100%) rename assets/{eip-groups => erc-8063}/IERCGroup.sol (100%) rename assets/{eip-groups => erc-8063}/README.md (100%) diff --git a/ERCS/erc-0000.md b/ERCS/erc-8063.md similarity index 99% rename from ERCS/erc-0000.md rename to ERCS/erc-8063.md index 2c82a7c4215..d4287632b45 100644 --- a/ERCS/erc-0000.md +++ b/ERCS/erc-8063.md @@ -1,6 +1,6 @@ --- -eip: 0000 -title: Groups — Multi-member onchain containers +eip: 8063 +title: Groups — Multi-Member Containers description: Onchain groups with membership, invitations, and shared resource metadata. author: James Savechives (@jamesavechives) discussions-to: https://ethereum-magicians.org/t/groups-multi-member-onchain-containers-for-shared-resources/25999 diff --git a/assets/eip-groups/GroupContainer.sol b/assets/erc-8063/GroupContainer.sol similarity index 100% rename from assets/eip-groups/GroupContainer.sol rename to assets/erc-8063/GroupContainer.sol diff --git a/assets/eip-groups/IERCGroup.sol b/assets/erc-8063/IERCGroup.sol similarity index 100% rename from assets/eip-groups/IERCGroup.sol rename to assets/erc-8063/IERCGroup.sol diff --git a/assets/eip-groups/README.md b/assets/erc-8063/README.md similarity index 100% rename from assets/eip-groups/README.md rename to assets/erc-8063/README.md From 07dda3806359d75b6150261e681fbcac9854320f Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Wed, 29 Oct 2025 09:51:58 +0800 Subject: [PATCH 05/16] update discussions-to link --- ERCS/erc-8063.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index d4287632b45..6f71f6a9b82 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -3,7 +3,7 @@ eip: 8063 title: Groups — Multi-Member Containers description: Onchain groups with membership, invitations, and shared resource metadata. author: James Savechives (@jamesavechives) -discussions-to: https://ethereum-magicians.org/t/groups-multi-member-onchain-containers-for-shared-resources/25999 +discussions-to: https://ethereum-magicians.org/t/erc-8063-groups-multi-member-onchain-containers-for-shared-resources/25999 status: Draft type: Standards Track category: ERC From 1633b7d5eb0f6feb05f8ba21dfdda52ba58f1341 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Thu, 30 Oct 2025 11:05:41 +0800 Subject: [PATCH 06/16] feat : apply group name & rename contracts --- ERCS/erc-8063.md | 22 ++++++++++++++----- .../{GroupContainer.sol => ERC8063.sol} | 18 ++++++++++----- .../erc-8063/{IERCGroup.sol => IERC8063.sol} | 14 +++++++----- 3 files changed, 36 insertions(+), 18 deletions(-) rename assets/erc-8063/{GroupContainer.sol => ERC8063.sol} (85%) rename assets/erc-8063/{IERCGroup.sol => IERC8063.sol} (84%) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index 6f71f6a9b82..6218a8c2a45 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -37,9 +37,9 @@ interface IERC165 { /// @title IERCGroup — Minimal interface for onchain groups /// @notice A group is a container with an owner, members, and shared resources -interface IERCGroup is IERC165 { +interface IERC8063 is IERC165 { /// @dev Emitted when a new group is created - event GroupCreated(uint256 indexed groupId, address indexed owner, string metadataURI); + event GroupCreated(uint256 indexed groupId, address indexed owner, string name, string metadataURI); /// @dev Emitted when the owner invites an account event MemberInvited(uint256 indexed groupId, address indexed inviter, address indexed invitee); @@ -54,13 +54,17 @@ interface IERCGroup is IERC165 { event ResourceUpdated(uint256 indexed groupId, bytes32 indexed key, string value, address indexed by); /// @notice Create a new group; caller becomes owner and initial member + /// @param name Optional human-readable group name /// @param metadataURI Optional offchain metadata (e.g., JSON document) /// @return groupId Newly created group identifier - function createGroup(string calldata metadataURI) external returns (uint256 groupId); + function createGroup(string calldata name, string calldata metadataURI) external returns (uint256 groupId); /// @notice Returns the owner of a group function groupOwner(uint256 groupId) external view returns (address); + /// @notice Returns the human-readable name of the group (may be empty) + function groupName(uint256 groupId) external view returns (string memory); + /// @notice Returns true if `account` is a member of the group function isMember(uint256 groupId, address account) external view returns (bool); @@ -92,7 +96,13 @@ interface IERCGroup is IERC165 { - Joining: `acceptInvite` MUST succeed only for invited accounts, add them as members exactly once, and emit `MemberJoined`. - Removal: Only the owner MAY call `removeMember`. Implementations MUST NOT allow removing the owner. - Resources: `setResource` MUST update a group-scoped key/value string resource and emit `ResourceUpdated`. Implementations MAY restrict who can update resources. A simple baseline allows any member to set resources; alternative policies are acceptable if clearly documented. -- Introspection: `supportsInterface` MUST return true for `type(IERC165).interfaceId` and for `type(IERCGroup).interfaceId`. +- Introspection: `supportsInterface` MUST return true for `type(IERC165).interfaceId` and for `type(IERC8063).interfaceId`. + +### Identifier model and naming + +- Single-group deployments: For implementations that deploy exactly one group per contract, the canonical group identifier SHOULD be the contract address (`address(this)`). In such cases, interfaces MAY omit the `groupId` parameter by using a thin wrapper or adapter while preserving ERC-165 detection for interoperability. +- Multi-group containers: For contracts that hold multiple groups, `groupId` is a `uint256` unique within the contract. The canonical global identifier becomes the pair `(contract address, groupId)`. +- Naming: Implementations SHOULD provide a human-readable name at creation via `createGroup(name, ...)` and expose it via `groupName(groupId)`. Empty names are permitted. ### Optional extensions (non-normative) @@ -114,8 +124,8 @@ No known backward compatibility issues. This is a new interface with [ERC-165](. Reference contracts are provided in this repository: -- `assets/eip-groups/IERCGroup.sol` -- `assets/eip-groups/GroupContainer.sol` +- `assets/erc-8063/IERC8063.sol` +- `assets/erc-8063/ERC8063.sol` ## Security Considerations diff --git a/assets/erc-8063/GroupContainer.sol b/assets/erc-8063/ERC8063.sol similarity index 85% rename from assets/erc-8063/GroupContainer.sol rename to assets/erc-8063/ERC8063.sol index 498efa1e263..77f3c532e5f 100644 --- a/assets/erc-8063/GroupContainer.sol +++ b/assets/erc-8063/ERC8063.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.20; -import "./IERCGroup.sol"; +import "./IERC8063.sol"; -/// @title GroupContainer — minimal reference implementation of IERCGroup -contract GroupContainer is IERCGroup { +/// @title ERC8063 — minimal reference implementation of IERC8063 +contract ERC8063 is IERC8063 { struct GroupData { address owner; + string name; string metadataURI; uint256 memberCount; mapping(address => bool) isMember; @@ -22,25 +23,30 @@ contract GroupContainer is IERCGroup { } function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { - return interfaceId == type(IERCGroup).interfaceId; + return interfaceId == type(IERC8063).interfaceId; } - function createGroup(string calldata metadataURI) external override returns (uint256 groupId) { + function createGroup(string calldata name, string calldata metadataURI) external override returns (uint256 groupId) { groupId = _nextGroupId++; GroupData storage g = _groups[groupId]; g.owner = msg.sender; + g.name = name; g.metadataURI = metadataURI; if (!g.isMember[msg.sender]) { g.isMember[msg.sender] = true; g.memberCount = 1; } - emit GroupCreated(groupId, msg.sender, metadataURI); + emit GroupCreated(groupId, msg.sender, name, metadataURI); } function groupOwner(uint256 groupId) public view override returns (address) { return _groups[groupId].owner; } + function groupName(uint256 groupId) public view override returns (string memory) { + return _groups[groupId].name; + } + function isMember(uint256 groupId, address account) public view override returns (bool) { return _groups[groupId].isMember[account]; } diff --git a/assets/erc-8063/IERCGroup.sol b/assets/erc-8063/IERC8063.sol similarity index 84% rename from assets/erc-8063/IERCGroup.sol rename to assets/erc-8063/IERC8063.sol index 012d8bb1559..16ba2f13a13 100644 --- a/assets/erc-8063/IERCGroup.sol +++ b/assets/erc-8063/IERC8063.sol @@ -5,11 +5,11 @@ interface IERC165 { function supportsInterface(bytes4 interfaceId) external view returns (bool); } -/// @title IERCGroup — Minimal interface for onchain groups +/// @title IERC8063 — Minimal interface for onchain groups /// @notice A group is a container with an owner, members, and shared resources -interface IERCGroup is IERC165 { +interface IERC8063 is IERC165 { /// @dev Emitted when a new group is created - event GroupCreated(uint256 indexed groupId, address indexed owner, string metadataURI); + event GroupCreated(uint256 indexed groupId, address indexed owner, string name, string metadataURI); /// @dev Emitted when the owner invites an account event MemberInvited(uint256 indexed groupId, address indexed inviter, address indexed invitee); @@ -24,13 +24,17 @@ interface IERCGroup is IERC165 { event ResourceUpdated(uint256 indexed groupId, bytes32 indexed key, string value, address indexed by); /// @notice Create a new group; caller becomes owner and initial member + /// @param name Optional human-readable group name /// @param metadataURI Optional offchain metadata (e.g., JSON document) /// @return groupId Newly created group identifier - function createGroup(string calldata metadataURI) external returns (uint256 groupId); + function createGroup(string calldata name, string calldata metadataURI) external returns (uint256 groupId); /// @notice Returns the owner of a group function groupOwner(uint256 groupId) external view returns (address); + /// @notice Returns the human-readable name of the group (may be empty) + function groupName(uint256 groupId) external view returns (string memory); + /// @notice Returns true if `account` is a member of the group function isMember(uint256 groupId, address account) external view returns (bool); @@ -53,5 +57,3 @@ interface IERCGroup is IERC165 { /// @notice Read a resource value for the group (empty string if unset) function getResource(uint256 groupId, bytes32 key) external view returns (string memory); } - - From 4dba24ba9d000e438823be7b88843b6feb9bad20 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Thu, 30 Oct 2025 11:21:32 +0800 Subject: [PATCH 07/16] fix format issue --- ERCS/erc-8063.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index 6218a8c2a45..9947c182785 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -124,8 +124,8 @@ No known backward compatibility issues. This is a new interface with [ERC-165](. Reference contracts are provided in this repository: -- `assets/erc-8063/IERC8063.sol` -- `assets/erc-8063/ERC8063.sol` +- assets/erc-8063/IERC8063.sol +- assets/erc-8063/ERC8063.sol ## Security Considerations From 21a99ba72636a0564364e55d5328fc661c1b191c Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Thu, 30 Oct 2025 11:27:44 +0800 Subject: [PATCH 08/16] fix format issue --- ERCS/erc-8063.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index 9947c182785..e9f2243fe7c 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -122,10 +122,10 @@ No known backward compatibility issues. This is a new interface with [ERC-165](. ## Reference Implementation -Reference contracts are provided in this repository: +Reference contracts are provided in the `assets/` directory: -- assets/erc-8063/IERC8063.sol -- assets/erc-8063/ERC8063.sol +- `IERC8063.sol` — Interface definition +- `ERC8063.sol` — Minimal reference implementation ## Security Considerations From 10876ac4886e6a9c2084203f43d2d0cb351a5f35 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Fri, 31 Oct 2025 12:11:04 +0800 Subject: [PATCH 09/16] Refactor ERC-8063: Add naming, remove internal resources, rename to standard conventions --- ERCS/erc-8063.md | 32 +++++++++++--------------------- assets/erc-8063/ERC8063.sol | 19 ------------------- assets/erc-8063/IERC8063.sol | 10 ---------- 3 files changed, 11 insertions(+), 50 deletions(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index e9f2243fe7c..6308bf11d9d 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -1,7 +1,7 @@ --- eip: 8063 -title: Groups — Multi-Member Containers -description: Onchain groups with membership, invitations, and shared resource metadata. +title: Group Standard +description: Onchain groups with membership, invitations, and shared metadata. author: James Savechives (@jamesavechives) discussions-to: https://ethereum-magicians.org/t/erc-8063-groups-multi-member-onchain-containers-for-shared-resources/25999 status: Draft @@ -13,11 +13,11 @@ requires: 165 ## Abstract -This proposal specifies a general-purpose "Group" primitive as a first-class, onchain object. A Group is identified by a `groupId` and has an owner, an extensible set of members, and arbitrary shared resources represented as key/value metadata. The standard defines a minimal interface for creating groups, inviting and joining members, membership introspection, and resource updates. Unlike token standards (e.g., [ERC-20](./eip-20.md)/[ERC-721](./eip-721.md)) that model units of transferable ownership, Groups model multi-party membership and coordination. The goal is to enable interoperable social, organizational, and collaborative primitives. +This proposal specifies a general-purpose "Group" primitive as a first-class, onchain object. A Group is identified by a `groupId` and has an owner, an extensible set of members, and optional metadata. The standard defines a minimal interface for creating groups, inviting and joining members, and membership introspection. Unlike token standards (e.g., [ERC-20](./eip-20.md)/[ERC-721](./eip-721.md)) that model units of transferable ownership, Groups model multi-party membership and coordination. The goal is to enable interoperable social, organizational, and collaborative primitives. ## Motivation -Tokens typically model ownership and transfer. Many applications instead need an addressable set of accounts with controlled join/leave semantics and shared state—e.g., project teams, DAOs, game parties, channels, or access cohorts. While [ERC-7743](./eip-7743.md) (MO-NFT) explores multi-ownership for a single token, it still anchors the abstraction to token semantics. Groups generalize this into a token-agnostic container where members can be added over time and share resources, without implying transfer or unitized ownership. +Tokens typically model ownership and transfer. Many applications instead need an addressable set of accounts with controlled join/leave semantics and shared state—e.g., project teams, DAOs, game parties, channels, or access cohorts. While [ERC-7743](./eip-7743.md) (MO-NFT) explores multi-ownership for a single token, it still anchors the abstraction to token semantics. Groups generalize this into a token-agnostic container where members can be added over time, without implying transfer or unitized ownership. External resources (tokens, NFTs, etc.) can be associated with groups through their own contracts, just as they would with any EOA or contract address. ## Specification @@ -35,8 +35,8 @@ interface IERC165 { function supportsInterface(bytes4 interfaceId) external view returns (bool); } -/// @title IERCGroup — Minimal interface for onchain groups -/// @notice A group is a container with an owner, members, and shared resources +/// @title IERC8063 — Minimal interface for onchain groups +/// @notice A group is a container with an owner and members interface IERC8063 is IERC165 { /// @dev Emitted when a new group is created event GroupCreated(uint256 indexed groupId, address indexed owner, string name, string metadataURI); @@ -50,9 +50,6 @@ interface IERC8063 is IERC165 { /// @dev Emitted when a member is removed (cannot remove the owner) event MemberRemoved(uint256 indexed groupId, address indexed account, address indexed by); - /// @dev Emitted when a resource key is set/updated/cleared (empty value means delete) - event ResourceUpdated(uint256 indexed groupId, bytes32 indexed key, string value, address indexed by); - /// @notice Create a new group; caller becomes owner and initial member /// @param name Optional human-readable group name /// @param metadataURI Optional offchain metadata (e.g., JSON document) @@ -79,13 +76,6 @@ interface IERC8063 is IERC165 { /// @notice Owner removes a member (owner cannot be removed) function removeMember(uint256 groupId, address account) external; - - /// @notice Set or clear a resource value for the group - /// @dev Setting to an empty string SHOULD be treated as deletion - function setResource(uint256 groupId, bytes32 key, string calldata value) external; - - /// @notice Read a resource value for the group (empty string if unset) - function getResource(uint256 groupId, bytes32 key) external view returns (string memory); } ``` @@ -95,7 +85,6 @@ interface IERC8063 is IERC165 { - Invitations: Only the group owner MAY call `inviteMember`. Implementations MUST record an outstanding invitation for `account` and emit `MemberInvited`. - Joining: `acceptInvite` MUST succeed only for invited accounts, add them as members exactly once, and emit `MemberJoined`. - Removal: Only the owner MAY call `removeMember`. Implementations MUST NOT allow removing the owner. -- Resources: `setResource` MUST update a group-scoped key/value string resource and emit `ResourceUpdated`. Implementations MAY restrict who can update resources. A simple baseline allows any member to set resources; alternative policies are acceptable if clearly documented. - Introspection: `supportsInterface` MUST return true for `type(IERC165).interfaceId` and for `type(IERC8063).interfaceId`. ### Identifier model and naming @@ -106,14 +95,16 @@ interface IERC8063 is IERC165 { ### Optional extensions (non-normative) -- Admin roles: Owner-delegated administrators that can invite/remove members and manage resources. +- Admin roles: Owner-delegated administrators that can invite/remove members. - Signature-based invites: Offchain signed invites verifiable onchain for gasless flows. - Open groups: Opt-in groups without invitations where `acceptInvite` is implicitly allowed for any account. +- Resource associations: External contracts (e.g., [ERC-20](./eip-20.md), [ERC-721](./eip-721.md)) MAY track group ownership or membership-based access using the group's contract address and `groupId`. ## Rationale - Token-agnostic: Separates membership from asset ownership/transfer semantics, suitable for social and coordination use cases. -- Minimal surface: Creation, invite/join, membership queries, and resource metadata are sufficient for broad interoperability while allowing richer policies via extensions. +- Minimal surface: Creation, invite/join, and membership queries are sufficient for broad interoperability while allowing richer policies via extensions. +- External resource model: Resources (tokens, NFTs) can be associated with groups externally, just as they would with any address, avoiding tight coupling and enabling flexible composition. - Relationship to [ERC-7743](./eip-7743.md): MO-NFT models multi-ownership of a single token; Groups model multi-membership of a container. Implementations MAY associate a Group with tokens, but the standard does not require token interfaces. ## Backwards Compatibility @@ -131,8 +122,7 @@ Reference contracts are provided in the `assets/` directory: - Membership griefing: Implementations SHOULD bound per-tx iterations and avoid unbounded member enumeration onchain. - Invite spoofing: Signature-based invites MUST protect against replay and domain separation if used. The baseline owner-called invites avoid this. -- Resource integrity: If resources drive authorization offchain, rely on event logs and content-addressed URIs (e.g., IPFS/Arweave) to prevent tampering. -- Access control: Clearly document who can set resources; conservative defaults recommend owner- or member-only writes. +- Access control: Clearly document who can invite and remove members; conservative defaults recommend owner-only operations. ## Copyright diff --git a/assets/erc-8063/ERC8063.sol b/assets/erc-8063/ERC8063.sol index 77f3c532e5f..55d879d9508 100644 --- a/assets/erc-8063/ERC8063.sol +++ b/assets/erc-8063/ERC8063.sol @@ -12,7 +12,6 @@ contract ERC8063 is IERC8063 { uint256 memberCount; mapping(address => bool) isMember; mapping(address => bool) pendingInvite; - mapping(bytes32 => string) resources; } uint256 private _nextGroupId; @@ -84,24 +83,6 @@ contract ERC8063 is IERC8063 { unchecked { g.memberCount -= 1; } emit MemberRemoved(groupId, account, msg.sender); } - - function setResource(uint256 groupId, bytes32 key, string calldata value) external override { - GroupData storage g = _groups[groupId]; - require(g.isMember[msg.sender] || msg.sender == g.owner, "Only member or owner"); - if (bytes(value).length == 0) { - // delete by setting to empty string (idempotent) - if (bytes(g.resources[key]).length != 0) { - g.resources[key] = ""; - } - } else { - g.resources[key] = value; - } - emit ResourceUpdated(groupId, key, value, msg.sender); - } - - function getResource(uint256 groupId, bytes32 key) external view override returns (string memory) { - return _groups[groupId].resources[key]; - } } diff --git a/assets/erc-8063/IERC8063.sol b/assets/erc-8063/IERC8063.sol index 16ba2f13a13..b7440fdad5c 100644 --- a/assets/erc-8063/IERC8063.sol +++ b/assets/erc-8063/IERC8063.sol @@ -20,9 +20,6 @@ interface IERC8063 is IERC165 { /// @dev Emitted when a member is removed (cannot remove the owner) event MemberRemoved(uint256 indexed groupId, address indexed account, address indexed by); - /// @dev Emitted when a resource key is set/updated/cleared (empty value means delete) - event ResourceUpdated(uint256 indexed groupId, bytes32 indexed key, string value, address indexed by); - /// @notice Create a new group; caller becomes owner and initial member /// @param name Optional human-readable group name /// @param metadataURI Optional offchain metadata (e.g., JSON document) @@ -49,11 +46,4 @@ interface IERC8063 is IERC165 { /// @notice Owner removes a member (owner cannot be removed) function removeMember(uint256 groupId, address account) external; - - /// @notice Set or clear a resource value for the group - /// @dev Setting to an empty string SHOULD be treated as deletion - function setResource(uint256 groupId, bytes32 key, string calldata value) external; - - /// @notice Read a resource value for the group (empty string if unset) - function getResource(uint256 groupId, bytes32 key) external view returns (string memory); } From 67e32641ab703452ff443cc15e6868ed39aee4b3 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Fri, 31 Oct 2025 12:22:39 +0800 Subject: [PATCH 10/16] Add transferMembership function to ERC-8063 --- ERCS/erc-8063.md | 14 ++++++++++++-- assets/erc-8063/ERC8063.sol | 12 ++++++++++++ assets/erc-8063/IERC8063.sol | 7 +++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index 6308bf11d9d..44a89a4e5c0 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -1,6 +1,6 @@ --- eip: 8063 -title: Group Standard +title: Groups - Multi-Member Containers description: Onchain groups with membership, invitations, and shared metadata. author: James Savechives (@jamesavechives) discussions-to: https://ethereum-magicians.org/t/erc-8063-groups-multi-member-onchain-containers-for-shared-resources/25999 @@ -50,6 +50,9 @@ interface IERC8063 is IERC165 { /// @dev Emitted when a member is removed (cannot remove the owner) event MemberRemoved(uint256 indexed groupId, address indexed account, address indexed by); + /// @dev Emitted when a member transfers their membership to another address + event MembershipTransferred(uint256 indexed groupId, address indexed from, address indexed to); + /// @notice Create a new group; caller becomes owner and initial member /// @param name Optional human-readable group name /// @param metadataURI Optional offchain metadata (e.g., JSON document) @@ -76,6 +79,10 @@ interface IERC8063 is IERC165 { /// @notice Owner removes a member (owner cannot be removed) function removeMember(uint256 groupId, address account) external; + + /// @notice Transfer membership to another address (caller loses membership) + /// @param to Address to receive membership (must not already be a member) + function transferMembership(uint256 groupId, address to) external; } ``` @@ -85,6 +92,7 @@ interface IERC8063 is IERC165 { - Invitations: Only the group owner MAY call `inviteMember`. Implementations MUST record an outstanding invitation for `account` and emit `MemberInvited`. - Joining: `acceptInvite` MUST succeed only for invited accounts, add them as members exactly once, and emit `MemberJoined`. - Removal: Only the owner MAY call `removeMember`. Implementations MUST NOT allow removing the owner. +- Membership transfer: Any member (except the owner) MAY call `transferMembership` to transfer their membership to a non-member address. The caller MUST lose membership, the recipient MUST become a member, and `MembershipTransferred` MUST be emitted. Member count remains unchanged. - Introspection: `supportsInterface` MUST return true for `type(IERC165).interfaceId` and for `type(IERC8063).interfaceId`. ### Identifier model and naming @@ -98,12 +106,14 @@ interface IERC8063 is IERC165 { - Admin roles: Owner-delegated administrators that can invite/remove members. - Signature-based invites: Offchain signed invites verifiable onchain for gasless flows. - Open groups: Opt-in groups without invitations where `acceptInvite` is implicitly allowed for any account. +- Transferable ownership: Allow the owner to transfer ownership to another member. - Resource associations: External contracts (e.g., [ERC-20](./eip-20.md), [ERC-721](./eip-721.md)) MAY track group ownership or membership-based access using the group's contract address and `groupId`. ## Rationale - Token-agnostic: Separates membership from asset ownership/transfer semantics, suitable for social and coordination use cases. -- Minimal surface: Creation, invite/join, and membership queries are sufficient for broad interoperability while allowing richer policies via extensions. +- Minimal surface: Creation, invite/join, membership queries, and transfers are sufficient for broad interoperability while allowing richer policies via extensions. +- Transferable membership: Enables members to delegate or reassign their position without owner intervention, supporting use cases like account migration or role handoffs. - External resource model: Resources (tokens, NFTs) can be associated with groups externally, just as they would with any address, avoiding tight coupling and enabling flexible composition. - Relationship to [ERC-7743](./eip-7743.md): MO-NFT models multi-ownership of a single token; Groups model multi-membership of a container. Implementations MAY associate a Group with tokens, but the standard does not require token interfaces. diff --git a/assets/erc-8063/ERC8063.sol b/assets/erc-8063/ERC8063.sol index 55d879d9508..9a4d66a0cb4 100644 --- a/assets/erc-8063/ERC8063.sol +++ b/assets/erc-8063/ERC8063.sol @@ -83,6 +83,18 @@ contract ERC8063 is IERC8063 { unchecked { g.memberCount -= 1; } emit MemberRemoved(groupId, account, msg.sender); } + + function transferMembership(uint256 groupId, address to) external override { + GroupData storage g = _groups[groupId]; + require(g.isMember[msg.sender], "Not a member"); + require(msg.sender != g.owner, "Owner cannot transfer"); + require(to != address(0), "Zero address"); + require(!g.isMember[to], "Already a member"); + + g.isMember[msg.sender] = false; + g.isMember[to] = true; + emit MembershipTransferred(groupId, msg.sender, to); + } } diff --git a/assets/erc-8063/IERC8063.sol b/assets/erc-8063/IERC8063.sol index b7440fdad5c..9e54668f262 100644 --- a/assets/erc-8063/IERC8063.sol +++ b/assets/erc-8063/IERC8063.sol @@ -20,6 +20,9 @@ interface IERC8063 is IERC165 { /// @dev Emitted when a member is removed (cannot remove the owner) event MemberRemoved(uint256 indexed groupId, address indexed account, address indexed by); + /// @dev Emitted when a member transfers their membership to another address + event MembershipTransferred(uint256 indexed groupId, address indexed from, address indexed to); + /// @notice Create a new group; caller becomes owner and initial member /// @param name Optional human-readable group name /// @param metadataURI Optional offchain metadata (e.g., JSON document) @@ -46,4 +49,8 @@ interface IERC8063 is IERC165 { /// @notice Owner removes a member (owner cannot be removed) function removeMember(uint256 groupId, address account) external; + + /// @notice Transfer membership to another address (caller loses membership) + /// @param to Address to receive membership (must not already be a member) + function transferMembership(uint256 groupId, address to) external; } From bd5462c38de46242303e828822d3f59e7ddd3a85 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Fri, 31 Oct 2025 12:52:09 +0800 Subject: [PATCH 11/16] remove groupId from Group contract --- ERCS/erc-8063.md | 68 +++++++++---------- assets/erc-8063/ERC8063.sol | 122 +++++++++++++++-------------------- assets/erc-8063/IERC8063.sol | 37 ++++------- 3 files changed, 99 insertions(+), 128 deletions(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index 44a89a4e5c0..a204c3c9527 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -13,11 +13,11 @@ requires: 165 ## Abstract -This proposal specifies a general-purpose "Group" primitive as a first-class, onchain object. A Group is identified by a `groupId` and has an owner, an extensible set of members, and optional metadata. The standard defines a minimal interface for creating groups, inviting and joining members, and membership introspection. Unlike token standards (e.g., [ERC-20](./eip-20.md)/[ERC-721](./eip-721.md)) that model units of transferable ownership, Groups model multi-party membership and coordination. The goal is to enable interoperable social, organizational, and collaborative primitives. +This proposal specifies a general-purpose "Group" primitive as a first-class, onchain object. Each group is deployed as its own contract (identified by its contract address) and has an owner, an extensible set of members, and optional metadata. The standard defines a minimal interface for inviting and joining members, and membership introspection. Unlike token standards (e.g., [ERC-20](./eip-20.md)/[ERC-721](./eip-721.md)) that model units of transferable ownership, Groups model multi-party membership and coordination. The goal is to enable interoperable social, organizational, and collaborative primitives. ## Motivation -Tokens typically model ownership and transfer. Many applications instead need an addressable set of accounts with controlled join/leave semantics and shared state—e.g., project teams, DAOs, game parties, channels, or access cohorts. While [ERC-7743](./eip-7743.md) (MO-NFT) explores multi-ownership for a single token, it still anchors the abstraction to token semantics. Groups generalize this into a token-agnostic container where members can be added over time, without implying transfer or unitized ownership. External resources (tokens, NFTs, etc.) can be associated with groups through their own contracts, just as they would with any EOA or contract address. +Tokens typically model ownership and transfer. Many applications instead need an addressable set of accounts with controlled join/leave semantics and shared state—e.g., project teams, DAOs, game parties, channels, or access cohorts. While [ERC-7743](./eip-7743.md) (MO-NFT) explores multi-ownership for a single token, it still anchors the abstraction to token semantics. Groups generalize this into a token-agnostic container where members can be added over time, without implying transfer or unitized ownership. Following the [ERC-20](./eip-20.md) design pattern, each group is its own contract identified by its address. External resources (tokens, NFTs, etc.) can be associated with groups through their own contracts, just as they would with any EOA or contract address. ## Specification @@ -36,70 +36,64 @@ interface IERC165 { } /// @title IERC8063 — Minimal interface for onchain groups -/// @notice A group is a container with an owner and members +/// @notice A group is a contract with an owner and members interface IERC8063 is IERC165 { - /// @dev Emitted when a new group is created - event GroupCreated(uint256 indexed groupId, address indexed owner, string name, string metadataURI); - /// @dev Emitted when the owner invites an account - event MemberInvited(uint256 indexed groupId, address indexed inviter, address indexed invitee); + event MemberInvited(address indexed inviter, address indexed invitee); /// @dev Emitted when an invited account accepts and becomes a member - event MemberJoined(uint256 indexed groupId, address indexed account); + event MemberJoined(address indexed account); /// @dev Emitted when a member is removed (cannot remove the owner) - event MemberRemoved(uint256 indexed groupId, address indexed account, address indexed by); + event MemberRemoved(address indexed account, address indexed by); /// @dev Emitted when a member transfers their membership to another address - event MembershipTransferred(uint256 indexed groupId, address indexed from, address indexed to); - - /// @notice Create a new group; caller becomes owner and initial member - /// @param name Optional human-readable group name - /// @param metadataURI Optional offchain metadata (e.g., JSON document) - /// @return groupId Newly created group identifier - function createGroup(string calldata name, string calldata metadataURI) external returns (uint256 groupId); + event MembershipTransferred(address indexed from, address indexed to); - /// @notice Returns the owner of a group - function groupOwner(uint256 groupId) external view returns (address); + /// @notice Returns the owner of the group + function owner() external view returns (address); /// @notice Returns the human-readable name of the group (may be empty) - function groupName(uint256 groupId) external view returns (string memory); + function name() external view returns (string memory); /// @notice Returns true if `account` is a member of the group - function isMember(uint256 groupId, address account) external view returns (bool); + function isMember(address account) external view returns (bool); /// @notice Returns current number of members (including owner) - function getMemberCount(uint256 groupId) external view returns (uint256); + function getMemberCount() external view returns (uint256); /// @notice Owner invites an account to join the group - function inviteMember(uint256 groupId, address account) external; + function inviteMember(address account) external; /// @notice Invitee accepts an outstanding invite and becomes a member - function acceptInvite(uint256 groupId) external; + function acceptInvite() external; /// @notice Owner removes a member (owner cannot be removed) - function removeMember(uint256 groupId, address account) external; + function removeMember(address account) external; /// @notice Transfer membership to another address (caller loses membership) /// @param to Address to receive membership (must not already be a member) - function transferMembership(uint256 groupId, address to) external; + function transferMembership(address to) external; } ``` ### Semantics -- Group creation: `createGroup` MUST assign the caller as the group owner and an initial member, emit `GroupCreated`, and return a non-zero `groupId` unique within the contract. +- Group creation: The contract constructor MUST set the deployer as the group owner and initial member. Implementations SHOULD accept a name and optional metadataURI as constructor parameters. - Invitations: Only the group owner MAY call `inviteMember`. Implementations MUST record an outstanding invitation for `account` and emit `MemberInvited`. - Joining: `acceptInvite` MUST succeed only for invited accounts, add them as members exactly once, and emit `MemberJoined`. - Removal: Only the owner MAY call `removeMember`. Implementations MUST NOT allow removing the owner. - Membership transfer: Any member (except the owner) MAY call `transferMembership` to transfer their membership to a non-member address. The caller MUST lose membership, the recipient MUST become a member, and `MembershipTransferred` MUST be emitted. Member count remains unchanged. - Introspection: `supportsInterface` MUST return true for `type(IERC165).interfaceId` and for `type(IERC8063).interfaceId`. -### Identifier model and naming +### Deployment model + +Following the [ERC-20](./eip-20.md) pattern, each group is its own contract deployment: -- Single-group deployments: For implementations that deploy exactly one group per contract, the canonical group identifier SHOULD be the contract address (`address(this)`). In such cases, interfaces MAY omit the `groupId` parameter by using a thin wrapper or adapter while preserving ERC-165 detection for interoperability. -- Multi-group containers: For contracts that hold multiple groups, `groupId` is a `uint256` unique within the contract. The canonical global identifier becomes the pair `(contract address, groupId)`. -- Naming: Implementations SHOULD provide a human-readable name at creation via `createGroup(name, ...)` and expose it via `groupName(groupId)`. Empty names are permitted. +- **Group identity**: A group is uniquely identified by its contract address, just as an ERC-20 token is identified by its contract address. +- **Deployment**: To create a group, deploy a new contract implementing `IERC8063`. The deployer becomes the owner and initial member. +- **Discovery**: Applications can discover groups through events, registries, or direct address references, similar to token discovery patterns. +- **Naming**: Implementations SHOULD accept a human-readable name in the constructor and expose it via `name()`. Empty names are permitted. ### Optional extensions (non-normative) @@ -107,15 +101,17 @@ interface IERC8063 is IERC165 { - Signature-based invites: Offchain signed invites verifiable onchain for gasless flows. - Open groups: Opt-in groups without invitations where `acceptInvite` is implicitly allowed for any account. - Transferable ownership: Allow the owner to transfer ownership to another member. -- Resource associations: External contracts (e.g., [ERC-20](./eip-20.md), [ERC-721](./eip-721.md)) MAY track group ownership or membership-based access using the group's contract address and `groupId`. +- Factory contracts: A factory pattern for deploying groups with standardized initialization. +- Resource associations: External contracts (e.g., [ERC-20](./eip-20.md), [ERC-721](./eip-721.md)) MAY track group ownership or membership-based access using the group's contract address. ## Rationale -- Token-agnostic: Separates membership from asset ownership/transfer semantics, suitable for social and coordination use cases. -- Minimal surface: Creation, invite/join, membership queries, and transfers are sufficient for broad interoperability while allowing richer policies via extensions. -- Transferable membership: Enables members to delegate or reassign their position without owner intervention, supporting use cases like account migration or role handoffs. -- External resource model: Resources (tokens, NFTs) can be associated with groups externally, just as they would with any address, avoiding tight coupling and enabling flexible composition. -- Relationship to [ERC-7743](./eip-7743.md): MO-NFT models multi-ownership of a single token; Groups model multi-membership of a container. Implementations MAY associate a Group with tokens, but the standard does not require token interfaces. +- **One contract per group**: Following the [ERC-20](./eip-20.md) model where each token is its own contract creates a simpler, more composable design. The contract address becomes the natural group identifier. +- **Token-agnostic**: Separates membership from asset ownership/transfer semantics, suitable for social and coordination use cases. +- **Minimal surface**: Invite/join, membership queries, and transfers are sufficient for broad interoperability while allowing richer policies via extensions. +- **Transferable membership**: Enables members to delegate or reassign their position without owner intervention, supporting use cases like account migration or role handoffs. +- **External resource model**: Resources (tokens, NFTs) can be associated with groups externally, just as they would with any address, avoiding tight coupling and enabling flexible composition. +- **Relationship to [ERC-7743](./eip-7743.md)**: MO-NFT models multi-ownership of a single token; Groups model multi-membership of a container. Implementations MAY associate a Group with tokens, but the standard does not require token interfaces. ## Backwards Compatibility diff --git a/assets/erc-8063/ERC8063.sol b/assets/erc-8063/ERC8063.sol index 9a4d66a0cb4..51b9b8e0fb4 100644 --- a/assets/erc-8063/ERC8063.sol +++ b/assets/erc-8063/ERC8063.sol @@ -5,96 +5,80 @@ import "./IERC8063.sol"; /// @title ERC8063 — minimal reference implementation of IERC8063 contract ERC8063 is IERC8063 { - struct GroupData { - address owner; - string name; - string metadataURI; - uint256 memberCount; - mapping(address => bool) isMember; - mapping(address => bool) pendingInvite; - } - - uint256 private _nextGroupId; - mapping(uint256 => GroupData) private _groups; - - constructor() { - _nextGroupId = 1; + address private immutable _owner; + string private _name; + string private _metadataURI; + uint256 private _memberCount; + + mapping(address => bool) private _isMember; + mapping(address => bool) private _pendingInvite; + + /// @notice Create a new group; caller becomes owner and initial member + /// @param groupName Human-readable group name + /// @param metadataURI Optional offchain metadata (e.g., JSON document) + constructor(string memory groupName, string memory metadataURI) { + _owner = msg.sender; + _name = groupName; + _metadataURI = metadataURI; + _isMember[msg.sender] = true; + _memberCount = 1; } function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { - return interfaceId == type(IERC8063).interfaceId; + return interfaceId == type(IERC8063).interfaceId || interfaceId == type(IERC165).interfaceId; } - function createGroup(string calldata name, string calldata metadataURI) external override returns (uint256 groupId) { - groupId = _nextGroupId++; - GroupData storage g = _groups[groupId]; - g.owner = msg.sender; - g.name = name; - g.metadataURI = metadataURI; - if (!g.isMember[msg.sender]) { - g.isMember[msg.sender] = true; - g.memberCount = 1; - } - emit GroupCreated(groupId, msg.sender, name, metadataURI); + function owner() public view override returns (address) { + return _owner; } - function groupOwner(uint256 groupId) public view override returns (address) { - return _groups[groupId].owner; + function name() public view override returns (string memory) { + return _name; } - function groupName(uint256 groupId) public view override returns (string memory) { - return _groups[groupId].name; + function isMember(address account) public view override returns (bool) { + return _isMember[account]; } - function isMember(uint256 groupId, address account) public view override returns (bool) { - return _groups[groupId].isMember[account]; + function getMemberCount() external view override returns (uint256) { + return _memberCount; } - function getMemberCount(uint256 groupId) external view override returns (uint256) { - return _groups[groupId].memberCount; - } - - function inviteMember(uint256 groupId, address account) external override { - GroupData storage g = _groups[groupId]; - require(msg.sender == g.owner, "Only owner can invite"); + function inviteMember(address account) external override { + require(msg.sender == _owner, "Only owner can invite"); require(account != address(0), "Zero address"); - require(!g.isMember[account], "Already member"); - require(!g.pendingInvite[account], "Already invited"); - g.pendingInvite[account] = true; - emit MemberInvited(groupId, msg.sender, account); + require(!_isMember[account], "Already member"); + require(!_pendingInvite[account], "Already invited"); + _pendingInvite[account] = true; + emit MemberInvited(msg.sender, account); } - function acceptInvite(uint256 groupId) external override { - GroupData storage g = _groups[groupId]; - require(g.pendingInvite[msg.sender], "No invite"); - require(!g.isMember[msg.sender], "Already member"); - g.pendingInvite[msg.sender] = false; - g.isMember[msg.sender] = true; - unchecked { g.memberCount += 1; } - emit MemberJoined(groupId, msg.sender); + function acceptInvite() external override { + require(_pendingInvite[msg.sender], "No invite"); + require(!_isMember[msg.sender], "Already member"); + _pendingInvite[msg.sender] = false; + _isMember[msg.sender] = true; + unchecked { _memberCount += 1; } + emit MemberJoined(msg.sender); } - function removeMember(uint256 groupId, address account) external override { - GroupData storage g = _groups[groupId]; - require(msg.sender == g.owner, "Only owner can remove"); - require(account != g.owner, "Cannot remove owner"); - require(g.isMember[account], "Not a member"); - g.isMember[account] = false; - unchecked { g.memberCount -= 1; } - emit MemberRemoved(groupId, account, msg.sender); + function removeMember(address account) external override { + require(msg.sender == _owner, "Only owner can remove"); + require(account != _owner, "Cannot remove owner"); + require(_isMember[account], "Not a member"); + _isMember[account] = false; + unchecked { _memberCount -= 1; } + emit MemberRemoved(account, msg.sender); } - function transferMembership(uint256 groupId, address to) external override { - GroupData storage g = _groups[groupId]; - require(g.isMember[msg.sender], "Not a member"); - require(msg.sender != g.owner, "Owner cannot transfer"); + function transferMembership(address to) external override { + require(_isMember[msg.sender], "Not a member"); + require(msg.sender != _owner, "Owner cannot transfer"); require(to != address(0), "Zero address"); - require(!g.isMember[to], "Already a member"); + require(!_isMember[to], "Already a member"); - g.isMember[msg.sender] = false; - g.isMember[to] = true; - emit MembershipTransferred(groupId, msg.sender, to); + _isMember[msg.sender] = false; + _isMember[to] = true; + emit MembershipTransferred(msg.sender, to); } } - - diff --git a/assets/erc-8063/IERC8063.sol b/assets/erc-8063/IERC8063.sol index 9e54668f262..fc710fb41fa 100644 --- a/assets/erc-8063/IERC8063.sol +++ b/assets/erc-8063/IERC8063.sol @@ -6,51 +6,42 @@ interface IERC165 { } /// @title IERC8063 — Minimal interface for onchain groups -/// @notice A group is a container with an owner, members, and shared resources +/// @notice A group is a contract with an owner and members interface IERC8063 is IERC165 { - /// @dev Emitted when a new group is created - event GroupCreated(uint256 indexed groupId, address indexed owner, string name, string metadataURI); - /// @dev Emitted when the owner invites an account - event MemberInvited(uint256 indexed groupId, address indexed inviter, address indexed invitee); + event MemberInvited(address indexed inviter, address indexed invitee); /// @dev Emitted when an invited account accepts and becomes a member - event MemberJoined(uint256 indexed groupId, address indexed account); + event MemberJoined(address indexed account); /// @dev Emitted when a member is removed (cannot remove the owner) - event MemberRemoved(uint256 indexed groupId, address indexed account, address indexed by); + event MemberRemoved(address indexed account, address indexed by); /// @dev Emitted when a member transfers their membership to another address - event MembershipTransferred(uint256 indexed groupId, address indexed from, address indexed to); - - /// @notice Create a new group; caller becomes owner and initial member - /// @param name Optional human-readable group name - /// @param metadataURI Optional offchain metadata (e.g., JSON document) - /// @return groupId Newly created group identifier - function createGroup(string calldata name, string calldata metadataURI) external returns (uint256 groupId); + event MembershipTransferred(address indexed from, address indexed to); - /// @notice Returns the owner of a group - function groupOwner(uint256 groupId) external view returns (address); + /// @notice Returns the owner of the group + function owner() external view returns (address); /// @notice Returns the human-readable name of the group (may be empty) - function groupName(uint256 groupId) external view returns (string memory); + function name() external view returns (string memory); /// @notice Returns true if `account` is a member of the group - function isMember(uint256 groupId, address account) external view returns (bool); + function isMember(address account) external view returns (bool); /// @notice Returns current number of members (including owner) - function getMemberCount(uint256 groupId) external view returns (uint256); + function getMemberCount() external view returns (uint256); /// @notice Owner invites an account to join the group - function inviteMember(uint256 groupId, address account) external; + function inviteMember(address account) external; /// @notice Invitee accepts an outstanding invite and becomes a member - function acceptInvite(uint256 groupId) external; + function acceptInvite() external; /// @notice Owner removes a member (owner cannot be removed) - function removeMember(uint256 groupId, address account) external; + function removeMember(address account) external; /// @notice Transfer membership to another address (caller loses membership) /// @param to Address to receive membership (must not already be a member) - function transferMembership(uint256 groupId, address to) external; + function transferMembership(address to) external; } From e406a070f1a3286e5e18edc5496de9e9584c032e Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Fri, 31 Oct 2025 12:56:29 +0800 Subject: [PATCH 12/16] remove intivation functions --- ERCS/erc-8063.md | 30 ++++++++++++------------------ assets/erc-8063/ERC8063.sol | 18 ++++-------------- assets/erc-8063/IERC8063.sol | 14 ++++---------- 3 files changed, 20 insertions(+), 42 deletions(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index a204c3c9527..9130160daeb 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -1,7 +1,7 @@ --- eip: 8063 title: Groups - Multi-Member Containers -description: Onchain groups with membership, invitations, and shared metadata. +description: Onchain groups with membership and direct member management. author: James Savechives (@jamesavechives) discussions-to: https://ethereum-magicians.org/t/erc-8063-groups-multi-member-onchain-containers-for-shared-resources/25999 status: Draft @@ -13,7 +13,7 @@ requires: 165 ## Abstract -This proposal specifies a general-purpose "Group" primitive as a first-class, onchain object. Each group is deployed as its own contract (identified by its contract address) and has an owner, an extensible set of members, and optional metadata. The standard defines a minimal interface for inviting and joining members, and membership introspection. Unlike token standards (e.g., [ERC-20](./eip-20.md)/[ERC-721](./eip-721.md)) that model units of transferable ownership, Groups model multi-party membership and coordination. The goal is to enable interoperable social, organizational, and collaborative primitives. +This proposal specifies a general-purpose "Group" primitive as a first-class, onchain object. Each group is deployed as its own contract (identified by its contract address) and has an owner, an extensible set of members, and optional metadata. The standard defines a minimal interface for direct member management and membership introspection. Unlike token standards (e.g., [ERC-20](./eip-20.md)/[ERC-721](./eip-721.md)) that model units of transferable ownership, Groups model multi-party membership and coordination. The goal is to enable interoperable social, organizational, and collaborative primitives. ## Motivation @@ -38,11 +38,8 @@ interface IERC165 { /// @title IERC8063 — Minimal interface for onchain groups /// @notice A group is a contract with an owner and members interface IERC8063 is IERC165 { - /// @dev Emitted when the owner invites an account - event MemberInvited(address indexed inviter, address indexed invitee); - - /// @dev Emitted when an invited account accepts and becomes a member - event MemberJoined(address indexed account); + /// @dev Emitted when a member is added to the group + event MemberAdded(address indexed account, address indexed by); /// @dev Emitted when a member is removed (cannot remove the owner) event MemberRemoved(address indexed account, address indexed by); @@ -62,11 +59,8 @@ interface IERC8063 is IERC165 { /// @notice Returns current number of members (including owner) function getMemberCount() external view returns (uint256); - /// @notice Owner invites an account to join the group - function inviteMember(address account) external; - - /// @notice Invitee accepts an outstanding invite and becomes a member - function acceptInvite() external; + /// @notice Owner adds an account as a member + function addMember(address account) external; /// @notice Owner removes a member (owner cannot be removed) function removeMember(address account) external; @@ -80,8 +74,7 @@ interface IERC8063 is IERC165 { ### Semantics - Group creation: The contract constructor MUST set the deployer as the group owner and initial member. Implementations SHOULD accept a name and optional metadataURI as constructor parameters. -- Invitations: Only the group owner MAY call `inviteMember`. Implementations MUST record an outstanding invitation for `account` and emit `MemberInvited`. -- Joining: `acceptInvite` MUST succeed only for invited accounts, add them as members exactly once, and emit `MemberJoined`. +- Adding members: Only the group owner MAY call `addMember`. The account MUST NOT already be a member. The implementation MUST add them as a member and emit `MemberAdded`. - Removal: Only the owner MAY call `removeMember`. Implementations MUST NOT allow removing the owner. - Membership transfer: Any member (except the owner) MAY call `transferMembership` to transfer their membership to a non-member address. The caller MUST lose membership, the recipient MUST become a member, and `MembershipTransferred` MUST be emitted. Member count remains unchanged. - Introspection: `supportsInterface` MUST return true for `type(IERC165).interfaceId` and for `type(IERC8063).interfaceId`. @@ -97,9 +90,9 @@ Following the [ERC-20](./eip-20.md) pattern, each group is its own contract depl ### Optional extensions (non-normative) -- Admin roles: Owner-delegated administrators that can invite/remove members. -- Signature-based invites: Offchain signed invites verifiable onchain for gasless flows. -- Open groups: Opt-in groups without invitations where `acceptInvite` is implicitly allowed for any account. +- Admin roles: Owner-delegated administrators that can add/remove members. +- Multi-signature member addition: Require multiple approvals before adding members. +- Open groups: Allow anyone to join without owner approval. - Transferable ownership: Allow the owner to transfer ownership to another member. - Factory contracts: A factory pattern for deploying groups with standardized initialization. - Resource associations: External contracts (e.g., [ERC-20](./eip-20.md), [ERC-721](./eip-721.md)) MAY track group ownership or membership-based access using the group's contract address. @@ -108,7 +101,8 @@ Following the [ERC-20](./eip-20.md) pattern, each group is its own contract depl - **One contract per group**: Following the [ERC-20](./eip-20.md) model where each token is its own contract creates a simpler, more composable design. The contract address becomes the natural group identifier. - **Token-agnostic**: Separates membership from asset ownership/transfer semantics, suitable for social and coordination use cases. -- **Minimal surface**: Invite/join, membership queries, and transfers are sufficient for broad interoperability while allowing richer policies via extensions. +- **Direct member management**: Owner can add members directly without invitation/acceptance flow, simplifying the interface and reducing transaction costs. +- **Minimal surface**: Add/remove members, membership queries, and transfers are sufficient for broad interoperability while allowing richer policies via extensions. - **Transferable membership**: Enables members to delegate or reassign their position without owner intervention, supporting use cases like account migration or role handoffs. - **External resource model**: Resources (tokens, NFTs) can be associated with groups externally, just as they would with any address, avoiding tight coupling and enabling flexible composition. - **Relationship to [ERC-7743](./eip-7743.md)**: MO-NFT models multi-ownership of a single token; Groups model multi-membership of a container. Implementations MAY associate a Group with tokens, but the standard does not require token interfaces. diff --git a/assets/erc-8063/ERC8063.sol b/assets/erc-8063/ERC8063.sol index 51b9b8e0fb4..6dfdc4de2d8 100644 --- a/assets/erc-8063/ERC8063.sol +++ b/assets/erc-8063/ERC8063.sol @@ -11,7 +11,6 @@ contract ERC8063 is IERC8063 { uint256 private _memberCount; mapping(address => bool) private _isMember; - mapping(address => bool) private _pendingInvite; /// @notice Create a new group; caller becomes owner and initial member /// @param groupName Human-readable group name @@ -44,22 +43,13 @@ contract ERC8063 is IERC8063 { return _memberCount; } - function inviteMember(address account) external override { - require(msg.sender == _owner, "Only owner can invite"); + function addMember(address account) external override { + require(msg.sender == _owner, "Only owner can add"); require(account != address(0), "Zero address"); require(!_isMember[account], "Already member"); - require(!_pendingInvite[account], "Already invited"); - _pendingInvite[account] = true; - emit MemberInvited(msg.sender, account); - } - - function acceptInvite() external override { - require(_pendingInvite[msg.sender], "No invite"); - require(!_isMember[msg.sender], "Already member"); - _pendingInvite[msg.sender] = false; - _isMember[msg.sender] = true; + _isMember[account] = true; unchecked { _memberCount += 1; } - emit MemberJoined(msg.sender); + emit MemberAdded(account, msg.sender); } function removeMember(address account) external override { diff --git a/assets/erc-8063/IERC8063.sol b/assets/erc-8063/IERC8063.sol index fc710fb41fa..2f094b100a6 100644 --- a/assets/erc-8063/IERC8063.sol +++ b/assets/erc-8063/IERC8063.sol @@ -8,11 +8,8 @@ interface IERC165 { /// @title IERC8063 — Minimal interface for onchain groups /// @notice A group is a contract with an owner and members interface IERC8063 is IERC165 { - /// @dev Emitted when the owner invites an account - event MemberInvited(address indexed inviter, address indexed invitee); - - /// @dev Emitted when an invited account accepts and becomes a member - event MemberJoined(address indexed account); + /// @dev Emitted when a member is added to the group + event MemberAdded(address indexed account, address indexed by); /// @dev Emitted when a member is removed (cannot remove the owner) event MemberRemoved(address indexed account, address indexed by); @@ -32,11 +29,8 @@ interface IERC8063 is IERC165 { /// @notice Returns current number of members (including owner) function getMemberCount() external view returns (uint256); - /// @notice Owner invites an account to join the group - function inviteMember(address account) external; - - /// @notice Invitee accepts an outstanding invite and becomes a member - function acceptInvite() external; + /// @notice Owner adds an account as a member + function addMember(address account) external; /// @notice Owner removes a member (owner cannot be removed) function removeMember(address account) external; From b71aa07ccab0c668667292016474b6eaec73eb79 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Fri, 31 Oct 2025 22:15:14 +0800 Subject: [PATCH 13/16] owner should not be able to remove members by default --- ERCS/erc-8063.md | 21 ++++++++++++--------- assets/erc-8063/ERC8063.sol | 11 +++++------ assets/erc-8063/IERC8063.sol | 8 ++++---- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index 9130160daeb..130b3683771 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -41,8 +41,8 @@ interface IERC8063 is IERC165 { /// @dev Emitted when a member is added to the group event MemberAdded(address indexed account, address indexed by); - /// @dev Emitted when a member is removed (cannot remove the owner) - event MemberRemoved(address indexed account, address indexed by); + /// @dev Emitted when a member voluntarily leaves the group + event MemberLeft(address indexed account); /// @dev Emitted when a member transfers their membership to another address event MembershipTransferred(address indexed from, address indexed to); @@ -62,8 +62,8 @@ interface IERC8063 is IERC165 { /// @notice Owner adds an account as a member function addMember(address account) external; - /// @notice Owner removes a member (owner cannot be removed) - function removeMember(address account) external; + /// @notice Member voluntarily leaves the group (owner cannot leave) + function leaveGroup() external; /// @notice Transfer membership to another address (caller loses membership) /// @param to Address to receive membership (must not already be a member) @@ -75,7 +75,7 @@ interface IERC8063 is IERC165 { - Group creation: The contract constructor MUST set the deployer as the group owner and initial member. Implementations SHOULD accept a name and optional metadataURI as constructor parameters. - Adding members: Only the group owner MAY call `addMember`. The account MUST NOT already be a member. The implementation MUST add them as a member and emit `MemberAdded`. -- Removal: Only the owner MAY call `removeMember`. Implementations MUST NOT allow removing the owner. +- Leaving: Any member (except the owner) MAY call `leaveGroup` to voluntarily exit. The implementation MUST remove their membership and emit `MemberLeft`. The owner MUST NOT be allowed to leave. - Membership transfer: Any member (except the owner) MAY call `transferMembership` to transfer their membership to a non-member address. The caller MUST lose membership, the recipient MUST become a member, and `MembershipTransferred` MUST be emitted. Member count remains unchanged. - Introspection: `supportsInterface` MUST return true for `type(IERC165).interfaceId` and for `type(IERC8063).interfaceId`. @@ -90,10 +90,11 @@ Following the [ERC-20](./eip-20.md) pattern, each group is its own contract depl ### Optional extensions (non-normative) -- Admin roles: Owner-delegated administrators that can add/remove members. +- Admin roles: Owner-delegated administrators that can add members. - Multi-signature member addition: Require multiple approvals before adding members. - Open groups: Allow anyone to join without owner approval. - Transferable ownership: Allow the owner to transfer ownership to another member. +- Removal by governance: Implement member removal through voting or consensus mechanisms for specific use cases requiring expulsion. - Factory contracts: A factory pattern for deploying groups with standardized initialization. - Resource associations: External contracts (e.g., [ERC-20](./eip-20.md), [ERC-721](./eip-721.md)) MAY track group ownership or membership-based access using the group's contract address. @@ -102,7 +103,8 @@ Following the [ERC-20](./eip-20.md) pattern, each group is its own contract depl - **One contract per group**: Following the [ERC-20](./eip-20.md) model where each token is its own contract creates a simpler, more composable design. The contract address becomes the natural group identifier. - **Token-agnostic**: Separates membership from asset ownership/transfer semantics, suitable for social and coordination use cases. - **Direct member management**: Owner can add members directly without invitation/acceptance flow, simplifying the interface and reducing transaction costs. -- **Minimal surface**: Add/remove members, membership queries, and transfers are sufficient for broad interoperability while allowing richer policies via extensions. +- **Voluntary exit only**: Members can leave voluntarily via `leaveGroup()` or transfer their position, but cannot be forcibly removed by the owner. This prevents centralization and ensures membership stability, similar to how ERC-20 token holders cannot have their tokens revoked. +- **Minimal surface**: Add members, voluntary leave, membership queries, and transfers are sufficient for broad interoperability while allowing richer policies via extensions. - **Transferable membership**: Enables members to delegate or reassign their position without owner intervention, supporting use cases like account migration or role handoffs. - **External resource model**: Resources (tokens, NFTs) can be associated with groups externally, just as they would with any address, avoiding tight coupling and enabling flexible composition. - **Relationship to [ERC-7743](./eip-7743.md)**: MO-NFT models multi-ownership of a single token; Groups model multi-membership of a container. Implementations MAY associate a Group with tokens, but the standard does not require token interfaces. @@ -121,8 +123,9 @@ Reference contracts are provided in the `assets/` directory: ## Security Considerations - Membership griefing: Implementations SHOULD bound per-tx iterations and avoid unbounded member enumeration onchain. -- Invite spoofing: Signature-based invites MUST protect against replay and domain separation if used. The baseline owner-called invites avoid this. -- Access control: Clearly document who can invite and remove members; conservative defaults recommend owner-only operations. +- Owner privileges: The owner has unilateral power to add members. Applications relying on group membership SHOULD consider this trust assumption. +- Permanent membership: Members cannot be forcibly removed by the owner, only voluntarily leave or transfer. This design choice prioritizes decentralization but means malicious members cannot be expelled without governance extensions. +- Access control: Clearly document who can add members; the baseline implementation grants this power exclusively to the owner. ## Copyright diff --git a/assets/erc-8063/ERC8063.sol b/assets/erc-8063/ERC8063.sol index 6dfdc4de2d8..2a5a36dfec9 100644 --- a/assets/erc-8063/ERC8063.sol +++ b/assets/erc-8063/ERC8063.sol @@ -52,13 +52,12 @@ contract ERC8063 is IERC8063 { emit MemberAdded(account, msg.sender); } - function removeMember(address account) external override { - require(msg.sender == _owner, "Only owner can remove"); - require(account != _owner, "Cannot remove owner"); - require(_isMember[account], "Not a member"); - _isMember[account] = false; + function leaveGroup() external override { + require(_isMember[msg.sender], "Not a member"); + require(msg.sender != _owner, "Owner cannot leave"); + _isMember[msg.sender] = false; unchecked { _memberCount -= 1; } - emit MemberRemoved(account, msg.sender); + emit MemberLeft(msg.sender); } function transferMembership(address to) external override { diff --git a/assets/erc-8063/IERC8063.sol b/assets/erc-8063/IERC8063.sol index 2f094b100a6..baeaa95355d 100644 --- a/assets/erc-8063/IERC8063.sol +++ b/assets/erc-8063/IERC8063.sol @@ -11,8 +11,8 @@ interface IERC8063 is IERC165 { /// @dev Emitted when a member is added to the group event MemberAdded(address indexed account, address indexed by); - /// @dev Emitted when a member is removed (cannot remove the owner) - event MemberRemoved(address indexed account, address indexed by); + /// @dev Emitted when a member voluntarily leaves the group + event MemberLeft(address indexed account); /// @dev Emitted when a member transfers their membership to another address event MembershipTransferred(address indexed from, address indexed to); @@ -32,8 +32,8 @@ interface IERC8063 is IERC165 { /// @notice Owner adds an account as a member function addMember(address account) external; - /// @notice Owner removes a member (owner cannot be removed) - function removeMember(address account) external; + /// @notice Member voluntarily leaves the group (owner cannot leave) + function leaveGroup() external; /// @notice Transfer membership to another address (caller loses membership) /// @param to Address to receive membership (must not already be a member) From 9b7fda9c3b7773cf4ce1dbd9f86f06e4a43ca6a4 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Mon, 3 Nov 2025 22:55:26 +0800 Subject: [PATCH 14/16] ERC-20 compatibility --- ERCS/erc-8063.md | 13 ++++ assets/erc-8063/ERC8063ERC20.sol | 129 +++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 assets/erc-8063/ERC8063ERC20.sol diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index 130b3683771..43dfce65c25 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -98,6 +98,19 @@ Following the [ERC-20](./eip-20.md) pattern, each group is its own contract depl - Factory contracts: A factory pattern for deploying groups with standardized initialization. - Resource associations: External contracts (e.g., [ERC-20](./eip-20.md), [ERC-721](./eip-721.md)) MAY track group ownership or membership-based access using the group's contract address. +#### ERC-20 compatibility (decimals=0) + +Implementations MAY expose an [ERC-20](./eip-20.md) interface for group membership under the following constraints: + +- `decimals()` MUST return `0`. +- `balanceOf(account)` MUST be either `0` (non-member) or `1` (member). +- `totalSupply()` MUST equal the current member count. +- `transfer(to, amount)` MUST require `amount == 1` and MUST transfer membership of the caller to `to`. +- `approve(spender, amount)` and `allowance(owner, spender)` MAY be implemented with values clamped to `{0,1}`; `transferFrom(from, to, amount)` MUST require `amount == 1` and, if permitted, MUST transfer membership from `from` to `to`. +- All operations MUST preserve the invariant that no account's balance exceeds `1` and MUST NOT allow the owner account to be transferred. + +Reference: see `assets/erc-8063/ERC8063ERC20.sol`. + ## Rationale - **One contract per group**: Following the [ERC-20](./eip-20.md) model where each token is its own contract creates a simpler, more composable design. The contract address becomes the natural group identifier. diff --git a/assets/erc-8063/ERC8063ERC20.sol b/assets/erc-8063/ERC8063ERC20.sol new file mode 100644 index 00000000000..f94012f8058 --- /dev/null +++ b/assets/erc-8063/ERC8063ERC20.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.20; + +import "./IERC8063.sol"; + +interface IERC20Minimal { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} + +/// @title ERC8063ERC20 — ERC-8063 group with optional ERC-20 compatibility (decimals=0) +/// @notice Balances are constrained to 0 or 1. Transfers MUST be exactly 1. +contract ERC8063ERC20 is IERC8063, IERC20Minimal { + address private immutable _owner; + string private _name; + string private _symbol; + string private _metadataURI; + uint256 private _memberCount; + + mapping(address => bool) private _isMember; + mapping(address => mapping(address => uint256)) private _allowances; // only 0 or 1 is valid + + constructor(string memory groupName, string memory symbol_, string memory metadataURI) { + _owner = msg.sender; + _name = groupName; + _symbol = symbol_; + _metadataURI = metadataURI; + _isMember[msg.sender] = true; + _memberCount = 1; + emit Transfer(address(0), msg.sender, 1); + emit MemberAdded(msg.sender, msg.sender); + } + + // IERC165 + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { + return interfaceId == type(IERC8063).interfaceId; + } + + // IERC8063 + function owner() public view override returns (address) { return _owner; } + function isMember(address account) public view override returns (bool) { return _isMember[account]; } + function getMemberCount() public view override returns (uint256) { return _memberCount; } + + function addMember(address account) external override { + require(msg.sender == _owner, "Only owner"); + require(account != address(0), "Zero address"); + require(!_isMember[account], "Already member"); + _isMember[account] = true; + unchecked { _memberCount += 1; } + emit MemberAdded(account, msg.sender); + emit Transfer(address(0), account, 1); + } + + function leaveGroup() external override { + require(_isMember[msg.sender], "Not member"); + require(msg.sender != _owner, "Owner cannot leave"); + _isMember[msg.sender] = false; + unchecked { _memberCount -= 1; } + emit MemberLeft(msg.sender); + emit Transfer(msg.sender, address(0), 1); + } + + function transferMembership(address to) external override { + require(_isMember[msg.sender], "Not member"); + require(msg.sender != _owner, "Owner cannot transfer"); + require(to != address(0), "Zero address"); + require(!_isMember[to], "Already member"); + _isMember[msg.sender] = false; + _isMember[to] = true; + emit MembershipTransferred(msg.sender, to); + emit Transfer(msg.sender, to, 1); + } + + function name() public view override(IERC20Minimal, IERC8063) returns (string memory) { return _name; } + function symbol() public view override returns (string memory) { return _symbol; } + function decimals() public pure override returns (uint8) { return 0; } + + // ERC-20 views + function totalSupply() public view override returns (uint256) { return _memberCount; } + function balanceOf(address account) public view override returns (uint256) { return _isMember[account] ? 1 : 0; } + + // ERC-20 actions (amount MUST be exactly 1) + function transfer(address to, uint256 amount) external override returns (bool) { + require(amount == 1, "amount must be 1"); + transferMembership(to); + return true; + } + + function allowance(address owner_, address spender) public view override returns (uint256) { + uint256 a = _allowances[owner_][spender]; + return a > 0 ? 1 : 0; + } + + function approve(address spender, uint256 amount) external override returns (bool) { + require(spender != address(0), "Zero spender"); + // Only 0 or 1 are meaningful; clamp to {0,1} + _allowances[msg.sender][spender] = amount > 0 ? 1 : 0; + emit Approval(msg.sender, spender, _allowances[msg.sender][spender]); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external override returns (bool) { + require(amount == 1, "amount must be 1"); + require(_allowances[from][msg.sender] >= 1, "insufficient allowance"); + require(_isMember[from], "from not member"); + require(from != _owner, "Owner cannot transfer"); + require(to != address(0), "Zero address"); + require(!_isMember[to], "to already member"); + _allowances[from][msg.sender] = 0; + _isMember[from] = false; + _isMember[to] = true; + emit Transfer(from, to, 1); + emit MembershipTransferred(from, to); + return true; + } +} + + From 6e043c3dc52da14ba56db693770889f212751e0f Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Mon, 3 Nov 2025 22:58:01 +0800 Subject: [PATCH 15/16] update format --- ERCS/erc-8063.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index 43dfce65c25..faad6dfac0a 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -109,7 +109,7 @@ Implementations MAY expose an [ERC-20](./eip-20.md) interface for group membersh - `approve(spender, amount)` and `allowance(owner, spender)` MAY be implemented with values clamped to `{0,1}`; `transferFrom(from, to, amount)` MUST require `amount == 1` and, if permitted, MUST transfer membership from `from` to `to`. - All operations MUST preserve the invariant that no account's balance exceeds `1` and MUST NOT allow the owner account to be transferred. -Reference: see `assets/erc-8063/ERC8063ERC20.sol`. +Reference: see the ERC8063ERC20.sol file in the assets directory. ## Rationale From 848ede66f9e1746df406d2173ba5e99875f0f9f0 Mon Sep 17 00:00:00 2001 From: jamesavechives Date: Mon, 3 Nov 2025 23:02:37 +0800 Subject: [PATCH 16/16] update format --- ERCS/erc-8063.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ERCS/erc-8063.md b/ERCS/erc-8063.md index faad6dfac0a..c83c2b9d27d 100644 --- a/ERCS/erc-8063.md +++ b/ERCS/erc-8063.md @@ -109,7 +109,7 @@ Implementations MAY expose an [ERC-20](./eip-20.md) interface for group membersh - `approve(spender, amount)` and `allowance(owner, spender)` MAY be implemented with values clamped to `{0,1}`; `transferFrom(from, to, amount)` MUST require `amount == 1` and, if permitted, MUST transfer membership from `from` to `to`. - All operations MUST preserve the invariant that no account's balance exceeds `1` and MUST NOT allow the owner account to be transferred. -Reference: see the ERC8063ERC20.sol file in the assets directory. +Reference: see the ERC-20 compatibility implementation in the assets directory. ## Rationale