-
Notifications
You must be signed in to change notification settings - Fork 806
Add ERC: Groups - Multi-Member Containers #1319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jamesavechives
wants to merge
19
commits into
ethereum:master
Choose a base branch
from
jamesavechives:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+398
−0
Open
Changes from 3 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
0972924
init group conception
4155b35
update format
3a5cb81
rename eip-0000.md to erc-0000.md
34141fd
Updated with assigned number
07dda38
update discussions-to link
3326b05
Merge branch 'master' into master
jamesavechives 1633b7d
feat : apply group name & rename contracts
4dba24b
fix format issue
21a99ba
fix format issue
7f9d15d
Merge branch 'master' into master
jamesavechives 10876ac
Refactor ERC-8063: Add naming, remove internal resources, rename to s…
67e3264
Add transferMembership function to ERC-8063
bd5462c
remove groupId from Group contract
e406a07
remove intivation functions
b71aa07
owner should not be able to remove members by default
9b7fda9
ERC-20 compatibility
6e043c3
update format
848ede6
update format
be64852
Merge branch 'master' into master
jamesavechives File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| --- | ||
| eip: 0000 | ||
| title: Groups — Multi-member onchain 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 | ||
| status: Draft | ||
| type: Standards Track | ||
| category: ERC | ||
| created: 2025-10-28 | ||
| 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. | ||
|
|
||
| ## 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. | ||
|
|
||
| ## Specification | ||
|
|
||
| The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. | ||
|
|
||
| Contracts that implement this standard MUST support [ERC-165](./eip-165.md). | ||
|
|
||
| ### Interface | ||
|
|
||
| ```solidity | ||
| /// 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); | ||
| } | ||
| ``` | ||
|
|
||
| ### 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. | ||
| - 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(IERCGroup).interfaceId`. | ||
|
|
||
| ### Optional extensions (non-normative) | ||
|
|
||
| - Admin roles: Owner-delegated administrators that can invite/remove members and manage resources. | ||
| - 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. | ||
|
|
||
| ## 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. | ||
| - 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 | ||
|
|
||
| No known backward compatibility issues. This is a new interface with [ERC-165](./eip-165.md) detection. | ||
|
|
||
| ## Reference Implementation | ||
|
|
||
| Reference contracts are provided in this repository: | ||
|
|
||
| - `assets/eip-groups/IERCGroup.sol` | ||
| - `assets/eip-groups/GroupContainer.sol` | ||
|
|
||
| ## 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. | ||
| - 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. | ||
|
|
||
| ## Copyright | ||
|
|
||
| Copyright and related rights waived via [CC0](../LICENSE.md). | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // SPDX-License-Identifier: CC0-1.0 | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import "./IERCGroup.sol"; | ||
|
|
||
| /// @title GroupContainer — minimal reference implementation of IERCGroup | ||
| contract GroupContainer is IERCGroup { | ||
| struct GroupData { | ||
| address owner; | ||
| string metadataURI; | ||
| uint256 memberCount; | ||
| mapping(address => 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]; | ||
| } | ||
| } | ||
|
|
||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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); | ||
| } | ||
|
|
||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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 | ||
|
|
||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.