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

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
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.
123 changes: 123 additions & 0 deletions contracts/token/ERC6909/draft-ERC6909.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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";

contract ERC6909 is Context, ERC165, IERC6909 {
error ERC6909InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 id);
error ERC6909InsufficientAllowance(address spender, uint256 allowance, uint256 needed, uint256 id);
error ERC6909InvalidReceiver(address receiver);
error ERC6909InvalidSender(address sender);

mapping(uint256 id => mapping(address owner => uint256)) private _balances;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not mapping(address owner => mapping(uint256 id => uint256))? I feel is more consistent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is following ERC1155 impl. Given that we've strayed pretty far from storage parity at this point its probably not necessary.


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

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

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

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

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

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

function approve(address spender, uint256 id, uint256 amount) external virtual override returns (bool) {
address caller = _msgSender();
_allowances[caller][spender][id] = amount;

emit Approval(caller, spender, id, amount);
return true;
}

function setOperator(address spender, bool approved) external virtual override returns (bool) {
address caller = _msgSender();
_operatorApprovals[caller][spender] = approved;

emit OperatorSet(caller, spender, approved);
return true;
}

function transfer(address receiver, uint256 id, uint256 amount) external virtual override returns (bool) {
_transfer(_msgSender(), receiver, id, amount);
return true;
}

function transferFrom(
address sender,
address receiver,
uint256 id,
uint256 amount
) external virtual override returns (bool) {
address caller = _msgSender();
if (caller != sender && !isOperator(sender, caller)) {
uint256 currentAllowance = allowance(sender, caller, id);
if (currentAllowance != type(uint256).max) {
if (currentAllowance < amount) {
revert ERC6909InsufficientAllowance(caller, currentAllowance, amount, id);
}
unchecked {
_allowances[sender][caller][id] = currentAllowance - amount;
}
}
}

_transfer(sender, receiver, id, amount);
return true;
}

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);
}

function _update(address from, address to, uint256 id, uint256 amount) internal virtual {
address caller = _msgSender();

if (from != address(0)) {
uint256 fromBalance = balanceOf(from, id);
if (fromBalance < amount) {
revert ERC6909InsufficientBalance(from, fromBalance, amount, id);
}
unchecked {
_balances[id][from] -= amount;
}
}
if (to != address(0)) {
_balances[id][to] += amount;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized this can't be unchecked given it's not bound by a totalSupply as ERC20 does, which means that multiple accounts can hold more than the maximum uint256 value.

I wonder if this is an issue we should worry about. An example where it can break is for off-chain indexers who may want to track the total supply but are restricted to an uint256 (as the graph)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ERC1155 is the same by default. Given that the ERC doesn't restrict this at all, and that it is already done in ERC1155, I think it is fine.

}

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

function _mint(address to, uint256 id, uint256 amount) internal {
if (to == address(0)) {
revert ERC6909InvalidReceiver(address(0));
}
_update(address(0), to, id, amount);
}

function _burn(address from, uint256 id, uint256 amount) internal {
if (from == address(0)) {
revert ERC6909InvalidSender(address(0));
}
_update(from, address(0), id, amount);
}
}
24 changes: 24 additions & 0 deletions contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

contract ER6909TokenSupply is ERC6909, IERC6909TokenSupply {
mapping(uint256 id => uint256) private _totalSupplies;

function totalSupply(uint256 id) external view virtual override returns (uint256) {
return _totalSupplies[id];
}

function _update(address from, address to, uint256 id, uint256 amount) internal virtual override {
super._update(from, to, id, amount);

if (from == address(0)) {
_totalSupplies[id] += amount;
} else if (to == address(0)) {
_totalSupplies[id] -= amount;
}
}
}
27 changes: 27 additions & 0 deletions contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

contract ERC6909ContentURI is ERC6909, IERC6909ContentURI {
string private _contractURI;
mapping(uint256 id => string) private _tokenURIs;

function contractURI() external view virtual override returns (string memory) {
return _contractURI;
}

function tokenURI(uint256 id) external view virtual override returns (string memory) {
return _tokenURIs[id];
}
ernestognw marked this conversation as resolved.
Show resolved Hide resolved

function _setContractURI(string memory newContractURI) internal {
_contractURI = newContractURI;
}

function _setTokenURI(uint256 id, string memory newTokenURI) internal {
_tokenURIs[id] = newTokenURI;
}
}
44 changes: 44 additions & 0 deletions contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

contract ERC6909Metadata is ERC6909, IERC6909Metadata {
struct TokenMetadata {
string name;
string symbol;
uint8 decimals;
}

mapping(uint256 id => TokenMetadata) private _tokenMetadata;

function name(uint256 id) external view virtual override returns (string memory) {
return _tokenMetadata[id].name;
}

function symbol(uint256 id) external view virtual override returns (string memory) {
return _tokenMetadata[id].symbol;
}

function decimals(uint256 id) external view virtual override returns (uint8) {
return _tokenMetadata[id].decimals;
}

function _setName(uint256 id, string memory newName) internal {
_tokenMetadata[id].name = newName;
}

function _setSymbol(uint256 id, string memory newSymbol) internal {
_tokenMetadata[id].symbol = newSymbol;
}

function _setDecimals(uint256 id, uint8 newDecimals) internal {
_tokenMetadata[id].decimals = newDecimals;
}

function _setTokenMetadata(uint256 id, TokenMetadata memory metadata) internal {
_tokenMetadata[id] = metadata;
}
}
Loading
Loading