Skip to content

Commit

Permalink
Add ERC6909 Implementation along with extensions (#5394)
Browse files Browse the repository at this point in the history
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: Ernesto García <ernestognw@gmail.com>
  • Loading branch information
3 people authored Feb 4, 2025
1 parent df878c8 commit 43b3319
Show file tree
Hide file tree
Showing 19 changed files with 1,008 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/brown-turkeys-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ER6909TokenSupply`: Add an extension of ERC6909 which tracks total supply for each token id.
5 changes: 5 additions & 0 deletions .changeset/dirty-bananas-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC6909ContentURI`: Add an extension of ERC6909 which adds content URI functionality.
5 changes: 5 additions & 0 deletions .changeset/proud-cooks-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC6909Metadata`: Add an extension of ERC6909 which adds metadata functionality.
5 changes: 5 additions & 0 deletions .changeset/ten-hats-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`ERC6909`: Add a standard implementation of ERC6909.
12 changes: 12 additions & 0 deletions contracts/interfaces/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ are useful to interact with third party contracts that implement them.
- {IERC5313}
- {IERC5805}
- {IERC6372}
- {IERC6909}
- {IERC6909ContentURI}
- {IERC6909Metadata}
- {IERC6909TokenSupply}
- {IERC7674}

== Detailed ABI
Expand Down Expand Up @@ -84,4 +88,12 @@ are useful to interact with third party contracts that implement them.

{{IERC6372}}

{{IERC6909}}

{{IERC6909ContentURI}}

{{IERC6909Metadata}}

{{IERC6909TokenSupply}}

{{IERC7674}}
26 changes: 26 additions & 0 deletions contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC6909Metadata} from "../../../../token/ERC6909/extensions/draft-ERC6909Metadata.sol";

contract ERC6909GameItems is ERC6909Metadata {
uint256 public constant GOLD = 0;
uint256 public constant SILVER = 1;
uint256 public constant THORS_HAMMER = 2;
uint256 public constant SWORD = 3;
uint256 public constant SHIELD = 4;

constructor() {
_setDecimals(GOLD, 18);
_setDecimals(SILVER, 18);
// Default decimals is 0
_setDecimals(SWORD, 9);
_setDecimals(SHIELD, 9);

_mint(msg.sender, GOLD, 10 ** 18);
_mint(msg.sender, SILVER, 10_000 ** 18);
_mint(msg.sender, THORS_HAMMER, 1);
_mint(msg.sender, SWORD, 10 ** 9);
_mint(msg.sender, SHIELD, 10 ** 9);
}
}
27 changes: 27 additions & 0 deletions contracts/token/ERC6909/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
= ERC-6909

[.readme-notice]
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc6909

This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-6909[ERC-6909 Minimal Multi-Token Interface].

The ERC consists of four interfaces which fulfill different roles--the interfaces are as follows:

. {IERC6909}: Base interface for a vanilla ERC6909 token.
. {IERC6909ContentURI}: Extends the base interface and adds content URI (contract and token level) functionality.
. {IERC6909Metadata}: Extends the base interface and adds metadata functionality, which exposes a name, symbol, and decimals for each token id.
. {IERC6909TokenSupply}: Extends the base interface and adds total supply functionality for each token id.

Implementations are provided for each of the 4 interfaces defined in the ERC.

== Core

{{ERC6909}}

== Extensions

{{ERC6909ContentURI}}

{{ERC6909Metadata}}

{{ERC6909TokenSupply}}
224 changes: 224 additions & 0 deletions contracts/token/ERC6909/draft-ERC6909.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IERC6909} from "../../interfaces/draft-IERC6909.sol";
import {Context} from "../../utils/Context.sol";
import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol";

/**
* @dev Implementation of ERC-6909.
* See https://eips.ethereum.org/EIPS/eip-6909
*/
contract ERC6909 is Context, ERC165, IERC6909 {
mapping(address owner => mapping(uint256 id => uint256)) private _balances;

mapping(address owner => mapping(address operator => bool)) private _operatorApprovals;

mapping(address owner => mapping(address spender => mapping(uint256 id => uint256))) private _allowances;

error ERC6909InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 id);
error ERC6909InsufficientAllowance(address spender, uint256 allowance, uint256 needed, uint256 id);
error ERC6909InvalidApprover(address approver);
error ERC6909InvalidReceiver(address receiver);
error ERC6909InvalidSender(address sender);
error ERC6909InvalidSpender(address spender);

/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return interfaceId == type(IERC6909).interfaceId || super.supportsInterface(interfaceId);
}

/// @inheritdoc IERC6909
function balanceOf(address owner, uint256 id) public view virtual override returns (uint256) {
return _balances[owner][id];
}

/// @inheritdoc IERC6909
function allowance(address owner, address spender, uint256 id) public view virtual override returns (uint256) {
return _allowances[owner][spender][id];
}

/// @inheritdoc IERC6909
function isOperator(address owner, address spender) public view virtual override returns (bool) {
return _operatorApprovals[owner][spender];
}

/// @inheritdoc IERC6909
function approve(address spender, uint256 id, uint256 amount) public virtual override returns (bool) {
_approve(_msgSender(), spender, id, amount);
return true;
}

/// @inheritdoc IERC6909
function setOperator(address spender, bool approved) public virtual override returns (bool) {
_setOperator(_msgSender(), spender, approved);
return true;
}

/// @inheritdoc IERC6909
function transfer(address receiver, uint256 id, uint256 amount) public virtual override returns (bool) {
_transfer(_msgSender(), receiver, id, amount);
return true;
}

/// @inheritdoc IERC6909
function transferFrom(
address sender,
address receiver,
uint256 id,
uint256 amount
) public virtual override returns (bool) {
address caller = _msgSender();
if (sender != caller && !isOperator(sender, caller)) {
_spendAllowance(sender, caller, id, amount);
}
_transfer(sender, receiver, id, amount);
return true;
}

/**
* @dev Creates `amount` of token `id` and assigns them to `account`, by transferring it from address(0).
* Relies on the `_update` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _mint(address to, uint256 id, uint256 amount) internal {
if (to == address(0)) {
revert ERC6909InvalidReceiver(address(0));
}
_update(address(0), to, id, amount);
}

/**
* @dev Moves `amount` of token `id` from `from` to `to` without checking for approvals.
*
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
function _transfer(address from, address to, uint256 id, uint256 amount) internal {
if (from == address(0)) {
revert ERC6909InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC6909InvalidReceiver(address(0));
}
_update(from, to, id, amount);
}

/**
* @dev Destroys a `amount` of token `id` from `account`.
* Relies on the `_update` mechanism.
*
* Emits a {Transfer} event with `to` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead
*/
function _burn(address from, uint256 id, uint256 amount) internal {
if (from == address(0)) {
revert ERC6909InvalidSender(address(0));
}
_update(from, address(0), id, amount);
}

/**
* @dev Transfers `amount` of token `id` from `from` to `to`, or alternatively mints (or burns) if `from`
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
* this function.
*
* Emits a {Transfer} event.
*/
function _update(address from, address to, uint256 id, uint256 amount) internal virtual {
address caller = _msgSender();

if (from != address(0)) {
uint256 fromBalance = _balances[from][id];
if (fromBalance < amount) {
revert ERC6909InsufficientBalance(from, fromBalance, amount, id);
}
unchecked {
// Overflow not possible: amount <= fromBalance.
_balances[from][id] = fromBalance - amount;
}
}
if (to != address(0)) {
_balances[to][id] += amount;
}

emit Transfer(caller, from, to, id, amount);
}

/**
* @dev Sets `amount` as the allowance of `spender` over the `owner`'s `id` tokens.
*
* This internal function is equivalent to `approve`, and can be used to e.g. set automatic allowances for certain
* subsystems, etc.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _approve(address owner, address spender, uint256 id, uint256 amount) internal virtual {
if (owner == address(0)) {
revert ERC6909InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC6909InvalidSpender(address(0));
}
_allowances[owner][spender][id] = amount;
emit Approval(owner, spender, id, amount);
}

/**
* @dev Approve `spender` to operate on all of `owner`'s tokens
*
* This internal function is equivalent to `setOperator`, and can be used to e.g. set automatic allowances for
* certain subsystems, etc.
*
* Emits an {OperatorSet} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
*/
function _setOperator(address owner, address spender, bool approved) internal virtual {
if (owner == address(0)) {
revert ERC6909InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC6909InvalidSpender(address(0));
}
_operatorApprovals[owner][spender] = approved;
emit OperatorSet(owner, spender, approved);
}

/**
* @dev Updates `owner`'s allowance for `spender` based on spent `amount`.
*
* Does not update the allowance value in case of infinite allowance.
* Revert if not enough allowance is available.
*
* Does not emit an {Approval} event.
*/
function _spendAllowance(address owner, address spender, uint256 id, uint256 amount) internal virtual {
uint256 currentAllowance = allowance(owner, spender, id);
if (currentAllowance < type(uint256).max) {
if (currentAllowance < amount) {
revert ERC6909InsufficientAllowance(spender, currentAllowance, amount, id);
}
unchecked {
_allowances[owner][spender][id] = currentAllowance - amount;
}
}
}
}
52 changes: 52 additions & 0 deletions contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {ERC6909} from "../draft-ERC6909.sol";
import {IERC6909ContentURI} from "../../../interfaces/draft-IERC6909.sol";

/**
* @dev Implementation of the Content URI extension defined in ERC6909.
*/
contract ERC6909ContentURI is ERC6909, IERC6909ContentURI {
string private _contractURI;
mapping(uint256 id => string) private _tokenURIs;

/// @dev Event emitted when the contract URI is changed. See https://eips.ethereum.org/EIPS/eip-7572[ERC-7572] for details.
event ContractURIUpdated();

/// @dev See {IERC1155-URI}
event URI(string value, uint256 indexed id);

/// @inheritdoc IERC6909ContentURI
function contractURI() public view virtual override returns (string memory) {
return _contractURI;
}

/// @inheritdoc IERC6909ContentURI
function tokenURI(uint256 id) public view virtual override returns (string memory) {
return _tokenURIs[id];
}

/**
* @dev Sets the {contractURI} for the contract.
*
* Emits a {ContractURIUpdated} event.
*/
function _setContractURI(string memory newContractURI) internal virtual {
_contractURI = newContractURI;

emit ContractURIUpdated();
}

/**
* @dev Sets the {tokenURI} for a given token of type `id`.
*
* Emits a {URI} event.
*/
function _setTokenURI(uint256 id, string memory newTokenURI) internal virtual {
_tokenURIs[id] = newTokenURI;

emit URI(newTokenURI, id);
}
}
Loading

0 comments on commit 43b3319

Please sign in to comment.