As discussed in a meeting with people from BitsPossed, Proof of Humanity, and Gnosis, we want to use this thread to discuss the final design of the Circles Group Currency contract.
Here’s my take on the issue:
Creating Group Currencies
All new Group Currencies must be created by a single ‘GroupCurrencyTokenFactory’ contract. The factory deploys a new GC, signs it up at the Circles Hub (GCs are organizations), and then emits a ‘GroupCurrencySetup’ event. Using the factory ensures that there is a ‘root’ on top of which a subgraph can be built.
function deployGroupCurrency(
address _treasury
, address _owner
, address _discriminator
, uint8 _mintFeePerThousand
, string _memory name
, string _memory symbol
) external
Managing memberships
The only customizable behavior of a Group Currency should be the way how members are added or removed. There are two different types of memberships which can be used together in the same GC:
- Direct members are represented by their Circles token addresses. Only this specific token is accepted as a collateral to mint.
- Delegate trustees are represented by their Circles safe address. All tokens trusted by this address can be added as a collateral.
Current implementation
This is currently implemented with the concept of an ‘owner’. Only owners can add or remove accepted collateral tokens or delegate trustees. The owner address is passed as a parameter to the GC factory and can be any of the following:
Single owner
In the simplest case, an owner is just an EOA. E.g., the one that happened to deploy the GC via the factory. This way one key would have the full power to decide who can participate in a group and who not. Adding and removing members is as straightforward as calling ‘addMemberToken’ of the GC with the owner EOA.
function addMemberToken(address _member) public onlyOwner
MultiSig owner
A more advanced variant would be to use a MultiSig as owner. This way, multiple signers could decide who’s in and who not or a threshold could be used. The ‘addMemberToken’ method is called by the MultiSig contract which, in turn, is called by the EOA.
The function signature is the same as for a single owner
Contract owner
Any arbitrary contract can also be used as an owner. An example would be the Proof of Humanity “PoHGroupCurrencyManager”. Such a contract can be used to “gate” the GC with any arbitrary logic. In the case of PoH, the owner requires a user either to:
- Call ‘createProfile’ from the wallet that’s registered in the PoH registry and then ‘registerToken’ from the Circles Safe (first proof that you are in the PoH registry, then proof that you own the token that should be bound to the PoH soul id).
- Or to call ‘directRegister’ in case the Circles token owner and the PoH registered wallet are the same.
// either:
function createProfile(address _token) external
function registerToken(bytes20 _pohId) external
// or:
function directRegister() external
Critique and proposed interface change
The interface can currently differ greatly when used with a contract owner. We should try to come up with an interface that will work out of the box for most custom implementations. Since, in this context (Managing memberships), a group is only defined by its discriminator, we should add a new primitive whose sole responsibility is to decide whether a collateral token/user is allowed into the group.
Adding a discriminator
I’d argue that most custom Group Currency implementations, in the end, will only check if a specific Circles token or user is listed in a registry or will otherwise evaluate a boolean expression with the user-address as an argument. For such GCs, it makes sense to use a common interface to add and remove members.
An example would be a GC that checks if a user owns e.g. a specific POA or Ticket. In this scenario, the user already fulfills all requirements by owning the NFT and can join the group right away with the familiar Circles wallet as soon as it’s created. Every wallet that supports Group Currencies allows the user to join any group without the need to know specific details about the implementation of the discriminator.
interface IGroupMembershipDiscriminator {
function requireIsMember(address _user) external
function isMember(address _user) public returns(bool)
}
The implementations of ‘requireIsMember’ must return meaningful error messages that are suitable to explain to the user why he cannot join a group. Instead of using the token-address, it is more consistent to use the user’s Circles address in every case.
The add/removeMember functions, utilizing the above discriminator could look like the following:
contract GroupCurrencyToken {
address public hub; // The Circles hub
address public discriminator; // The discriminator implementation
function addMember(address _user) external {
IGroupMembershipDiscriminator(discriminator).requireIsMember(_user); // Must revert if not a member, everyone can add qualified members
// Add a trust relation between this org and the user
}
function removeMember(address _user) external {
if (!IGroupMembershipDiscriminator(discriminator).isMember(_user)) {
// remove member, no matter who called the function
}
// If sender is the member in question, remove him from the group.
// If sender is the owner, also remove the member from the group.
}
}
Removing addDelegatedTrustee
The current implementation of delegate trustees allows users to invite themselves to the group if they are trusted by a delegate trustee who’s already added to the group. However, it doesn’t look like the delegate trustee itself is actually trusted by the GC which is a bit strange but could be ‘fixed’ by simply adding the delegate trustee’s token as a member.
All in all, I have the impression that this feature was implemented mostly as a workaround to let users invite themselves to the group if someone they know is already in the group. If really necessary, the same behavior can be implemented in the discriminator, so I’d advocate to remove this distinction.
Resulting new possibilities
With the proposed changes, groups can be much more dynamic. Not only can users add and remove themselves to/from the group, other people can too if the discriminator matches or doesn’t match. It also allows users to interact with the basic functions of all GC implementations from any wallet that supports the common GC interface.
Reverse trust (optional - maybe off-topic )
If we decide to implement the GC as described above, users could remove themselves from a group. In normal use, this would give users a bit more flexibility with their memberships, but it won’t help against e.g., spam groups or other malicious groups that add users against their will.
For cases like this, and generally to measure the reputation of groups or organizations, I propose an additional trust hub that allows expressing trust between users and organizations and maybe even from organization to organization. This way, Circles wallets could show a metric on how reputable a group or organization is. Users who don’t want to be part of a group or organization can express their distrust explicitly, which also makes it easier for external reputation systems.
This would also make it easy for organizations (like e.g. the wallet operator) to maintain a list of recommended/supported GCs solely on-chain.
Links
Some of the ideas already came up in Proposal to update the Hub contract (Circles v2). The missing “reverse trust” limits some possibilities discussed in GitHub - jaensen/circles-reputation-metrics: Collection of resources needed to build a reputation/recommodation system for Circles
As always, with the kind request for comments. Have a good night