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

Add ERC6909 Implementation along with extensions #5394

Merged
merged 50 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f16cff1
draft
arr00 Dec 13, 2024
9a3ea2f
add extensions
arr00 Dec 16, 2024
4dbeace
nit
arr00 Dec 16, 2024
f038064
add more setters `ERC6909Metadata`
arr00 Dec 16, 2024
b0b0e3e
rename vars in impl and add tests
arr00 Dec 19, 2024
1b76245
add more tests
arr00 Dec 19, 2024
9ae0429
add more tests
arr00 Dec 20, 2024
f69bdfd
additional testing
arr00 Dec 20, 2024
7d78b5a
await when checking logs
arr00 Dec 20, 2024
6abfd92
address comments and fix tests
arr00 Dec 30, 2024
15ece65
add test for total supply
arr00 Dec 30, 2024
499f79b
format
arr00 Dec 30, 2024
d79198d
add tests
arr00 Dec 30, 2024
85196e7
add internal function test
arr00 Dec 30, 2024
8969005
add changesets
arr00 Jan 2, 2025
607c119
fix spelling
arr00 Jan 2, 2025
5763111
Add tests for `ERC6909Metadata`
arr00 Jan 3, 2025
673124c
test `ERC6909ContentURI`
arr00 Jan 3, 2025
ff3ee75
Add docs
arr00 Jan 10, 2025
1f34734
add content uri docs
arr00 Jan 10, 2025
2d9bc74
start on additional docs
arr00 Jan 14, 2025
14a06a0
add reference contract
arr00 Jan 16, 2025
7089ec5
further documentation
arr00 Jan 16, 2025
a6601c8
Update docs and fix incorrectly named file
arr00 Jan 16, 2025
e711da6
update docs
arr00 Jan 17, 2025
361f812
move interface
arr00 Jan 17, 2025
a2e9c98
add interface reference to interfaces folder
arr00 Jan 17, 2025
78bf584
emit events on uri changes
arr00 Jan 17, 2025
521335c
move game items file for 6909
arr00 Jan 17, 2025
d630d84
check that event is emitted on uri set
arr00 Jan 18, 2025
b3c83c7
fix lint
arr00 Jan 18, 2025
9f6506b
Apply suggestions from code review
arr00 Jan 20, 2025
af6769f
updates
Amxx Jan 21, 2025
d09f7fc
fix total supply tracking during updates from 0 to 0
Amxx Jan 21, 2025
3219c91
typo
Amxx Jan 21, 2025
37515f3
Update draft-ERC6909.sol
Amxx Jan 21, 2025
2eff205
up
Amxx Jan 21, 2025
e864372
reorder `_balances` mapping
arr00 Jan 21, 2025
fa66666
Apply suggestions from code review
arr00 Jan 22, 2025
78d9d92
update docs
arr00 Jan 24, 2025
e81d333
add metadata events and use `URI` instead of `MetadataUpdate`
arr00 Jan 24, 2025
b4eb0bc
formatting
arr00 Jan 26, 2025
a9bb063
add missing dev tag
arr00 Jan 26, 2025
267df51
add docs
arr00 Jan 27, 2025
de3d5dc
add missing internal setters
Amxx Jan 27, 2025
8f239ea
testing
Amxx Jan 27, 2025
d1297b0
fix apostrophe
arr00 Jan 27, 2025
f0c0571
refactor tests
Amxx Feb 4, 2025
66cd8cc
Update erc6909.adoc
Amxx Feb 4, 2025
edf94a4
Merge branch 'master' into feat/6909-impl
Amxx Feb 4, 2025
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
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