Skip to content
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

EIP-4974: Fungible Non-tradable Tokens, or EXP #4974

Merged
merged 8 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions EIPS/eip-4974.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
eip: 4974
title: Experience (EXP) Token Standard
description: A standard interface for fungible, non-tradable tokens, also known as EXP.
author: Daniel Tedesco (@dtedesco1)
discussions-to: https://ethereum-magicians.org/t/8805
status: Draft
type: Standards Track
category: ERC
created: 2022-04-02
requires: 165
---

## Abstract
The following describes a standard interface for fungible non-tradable tokens, or EXP. This standard provides basic functionality for participant addresses to consent to receive tokens and for an operator address to transfer tokens.

EXP, shorthand for "experience points", may represent accumulated recognition within a smart contract. Like experience points in video games, citations on an academic paper, or Reddit Karma, EXP is bestowed for useful contributions, accumulates as indistinguishable units, and should only be reallocated or destroyed by a reliable authority so empowered.

The standard described here allows reputation earned to be codified within a smart contract and recognized by other applications: from a five-member local bicycle club to a million-member green energy DAO.

## Motivation
How reputation manifests across groups can vary widely. Healthy communities allocate reputation to their participants using three key principles:
1. Consent -- No one is forced to be part of the group, but joining requires abiding by the governance structure of the group.
2. Meritocracy -- Reputation is earned by recognition from the group. It cannot be claimed, purchased, or sold.
3. Ethics -- The group can decrease an individual's reputation after bad behavior.

Since the creation of Bitcoin in 2008, the vast majority of blockchain applications have centered on buying and selling digital assets. While these use cases are substantial, digital assets need not be created with trading in mind. In fact, trading can be detrimental to community-based blockchain projects. This was evident in the pay-to-play dynamics of many EVM-based games and DAOs in 2021.

A smart contract cannot directly imbue consent, meritocracy, and ethics into a community, but it can encourage those principles. In doing so, the standard set out below will hopefully unlock a diverse array of new use cases for tokens:
- Voting weight in a DAO
- Experience points in a decentralized game
- Loyalty points for customers of a business

This standard is influenced by the [ERC-20](./eip-20) and [ERC-721](./eip-721) token standards and takes cues from each in its structure, style, and semantics. Neither, however, was created for fungible operator-managed token contracts such as EXP. Nor do existing proposals for non-tradable tokens meet the requirements of EXP use cases.

## Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Every ERC-4974 compliant contract MUST implement the ERC4974 and ERC165 interfaces:

```
// SPDX-License-Identifier: CC0

pragma solidity ^0.8.0;

/// @title ERC-4974 Experience (EXP) Token Standard
/// @dev See https://eips.ethereum.org/EIPS/EIP-4974
/// Note: the ERC-165 identifier for this interface is 0x696e7752.
/// Must initialize contracts with an `operator` address that is not `address(0)`.
/// Must initialize contracts assigning participation as `true` for both `operator` and `address(0)`.
interface IERC4974 /* is ERC165 */ {

/// @dev Emits when operator changes.
/// MUST emit when `operator` changes by any mechanism.
/// MUST ONLY emit by `setOperator`.
event Appointment(address indexed _operator);

/// @dev Emits when an address activates or deactivates its participation.
/// MUST emit emit when participation status changes by any mechanism.
/// MUST ONLY emit by `setParticipation`.
event Participation(address indexed _participant, bool _participation);

/// @dev Emits when operator transfers EXP.
/// MUST emit when EXP is transferred by any mechanism.
/// MUST ONLY emit by `transfer`.
event Transfer(address indexed _from, address indexed _to, uint256 _amount);

/// @notice Appoint operator authority.
/// @dev MUST throw unless `msg.sender` is `operator`.
/// MUST throw if `operator` address is either already current `operator`
/// or is the zero address.
/// MUST emit an `Appointment` event.
/// @param _operator New operator of the smart contract.
function setOperator(address _operator) external;

/// @notice Activate or deactivate participation. CALLER IS RESPONSIBLE TO
/// UNDERSTAND THE TERMS OF THEIR PARTICIPATION.
/// @dev MUST throw unless `msg.sender` is `participant`.
/// MUST throw if `participant` is `operator` or zero address.
/// MUST emit a `Participation` event for status changes.
/// @param _participant Address opting in or out of participation.
/// @param _participation Participation status of _participant.
function setParticipation(address _participant, bool _participation) external;

/// @notice Transfer EXP from one address to a participating address.
/// @dev MUST throw unless `msg.sender` is `operator`.
/// MUST throw unless `to` address is participating.
/// MUST throw if `to` and `from` are the same address.
/// MUST emit a Transfer event with each successful call.
/// SHOULD throw if `amount` is zero.
/// MAY allow minting from zero address, burning to the zero address,
/// transferring between accounts, and transferring between contracts.
/// MAY limit interaction with non-participating `from` addresses.
/// @param _from Address from which to transfer EXP tokens.
/// @param _to Address to which EXP tokens at `from` address will transfer.
/// @param _amount Total EXP tokens to reallocate.
function transfer(address _from, address _to, uint256 _amount) external;

/// @notice Return total EXP managed by this contract.
/// @dev MUST sum EXP tokens of all `participant` addresses,
/// regardless of participation status, excluding only the zero address.
function totalSupply() external view returns (uint256);

/// @notice Return total EXP allocated to a participant.
/// @dev MUST register each time `Transfer` emits.
/// SHOULD throw for queries about the zero address.
/// @param _participant An address for whom to query EXP total.
/// @return uint256 The number of EXP allocated to `participant`, possibly zero.
function balanceOf(address _participant) external view returns (uint256);
}

interface IERC165 {
/// @notice Query if a contract implements an interface.
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @param interfaceID The interface identifier, as specified in ERC-165.
/// @return bool `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise.
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
```

The *metadata extension* is OPTIONAL for ERC-4974 smart contracts. This allows an EXP smart contract to be interrogated for its name and description.
```
// SPDX-License-Identifier: CC0

pragma solidity ^0.8.0;

import "./IERC4974.sol";

/// @title ERC-4974 EXP Token Standard, optional metadata extension
/// @dev See https://eips.ethereum.org/EIPS/EIP-4974
/// Note: the ERC-165 identifier for this interface is 0x74793a15.
interface IERC4974Metadata is IERC4974 {
/// @notice A descriptive name for the EXP in this contract.
function name() external view returns (string memory);

/// @notice A one-line description of the EXP in this contract.
function description() external view returns (string memory);
}
```

## Rationale
### Participation
EXP drops SHALL require pre-approval from the delivery address. This ensures the receiver is a consenting participant in the smart contract.

### Transfers
EXP transfers SHALL be at the sole discretion of the contract operator. This party may be a sports team coach or a multisig DAO wallet. We decide not to specify how governance occurs, but only *that* governance occurs. This allows for a wider range of potential use cases than optimizing for particular decision-making forms.

ERC-4974 standardizes a control mechanism to allocate community recognition without encouraging financialization of that recognition or easily allowing non-contributors to acquire EXP representing contribution. While it does not ensure meritocracy, it opens the door.

### Token Destruction
EXP SHOULD allow burning tokens by contract operators. If Bob has contributed greatly to the community, but then is caught stealing from Alice, the community may decide this should lower Bob's standing and influence in the community. Again, while this does not ensure an ethical standard within the community, it opens the door.

### EXP Word Choice
EXP, or experience points, are common parlance in the video game industry and generally known among modern internet users. Allocated EXP typically confers to strength and accumulates as one progresses in a game. This serves as a fair analogy to what we aim to achieve with ERC-4974 by encouraging members of a community to have more strength in that community the more they contribute.

*Alternatives Considered: Soulbound Tokens, Soulbounds, Fungible Soulbound Tokens, Non-tradable Fungible Tokens, Non-transferrable Fungible Tokens, Karma Points, Reputation Tokens, Kudos*

### Participants Word Choice
Participants have agency over their *participation* in an activity, but not over the *outcomes*. Parties to ERC-4974 contracts are not owners in the same sense as owners of ERC-20 or ERC-721 tokens. The EXP sits in their wallets, but those wallets do not directly control any use of the EXP.

*Alternatives Considered: members, parties, contributors, players, entrants*

### ERC-165 Interface
We chose Standard Interface Detection (ERC-165) to expose the interfaces that an ERC-4974 smart contract supports.

### Metadata Choices
We have required `name` and `description` functions in the metadata extension. Name common among major token standards (namely, ERC-20 and ERC-721). We eschewed `symbol` as we do not wish them to be listed on any tickers that might tempt operators to engage in financial activities with these assets. We included a `description` function that may be helpful for games or other applications with multiple ERC-4974 tokens.

We remind implementation authors that the empty string is a valid response to `name` and `description` if you protest to the usage of this mechanism. We also remind everyone that any smart contract can use the same name and symbol as your contract. How a client may determine which ERC-4974 smart contracts are well-known (canonical) is outside the scope of this standard.

### Privacy
Users identified in the motivation section have a strong need to identify how much EXP an address holds. Since EXP contracts are opt-in, we hope users will be proud of their accumulated recognition and not wish to keep it secret. Without metadata associated to individual tokens or wallets, the privacy risks of this standard are limited.

## Backwards Compatibility
We have adopted `Transfer`, `transfer`, `balanceOf`, `totalSupply`, and `name` semantics from the ERC-20 and ERC-721 specifications. An implementation may also include a function `decimals` that returns `uint8(0)` if its goal is to be more compatible with ERC-20 while supporting this standard.

## Reference Implementation

A reference implementation of this standard can be found in the assets folder.
<!-- [../assets/EIP-4974/ERC4974.sol](../assets/EIP-4974/ERC4974.sol). -->

## Security Considerations
The `operator` address has total control over the allocation and transfer of tokens. Therefore, ensuring this party is secure and trustworthy is critical for the contract to function. No alternative exists if the operator is corrupted or lost.

We strongly encourage `operator` to be a smart contract with robust access control features to manage EXP.

## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
202 changes: 202 additions & 0 deletions assets/eip-4974/ERC4974.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// SPDX-License-Identifier: CC0

pragma solidity ^0.8.0;
import "./IERC4974.sol";
import "./IERC4974Metadata.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/utils/Context.sol";

/**
* See {IERC4974}
* Implements the ERC4974 Metadata extension.
*/
contract ERC4974 is Context, IERC165, IERC4974, IERC4974Metadata {
mapping(address => uint256) private _balances;
mapping(address => bool) private _participants;
address private _operator;
uint256 private _totalSupply;
string private _name;
string private _description;

/**
* @notice Sets the values for {name} and {symbol}.
* @dev Name and description are both immutable: they can only be set once during
* construction. Operator cannot be the zero address.
*/
constructor(string memory name_, string memory description_, address operator_) {
// constructor(address operator_) {
require(operator_ != address(0), "Operator cannot be the zero address.");
_name = name_;
_description = description_;
_operator = operator_;
_participants[_operator] = true;
_participants[address(0)] = true;
}

/**
*
* External Functions
*
*/

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165) returns (bool) {
return
interfaceId == type(IERC4974).interfaceId ||
interfaceId == type(IERC4974Metadata).interfaceId ||
supportsInterface(interfaceId);
}

/**
* @notice Assigns a new operator address.
* @dev Throws if sender is not operator or `newOperator` equals current `_operator`
* @param newOperator Address to reassign operator role.
*/
function setOperator(address newOperator) external virtual override {
_setOperator(newOperator);
}

/**
* @notice Sets participation status for an address.
* @dev Throws if msg.sender is not the address in question.
*/
function setParticipation(address participant, bool participation) external virtual override {
_participation(participant, participation);
}

/**
* @notice Transfer `amount` EXP to an account.
* @dev Emits `Transfer` event if successful.
* Throws if:
* - Sender is not operator
* - `to` address is not participating.
* - Zero address mints to itself.
* @param from The address from which to transfer. Zero address for mints.
* @param to The address of the recipient. Zero address for burns.
* @param amount The amount to be transferred.
*/
function transfer(address from, address to, uint256 amount) external virtual override {
_transfer(from, to, amount);
}

/**
* @notice Returns the name of the EXP token.
* @return string The name of the EXP token.
*/
function name() external view virtual override returns (string memory) {
return _name;
}

/**
* @notice Returns the description of the EXP token,
* usually a one-line description.
* @return string The description of the EXP token.
*/
function description() external view virtual override returns (string memory) {
return _description;
}

/**
* @notice Returns the current operator of the EXP token,
* @return address The current operator of the EXP token.
*/
function operator() external view virtual returns (address) {
return _operator;
}

/**
* @notice Returns the EXP balance of the account.
* @param account The address to query.
* @return uint256 The EXP balance of the account.
*/
function balanceOf(address account) external view virtual override returns (uint256) {
require(account != address(0), "Zero address has no balance.");
return _balances[account];
}

/**
* @notice Returns the participation status of the account.
* @param account The address to query.
* @return bool The participation status of the queried account.
*/
function participationOf(address account) external view virtual returns (bool) {
return _participants[account];
}

/**
* @notice Returns the total supply of EXP tokens.
* @dev Result includes inactive accounts, but not destroyed tokens.
* @return uint256 The total supply of EXP tokens.
*/
function totalSupply() external view virtual override returns (uint256) {
return _totalSupply;
}

/**
*
* Internal Functions
*
*/

/**
* @notice Assign a new operator.
* @dev Throws is sender is not current operator.
* Emits {Appointment} event.
* @param newOperator address to be assigned operator authority.
*/
function _setOperator(address newOperator) internal virtual {
require(_msgSender() == _operator, "Sender is not operator.");
require(newOperator != address(0), "Operator cannot be the zero address.");
require(_operator != newOperator, "{address} is already assigned as operator");
_operator = newOperator;
emit Appointment(newOperator);
}

/**
* @notice Sets participation status for `participant`.
* @dev Throws if sender is not `participant`.
* Emits a {Participation} event.
* @param participant Address for which to set participation.
* @param participation Requested participation status.
*/
function _participation(address participant, bool participation) internal virtual {
require(_msgSender() == participant, "Sender is not {participant}.");
require(_msgSender() != _operator, "Operator cannot change participation");
require(participant != address(0), "Zero address cannot be removed.");
require(_participants[participant] != participation, "Participant already has {participation} status");
_participants[participant] = participation;
emit Participation(participant, participation);
}

/**
* @notice Moves `amount` of tokens from `from` address to `to` address.
* @dev Throws if sender is not operator.
* Throws if `to` is not participating.
* Emits a {Transfer} event.
* @param from Address from which to transfer. If zero address, then add to totalSupply.
* @param to Address to which to transfer. If zero address, then subtract from totalSupply.
* @param amount Number of EXP transfer.
*/
function _transfer(
address from,
address to,
uint256 amount
) internal virtual {
require(_msgSender() == _operator, "Sender is not the operator.");
require(_participants[to] == true, "{to} address is not an active participant.");
require(from != to, "{to} and {from} cannot be the same address.");
if (from == address(0)) {
_totalSupply += amount;
} else {
require(_balances[from] >= amount, "{from} address holds less EXP than {amount}.");
_balances[from] -= amount;
if (to == address(0)) {
_totalSupply -= amount;
}
}
_balances[to] += amount;
emit Transfer(from, to, amount);
}
}
Loading