From f16cff19af808d1f1272d8fd27be530310c605f7 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:37:08 -0500 Subject: [PATCH 01/47] draft --- contracts/token/ERC6909/draft-ERC6909.sol | 96 +++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 contracts/token/ERC6909/draft-ERC6909.sol diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol new file mode 100644 index 00000000000..5796ebb851c --- /dev/null +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (token/ERC6909/draft-ERC6909.sol) + +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 { + mapping(uint256 id => mapping(address account => uint256)) private _balances; + + mapping(address account => mapping(address operator => bool)) private _operatorApprovals; + + // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json + string private _uri; + + mapping(address account => mapping(address operator => mapping(uint256 => 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 account, uint256 id) public view virtual override returns (uint256) { + return _balances[id][account]; + } + + function allowance(address owner, address spender, uint256 id) public view virtual override returns (uint256) { + return _allowances[owner][spender][id]; + } + + function isOperator(address owner, address operator) public view virtual override returns (bool) { + return _operatorApprovals[owner][operator]; + } + + function approve(address spender, uint256 id, uint256 amount) external virtual override returns (bool) { + _allowances[_msgSender()][spender][id] = amount; + + emit Approval(_msgSender(), spender, id, amount); + + return true; + } + + function setOperator(address spender, bool approved) external virtual override returns (bool) { + _operatorApprovals[_msgSender()][spender] = approved; + + emit OperatorSet(_msgSender(), spender, approved); + + return true; + } + + function transfer(address to, uint256 id, uint256 amount) external virtual override returns (bool) { + _update(_msgSender(), to, id, amount); + + return true; + } + + function transferFrom( + address from, + address to, + uint256 id, + uint256 amount + ) external virtual override returns (bool) { + address caller = _msgSender(); + if (caller != from && !isOperator(from, caller)) { + if (_allowances[from][_msgSender()][id] != type(uint256).max) { + _allowances[from][_msgSender()][id] -= amount; + } + } + + _update(from, to, id, amount); + + return true; + } + + function _update(address from, address to, uint256 id, uint256 amount) internal virtual { + address caller = _msgSender(); + + if (from != address(0)) { + _balances[id][from] -= amount; + } + if (to != address(0)) { + _balances[id][to] += amount; + } + + emit Transfer(caller, from, to, id, amount); + } + + function _mint(address to, uint256 id, uint256 amount) internal { + _update(address(0), to, id, amount); + } + + function _burn(address from, uint256 id, uint256 amount) internal { + _update(from, address(0), id, amount); + } +} From 9a3ea2f89bfe9a06d72e800246d005d1fe4c8d10 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:13:46 -0500 Subject: [PATCH 02/47] add extensions --- contracts/token/ERC6909/draft-ERC6909.sol | 4 +-- .../extensions/draft-ER6909TokenSupply.sol | 25 ++++++++++++++ .../extensions/draft-ERC6909ContentURI.sol | 28 ++++++++++++++++ .../extensions/draft-ERC6909Metadata.sol | 33 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol create mode 100644 contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol create mode 100644 contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 5796ebb851c..9bcde8b82b4 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -12,8 +12,8 @@ contract ERC6909 is Context, ERC165, IERC6909 { mapping(address account => mapping(address operator => bool)) private _operatorApprovals; - // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json - string private _uri; + // ERC1155 stores the `_uri` in the 3rd slot. We leave it empty to avoid conflicts on upgrades. + string private __gap; mapping(address account => mapping(address operator => mapping(uint256 => uint256))) private _allowances; diff --git a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol new file mode 100644 index 00000000000..515c0df2a0f --- /dev/null +++ b/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (token/ERC6909/extensions/draft-ER6909TokenSupply.sol) + +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 totalSupply) 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; + } + } +} diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol new file mode 100644 index 00000000000..683480e226a --- /dev/null +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (token/ERC6909/extensions/draft-ERC6909ContentURI.sol) + +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]; + } + + function _setContractURI(string memory contractURI_) internal { + _contractURI = contractURI_; + } + + function _setTokenURI(uint256 id, string memory tokenURI_) internal { + _tokenURIs[id] = tokenURI_; + } +} diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol new file mode 100644 index 00000000000..ec8a6a16c26 --- /dev/null +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (token/ERC6909/extensions/draft-ERC6909Metadata.sol) + +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 uri; + string contentHash; + uint8 decimals; + } + + mapping(uint256 => TokenMetadata) private _tokenMetadata; + + function name(uint256 id) external view virtual override returns (string memory) { + return _tokenMetadata[id].uri; + } + + function symbol(uint256 id) external view virtual override returns (string memory) { + return _tokenMetadata[id].contentHash; + } + + function decimals(uint256 id) external view virtual override returns (uint8) { + return _tokenMetadata[id].decimals; + } + + function _setTokenMetadata(uint256 id, TokenMetadata memory metadata) internal { + _tokenMetadata[id] = metadata; + } +} From 4dbeace86696da4c91d238ac910f2e34a81bdb8d Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:40:09 -0500 Subject: [PATCH 03/47] nit --- contracts/token/ERC6909/draft-ERC6909.sol | 6 +----- .../token/ERC6909/extensions/draft-ER6909TokenSupply.sol | 2 +- .../token/ERC6909/extensions/draft-ERC6909Metadata.sol | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 9bcde8b82b4..25941a3a48f 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -15,7 +15,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { // ERC1155 stores the `_uri` in the 3rd slot. We leave it empty to avoid conflicts on upgrades. string private __gap; - mapping(address account => mapping(address operator => mapping(uint256 => uint256))) private _allowances; + mapping(address account => mapping(address operator => 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); @@ -37,7 +37,6 @@ contract ERC6909 is Context, ERC165, IERC6909 { _allowances[_msgSender()][spender][id] = amount; emit Approval(_msgSender(), spender, id, amount); - return true; } @@ -45,13 +44,11 @@ contract ERC6909 is Context, ERC165, IERC6909 { _operatorApprovals[_msgSender()][spender] = approved; emit OperatorSet(_msgSender(), spender, approved); - return true; } function transfer(address to, uint256 id, uint256 amount) external virtual override returns (bool) { _update(_msgSender(), to, id, amount); - return true; } @@ -69,7 +66,6 @@ contract ERC6909 is Context, ERC165, IERC6909 { } _update(from, to, id, amount); - return true; } diff --git a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol index 515c0df2a0f..0063acad7b4 100644 --- a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol +++ b/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol @@ -7,7 +7,7 @@ import {ERC6909} from "../draft-ERC6909.sol"; import {IERC6909TokenSupply} from "../../../interfaces/draft-IERC6909.sol"; contract ER6909TokenSupply is ERC6909, IERC6909TokenSupply { - mapping(uint256 id => uint256 totalSupply) private _totalSupplies; + mapping(uint256 id => uint256) private _totalSupplies; function totalSupply(uint256 id) external view virtual override returns (uint256) { return _totalSupplies[id]; diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index ec8a6a16c26..8546d4b23f8 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -13,7 +13,7 @@ contract ERC6909Metadata is ERC6909, IERC6909Metadata { uint8 decimals; } - mapping(uint256 => TokenMetadata) private _tokenMetadata; + mapping(uint256 id => TokenMetadata) private _tokenMetadata; function name(uint256 id) external view virtual override returns (string memory) { return _tokenMetadata[id].uri; From f038064d48bcc330b2165ec306335f370898997b Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:14:22 -0500 Subject: [PATCH 04/47] add more setters `ERC6909Metadata` --- contracts/token/ERC6909/draft-ERC6909.sol | 3 --- .../extensions/draft-ERC6909ContentURI.sol | 8 ++++---- .../extensions/draft-ERC6909Metadata.sol | 20 +++++++++++++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 25941a3a48f..8633b71e125 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -12,9 +12,6 @@ contract ERC6909 is Context, ERC165, IERC6909 { mapping(address account => mapping(address operator => bool)) private _operatorApprovals; - // ERC1155 stores the `_uri` in the 3rd slot. We leave it empty to avoid conflicts on upgrades. - string private __gap; - mapping(address account => mapping(address operator => mapping(uint256 id => uint256))) private _allowances; function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index 683480e226a..85c2931e7a4 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -18,11 +18,11 @@ contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { return _tokenURIs[id]; } - function _setContractURI(string memory contractURI_) internal { - _contractURI = contractURI_; + function _setContractURI(string memory newContractURI) internal { + _contractURI = newContractURI; } - function _setTokenURI(uint256 id, string memory tokenURI_) internal { - _tokenURIs[id] = tokenURI_; + function _setTokenURI(uint256 id, string memory newTokenURI) internal { + _tokenURIs[id] = newTokenURI; } } diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index 8546d4b23f8..002e76921cd 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -8,25 +8,37 @@ import {IERC6909Metadata} from "../../../interfaces/draft-IERC6909.sol"; contract ERC6909Metadata is ERC6909, IERC6909Metadata { struct TokenMetadata { - string uri; - string contentHash; + 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].uri; + return _tokenMetadata[id].name; } function symbol(uint256 id) external view virtual override returns (string memory) { - return _tokenMetadata[id].contentHash; + 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 _setTokenSymbol(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; } From b0b0e3e49a5eb69b1d297f0b244ca57bd4d07ac8 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:13:01 -0500 Subject: [PATCH 05/47] rename vars in impl and add tests --- contracts/token/ERC6909/draft-ERC6909.sol | 41 ++++--- test/token/ERC6909/ERC6909.behavior.js | 110 ++++++++++++++++++ test/token/ERC6909/ERC6909.test.js | 18 +++ .../SupportsInterface.behavior.js | 9 ++ 4 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 test/token/ERC6909/ERC6909.behavior.js create mode 100644 test/token/ERC6909/ERC6909.test.js diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 8633b71e125..efafc909338 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -8,61 +8,64 @@ import {Context} from "../../utils/Context.sol"; import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; contract ERC6909 is Context, ERC165, IERC6909 { - mapping(uint256 id => mapping(address account => uint256)) private _balances; + mapping(uint256 id => mapping(address owner => uint256)) private _balances; - mapping(address account => mapping(address operator => bool)) private _operatorApprovals; + mapping(address owner => mapping(address operator => bool)) private _operatorApprovals; - mapping(address account => mapping(address operator => mapping(uint256 id => uint256))) private _allowances; + 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 account, uint256 id) public view virtual override returns (uint256) { - return _balances[id][account]; + 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 operator) public view virtual override returns (bool) { - return _operatorApprovals[owner][operator]; + 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) { - _allowances[_msgSender()][spender][id] = amount; + address caller = _msgSender(); + _allowances[caller][spender][id] = amount; - emit Approval(_msgSender(), spender, id, amount); + emit Approval(caller, spender, id, amount); return true; } function setOperator(address spender, bool approved) external virtual override returns (bool) { - _operatorApprovals[_msgSender()][spender] = approved; + address caller = _msgSender(); + _operatorApprovals[caller][spender] = approved; - emit OperatorSet(_msgSender(), spender, approved); + emit OperatorSet(caller, spender, approved); return true; } - function transfer(address to, uint256 id, uint256 amount) external virtual override returns (bool) { - _update(_msgSender(), to, id, amount); + function transfer(address receiver, uint256 id, uint256 amount) external virtual override returns (bool) { + _update(_msgSender(), receiver, id, amount); return true; } function transferFrom( - address from, - address to, + address sender, + address receiver, uint256 id, uint256 amount ) external virtual override returns (bool) { address caller = _msgSender(); - if (caller != from && !isOperator(from, caller)) { - if (_allowances[from][_msgSender()][id] != type(uint256).max) { - _allowances[from][_msgSender()][id] -= amount; + if (caller != sender && !isOperator(sender, caller)) { + uint256 currentAllowance = _allowances[sender][caller][id]; + if (currentAllowance != type(uint256).max) { + _allowances[sender][_msgSender()][id] = currentAllowance - amount; } } - _update(from, to, id, amount); + _update(sender, receiver, id, amount); return true; } diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js new file mode 100644 index 00000000000..2b25e7f99e9 --- /dev/null +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -0,0 +1,110 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); + +const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); + +function shouldBehaveLikeERC6909() { + const firstTokenId = 1n; + const secondTokenId = 2n; + const randomTokenId = 125523n; + + const firstTokenAmount = 2000n; + const secondTokenAmount = 3000n; + + beforeEach(async function () { + [this.recipient, this.proxy, this.alice, this.bruce] = this.otherAccounts; + }); + + describe('like an ERC6909', function () { + describe('balanceOf', function () { + describe("when accounts don't own tokens", function () { + it('return zero', async function () { + expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.be.equal(0); + expect(this.token.balanceOf(this.bruce, secondTokenId)).to.eventually.be.equal(0); + expect(this.token.balanceOf(this.alice, randomTokenId)).to.eventually.be.equal(0); + }); + }); + + describe('when accounts own some tokens', function () { + beforeEach(async function () { + await this.token.$_mint(this.alice, firstTokenId, firstTokenAmount); + await this.token.$_mint(this.bruce, secondTokenId, secondTokenAmount); + }); + + it('returns amount owned by the given address', async function () { + expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.be.equal(firstTokenAmount); + expect(this.token.balanceOf(this.bruce, secondTokenId)).to.eventually.be.equal(secondTokenAmount); + expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.be.equal(0); + }); + }); + }); + + describe('setOperator', function () { + beforeEach(async function () { + this.tx = await this.token.connect(this.holder).setOperator(this.operator, true); + }); + + it('emits an an OperatorSet event', async function () { + expect(this.tx).to.emit(this.token, 'OperatorSet').withArgs(this.holder, this.operator, true); + }); + + it('should be reflected in isOperator call', async function () { + expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true; + // not operator for other account + expect(this.token.isOperator(this.alice, this.operator)).to.eventually.be.false; + }); + + it('can unset the operator approval', async function () { + await expect(this.token.connect(this.holder).setOperator(this.operator, false)) + .to.emit(this.token, 'OperatorSet') + .withArgs(this.holder, this.operator, false); + }); + }); + + describe('approve', function () { + beforeEach(async function () { + this.tx = await this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenAmount); + }); + + it('emits an Approval event', async function () { + expect(this.tx) + .to.emit(this.token, 'Approval') + .withArgs(this.holder, this.operator, firstTokenId, firstTokenAmount); + }); + + it('is reflected in allowance', async function () { + expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(firstTokenAmount); + // not operator for other account + expect(this.token.allowance(this.alice, this.operator, firstTokenId)).to.eventually.be.equal(0); + }); + + it('can unset the approval', async function () { + await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, 0)) + .to.emit(this.token, 'Approval') + .withArgs(this.holder, this.operator, firstTokenId, 0); + expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(0); + }); + }); + + describe('transfer', function () { + beforeEach(async function () { + await this.token.$_mint(this.alice, firstTokenId, firstTokenAmount); + await this.token.$_mint(this.bruce, secondTokenId, secondTokenAmount); + }); + + it('transfers to the zero address are allowed', async function () { + await expect( + this.token.connect(this.alice).transfer(ethers.constants.AddressZero, firstTokenId, firstTokenAmount), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.alice, ethers.constants.AddressZero, firstTokenId, firstTokenAmount); + }); + }); + + shouldSupportInterfaces(['ERC6909']); + }); +} + +module.exports = { + shouldBehaveLikeERC6909, +}; diff --git a/test/token/ERC6909/ERC6909.test.js b/test/token/ERC6909/ERC6909.test.js new file mode 100644 index 00000000000..facf40490e1 --- /dev/null +++ b/test/token/ERC6909/ERC6909.test.js @@ -0,0 +1,18 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { shouldBehaveLikeERC6909 } = require('./ERC6909.behavior'); + +async function fixture() { + const [operator, holder, ...otherAccounts] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC6909'); + return { token, operator, holder, otherAccounts }; +} + +describe.only('ERC6909', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeERC6909(); +}); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index bfcddee7a5e..6e716d1304b 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -90,6 +90,15 @@ const SIGNATURES = { Governor: GOVERNOR_INTERFACE, Governor_5_3: GOVERNOR_INTERFACE.concat('getProposalId(address[],uint256[],bytes[],bytes32)'), ERC2981: ['royaltyInfo(uint256,uint256)'], + ERC6909: [ + 'balanceOf(address,uint256)', + 'allowance(address,address,uint256)', + 'isOperator(address,address)', + 'transfer(address,uint256,uint256)', + 'transferFrom(address,address,uint256,uint256)', + 'approve(address,uint256,uint256)', + 'setOperator(address,bool)', + ], }; const INTERFACE_IDS = mapValues(SIGNATURES, interfaceId); From 1b762457e9bc1a480b2ceae94231dc8d6a5c6555 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:37:39 -0500 Subject: [PATCH 06/47] add more tests --- contracts/token/ERC6909/draft-ERC6909.sol | 19 ++++++++++++++++--- .../extensions/draft-ER6909TokenSupply.sol | 1 - .../extensions/draft-ERC6909ContentURI.sol | 1 - .../extensions/draft-ERC6909Metadata.sol | 1 - test/token/ERC6909/ERC6909.behavior.js | 6 ++---- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index efafc909338..1e541aa7aa8 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (token/ERC6909/draft-ERC6909.sol) pragma solidity ^0.8.20; @@ -8,6 +7,9 @@ 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); + mapping(uint256 id => mapping(address owner => uint256)) private _balances; mapping(address owner => mapping(address operator => bool)) private _operatorApprovals; @@ -61,7 +63,12 @@ contract ERC6909 is Context, ERC165, IERC6909 { if (caller != sender && !isOperator(sender, caller)) { uint256 currentAllowance = _allowances[sender][caller][id]; if (currentAllowance != type(uint256).max) { - _allowances[sender][_msgSender()][id] = currentAllowance - amount; + if (currentAllowance < amount) { + revert ERC6909InsufficientAllowance(caller, currentAllowance, amount, id); + } + unchecked { + _allowances[sender][_msgSender()][id] = currentAllowance - amount; + } } } @@ -73,7 +80,13 @@ contract ERC6909 is Context, ERC165, IERC6909 { address caller = _msgSender(); if (from != address(0)) { - _balances[id][from] -= amount; + uint256 fromBalance = _balances[id][from]; + if (fromBalance < amount) { + revert ERC6909InsufficientBalance(from, fromBalance, amount, id); + } + unchecked { + _balances[id][from] -= amount; + } } if (to != address(0)) { _balances[id][to] += amount; diff --git a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol index 0063acad7b4..1cdc39985dc 100644 --- a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol +++ b/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (token/ERC6909/extensions/draft-ER6909TokenSupply.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index 85c2931e7a4..9f9c9bd7f5d 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (token/ERC6909/extensions/draft-ERC6909ContentURI.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index 002e76921cd..d244f5d7ba0 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (token/ERC6909/extensions/draft-ERC6909Metadata.sol) pragma solidity ^0.8.20; diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js index 2b25e7f99e9..1cd25248bb6 100644 --- a/test/token/ERC6909/ERC6909.behavior.js +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -93,11 +93,9 @@ function shouldBehaveLikeERC6909() { }); it('transfers to the zero address are allowed', async function () { - await expect( - this.token.connect(this.alice).transfer(ethers.constants.AddressZero, firstTokenId, firstTokenAmount), - ) + await expect(this.token.connect(this.alice).transfer(ethers.ZeroAddress, firstTokenId, firstTokenAmount)) .to.emit(this.token, 'Transfer') - .withArgs(this.alice, ethers.constants.AddressZero, firstTokenId, firstTokenAmount); + .withArgs(this.alice, this.alice, ethers.ZeroAddress, firstTokenId, firstTokenAmount); }); }); From 9ae0429fcf00bfc1ed4c0fa2840ddbf9bc7b5040 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:32:49 -0500 Subject: [PATCH 07/47] add more tests --- test/token/ERC6909/ERC6909.behavior.js | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js index 1cd25248bb6..5cf23ced0b5 100644 --- a/test/token/ERC6909/ERC6909.behavior.js +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -96,9 +96,66 @@ function shouldBehaveLikeERC6909() { await expect(this.token.connect(this.alice).transfer(ethers.ZeroAddress, firstTokenId, firstTokenAmount)) .to.emit(this.token, 'Transfer') .withArgs(this.alice, this.alice, ethers.ZeroAddress, firstTokenId, firstTokenAmount); + + // expect(this.token.balanceOf(ethers.ZeroAddress, firstTokenId)).to.eventually.equal(firstTokenAmount); TODO: fix + expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(0); + }); + + it('reverts when insufficient balance', async function () { + await expect(this.token.connect(this.alice).transfer(this.bruce, firstTokenId, firstTokenAmount + 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientBalance') + .withArgs(this.alice, firstTokenAmount, firstTokenAmount + 1n, firstTokenId); + }); + }); + + describe('transferFrom', function () { + beforeEach(async function () { + await this.token.$_mint(this.alice, firstTokenId, firstTokenAmount); + await this.token.$_mint(this.bruce, secondTokenId, secondTokenAmount); + }); + + it('transfer from self', async function () { + await this.token.connect(this.alice).transferFrom(this.alice, this.bruce, firstTokenId, firstTokenAmount); + expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(0); + expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.equal(firstTokenAmount); + }); + + describe('with approval', async function () { + beforeEach(async function () { + await this.token.connect(this.alice).approve(this.operator, firstTokenId, firstTokenAmount - 1n); + this.tx = await this.token + .connect(this.operator) + .transferFrom(this.alice, this.bruce, firstTokenId, firstTokenAmount - 1n); + }); + + it('reverts when insufficient allowance', async function () { + await expect(this.token.connect(this.operator).transferFrom(this.alice, this.bruce, firstTokenId, 1)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientAllowance') + .withArgs(this.operator, 0, 1, firstTokenId); + }); + + it('should emit transfer event', async function () { + expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, this.alice, this.bruce, firstTokenId, firstTokenAmount - 1n); + }); + + it('should update approval', async function () { + expect(this.token.allowance(this.alice, this.operator, firstTokenId)).to.eventually.equal(0); + }); + + it("shouldn't reduce allowance when infinite", async function () { + await this.token.connect(this.bruce).approve(this.operator, secondTokenId, ethers.MaxUint256); + await this.token + .connect(this.operator) + .transferFrom(this.bruce, this.alice, secondTokenId, secondTokenAmount); + expect(this.token.allowance(this.bruce, this.operator, secondTokenId)).to.eventually.equal(ethers.MaxUint256); + }); }); }); + describe('with operator approval', function () {}); + shouldSupportInterfaces(['ERC6909']); }); } From f69bdfdd9dc89c126b55b2b6f2bcd9c9b67cd0f7 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:27:16 -0500 Subject: [PATCH 08/47] additional testing --- contracts/token/ERC6909/draft-ERC6909.sol | 20 +++++++- test/token/ERC6909/ERC6909.behavior.js | 24 +++++++++- test/token/ERC6909/ERC6909.test.js | 58 ++++++++++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 1e541aa7aa8..c84822a8761 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -9,6 +9,8 @@ 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; @@ -72,10 +74,20 @@ contract ERC6909 is Context, ERC165, IERC6909 { } } - _update(sender, receiver, id, 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(); @@ -96,10 +108,16 @@ contract ERC6909 is Context, ERC165, IERC6909 { } 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); } } diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js index 5cf23ced0b5..44e98311917 100644 --- a/test/token/ERC6909/ERC6909.behavior.js +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -154,7 +154,29 @@ function shouldBehaveLikeERC6909() { }); }); - describe('with operator approval', function () {}); + describe('with operator approval', function () { + beforeEach(async function () { + await this.token.connect(this.holder).setOperator(this.operator, true); + await this.token.$_mint(this.holder, firstTokenId, firstTokenAmount); + }); + + it('operator can transfer', async function () { + await expect( + this.token.connect(this.operator).transferFrom(this.holder, this.alice, firstTokenId, firstTokenAmount), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, this.holder, this.alice, firstTokenId, firstTokenAmount); + expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(0); + expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(firstTokenAmount); + }); + + it('operator transfer does not reduce allowance', async function () { + // Also give allowance + await this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenAmount); + await this.token.connect(this.operator).transferFrom(this.holder, this.alice, firstTokenId, firstTokenAmount); + expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal(firstTokenAmount); + }); + }); shouldSupportInterfaces(['ERC6909']); }); diff --git a/test/token/ERC6909/ERC6909.test.js b/test/token/ERC6909/ERC6909.test.js index facf40490e1..240b01df807 100644 --- a/test/token/ERC6909/ERC6909.test.js +++ b/test/token/ERC6909/ERC6909.test.js @@ -2,6 +2,7 @@ const { ethers } = require('hardhat'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC6909 } = require('./ERC6909.behavior'); +const { expect } = require('chai'); async function fixture() { const [operator, holder, ...otherAccounts] = await ethers.getSigners(); @@ -9,10 +10,65 @@ async function fixture() { return { token, operator, holder, otherAccounts }; } -describe.only('ERC6909', function () { +describe('ERC6909', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); shouldBehaveLikeERC6909(); + + describe('internal functions', function () { + const tokenId = 1990n; + const mintValue = 9001n; + const burnValue = 3000n; + + describe('_mint', function () { + it('reverts with a zero destination address', async function () { + await expect(this.token.$_mint(ethers.ZeroAddress, tokenId, mintValue)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver') + .withArgs(ethers.ZeroAddress); + }); + + describe('with minted tokens', function () { + beforeEach(async function () { + this.tx = await this.token.connect(this.operator).$_mint(this.holder, tokenId, mintValue); + }); + + it('emits a Transfer event from 0 address', async function () { + expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, ethers.ZeroAddress, this.holder, tokenId, mintValue); + }); + + it('credits the minted token value', async function () { + expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue); + }); + }); + }); + + describe('_burn', function () { + it('reverts with a zero from address', async function () { + await expect(this.token.$_burn(ethers.ZeroAddress, tokenId, burnValue)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSender') + .withArgs(ethers.ZeroAddress); + }); + + describe('with burned tokens', function () { + beforeEach(async function () { + await this.token.connect(this.operator).$_mint(this.holder, tokenId, mintValue); + this.tx = await this.token.connect(this.operator).$_burn(this.holder, tokenId, burnValue); + }); + + it('emits a Transfer event to 0 address', async function () { + expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, this.holder, ethers.ZeroAddress, tokenId, burnValue); + }); + + it('debits the burned token value', async function () { + expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue - burnValue); + }); + }); + }); + }); }); From 7d78b5a2b9a11474178dad360a22761f6755bee7 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:42:01 -0500 Subject: [PATCH 09/47] await when checking logs --- test/token/ERC6909/ERC6909.behavior.js | 6 +++--- test/token/ERC6909/ERC6909.test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js index 44e98311917..abc6b06ff68 100644 --- a/test/token/ERC6909/ERC6909.behavior.js +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -45,7 +45,7 @@ function shouldBehaveLikeERC6909() { }); it('emits an an OperatorSet event', async function () { - expect(this.tx).to.emit(this.token, 'OperatorSet').withArgs(this.holder, this.operator, true); + await expect(this.tx).to.emit(this.token, 'OperatorSet').withArgs(this.holder, this.operator, true); }); it('should be reflected in isOperator call', async function () { @@ -67,7 +67,7 @@ function shouldBehaveLikeERC6909() { }); it('emits an Approval event', async function () { - expect(this.tx) + await expect(this.tx) .to.emit(this.token, 'Approval') .withArgs(this.holder, this.operator, firstTokenId, firstTokenAmount); }); @@ -135,7 +135,7 @@ function shouldBehaveLikeERC6909() { }); it('should emit transfer event', async function () { - expect(this.tx) + await expect(this.tx) .to.emit(this.token, 'Transfer') .withArgs(this.operator, this.alice, this.bruce, firstTokenId, firstTokenAmount - 1n); }); diff --git a/test/token/ERC6909/ERC6909.test.js b/test/token/ERC6909/ERC6909.test.js index 240b01df807..f2dc83afcf7 100644 --- a/test/token/ERC6909/ERC6909.test.js +++ b/test/token/ERC6909/ERC6909.test.js @@ -35,7 +35,7 @@ describe('ERC6909', function () { }); it('emits a Transfer event from 0 address', async function () { - expect(this.tx) + await expect(this.tx) .to.emit(this.token, 'Transfer') .withArgs(this.operator, ethers.ZeroAddress, this.holder, tokenId, mintValue); }); @@ -60,7 +60,7 @@ describe('ERC6909', function () { }); it('emits a Transfer event to 0 address', async function () { - expect(this.tx) + await expect(this.tx) .to.emit(this.token, 'Transfer') .withArgs(this.operator, this.holder, ethers.ZeroAddress, tokenId, burnValue); }); From 6abfd920ce44c6ecf69b43d90a257eca1d389a65 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:30:52 -0500 Subject: [PATCH 10/47] address comments and fix tests --- contracts/token/ERC6909/draft-ERC6909.sol | 8 ++-- test/token/ERC6909/ERC6909.behavior.js | 52 ++++++++++++----------- test/token/ERC6909/ERC6909.test.js | 4 +- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index c84822a8761..23786b9f047 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -51,7 +51,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { } function transfer(address receiver, uint256 id, uint256 amount) external virtual override returns (bool) { - _update(_msgSender(), receiver, id, amount); + _transfer(_msgSender(), receiver, id, amount); return true; } @@ -63,13 +63,13 @@ contract ERC6909 is Context, ERC165, IERC6909 { ) external virtual override returns (bool) { address caller = _msgSender(); if (caller != sender && !isOperator(sender, caller)) { - uint256 currentAllowance = _allowances[sender][caller][id]; + uint256 currentAllowance = allowance(sender, caller, id); if (currentAllowance != type(uint256).max) { if (currentAllowance < amount) { revert ERC6909InsufficientAllowance(caller, currentAllowance, amount, id); } unchecked { - _allowances[sender][_msgSender()][id] = currentAllowance - amount; + _allowances[sender][caller][id] = currentAllowance - amount; } } } @@ -92,7 +92,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { address caller = _msgSender(); if (from != address(0)) { - uint256 fromBalance = _balances[id][from]; + uint256 fromBalance = balanceOf(from, id); if (fromBalance < amount) { revert ERC6909InsufficientBalance(from, fromBalance, amount, id); } diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js index abc6b06ff68..518761edcb0 100644 --- a/test/token/ERC6909/ERC6909.behavior.js +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -19,9 +19,9 @@ function shouldBehaveLikeERC6909() { describe('balanceOf', function () { describe("when accounts don't own tokens", function () { it('return zero', async function () { - expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.be.equal(0); - expect(this.token.balanceOf(this.bruce, secondTokenId)).to.eventually.be.equal(0); - expect(this.token.balanceOf(this.alice, randomTokenId)).to.eventually.be.equal(0); + await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.be.equal(0); + await expect(this.token.balanceOf(this.bruce, secondTokenId)).to.eventually.be.equal(0); + await expect(this.token.balanceOf(this.alice, randomTokenId)).to.eventually.be.equal(0); }); }); @@ -32,9 +32,9 @@ function shouldBehaveLikeERC6909() { }); it('returns amount owned by the given address', async function () { - expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.be.equal(firstTokenAmount); - expect(this.token.balanceOf(this.bruce, secondTokenId)).to.eventually.be.equal(secondTokenAmount); - expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.be.equal(0); + await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.be.equal(firstTokenAmount); + await expect(this.token.balanceOf(this.bruce, secondTokenId)).to.eventually.be.equal(secondTokenAmount); + await expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.be.equal(0); }); }); }); @@ -49,9 +49,9 @@ function shouldBehaveLikeERC6909() { }); it('should be reflected in isOperator call', async function () { - expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true; + await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true; // not operator for other account - expect(this.token.isOperator(this.alice, this.operator)).to.eventually.be.false; + await expect(this.token.isOperator(this.alice, this.operator)).to.eventually.be.false; }); it('can unset the operator approval', async function () { @@ -73,16 +73,18 @@ function shouldBehaveLikeERC6909() { }); it('is reflected in allowance', async function () { - expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(firstTokenAmount); + await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal( + firstTokenAmount, + ); // not operator for other account - expect(this.token.allowance(this.alice, this.operator, firstTokenId)).to.eventually.be.equal(0); + await expect(this.token.allowance(this.alice, this.operator, firstTokenId)).to.eventually.be.equal(0); }); it('can unset the approval', async function () { await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, 0)) .to.emit(this.token, 'Approval') .withArgs(this.holder, this.operator, firstTokenId, 0); - expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(0); + await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(0); }); }); @@ -92,13 +94,11 @@ function shouldBehaveLikeERC6909() { await this.token.$_mint(this.bruce, secondTokenId, secondTokenAmount); }); - it('transfers to the zero address are allowed', async function () { - await expect(this.token.connect(this.alice).transfer(ethers.ZeroAddress, firstTokenId, firstTokenAmount)) - .to.emit(this.token, 'Transfer') - .withArgs(this.alice, this.alice, ethers.ZeroAddress, firstTokenId, firstTokenAmount); - - // expect(this.token.balanceOf(ethers.ZeroAddress, firstTokenId)).to.eventually.equal(firstTokenAmount); TODO: fix - expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(0); + it('transfers to the zero address are blocked', async function () { + await expect( + this.token.connect(this.alice).transfer(ethers.ZeroAddress, firstTokenId, firstTokenAmount), + ).to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver'); + await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(firstTokenAmount); }); it('reverts when insufficient balance', async function () { @@ -116,8 +116,8 @@ function shouldBehaveLikeERC6909() { it('transfer from self', async function () { await this.token.connect(this.alice).transferFrom(this.alice, this.bruce, firstTokenId, firstTokenAmount); - expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(0); - expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.equal(firstTokenAmount); + await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(0); + await expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.equal(firstTokenAmount); }); describe('with approval', async function () { @@ -149,7 +149,9 @@ function shouldBehaveLikeERC6909() { await this.token .connect(this.operator) .transferFrom(this.bruce, this.alice, secondTokenId, secondTokenAmount); - expect(this.token.allowance(this.bruce, this.operator, secondTokenId)).to.eventually.equal(ethers.MaxUint256); + await expect(this.token.allowance(this.bruce, this.operator, secondTokenId)).to.eventually.equal( + ethers.MaxUint256, + ); }); }); }); @@ -166,15 +168,17 @@ function shouldBehaveLikeERC6909() { ) .to.emit(this.token, 'Transfer') .withArgs(this.operator, this.holder, this.alice, firstTokenId, firstTokenAmount); - expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(0); - expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(firstTokenAmount); + await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(0); + await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(firstTokenAmount); }); it('operator transfer does not reduce allowance', async function () { // Also give allowance await this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenAmount); await this.token.connect(this.operator).transferFrom(this.holder, this.alice, firstTokenId, firstTokenAmount); - expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal(firstTokenAmount); + await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal( + firstTokenAmount, + ); }); }); diff --git a/test/token/ERC6909/ERC6909.test.js b/test/token/ERC6909/ERC6909.test.js index f2dc83afcf7..80242493900 100644 --- a/test/token/ERC6909/ERC6909.test.js +++ b/test/token/ERC6909/ERC6909.test.js @@ -41,7 +41,7 @@ describe('ERC6909', function () { }); it('credits the minted token value', async function () { - expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue); + await expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue); }); }); }); @@ -66,7 +66,7 @@ describe('ERC6909', function () { }); it('debits the burned token value', async function () { - expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue - burnValue); + await expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue - burnValue); }); }); }); From 15ece65877d5bd126e91f9f5fddc2d72de7fe27b Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:14:50 -0500 Subject: [PATCH 11/47] add test for total supply --- .../extensions/ER6909TokenSupply.test.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/token/ERC6909/extensions/ER6909TokenSupply.test.js diff --git a/test/token/ERC6909/extensions/ER6909TokenSupply.test.js b/test/token/ERC6909/extensions/ER6909TokenSupply.test.js new file mode 100644 index 00000000000..b47c8536567 --- /dev/null +++ b/test/token/ERC6909/extensions/ER6909TokenSupply.test.js @@ -0,0 +1,34 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { expect } = require('chai'); + +const { shouldBehaveLikeERC6909 } = require('../ERC6909.behavior'); + +async function fixture() { + const [operator, holder, ...otherAccounts] = await ethers.getSigners(); + const token = await ethers.deployContract('$ER6909TokenSupply'); + return { token, operator, holder, otherAccounts }; +} + +describe.only('ERC6909TokenSupply', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeERC6909(); + + describe('totalSupply', function () { + beforeEach(async function () { + await this.token.$_mint(this.holder, 1n, 1000n); + }); + + it('Minting tokens increases the total supply', async function () { + return expect(this.token.totalSupply(1n)).to.eventually.be.equal(1000n); + }); + + it('Burning tokens decreases the total supply', async function () { + await this.token.$_burn(this.holder, 1n, 500n); + return expect(this.token.totalSupply(1n)).to.eventually.be.equal(500n); + }); + }); +}); From 499f79b0ac1872f827d6b1a681c277920c6c219e Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:55:20 -0500 Subject: [PATCH 12/47] format --- test/token/ERC6909/extensions/ER6909TokenSupply.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token/ERC6909/extensions/ER6909TokenSupply.test.js b/test/token/ERC6909/extensions/ER6909TokenSupply.test.js index b47c8536567..6124b1ff333 100644 --- a/test/token/ERC6909/extensions/ER6909TokenSupply.test.js +++ b/test/token/ERC6909/extensions/ER6909TokenSupply.test.js @@ -22,11 +22,11 @@ describe.only('ERC6909TokenSupply', function () { await this.token.$_mint(this.holder, 1n, 1000n); }); - it('Minting tokens increases the total supply', async function () { + it('minting tokens increases the total supply', async function () { return expect(this.token.totalSupply(1n)).to.eventually.be.equal(1000n); }); - it('Burning tokens decreases the total supply', async function () { + it('burning tokens decreases the total supply', async function () { await this.token.$_burn(this.holder, 1n, 500n); return expect(this.token.totalSupply(1n)).to.eventually.be.equal(500n); }); From d79198d736ab456c3cad43bb9e8fe965064b1aa3 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 30 Dec 2024 16:26:14 -0500 Subject: [PATCH 13/47] add tests --- test/token/ERC6909/ERC6909.behavior.js | 8 ++++++++ test/token/ERC6909/extensions/ER6909TokenSupply.test.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js index 518761edcb0..abbc8c78e98 100644 --- a/test/token/ERC6909/ERC6909.behavior.js +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -106,6 +106,14 @@ function shouldBehaveLikeERC6909() { .to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientBalance') .withArgs(this.alice, firstTokenAmount, firstTokenAmount + 1n, firstTokenId); }); + + it('emits event and transfers tokens', async function () { + await expect(this.token.connect(this.alice).transfer(this.bruce, firstTokenId, firstTokenAmount)) + .to.emit(this.token, 'Transfer') + .withArgs(this.alice, this.alice, this.bruce, firstTokenId, firstTokenAmount); + await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(0); + return expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.equal(firstTokenAmount); + }); }); describe('transferFrom', function () { diff --git a/test/token/ERC6909/extensions/ER6909TokenSupply.test.js b/test/token/ERC6909/extensions/ER6909TokenSupply.test.js index 6124b1ff333..0d8c732dde3 100644 --- a/test/token/ERC6909/extensions/ER6909TokenSupply.test.js +++ b/test/token/ERC6909/extensions/ER6909TokenSupply.test.js @@ -10,7 +10,7 @@ async function fixture() { return { token, operator, holder, otherAccounts }; } -describe.only('ERC6909TokenSupply', function () { +describe('ERC6909TokenSupply', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); From 85196e78641b707a66553484244f9223f4401658 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:12:52 -0500 Subject: [PATCH 14/47] add internal function test --- test/token/ERC6909/ERC6909.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/token/ERC6909/ERC6909.test.js b/test/token/ERC6909/ERC6909.test.js index 80242493900..dedd7274556 100644 --- a/test/token/ERC6909/ERC6909.test.js +++ b/test/token/ERC6909/ERC6909.test.js @@ -70,5 +70,11 @@ describe('ERC6909', function () { }); }); }); + + it('reverts when transferring from the zero address', async function () { + await expect(this.token.$_transfer(ethers.ZeroAddress, this.holder, 1n, 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSender') + .withArgs(ethers.ZeroAddress); + }); }); }); From 89690059cece5fb9b279f4dcf26712db29660218 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:19:03 -0500 Subject: [PATCH 15/47] add changesets --- .changeset/brown-turkeys-marry.md | 5 +++++ .changeset/dirty-bananas-shake.md | 5 +++++ .changeset/proud-cooks-do.md | 5 +++++ .changeset/ten-hats-begin.md | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 .changeset/brown-turkeys-marry.md create mode 100644 .changeset/dirty-bananas-shake.md create mode 100644 .changeset/proud-cooks-do.md create mode 100644 .changeset/ten-hats-begin.md diff --git a/.changeset/brown-turkeys-marry.md b/.changeset/brown-turkeys-marry.md new file mode 100644 index 00000000000..0440f0d9464 --- /dev/null +++ b/.changeset/brown-turkeys-marry.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ER6909TokenSupply`: Add an extension of ERC6909 which tracks total supply for each token id. diff --git a/.changeset/dirty-bananas-shake.md b/.changeset/dirty-bananas-shake.md new file mode 100644 index 00000000000..4e10a427c40 --- /dev/null +++ b/.changeset/dirty-bananas-shake.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC6909ContentURI`: Add an extension of ERC6909 which adds content URI functionality. diff --git a/.changeset/proud-cooks-do.md b/.changeset/proud-cooks-do.md new file mode 100644 index 00000000000..e3d4331aeb2 --- /dev/null +++ b/.changeset/proud-cooks-do.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC6909Metadata`: Add an extension of ERC6909 which adds metadata functionality. diff --git a/.changeset/ten-hats-begin.md b/.changeset/ten-hats-begin.md new file mode 100644 index 00000000000..393e0e7db5d --- /dev/null +++ b/.changeset/ten-hats-begin.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC6909`: Add a standard implmentation of ERC6909. From 607c11933640bc1ba717409cd5a3c561086b7ac3 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:31:18 -0500 Subject: [PATCH 16/47] fix spelling --- .changeset/ten-hats-begin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/ten-hats-begin.md b/.changeset/ten-hats-begin.md index 393e0e7db5d..bb7ab77e2ff 100644 --- a/.changeset/ten-hats-begin.md +++ b/.changeset/ten-hats-begin.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`ERC6909`: Add a standard implmentation of ERC6909. +`ERC6909`: Add a standard implementation of ERC6909. From 57631119f3a92e5724ecda18eed8aa73f8e9ad5f Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:37:47 -0500 Subject: [PATCH 17/47] Add tests for `ERC6909Metadata` --- .../extensions/draft-ERC6909Metadata.sol | 2 +- .../extensions/ERC6909Metadata.test.js | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 test/token/ERC6909/extensions/ERC6909Metadata.test.js diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index d244f5d7ba0..fd946234a2a 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -30,7 +30,7 @@ contract ERC6909Metadata is ERC6909, IERC6909Metadata { _tokenMetadata[id].name = newName; } - function _setTokenSymbol(uint256 id, string memory newSymbol) internal { + function _setSymbol(uint256 id, string memory newSymbol) internal { _tokenMetadata[id].symbol = newSymbol; } diff --git a/test/token/ERC6909/extensions/ERC6909Metadata.test.js b/test/token/ERC6909/extensions/ERC6909Metadata.test.js new file mode 100644 index 00000000000..270a0bcddc8 --- /dev/null +++ b/test/token/ERC6909/extensions/ERC6909Metadata.test.js @@ -0,0 +1,72 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { expect } = require('chai'); + +async function fixture() { + const [operator, holder, ...otherAccounts] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC6909Metadata'); + return { token, operator, holder, otherAccounts }; +} + +describe('ERC6909Metadata', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('name', function () { + it('is empty string be default', async function () { + return expect(this.token.name(1n)).to.eventually.equal(''); + }); + + it('can be set by dedicated setter', async function () { + await this.token.$_setName(1n, 'My Token'); + await expect(this.token.name(1n)).to.eventually.equal('My Token'); + + // Only set for the specified token ID + return expect(this.token.name(2n)).to.eventually.equal(''); + }); + + it('can be set by global setter', async function () { + await this.token.$_setTokenMetadata(1n, { name: 'My Token', symbol: '', decimals: '0' }); + return expect(this.token.name(1n)).to.eventually.equal('My Token'); + }); + }); + + describe('symbol', function () { + it('is empty string be default', async function () { + return expect(this.token.symbol(1n)).to.eventually.equal(''); + }); + + it('can be set by dedicated setter', async function () { + await this.token.$_setSymbol(1n, 'MTK'); + await expect(this.token.symbol(1n)).to.eventually.equal('MTK'); + + // Only set for the specified token ID + return expect(this.token.symbol(2n)).to.eventually.equal(''); + }); + + it('can be set by global setter', async function () { + await this.token.$_setTokenMetadata(1n, { name: '', symbol: 'MTK', decimals: '0' }); + return expect(this.token.symbol(1n)).to.eventually.equal('MTK'); + }); + }); + + describe('decimals', function () { + it('is 0 by default', async function () { + return expect(this.token.decimals(1n)).to.eventually.equal(0); + }); + + it('can be set by dedicated setter', async function () { + await this.token.$_setDecimals(1n, 18); + await expect(this.token.decimals(1n)).to.eventually.equal(18); + + // Only set for the specified token ID + return expect(this.token.decimals(2n)).to.eventually.equal(0); + }); + + it('can be set by global setter', async function () { + await this.token.$_setTokenMetadata(1n, { name: '', symbol: '', decimals: '18' }); + return expect(this.token.decimals(1n)).to.eventually.equal(18); + }); + }); +}); From 673124c820bb8a6b4af9af6aa209c27f8437ab50 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 3 Jan 2025 15:17:38 -0500 Subject: [PATCH 18/47] test `ERC6909ContentURI` --- .../extensions/ERC6909ContentURI.test.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/token/ERC6909/extensions/ERC6909ContentURI.test.js diff --git a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js new file mode 100644 index 00000000000..ba87b74b761 --- /dev/null +++ b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js @@ -0,0 +1,40 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { expect } = require('chai'); + +async function fixture() { + const [operator, holder, ...otherAccounts] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC6909ContentURI'); + return { token, operator, holder, otherAccounts }; +} + +describe('ERC6909ContentURI', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('contractURI', function () { + it('is empty string be default', async function () { + return expect(this.token.contractURI()).to.eventually.equal(''); + }); + + it('is settable by internal setter', async function () { + await this.token.$_setContractURI('https://example.com'); + return expect(this.token.contractURI()).to.eventually.equal('https://example.com'); + }); + }); + + describe('tokenURI', function () { + it('is empty string be default', async function () { + return expect(this.token.tokenURI(1n)).to.eventually.equal(''); + }); + + it('can be set by dedicated setter', async function () { + await this.token.$_setTokenURI(1n, 'https://example.com/1'); + await expect(this.token.tokenURI(1n)).to.eventually.equal('https://example.com/1'); + + // Only set for the specified token ID + return expect(this.token.tokenURI(2n)).to.eventually.equal(''); + }); + }); +}); From ff3ee754e2b7b7b6c82844c0d61810846aa9fd46 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:04:49 -0500 Subject: [PATCH 19/47] Add docs --- contracts/token/ERC6909/draft-ERC6909.sol | 45 +++++++++++++++++++ .../extensions/draft-ER6909TokenSupply.sol | 5 +++ .../extensions/draft-ERC6909ContentURI.sol | 2 + .../extensions/draft-ERC6909Metadata.sol | 6 +++ 4 files changed, 58 insertions(+) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 23786b9f047..8d5282b5484 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -6,6 +6,10 @@ import {IERC6909} from "../../interfaces/draft-IERC6909.sol"; import {Context} from "../../utils/Context.sol"; import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; +/** + * @dev Basic implementation of ERC6909. + * See https://eips.ethereum.org/EIPS/eip-6909 + */ 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); @@ -18,22 +22,27 @@ contract ERC6909 is Context, ERC165, IERC6909 { mapping(address owner => mapping(address spender => mapping(uint256 id => uint256))) private _allowances; + /// @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[id][owner]; } + /// @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) external virtual override returns (bool) { address caller = _msgSender(); _allowances[caller][spender][id] = amount; @@ -42,6 +51,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { return true; } + /// @inheritdoc IERC6909 function setOperator(address spender, bool approved) external virtual override returns (bool) { address caller = _msgSender(); _operatorApprovals[caller][spender] = approved; @@ -50,11 +60,13 @@ contract ERC6909 is Context, ERC165, IERC6909 { return true; } + /// @inheritdoc IERC6909 function transfer(address receiver, uint256 id, uint256 amount) external virtual override returns (bool) { _transfer(_msgSender(), receiver, id, amount); return true; } + /// @inheritdoc IERC6909 function transferFrom( address sender, address receiver, @@ -78,6 +90,16 @@ contract ERC6909 is Context, ERC165, IERC6909 { return true; } + /** + * @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)); @@ -88,6 +110,13 @@ contract ERC6909 is Context, ERC165, IERC6909 { _update(from, to, 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(); @@ -107,6 +136,14 @@ contract ERC6909 is Context, ERC165, IERC6909 { emit Transfer(caller, from, to, id, amount); } + /** + * @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)); @@ -114,6 +151,14 @@ contract ERC6909 is Context, ERC165, IERC6909 { _update(address(0), 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)); diff --git a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol index 1cdc39985dc..8586637bac0 100644 --- a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol +++ b/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol @@ -5,9 +5,14 @@ pragma solidity ^0.8.20; import {ERC6909} from "../draft-ERC6909.sol"; import {IERC6909TokenSupply} from "../../../interfaces/draft-IERC6909.sol"; +/** + * @dev Implementation of the Token Supply extension defined in ERC6909. + * Tracks the total supply of each token id individually. + */ contract ER6909TokenSupply is ERC6909, IERC6909TokenSupply { mapping(uint256 id => uint256) private _totalSupplies; + /// @inheritdoc IERC6909TokenSupply function totalSupply(uint256 id) external view virtual override returns (uint256) { return _totalSupplies[id]; } diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index 9f9c9bd7f5d..ca3517421df 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -9,10 +9,12 @@ contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { string private _contractURI; mapping(uint256 id => string) private _tokenURIs; + /// @inheritdoc IERC6909ContentURI function contractURI() external view virtual override returns (string memory) { return _contractURI; } + /// @inheritdoc IERC6909ContentURI function tokenURI(uint256 id) external view virtual override returns (string memory) { return _tokenURIs[id]; } diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index fd946234a2a..fd184ce3cf4 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -5,6 +5,9 @@ pragma solidity ^0.8.20; import {ERC6909} from "../draft-ERC6909.sol"; import {IERC6909Metadata} from "../../../interfaces/draft-IERC6909.sol"; +/** + * @dev Implementation of the Metadata extension defined in ERC6909. Exposes the name, symbol, and decimals of each token id. + */ contract ERC6909Metadata is ERC6909, IERC6909Metadata { struct TokenMetadata { string name; @@ -14,14 +17,17 @@ contract ERC6909Metadata is ERC6909, IERC6909Metadata { mapping(uint256 id => TokenMetadata) private _tokenMetadata; + /// @inheritdoc IERC6909Metadata function name(uint256 id) external view virtual override returns (string memory) { return _tokenMetadata[id].name; } + /// @inheritdoc IERC6909Metadata function symbol(uint256 id) external view virtual override returns (string memory) { return _tokenMetadata[id].symbol; } + /// @inheritdoc IERC6909Metadata function decimals(uint256 id) external view virtual override returns (uint8) { return _tokenMetadata[id].decimals; } From 1f347348fe243b996381ce71d1005f73ff7a10c5 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:28:05 -0500 Subject: [PATCH 20/47] add content uri docs --- contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol | 3 +++ contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol | 3 +++ 2 files changed, 6 insertions(+) diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index ca3517421df..5f8c43d3921 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -5,6 +5,9 @@ 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; diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index fd184ce3cf4..d662043504f 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -44,6 +44,9 @@ contract ERC6909Metadata is ERC6909, IERC6909Metadata { _tokenMetadata[id].decimals = newDecimals; } + /** + * @dev Sets the metadata for a given token `id` to the provided `metadata` struct. Overwrites any previous metadata for this token id. + */ function _setTokenMetadata(uint256 id, TokenMetadata memory metadata) internal { _tokenMetadata[id] = metadata; } From 2d9bc74b5230b32670fe56fb92d553fc45b89d7c Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:22:33 -1000 Subject: [PATCH 21/47] start on additional docs --- contracts/token/ERC6909/README.adoc | 30 ++++++++++++++++++++++++++++ docs/modules/ROOT/pages/erc6909.adoc | 5 +++++ 2 files changed, 35 insertions(+) create mode 100644 contracts/token/ERC6909/README.adoc create mode 100644 docs/modules/ROOT/pages/erc6909.adoc diff --git a/contracts/token/ERC6909/README.adoc b/contracts/token/ERC6909/README.adoc new file mode 100644 index 00000000000..b93cf8507d8 --- /dev/null +++ b/contracts/token/ERC6909/README.adoc @@ -0,0 +1,30 @@ += 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, found here as {IERC6909}, {IERC6909Metadata}, {IERC6909ContentURI}, and {IERC6909TokenSupply}. + +Implementations are provided for each of the 4 interfaces defined in the EIP. + +== Core + +{{IERC6909}} + +{{ERC6909}} + +== Extensions + +{{IERC6909Metadata}} + +{{IERC6909ContentURI}} + +{{IERC6909TokenSupply}} + +{{ERC6909ContentURI}} + +{{ERC6909Metadata}} + +{{ERC6909TokenSupply}} \ No newline at end of file diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc new file mode 100644 index 00000000000..2848e9f5f59 --- /dev/null +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -0,0 +1,5 @@ += ERC-6909 + +ERC-6909 is a draft EIP that draws on ERC-1155 and learnings since it was published in 2018. The main goals of ERC-6909 is to decrease gas costs and complexity--this is mainly accomplished by removing batching and callbacks. + +TIP: To understand the inspiration for a multi token standard, see the xref:erc1155.adoc#multi-token-standard[multi token standard] section within the EIP-1155 docs. \ No newline at end of file From 14a06a00a4aafc9bbaa69f7e0048f397218bb0b1 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:28:13 -1000 Subject: [PATCH 22/47] add reference contract --- .../mocks/docs/token/ERC6909/GameItems.sol | 20 ++++++++++++++++ docs/modules/ROOT/pages/erc6909.adoc | 24 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 contracts/mocks/docs/token/ERC6909/GameItems.sol diff --git a/contracts/mocks/docs/token/ERC6909/GameItems.sol b/contracts/mocks/docs/token/ERC6909/GameItems.sol new file mode 100644 index 00000000000..4bc18eeb560 --- /dev/null +++ b/contracts/mocks/docs/token/ERC6909/GameItems.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC6909} from "../../../../token/ERC6909/draft-ERC6909.sol"; + +contract GameItems is ERC6909 { + 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() { + _mint(msg.sender, GOLD, 10 ** 18); + _mint(msg.sender, SILVER, 10 ** 27); + _mint(msg.sender, THORS_HAMMER, 1); + _mint(msg.sender, SWORD, 10 ** 9); + _mint(msg.sender, SHIELD, 10 ** 9); + } +} diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index 2848e9f5f59..497309db864 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -2,4 +2,26 @@ ERC-6909 is a draft EIP that draws on ERC-1155 and learnings since it was published in 2018. The main goals of ERC-6909 is to decrease gas costs and complexity--this is mainly accomplished by removing batching and callbacks. -TIP: To understand the inspiration for a multi token standard, see the xref:erc1155.adoc#multi-token-standard[multi token standard] section within the EIP-1155 docs. \ No newline at end of file +TIP: To understand the inspiration for a multi token standard, see the xref:erc1155.adoc#multi-token-standard[multi token standard] section within the EIP-1155 docs. + +== Changes from ERC-1155 +There are three main changes from ERC-1155 which are as follows: +1. The removal of batch operations. +2. The removal of transfer callbacks. +3. Granularization in approvals--approvals can be set globally (as operators) or as amounts per token (inspired by ERC20). + +== Constructing an ERC-6909 Token Contract + +We'll use ERC-6909 to track multiple items in a game, each having their own unique attributes. All item types with by minted to the deployer of the contract, which we can later transfer to players. + +For simplicity, we will mint all items in the constructor--however, minting functionality could be added to the contract to min on demand to players. + +TIP: For an overview of minting mechanisms, check out xref:erc20-supply.adoc[Creating ERC-20 Supply]. + +Here's what a contract for tokenized items might look like: + +[source,solidity] +---- +include::api:example$token/ERC6909/GameItems.sol[] +---- + From 7089ec5948a2dc603b8e9511e5a5c00af51c2baf Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:10:56 -1000 Subject: [PATCH 23/47] further documentation --- contracts/interfaces/draft-IERC6909.sol | 2 ++ docs/modules/ROOT/pages/erc6909.adoc | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/contracts/interfaces/draft-IERC6909.sol b/contracts/interfaces/draft-IERC6909.sol index 12151e88656..51b5df05daa 100644 --- a/contracts/interfaces/draft-IERC6909.sol +++ b/contracts/interfaces/draft-IERC6909.sol @@ -108,6 +108,8 @@ interface IERC6909ContentURI is IERC6909 { /** * @dev Returns the URI for the token of type `id`. + * + * NOTE: MUST replace occurrences of {id} in the returned URI string by the client. */ function tokenURI(uint256 id) external view returns (string memory); } diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index 497309db864..644bd2f456f 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -25,3 +25,21 @@ Here's what a contract for tokenized items might look like: include::api:example$token/ERC6909/GameItems.sol[] ---- +Note that the vanilla xref:api:token/ERC6909.adoc#ERC6909[`ERC6909`] implementation does not have any concept of decimals, but the xref:api:token/ERC6909.adoc#ERC6909Metadata[`ERC6909Metadata`] extension does. This is very useful when some (or all) of the tokens represented by the ERC-6909 contract are fungible. Additionally, there is no content URI functionality in the base implementation, but the xref:api:token/ERC6909.adoc#ERC6909ContentURI[`ERC6909ContentURI`] extension adds it. + +Once the contract is deployed, we will be able to query the deployer’s balance: +[source,javascript] +---- +> gameItems.balanceOf(deployerAddress,3) +1000000000 +---- + +We can transfer items to player accounts: +[source,javascript] +---- +> gameItems.transfer(playerAddress, 2, 1) +> gameItems.balanceOf(playerAddress, 2) +1 +> gameItems.balanceOf(deployerAddress, 2) +0 +---- \ No newline at end of file From a6601c84e249556274b958992cd4eefceb08da7d Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:35:13 -1000 Subject: [PATCH 24/47] Update docs and fix incorrectly named file --- contracts/interfaces/README.adoc | 12 ++++++++++++ contracts/interfaces/draft-IERC6909.sol | 2 +- ...9TokenSupply.sol => draft-ERC6909TokenSupply.sol} | 3 ++- docs/modules/ROOT/nav.adoc | 1 + docs/modules/ROOT/pages/erc6909.adoc | 7 ++++--- ...okenSupply.test.js => ERC6909TokenSupply.test.js} | 2 +- 6 files changed, 21 insertions(+), 6 deletions(-) rename contracts/token/ERC6909/extensions/{draft-ER6909TokenSupply.sol => draft-ERC6909TokenSupply.sol} (84%) rename test/token/ERC6909/extensions/{ER6909TokenSupply.test.js => ERC6909TokenSupply.test.js} (94%) diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 61aae05d167..25717bcda03 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -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 @@ -82,4 +86,12 @@ are useful to interact with third party contracts that implement them. {{IERC6372}} +{{IERC6909}} + +{{IERC6909ContentURI}} + +{{IERC6909Metadata}} + +{{IERC6909TokenSupply}} + {{IERC7674}} diff --git a/contracts/interfaces/draft-IERC6909.sol b/contracts/interfaces/draft-IERC6909.sol index 51b5df05daa..bcab244fb94 100644 --- a/contracts/interfaces/draft-IERC6909.sol +++ b/contracts/interfaces/draft-IERC6909.sol @@ -109,7 +109,7 @@ interface IERC6909ContentURI is IERC6909 { /** * @dev Returns the URI for the token of type `id`. * - * NOTE: MUST replace occurrences of {id} in the returned URI string by the client. + * NOTE: MUST replace occurrences of `{id}` in the returned URI string by the client. */ function tokenURI(uint256 id) external view returns (string memory); } diff --git a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol similarity index 84% rename from contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol rename to contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol index 8586637bac0..d20cb7717b9 100644 --- a/contracts/token/ERC6909/extensions/draft-ER6909TokenSupply.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol @@ -9,7 +9,7 @@ import {IERC6909TokenSupply} from "../../../interfaces/draft-IERC6909.sol"; * @dev Implementation of the Token Supply extension defined in ERC6909. * Tracks the total supply of each token id individually. */ -contract ER6909TokenSupply is ERC6909, IERC6909TokenSupply { +contract ERC6909TokenSupply is ERC6909, IERC6909TokenSupply { mapping(uint256 id => uint256) private _totalSupplies; /// @inheritdoc IERC6909TokenSupply @@ -17,6 +17,7 @@ contract ER6909TokenSupply is ERC6909, IERC6909TokenSupply { return _totalSupplies[id]; } + /// @dev Override the `_update` function to update the total supply of each token id as necessary. function _update(address from, address to, uint256 id, uint256 amount) internal virtual override { super._update(from, to, id, amount); diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 15af8b40ea3..59164ff9110 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -12,6 +12,7 @@ *** xref:erc20-supply.adoc[Creating Supply] ** xref:erc721.adoc[ERC-721] ** xref:erc1155.adoc[ERC-1155] +** xref:erc6909.adoc[ERC-6909] ** xref:erc4626.adoc[ERC-4626] * xref:governance.adoc[Governance] diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index 644bd2f456f..8ece07543b1 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -6,9 +6,10 @@ TIP: To understand the inspiration for a multi token standard, see the xref:erc1 == Changes from ERC-1155 There are three main changes from ERC-1155 which are as follows: -1. The removal of batch operations. -2. The removal of transfer callbacks. -3. Granularization in approvals--approvals can be set globally (as operators) or as amounts per token (inspired by ERC20). + +. The removal of batch operations. +. The removal of transfer callbacks. +. Granularization in approvals--approvals can be set globally (as operators) or as amounts per token (inspired by ERC20). == Constructing an ERC-6909 Token Contract diff --git a/test/token/ERC6909/extensions/ER6909TokenSupply.test.js b/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js similarity index 94% rename from test/token/ERC6909/extensions/ER6909TokenSupply.test.js rename to test/token/ERC6909/extensions/ERC6909TokenSupply.test.js index 0d8c732dde3..be2c990a2b6 100644 --- a/test/token/ERC6909/extensions/ER6909TokenSupply.test.js +++ b/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js @@ -6,7 +6,7 @@ const { shouldBehaveLikeERC6909 } = require('../ERC6909.behavior'); async function fixture() { const [operator, holder, ...otherAccounts] = await ethers.getSigners(); - const token = await ethers.deployContract('$ER6909TokenSupply'); + const token = await ethers.deployContract('$ERC6909TokenSupply'); return { token, operator, holder, otherAccounts }; } From e711da686426e4f4781869a3644d43ef4c00bceb Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:40:52 -1000 Subject: [PATCH 25/47] update docs --- contracts/mocks/docs/token/ERC6909/GameItems.sol | 12 +++++++++--- docs/modules/ROOT/pages/erc6909.adoc | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/contracts/mocks/docs/token/ERC6909/GameItems.sol b/contracts/mocks/docs/token/ERC6909/GameItems.sol index 4bc18eeb560..022395b19fc 100644 --- a/contracts/mocks/docs/token/ERC6909/GameItems.sol +++ b/contracts/mocks/docs/token/ERC6909/GameItems.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -import {ERC6909} from "../../../../token/ERC6909/draft-ERC6909.sol"; +import {ERC6909Metadata} from "../../../../token/ERC6909/extensions/draft-ERC6909Metadata.sol"; -contract GameItems is ERC6909 { +contract GameItems is ERC6909Metadata { uint256 public constant GOLD = 0; uint256 public constant SILVER = 1; uint256 public constant THORS_HAMMER = 2; @@ -11,8 +11,14 @@ contract GameItems is ERC6909 { 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 ** 27); + _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); diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index 8ece07543b1..c5c53be8d7f 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -13,7 +13,7 @@ There are three main changes from ERC-1155 which are as follows: == Constructing an ERC-6909 Token Contract -We'll use ERC-6909 to track multiple items in a game, each having their own unique attributes. All item types with by minted to the deployer of the contract, which we can later transfer to players. +We'll use ERC-6909 to track multiple items in a game, each having their own unique attributes. All item types with by minted to the deployer of the contract, which we can later transfer to players. We'll also use the xref:api:token/ERC6909.adoc#ERC6909Metadata[`ERC6909Metadata`] extension to add decimals to our fungible items (the vanilla ERC-6909 implementation does not have decimals). For simplicity, we will mint all items in the constructor--however, minting functionality could be added to the contract to min on demand to players. @@ -26,7 +26,7 @@ Here's what a contract for tokenized items might look like: include::api:example$token/ERC6909/GameItems.sol[] ---- -Note that the vanilla xref:api:token/ERC6909.adoc#ERC6909[`ERC6909`] implementation does not have any concept of decimals, but the xref:api:token/ERC6909.adoc#ERC6909Metadata[`ERC6909Metadata`] extension does. This is very useful when some (or all) of the tokens represented by the ERC-6909 contract are fungible. Additionally, there is no content URI functionality in the base implementation, but the xref:api:token/ERC6909.adoc#ERC6909ContentURI[`ERC6909ContentURI`] extension adds it. +Note that there is no content URI functionality in the base implementation, but the xref:api:token/ERC6909.adoc#ERC6909ContentURI[`ERC6909ContentURI`] extension adds it. Additionally, the base implementation does not track total supplies, but the xref:api:token/ERC6909.adoc#ERC6909TokenSupply[`ERC6909TokenSupply`] extension tracks the total supply of each token id. Once the contract is deployed, we will be able to query the deployer’s balance: [source,javascript] From 361f81272fbd49f5b94b5986d30aa65855acc807 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:54:40 -1000 Subject: [PATCH 26/47] move interface --- contracts/token/ERC6909/README.adoc | 2 +- contracts/token/ERC6909/draft-ERC6909.sol | 2 +- contracts/{interfaces => token/ERC6909}/draft-IERC6909.sol | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename contracts/{interfaces => token/ERC6909}/draft-IERC6909.sol (100%) diff --git a/contracts/token/ERC6909/README.adoc b/contracts/token/ERC6909/README.adoc index b93cf8507d8..48218f8692f 100644 --- a/contracts/token/ERC6909/README.adoc +++ b/contracts/token/ERC6909/README.adoc @@ -7,7 +7,7 @@ This set of interfaces and contracts are all related to the https://eips.ethereu The ERC consists of four interfaces which fulfill different roles, found here as {IERC6909}, {IERC6909Metadata}, {IERC6909ContentURI}, and {IERC6909TokenSupply}. -Implementations are provided for each of the 4 interfaces defined in the EIP. +Implementations are provided for each of the 4 interfaces defined in the ERC. == Core diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 8d5282b5484..cc75e98260b 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; -import {IERC6909} from "../../interfaces/draft-IERC6909.sol"; +import {IERC6909} from "./draft-IERC6909.sol"; import {Context} from "../../utils/Context.sol"; import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; diff --git a/contracts/interfaces/draft-IERC6909.sol b/contracts/token/ERC6909/draft-IERC6909.sol similarity index 100% rename from contracts/interfaces/draft-IERC6909.sol rename to contracts/token/ERC6909/draft-IERC6909.sol From a2e9c98de981356fa8ec45f69ead947252858248 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:57:58 -1000 Subject: [PATCH 27/47] add interface reference to interfaces folder --- contracts/interfaces/draft-IERC6909.sol | 4 ++++ contracts/token/ERC6909/draft-ERC6909.sol | 2 +- contracts/token/ERC6909/draft-IERC6909.sol | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 contracts/interfaces/draft-IERC6909.sol diff --git a/contracts/interfaces/draft-IERC6909.sol b/contracts/interfaces/draft-IERC6909.sol new file mode 100644 index 00000000000..0c05f81c944 --- /dev/null +++ b/contracts/interfaces/draft-IERC6909.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC6909, IERC6909Metadata, IERC6909ContentURI, IERC6909TokenSupply} from "../token/ERC6909/draft-IERC6909.sol"; diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index cc75e98260b..8d5282b5484 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; -import {IERC6909} from "./draft-IERC6909.sol"; +import {IERC6909} from "../../interfaces/draft-IERC6909.sol"; import {Context} from "../../utils/Context.sol"; import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; diff --git a/contracts/token/ERC6909/draft-IERC6909.sol b/contracts/token/ERC6909/draft-IERC6909.sol index bcab244fb94..33688dad540 100644 --- a/contracts/token/ERC6909/draft-IERC6909.sol +++ b/contracts/token/ERC6909/draft-IERC6909.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; -import {IERC165} from "../utils/introspection/IERC165.sol"; +import {IERC165} from "../../utils/introspection/IERC165.sol"; /** * @dev Required interface of an ERC-6909 compliant contract, as defined in the From 78bf584c8e31da061c1cce9b36ba3dff449150eb Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:14:43 -1000 Subject: [PATCH 28/47] emit events on uri changes --- .../ERC6909/extensions/draft-ERC6909ContentURI.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index 5f8c43d3921..1547d7a4e29 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -12,6 +12,16 @@ 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 {IERC4906-MetadataUpdate}. This contract does not inherit {IERC4906} as it requires {IERC721}. + */ + event MetadataUpdate(uint256 _tokenId); + /// @inheritdoc IERC6909ContentURI function contractURI() external view virtual override returns (string memory) { return _contractURI; @@ -24,9 +34,13 @@ contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { function _setContractURI(string memory newContractURI) internal { _contractURI = newContractURI; + + emit ContractURIUpdated(); } function _setTokenURI(uint256 id, string memory newTokenURI) internal { _tokenURIs[id] = newTokenURI; + + emit MetadataUpdate(id); } } From 521335ca9b1785f3d97a7943d10948c136be2b8d Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:25:53 -1000 Subject: [PATCH 29/47] move game items file for 6909 --- .../docs/token/ERC6909/{GameItems.sol => ERC6909GameItems.sol} | 2 +- docs/modules/ROOT/pages/erc6909.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename contracts/mocks/docs/token/ERC6909/{GameItems.sol => ERC6909GameItems.sol} (94%) diff --git a/contracts/mocks/docs/token/ERC6909/GameItems.sol b/contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sol similarity index 94% rename from contracts/mocks/docs/token/ERC6909/GameItems.sol rename to contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sol index 022395b19fc..611e1dd667b 100644 --- a/contracts/mocks/docs/token/ERC6909/GameItems.sol +++ b/contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {ERC6909Metadata} from "../../../../token/ERC6909/extensions/draft-ERC6909Metadata.sol"; -contract GameItems is ERC6909Metadata { +contract ERC6909GameItems is ERC6909Metadata { uint256 public constant GOLD = 0; uint256 public constant SILVER = 1; uint256 public constant THORS_HAMMER = 2; diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc index c5c53be8d7f..37985ddfd45 100644 --- a/docs/modules/ROOT/pages/erc6909.adoc +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -23,7 +23,7 @@ Here's what a contract for tokenized items might look like: [source,solidity] ---- -include::api:example$token/ERC6909/GameItems.sol[] +include::api:example$token/ERC6909/ERC6909GameItems.sol[] ---- Note that there is no content URI functionality in the base implementation, but the xref:api:token/ERC6909.adoc#ERC6909ContentURI[`ERC6909ContentURI`] extension adds it. Additionally, the base implementation does not track total supplies, but the xref:api:token/ERC6909.adoc#ERC6909TokenSupply[`ERC6909TokenSupply`] extension tracks the total supply of each token id. From d630d84ec2586b7404ba5079161ffb20af02229f Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:54:53 -1000 Subject: [PATCH 30/47] check that event is emitted on uri set --- test/token/ERC6909/extensions/ERC6909ContentURI.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js index ba87b74b761..579b7c1cccb 100644 --- a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js +++ b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js @@ -22,6 +22,10 @@ describe('ERC6909ContentURI', function () { await this.token.$_setContractURI('https://example.com'); return expect(this.token.contractURI()).to.eventually.equal('https://example.com'); }); + + it('emits an event when set', async function () { + await expect(this.token.$_setContractURI('https://example.com')).to.emit(this.token, 'ContractURIUpdated'); + }); }); describe('tokenURI', function () { @@ -36,5 +40,9 @@ describe('ERC6909ContentURI', function () { // Only set for the specified token ID return expect(this.token.tokenURI(2n)).to.eventually.equal(''); }); + + it('emits an event when set', async function () { + await expect(this.token.$_setTokenURI(1n, 'https://example.com/1')).to.emit(this.token, 'MetadataUpdate').withArgs(1n); + }); }); }); From b3c83c760d1251a2a20ae6065f61f05d68b8f95b Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:03:52 -1000 Subject: [PATCH 31/47] fix lint --- test/token/ERC6909/extensions/ERC6909ContentURI.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js index 579b7c1cccb..105da7a22d6 100644 --- a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js +++ b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js @@ -42,7 +42,9 @@ describe('ERC6909ContentURI', function () { }); it('emits an event when set', async function () { - await expect(this.token.$_setTokenURI(1n, 'https://example.com/1')).to.emit(this.token, 'MetadataUpdate').withArgs(1n); + await expect(this.token.$_setTokenURI(1n, 'https://example.com/1')) + .to.emit(this.token, 'MetadataUpdate') + .withArgs(1n); }); }); }); From 9f6506bee375f7103c81a826e2811a30ba2d7b92 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:34:06 -0500 Subject: [PATCH 32/47] Apply suggestions from code review Co-authored-by: Hadrien Croubois --- contracts/token/ERC6909/draft-ERC6909.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 8d5282b5484..667cfa4eeb1 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -7,7 +7,7 @@ import {Context} from "../../utils/Context.sol"; import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; /** - * @dev Basic implementation of ERC6909. + * @dev Implementation of ERC-6909. * See https://eips.ethereum.org/EIPS/eip-6909 */ contract ERC6909 is Context, ERC165, IERC6909 { @@ -43,7 +43,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { } /// @inheritdoc IERC6909 - function approve(address spender, uint256 id, uint256 amount) external virtual override returns (bool) { + function approve(address spender, uint256 id, uint256 amount) public virtual override returns (bool) { address caller = _msgSender(); _allowances[caller][spender][id] = amount; @@ -52,7 +52,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { } /// @inheritdoc IERC6909 - function setOperator(address spender, bool approved) external virtual override returns (bool) { + function setOperator(address spender, bool approved) public virtual override returns (bool) { address caller = _msgSender(); _operatorApprovals[caller][spender] = approved; @@ -61,7 +61,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { } /// @inheritdoc IERC6909 - function transfer(address receiver, uint256 id, uint256 amount) external virtual override returns (bool) { + function transfer(address receiver, uint256 id, uint256 amount) public virtual override returns (bool) { _transfer(_msgSender(), receiver, id, amount); return true; } @@ -72,7 +72,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { address receiver, uint256 id, uint256 amount - ) external virtual override returns (bool) { + ) public virtual override returns (bool) { address caller = _msgSender(); if (caller != sender && !isOperator(sender, caller)) { uint256 currentAllowance = allowance(sender, caller, id); @@ -121,7 +121,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { address caller = _msgSender(); if (from != address(0)) { - uint256 fromBalance = balanceOf(from, id); + uint256 fromBalance = _balances[id][from]; if (fromBalance < amount) { revert ERC6909InsufficientBalance(from, fromBalance, amount, id); } From af6769f71d3c32b5a7d6d3aa6a01b6a71527623d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 Jan 2025 11:24:47 +0100 Subject: [PATCH 33/47] updates --- contracts/interfaces/draft-IERC6909.sol | 121 ++++++++++++++++- contracts/token/ERC6909/draft-ERC6909.sol | 37 ++++-- contracts/token/ERC6909/draft-IERC6909.sol | 125 ------------------ .../extensions/draft-ERC6909Metadata.sol | 19 +-- .../extensions/draft-ERC6909TokenSupply.sol | 5 +- docs/modules/ROOT/nav.adoc | 2 +- test/token/ERC6909/ERC6909.behavior.js | 45 ++++--- .../extensions/ERC6909ContentURI.test.js | 5 +- .../extensions/ERC6909Metadata.test.js | 20 +-- .../extensions/ERC6909TokenSupply.test.js | 2 +- 10 files changed, 187 insertions(+), 194 deletions(-) delete mode 100644 contracts/token/ERC6909/draft-IERC6909.sol diff --git a/contracts/interfaces/draft-IERC6909.sol b/contracts/interfaces/draft-IERC6909.sol index 0c05f81c944..12151e88656 100644 --- a/contracts/interfaces/draft-IERC6909.sol +++ b/contracts/interfaces/draft-IERC6909.sol @@ -1,4 +1,123 @@ // SPDX-License-Identifier: MIT + pragma solidity ^0.8.20; -import {IERC6909, IERC6909Metadata, IERC6909ContentURI, IERC6909TokenSupply} from "../token/ERC6909/draft-IERC6909.sol"; +import {IERC165} from "../utils/introspection/IERC165.sol"; + +/** + * @dev Required interface of an ERC-6909 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-6909[ERC]. + */ +interface IERC6909 is IERC165 { + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set for a token of type `id`. + * The new allowance is `amount`. + */ + event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount); + + /** + * @dev Emitted when `owner` grants or revokes operator status for a `spender`. + */ + event OperatorSet(address indexed owner, address indexed spender, bool approved); + + /** + * @dev Emitted when `amount` tokens of type `id` are moved from `sender` to `receiver` initiated by `caller`. + */ + event Transfer( + address caller, + address indexed sender, + address indexed receiver, + uint256 indexed id, + uint256 amount + ); + + /** + * @dev Returns the amount of tokens of type `id` owned by `owner`. + */ + function balanceOf(address owner, uint256 id) external view returns (uint256); + + /** + * @dev Returns the amount of tokens of type `id` that `spender` is allowed to spend on behalf of `owner`. + * + * NOTE: Does not include operator allowances. + */ + function allowance(address owner, address spender, uint256 id) external view returns (uint256); + + /** + * @dev Returns true if `spender` is set as an operator for `owner`. + */ + function isOperator(address owner, address spender) external view returns (bool); + + /** + * @dev Sets an approval to `spender` for `amount` tokens of type `id` from the caller's tokens. + * + * Must return true. + */ + function approve(address spender, uint256 id, uint256 amount) external returns (bool); + + /** + * @dev Grants or revokes unlimited transfer permission of any token id to `spender` for the caller's tokens. + * + * Must return true. + */ + function setOperator(address spender, bool approved) external returns (bool); + + /** + * @dev Transfers `amount` of token type `id` from the caller's account to `receiver`. + * + * Must return true. + */ + function transfer(address receiver, uint256 id, uint256 amount) external returns (bool); + + /** + * @dev Transfers `amount` of token type `id` from `sender` to `receiver`. + * + * Must return true. + */ + function transferFrom(address sender, address receiver, uint256 id, uint256 amount) external returns (bool); +} + +/** + * @dev Optional extension of {IERC6909} that adds metadata functions. + */ +interface IERC6909Metadata is IERC6909 { + /** + * @dev Returns the name of the token of type `id`. + */ + function name(uint256 id) external view returns (string memory); + + /** + * @dev Returns the ticker symbol of the token of type `id`. + */ + function symbol(uint256 id) external view returns (string memory); + + /** + * @dev Returns the number of decimals for the token of type `id`. + */ + function decimals(uint256 id) external view returns (uint8); +} + +/** + * @dev Optional extension of {IERC6909} that adds content URI functions. + */ +interface IERC6909ContentURI is IERC6909 { + /** + * @dev Returns URI for the contract. + */ + function contractURI() external view returns (string memory); + + /** + * @dev Returns the URI for the token of type `id`. + */ + function tokenURI(uint256 id) external view returns (string memory); +} + +/** + * @dev Optional extension of {IERC6909} that adds a token supply function. + */ +interface IERC6909TokenSupply is IERC6909 { + /** + * @dev Returns the total supply of the token of type `id`. + */ + function totalSupply(uint256 id) external view returns (uint256); +} diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 667cfa4eeb1..695f64ee897 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -74,18 +74,9 @@ contract ERC6909 is Context, ERC165, IERC6909 { uint256 amount ) public 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; - } - } + if (sender != caller && !isOperator(sender, caller)) { + _spendAllowance(sender, caller, id, amount); } - _transfer(sender, receiver, id, amount); return true; } @@ -126,7 +117,8 @@ contract ERC6909 is Context, ERC165, IERC6909 { revert ERC6909InsufficientBalance(from, fromBalance, amount, id); } unchecked { - _balances[id][from] -= amount; + // Overflow not possible: amount <= fromBalance. + _balances[id][from] = fromBalance - amount; } } if (to != address(0)) { @@ -165,4 +157,25 @@ contract ERC6909 is Context, ERC165, IERC6909 { } _update(from, address(0), id, amount); } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * + * 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); + // uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance < type(uint256).max) { + if (currentAllowance < amount) { + revert ERC6909InsufficientAllowance(spender, currentAllowance, amount, id); + } + unchecked { + _allowances[owner][spender][id] = currentAllowance - amount; + } + } + } } diff --git a/contracts/token/ERC6909/draft-IERC6909.sol b/contracts/token/ERC6909/draft-IERC6909.sol deleted file mode 100644 index 33688dad540..00000000000 --- a/contracts/token/ERC6909/draft-IERC6909.sol +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {IERC165} from "../../utils/introspection/IERC165.sol"; - -/** - * @dev Required interface of an ERC-6909 compliant contract, as defined in the - * https://eips.ethereum.org/EIPS/eip-6909[ERC]. - */ -interface IERC6909 is IERC165 { - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set for a token of type `id`. - * The new allowance is `amount`. - */ - event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount); - - /** - * @dev Emitted when `owner` grants or revokes operator status for a `spender`. - */ - event OperatorSet(address indexed owner, address indexed spender, bool approved); - - /** - * @dev Emitted when `amount` tokens of type `id` are moved from `sender` to `receiver` initiated by `caller`. - */ - event Transfer( - address caller, - address indexed sender, - address indexed receiver, - uint256 indexed id, - uint256 amount - ); - - /** - * @dev Returns the amount of tokens of type `id` owned by `owner`. - */ - function balanceOf(address owner, uint256 id) external view returns (uint256); - - /** - * @dev Returns the amount of tokens of type `id` that `spender` is allowed to spend on behalf of `owner`. - * - * NOTE: Does not include operator allowances. - */ - function allowance(address owner, address spender, uint256 id) external view returns (uint256); - - /** - * @dev Returns true if `spender` is set as an operator for `owner`. - */ - function isOperator(address owner, address spender) external view returns (bool); - - /** - * @dev Sets an approval to `spender` for `amount` tokens of type `id` from the caller's tokens. - * - * Must return true. - */ - function approve(address spender, uint256 id, uint256 amount) external returns (bool); - - /** - * @dev Grants or revokes unlimited transfer permission of any token id to `spender` for the caller's tokens. - * - * Must return true. - */ - function setOperator(address spender, bool approved) external returns (bool); - - /** - * @dev Transfers `amount` of token type `id` from the caller's account to `receiver`. - * - * Must return true. - */ - function transfer(address receiver, uint256 id, uint256 amount) external returns (bool); - - /** - * @dev Transfers `amount` of token type `id` from `sender` to `receiver`. - * - * Must return true. - */ - function transferFrom(address sender, address receiver, uint256 id, uint256 amount) external returns (bool); -} - -/** - * @dev Optional extension of {IERC6909} that adds metadata functions. - */ -interface IERC6909Metadata is IERC6909 { - /** - * @dev Returns the name of the token of type `id`. - */ - function name(uint256 id) external view returns (string memory); - - /** - * @dev Returns the ticker symbol of the token of type `id`. - */ - function symbol(uint256 id) external view returns (string memory); - - /** - * @dev Returns the number of decimals for the token of type `id`. - */ - function decimals(uint256 id) external view returns (uint8); -} - -/** - * @dev Optional extension of {IERC6909} that adds content URI functions. - */ -interface IERC6909ContentURI is IERC6909 { - /** - * @dev Returns URI for the contract. - */ - function contractURI() external view returns (string memory); - - /** - * @dev Returns the URI for the token of type `id`. - * - * NOTE: MUST replace occurrences of `{id}` in the returned URI string by the client. - */ - function tokenURI(uint256 id) external view returns (string memory); -} - -/** - * @dev Optional extension of {IERC6909} that adds a token supply function. - */ -interface IERC6909TokenSupply is IERC6909 { - /** - * @dev Returns the total supply of the token of type `id`. - */ - function totalSupply(uint256 id) external view returns (uint256); -} diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index d662043504f..857653db6e2 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -18,36 +18,29 @@ contract ERC6909Metadata is ERC6909, IERC6909Metadata { mapping(uint256 id => TokenMetadata) private _tokenMetadata; /// @inheritdoc IERC6909Metadata - function name(uint256 id) external view virtual override returns (string memory) { + function name(uint256 id) public view virtual override returns (string memory) { return _tokenMetadata[id].name; } /// @inheritdoc IERC6909Metadata - function symbol(uint256 id) external view virtual override returns (string memory) { + function symbol(uint256 id) public view virtual override returns (string memory) { return _tokenMetadata[id].symbol; } /// @inheritdoc IERC6909Metadata - function decimals(uint256 id) external view virtual override returns (uint8) { + function decimals(uint256 id) public view virtual override returns (uint8) { return _tokenMetadata[id].decimals; } - function _setName(uint256 id, string memory newName) internal { + function _setName(uint256 id, string memory newName) internal virtual { _tokenMetadata[id].name = newName; } - function _setSymbol(uint256 id, string memory newSymbol) internal { + function _setSymbol(uint256 id, string memory newSymbol) internal virtual { _tokenMetadata[id].symbol = newSymbol; } - function _setDecimals(uint256 id, uint8 newDecimals) internal { + function _setDecimals(uint256 id, uint8 newDecimals) internal virtual { _tokenMetadata[id].decimals = newDecimals; } - - /** - * @dev Sets the metadata for a given token `id` to the provided `metadata` struct. Overwrites any previous metadata for this token id. - */ - function _setTokenMetadata(uint256 id, TokenMetadata memory metadata) internal { - _tokenMetadata[id] = metadata; - } } diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol index d20cb7717b9..1ab14b71616 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol @@ -24,7 +24,10 @@ contract ERC6909TokenSupply is ERC6909, IERC6909TokenSupply { if (from == address(0)) { _totalSupplies[id] += amount; } else if (to == address(0)) { - _totalSupplies[id] -= amount; + unchecked { + // amount <= _balances[id][from] <= _totalSupplies[id] + _totalSupplies[id] -= amount; + } } } } diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 59164ff9110..52f7e37b09d 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -12,8 +12,8 @@ *** xref:erc20-supply.adoc[Creating Supply] ** xref:erc721.adoc[ERC-721] ** xref:erc1155.adoc[ERC-1155] -** xref:erc6909.adoc[ERC-6909] ** xref:erc4626.adoc[ERC-4626] +** xref:erc6909.adoc[ERC-6909] * xref:governance.adoc[Governance] diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js index abbc8c78e98..d8989e243a1 100644 --- a/test/token/ERC6909/ERC6909.behavior.js +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -95,9 +95,9 @@ function shouldBehaveLikeERC6909() { }); it('transfers to the zero address are blocked', async function () { - await expect( - this.token.connect(this.alice).transfer(ethers.ZeroAddress, firstTokenId, firstTokenAmount), - ).to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver'); + await expect(this.token.connect(this.alice).transfer(ethers.ZeroAddress, firstTokenId, firstTokenAmount)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver') + .withArgs(ethers.ZeroAddress); await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(firstTokenAmount); }); @@ -112,7 +112,7 @@ function shouldBehaveLikeERC6909() { .to.emit(this.token, 'Transfer') .withArgs(this.alice, this.alice, this.bruce, firstTokenId, firstTokenAmount); await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(0); - return expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.equal(firstTokenAmount); + await expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.equal(firstTokenAmount); }); }); @@ -123,33 +123,36 @@ function shouldBehaveLikeERC6909() { }); it('transfer from self', async function () { - await this.token.connect(this.alice).transferFrom(this.alice, this.bruce, firstTokenId, firstTokenAmount); + await expect( + this.token.connect(this.alice).transferFrom(this.alice, this.bruce, firstTokenId, firstTokenAmount), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.alice, this.alice, this.bruce, firstTokenId, firstTokenAmount); await expect(this.token.balanceOf(this.alice, firstTokenId)).to.eventually.equal(0); await expect(this.token.balanceOf(this.bruce, firstTokenId)).to.eventually.equal(firstTokenAmount); }); describe('with approval', async function () { beforeEach(async function () { - await this.token.connect(this.alice).approve(this.operator, firstTokenId, firstTokenAmount - 1n); - this.tx = await this.token - .connect(this.operator) - .transferFrom(this.alice, this.bruce, firstTokenId, firstTokenAmount - 1n); + await this.token.connect(this.alice).approve(this.operator, firstTokenId, firstTokenAmount); }); it('reverts when insufficient allowance', async function () { - await expect(this.token.connect(this.operator).transferFrom(this.alice, this.bruce, firstTokenId, 1)) + await expect( + this.token.connect(this.operator).transferFrom(this.alice, this.bruce, firstTokenId, firstTokenAmount + 1n), + ) .to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientAllowance') - .withArgs(this.operator, 0, 1, firstTokenId); + .withArgs(this.operator, firstTokenAmount, firstTokenAmount + 1n, firstTokenId); }); - it('should emit transfer event', async function () { - await expect(this.tx) + it('should emit transfer event and update approval (without an Approval event)', async function () { + await expect( + this.token.connect(this.operator).transferFrom(this.alice, this.bruce, firstTokenId, firstTokenAmount), + ) .to.emit(this.token, 'Transfer') - .withArgs(this.operator, this.alice, this.bruce, firstTokenId, firstTokenAmount - 1n); - }); - - it('should update approval', async function () { - expect(this.token.allowance(this.alice, this.operator, firstTokenId)).to.eventually.equal(0); + .withArgs(this.operator, this.alice, this.bruce, firstTokenId, firstTokenAmount) + .to.not.emit(this.token, 'Approval'); + await expect(this.token.allowance(this.alice, this.operator, firstTokenId)).to.eventually.equal(0); }); it("shouldn't reduce allowance when infinite", async function () { @@ -183,7 +186,11 @@ function shouldBehaveLikeERC6909() { it('operator transfer does not reduce allowance', async function () { // Also give allowance await this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenAmount); - await this.token.connect(this.operator).transferFrom(this.holder, this.alice, firstTokenId, firstTokenAmount); + await expect( + this.token.connect(this.operator).transferFrom(this.holder, this.alice, firstTokenId, firstTokenAmount), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, this.holder, this.alice, firstTokenId, firstTokenAmount); await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal( firstTokenAmount, ); diff --git a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js index 105da7a22d6..9a61fbafcf4 100644 --- a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js +++ b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js @@ -1,11 +1,10 @@ const { ethers } = require('hardhat'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { - const [operator, holder, ...otherAccounts] = await ethers.getSigners(); const token = await ethers.deployContract('$ERC6909ContentURI'); - return { token, operator, holder, otherAccounts }; + return { token }; } describe('ERC6909ContentURI', function () { diff --git a/test/token/ERC6909/extensions/ERC6909Metadata.test.js b/test/token/ERC6909/extensions/ERC6909Metadata.test.js index 270a0bcddc8..d9084f01853 100644 --- a/test/token/ERC6909/extensions/ERC6909Metadata.test.js +++ b/test/token/ERC6909/extensions/ERC6909Metadata.test.js @@ -1,11 +1,10 @@ const { ethers } = require('hardhat'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { - const [operator, holder, ...otherAccounts] = await ethers.getSigners(); const token = await ethers.deployContract('$ERC6909Metadata'); - return { token, operator, holder, otherAccounts }; + return { token }; } describe('ERC6909Metadata', function () { @@ -25,11 +24,6 @@ describe('ERC6909Metadata', function () { // Only set for the specified token ID return expect(this.token.name(2n)).to.eventually.equal(''); }); - - it('can be set by global setter', async function () { - await this.token.$_setTokenMetadata(1n, { name: 'My Token', symbol: '', decimals: '0' }); - return expect(this.token.name(1n)).to.eventually.equal('My Token'); - }); }); describe('symbol', function () { @@ -44,11 +38,6 @@ describe('ERC6909Metadata', function () { // Only set for the specified token ID return expect(this.token.symbol(2n)).to.eventually.equal(''); }); - - it('can be set by global setter', async function () { - await this.token.$_setTokenMetadata(1n, { name: '', symbol: 'MTK', decimals: '0' }); - return expect(this.token.symbol(1n)).to.eventually.equal('MTK'); - }); }); describe('decimals', function () { @@ -63,10 +52,5 @@ describe('ERC6909Metadata', function () { // Only set for the specified token ID return expect(this.token.decimals(2n)).to.eventually.equal(0); }); - - it('can be set by global setter', async function () { - await this.token.$_setTokenMetadata(1n, { name: '', symbol: '', decimals: '18' }); - return expect(this.token.decimals(1n)).to.eventually.equal(18); - }); }); }); diff --git a/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js b/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js index be2c990a2b6..8c6c19d372b 100644 --- a/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js +++ b/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js @@ -1,6 +1,6 @@ const { ethers } = require('hardhat'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC6909 } = require('../ERC6909.behavior'); From d09f7fce14362dd44f14595135ac01abca1490ba Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 Jan 2025 11:34:47 +0100 Subject: [PATCH 34/47] fix total supply tracking during updates from 0 to 0 --- .../extensions/draft-ERC6909TokenSupply.sol | 3 +- .../extensions/ERC6909ContentURI.test.js | 8 ++--- .../extensions/ERC6909Metadata.test.js | 10 +++--- .../extensions/ERC6909TokenSupply.test.js | 35 ++++++++++++++----- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol index 1ab14b71616..9c6f7a11ca0 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol @@ -23,7 +23,8 @@ contract ERC6909TokenSupply is ERC6909, IERC6909TokenSupply { if (from == address(0)) { _totalSupplies[id] += amount; - } else if (to == address(0)) { + } + if (to == address(0)) { unchecked { // amount <= _balances[id][from] <= _totalSupplies[id] _totalSupplies[id] -= amount; diff --git a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js index 9a61fbafcf4..904c3a912af 100644 --- a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js +++ b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js @@ -14,12 +14,12 @@ describe('ERC6909ContentURI', function () { describe('contractURI', function () { it('is empty string be default', async function () { - return expect(this.token.contractURI()).to.eventually.equal(''); + await expect(this.token.contractURI()).to.eventually.equal(''); }); it('is settable by internal setter', async function () { await this.token.$_setContractURI('https://example.com'); - return expect(this.token.contractURI()).to.eventually.equal('https://example.com'); + await expect(this.token.contractURI()).to.eventually.equal('https://example.com'); }); it('emits an event when set', async function () { @@ -29,7 +29,7 @@ describe('ERC6909ContentURI', function () { describe('tokenURI', function () { it('is empty string be default', async function () { - return expect(this.token.tokenURI(1n)).to.eventually.equal(''); + await expect(this.token.tokenURI(1n)).to.eventually.equal(''); }); it('can be set by dedicated setter', async function () { @@ -37,7 +37,7 @@ describe('ERC6909ContentURI', function () { await expect(this.token.tokenURI(1n)).to.eventually.equal('https://example.com/1'); // Only set for the specified token ID - return expect(this.token.tokenURI(2n)).to.eventually.equal(''); + await expect(this.token.tokenURI(2n)).to.eventually.equal(''); }); it('emits an event when set', async function () { diff --git a/test/token/ERC6909/extensions/ERC6909Metadata.test.js b/test/token/ERC6909/extensions/ERC6909Metadata.test.js index d9084f01853..2d6fcc8c5d9 100644 --- a/test/token/ERC6909/extensions/ERC6909Metadata.test.js +++ b/test/token/ERC6909/extensions/ERC6909Metadata.test.js @@ -22,13 +22,13 @@ describe('ERC6909Metadata', function () { await expect(this.token.name(1n)).to.eventually.equal('My Token'); // Only set for the specified token ID - return expect(this.token.name(2n)).to.eventually.equal(''); + await expect(this.token.name(2n)).to.eventually.equal(''); }); }); describe('symbol', function () { it('is empty string be default', async function () { - return expect(this.token.symbol(1n)).to.eventually.equal(''); + await expect(this.token.symbol(1n)).to.eventually.equal(''); }); it('can be set by dedicated setter', async function () { @@ -36,13 +36,13 @@ describe('ERC6909Metadata', function () { await expect(this.token.symbol(1n)).to.eventually.equal('MTK'); // Only set for the specified token ID - return expect(this.token.symbol(2n)).to.eventually.equal(''); + await expect(this.token.symbol(2n)).to.eventually.equal(''); }); }); describe('decimals', function () { it('is 0 by default', async function () { - return expect(this.token.decimals(1n)).to.eventually.equal(0); + await expect(this.token.decimals(1n)).to.eventually.equal(0); }); it('can be set by dedicated setter', async function () { @@ -50,7 +50,7 @@ describe('ERC6909Metadata', function () { await expect(this.token.decimals(1n)).to.eventually.equal(18); // Only set for the specified token ID - return expect(this.token.decimals(2n)).to.eventually.equal(0); + await expect(this.token.decimals(2n)).to.eventually.equal(0); }); }); }); diff --git a/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js b/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js index 8c6c19d372b..df0c877a054 100644 --- a/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js +++ b/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js @@ -5,9 +5,9 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC6909 } = require('../ERC6909.behavior'); async function fixture() { - const [operator, holder, ...otherAccounts] = await ethers.getSigners(); + const [operator, holder, receiver, ...otherAccounts] = await ethers.getSigners(); const token = await ethers.deployContract('$ERC6909TokenSupply'); - return { token, operator, holder, otherAccounts }; + return { token, operator, holder, receiver, otherAccounts }; } describe('ERC6909TokenSupply', function () { @@ -18,17 +18,36 @@ describe('ERC6909TokenSupply', function () { shouldBehaveLikeERC6909(); describe('totalSupply', function () { - beforeEach(async function () { - await this.token.$_mint(this.holder, 1n, 1000n); + it('is zero before any mint', async function () { + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(0n); }); it('minting tokens increases the total supply', async function () { - return expect(this.token.totalSupply(1n)).to.eventually.be.equal(1000n); + await this.token.$_mint(this.receiver, 1n, 17n); + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(17n); }); - it('burning tokens decreases the total supply', async function () { - await this.token.$_burn(this.holder, 1n, 500n); - return expect(this.token.totalSupply(1n)).to.eventually.be.equal(500n); + describe('with tokens minted', function () { + const supply = 1000n; + + beforeEach(async function () { + await this.token.$_mint(this.holder, 1n, supply); + }); + + it('burning tokens decreases the total supply', async function () { + await this.token.$_burn(this.holder, 1n, 17n); + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(supply - 17n); + }); + + it('supply unaffected by transfers', async function () { + await this.token.$_transfer(this.holder, this.receiver, 1n, 42n); + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(supply); + }); + + it('supply unaffected by no-op', async function () { + await this.token.$_update(ethers.ZeroAddress, ethers.ZeroAddress, 1n, 42n); + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(supply); + }); }); }); }); From 3219c9129b0a1c4963a4f6e97630278d5e43bcf9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 Jan 2025 11:41:37 +0100 Subject: [PATCH 35/47] typo --- contracts/token/ERC6909/draft-ERC6909.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 695f64ee897..237852d8898 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -159,7 +159,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { } /** - * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * @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. From 37515f3a55808a7e5e271710a54d9a1102a00cac Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 Jan 2025 13:42:49 +0100 Subject: [PATCH 36/47] Update draft-ERC6909.sol --- contracts/token/ERC6909/draft-ERC6909.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 237852d8898..e86034eb546 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -168,7 +168,6 @@ contract ERC6909 is Context, ERC165, IERC6909 { */ function _spendAllowance(address owner, address spender, uint256 id, uint256 amount) internal virtual { uint256 currentAllowance = allowance(owner, spender, id); - // uint256 currentAllowance = allowance(owner, spender); if (currentAllowance < type(uint256).max) { if (currentAllowance < amount) { revert ERC6909InsufficientAllowance(spender, currentAllowance, amount, id); From 2eff2056baf2f40e8711c36fa44a77065bfb96c7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 Jan 2025 13:43:57 +0100 Subject: [PATCH 37/47] up --- test/token/ERC6909/ERC6909.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/token/ERC6909/ERC6909.test.js b/test/token/ERC6909/ERC6909.test.js index dedd7274556..a72f5245b94 100644 --- a/test/token/ERC6909/ERC6909.test.js +++ b/test/token/ERC6909/ERC6909.test.js @@ -1,8 +1,8 @@ const { ethers } = require('hardhat'); +const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC6909 } = require('./ERC6909.behavior'); -const { expect } = require('chai'); async function fixture() { const [operator, holder, ...otherAccounts] = await ethers.getSigners(); From e864372875cb94b19271f741d4a6a1393dc6844e Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:32:08 -0500 Subject: [PATCH 38/47] reorder `_balances` mapping --- contracts/token/ERC6909/draft-ERC6909.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index e86034eb546..95055926e5e 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -16,7 +16,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { error ERC6909InvalidReceiver(address receiver); error ERC6909InvalidSender(address sender); - mapping(uint256 id => mapping(address owner => uint256)) private _balances; + mapping(address owner => mapping(uint256 id => uint256)) private _balances; mapping(address owner => mapping(address operator => bool)) private _operatorApprovals; @@ -29,7 +29,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { /// @inheritdoc IERC6909 function balanceOf(address owner, uint256 id) public view virtual override returns (uint256) { - return _balances[id][owner]; + return _balances[owner][id]; } /// @inheritdoc IERC6909 @@ -112,17 +112,17 @@ contract ERC6909 is Context, ERC165, IERC6909 { address caller = _msgSender(); if (from != address(0)) { - uint256 fromBalance = _balances[id][from]; + uint256 fromBalance = _balances[from][id]; if (fromBalance < amount) { revert ERC6909InsufficientBalance(from, fromBalance, amount, id); } unchecked { // Overflow not possible: amount <= fromBalance. - _balances[id][from] = fromBalance - amount; + _balances[from][id] = fromBalance - amount; } } if (to != address(0)) { - _balances[id][to] += amount; + _balances[to][id] += amount; } emit Transfer(caller, from, to, id, amount); From fa66666352635030239bfd198095f7b9ace884f8 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:08:37 -0500 Subject: [PATCH 39/47] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- .../token/ERC6909/extensions/draft-ERC6909ContentURI.sol | 4 ++-- .../token/ERC6909/extensions/draft-ERC6909TokenSupply.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index 1547d7a4e29..8b3353aa29c 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -23,12 +23,12 @@ contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { event MetadataUpdate(uint256 _tokenId); /// @inheritdoc IERC6909ContentURI - function contractURI() external view virtual override returns (string memory) { + function contractURI() public view virtual override returns (string memory) { return _contractURI; } /// @inheritdoc IERC6909ContentURI - function tokenURI(uint256 id) external view virtual override returns (string memory) { + function tokenURI(uint256 id) public view virtual override returns (string memory) { return _tokenURIs[id]; } diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol index 9c6f7a11ca0..476935f8fe1 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol @@ -13,7 +13,7 @@ contract ERC6909TokenSupply is ERC6909, IERC6909TokenSupply { mapping(uint256 id => uint256) private _totalSupplies; /// @inheritdoc IERC6909TokenSupply - function totalSupply(uint256 id) external view virtual override returns (uint256) { + function totalSupply(uint256 id) public view virtual override returns (uint256) { return _totalSupplies[id]; } From 78d9d92feacc1796619f684730d496b88b306317 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:34:44 -0500 Subject: [PATCH 40/47] update docs --- contracts/token/ERC6909/README.adoc | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/contracts/token/ERC6909/README.adoc b/contracts/token/ERC6909/README.adoc index 48218f8692f..3fc9e1e5246 100644 --- a/contracts/token/ERC6909/README.adoc +++ b/contracts/token/ERC6909/README.adoc @@ -5,24 +5,21 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ 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, found here as {IERC6909}, {IERC6909Metadata}, {IERC6909ContentURI}, and {IERC6909TokenSupply}. +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 -{{IERC6909}} - {{ERC6909}} == Extensions -{{IERC6909Metadata}} - -{{IERC6909ContentURI}} - -{{IERC6909TokenSupply}} - {{ERC6909ContentURI}} {{ERC6909Metadata}} From e81d3332505e3a0fe04357a45be8b5369569db9c Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:38:26 -0500 Subject: [PATCH 41/47] add metadata events and use `URI` instead of `MetadataUpdate` --- .../extensions/draft-ERC6909ContentURI.sol | 9 +++--- .../extensions/draft-ERC6909Metadata.sol | 30 +++++++++++++++++++ .../extensions/ERC6909ContentURI.test.js | 4 +-- .../extensions/ERC6909Metadata.test.js | 10 ++++--- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index 8b3353aa29c..2c8a85c84ac 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -17,10 +17,9 @@ contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { */ event ContractURIUpdated(); - /** - * @dev See {IERC4906-MetadataUpdate}. This contract does not inherit {IERC4906} as it requires {IERC721}. - */ - event MetadataUpdate(uint256 _tokenId); + /// @dev See {IERC1155-URI} + + event URI(string value, uint256 indexed id); /// @inheritdoc IERC6909ContentURI function contractURI() public view virtual override returns (string memory) { @@ -41,6 +40,6 @@ contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { function _setTokenURI(uint256 id, string memory newTokenURI) internal { _tokenURIs[id] = newTokenURI; - emit MetadataUpdate(id); + emit URI(newTokenURI, id); } } diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index 857653db6e2..352b0e0edea 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -17,6 +17,15 @@ contract ERC6909Metadata is ERC6909, IERC6909Metadata { mapping(uint256 id => TokenMetadata) private _tokenMetadata; + /// The name of the token of type `id` was updated to `newName`. + event ERC6909NameUpdated(uint256 indexed id, string newName); + + /// @dev The symbol for the token of type `id` was updated to `newSymbol`. + event ERC6909SymbolUpdated(uint256 indexed id, string newSymbol); + + /// @dev The decimals value for token of type `id` was updated to `newDecimals`. + event ERC6909DecimalsUpdated(uint256 indexed id, uint8 newDecimals); + /// @inheritdoc IERC6909Metadata function name(uint256 id) public view virtual override returns (string memory) { return _tokenMetadata[id].name; @@ -32,15 +41,36 @@ contract ERC6909Metadata is ERC6909, IERC6909Metadata { return _tokenMetadata[id].decimals; } + /** + * @dev Sets the `name` for a given token of type `id`. + * + * Emits an {ERC6909NameUpdated} event. + */ function _setName(uint256 id, string memory newName) internal virtual { _tokenMetadata[id].name = newName; + + emit ERC6909NameUpdated(id, newName); } + /** + * @dev Sets the `symbol` for a given token of type `id`. + * + * Emits an {ERC6909SymbolUpdated} event. + */ function _setSymbol(uint256 id, string memory newSymbol) internal virtual { _tokenMetadata[id].symbol = newSymbol; + + emit ERC6909SymbolUpdated(id, newSymbol); } + /** + * @dev Sets the `decimals` for a given token of type `id`. + * + * Emits an {ERC6909DecimalsUpdated} event. + */ function _setDecimals(uint256 id, uint8 newDecimals) internal virtual { _tokenMetadata[id].decimals = newDecimals; + + emit ERC6909DecimalsUpdated(id, newDecimals); } } diff --git a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js index 904c3a912af..3597eb78e8a 100644 --- a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js +++ b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js @@ -42,8 +42,8 @@ describe('ERC6909ContentURI', function () { it('emits an event when set', async function () { await expect(this.token.$_setTokenURI(1n, 'https://example.com/1')) - .to.emit(this.token, 'MetadataUpdate') - .withArgs(1n); + .to.emit(this.token, 'URI') + .withArgs('https://example.com/1', 1n); }); }); }); diff --git a/test/token/ERC6909/extensions/ERC6909Metadata.test.js b/test/token/ERC6909/extensions/ERC6909Metadata.test.js index 2d6fcc8c5d9..e6d3dd9f32d 100644 --- a/test/token/ERC6909/extensions/ERC6909Metadata.test.js +++ b/test/token/ERC6909/extensions/ERC6909Metadata.test.js @@ -14,11 +14,13 @@ describe('ERC6909Metadata', function () { describe('name', function () { it('is empty string be default', async function () { - return expect(this.token.name(1n)).to.eventually.equal(''); + await expect(this.token.name(1n)).to.eventually.equal(''); }); it('can be set by dedicated setter', async function () { - await this.token.$_setName(1n, 'My Token'); + await expect(this.token.$_setName(1n, 'My Token')) + .to.emit(this.token, 'ERC6909NameUpdated') + .withArgs(1n, 'My Token'); await expect(this.token.name(1n)).to.eventually.equal('My Token'); // Only set for the specified token ID @@ -32,7 +34,7 @@ describe('ERC6909Metadata', function () { }); it('can be set by dedicated setter', async function () { - await this.token.$_setSymbol(1n, 'MTK'); + await expect(this.token.$_setSymbol(1n, 'MTK')).to.emit(this.token, 'ERC6909SymbolUpdated').withArgs(1n, 'MTK'); await expect(this.token.symbol(1n)).to.eventually.equal('MTK'); // Only set for the specified token ID @@ -46,7 +48,7 @@ describe('ERC6909Metadata', function () { }); it('can be set by dedicated setter', async function () { - await this.token.$_setDecimals(1n, 18); + await expect(this.token.$_setDecimals(1n, 18)).to.emit(this.token, 'ERC6909DecimalsUpdated').withArgs(1n, 18); await expect(this.token.decimals(1n)).to.eventually.equal(18); // Only set for the specified token ID From b4eb0bc5904ebbe4d1041c0c1945af857bb204db Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:11:33 -0500 Subject: [PATCH 42/47] formatting --- contracts/token/ERC6909/draft-ERC6909.sol | 10 +++++----- .../ERC6909/extensions/draft-ERC6909ContentURI.sol | 9 +++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 95055926e5e..e702a440fa7 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -11,17 +11,17 @@ import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; * See https://eips.ethereum.org/EIPS/eip-6909 */ 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(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 ERC6909InvalidReceiver(address receiver); + error ERC6909InvalidSender(address sender); + /// @inheritdoc IERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { return interfaceId == type(IERC6909).interfaceId || super.supportsInterface(interfaceId); diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index 2c8a85c84ac..bca24c0332a 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -12,13 +12,10 @@ 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. - */ + /// @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 @@ -31,13 +28,13 @@ contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { return _tokenURIs[id]; } - function _setContractURI(string memory newContractURI) internal { + function _setContractURI(string memory newContractURI) internal virtual { _contractURI = newContractURI; emit ContractURIUpdated(); } - function _setTokenURI(uint256 id, string memory newTokenURI) internal { + function _setTokenURI(uint256 id, string memory newTokenURI) internal virtual { _tokenURIs[id] = newTokenURI; emit URI(newTokenURI, id); From a9bb0637bde8008e085364b467155f5998435369 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:16:48 -0500 Subject: [PATCH 43/47] add missing dev tag --- contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol index 352b0e0edea..4132863863a 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -17,7 +17,7 @@ contract ERC6909Metadata is ERC6909, IERC6909Metadata { mapping(uint256 id => TokenMetadata) private _tokenMetadata; - /// The name of the token of type `id` was updated to `newName`. + /// @dev The name of the token of type `id` was updated to `newName`. event ERC6909NameUpdated(uint256 indexed id, string newName); /// @dev The symbol for the token of type `id` was updated to `newSymbol`. From 267df513c7a5ee39db00cdd80003ccacf5a704e9 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Sun, 26 Jan 2025 22:14:09 -0500 Subject: [PATCH 44/47] add docs --- .../ERC6909/extensions/draft-ERC6909ContentURI.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol index bca24c0332a..8839947936d 100644 --- a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -28,12 +28,22 @@ contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { 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; From de3d5dc71dc723371312d78b0fbc065f4665c614 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 27 Jan 2025 14:01:10 +0100 Subject: [PATCH 45/47] add missing internal setters --- contracts/token/ERC6909/draft-ERC6909.sol | 92 +++++++++++++++++------ 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index e702a440fa7..6ba51dac17a 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -19,8 +19,10 @@ 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 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) { @@ -44,19 +46,13 @@ contract ERC6909 is Context, ERC165, IERC6909 { /// @inheritdoc IERC6909 function approve(address spender, uint256 id, uint256 amount) public virtual override returns (bool) { - address caller = _msgSender(); - _allowances[caller][spender][id] = amount; - - emit Approval(caller, spender, id, amount); + _approve(_msgSender(), spender, id, amount); return true; } /// @inheritdoc IERC6909 function setOperator(address spender, bool approved) public virtual override returns (bool) { - address caller = _msgSender(); - _operatorApprovals[caller][spender] = approved; - - emit OperatorSet(caller, spender, approved); + _setOperator(_msgSender(), spender, approved); return true; } @@ -81,6 +77,21 @@ contract ERC6909 is Context, ERC165, IERC6909 { 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. * @@ -101,6 +112,21 @@ contract ERC6909 is Context, ERC165, IERC6909 { _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 @@ -129,33 +155,51 @@ contract ERC6909 is Context, ERC165, IERC6909 { } /** - * @dev Creates `amount` of token `id` and assigns them to `account`, by transferring it from address(0). - * Relies on the `_update` mechanism + * @dev Sets `amount` as the allowance of `spender` over the `owner` s `id` tokens. * - * Emits a {Transfer} event with `from` set to the zero address. + * This internal function is equivalent to `approve`, and can be used to e.g. set automatic allowances for certain + * subsystems, etc. * - * NOTE: This function is not virtual, {_update} should be overridden instead. + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. */ - function _mint(address to, uint256 id, uint256 amount) internal { - if (to == address(0)) { - revert ERC6909InvalidReceiver(address(0)); + function _approve(address owner, address spender, uint256 id, uint256 amount) internal virtual { + if (owner == address(0)) { + revert ERC6909InvalidApprover(address(0)); } - _update(address(0), to, id, amount); + if (spender == address(0)) { + revert ERC6909InvalidSpender(address(0)); + } + _allowances[owner][spender][id] = amount; + emit Approval(owner, spender, id, amount); } /** - * @dev Destroys a `amount` of token `id` from `account`. - * Relies on the `_update` mechanism. + * @dev Approve `spender` to operate on all of `owner` tokens * - * Emits a {Transfer} event with `to` set to the zero address. + * This internal function is equivalent to `setOperator`, and can be used to e.g. set automatic allowances for + * certain subsystems, etc. * - * NOTE: This function is not virtual, {_update} should be overridden instead + * Emits an {OperatorSet} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. */ - function _burn(address from, uint256 id, uint256 amount) internal { - if (from == address(0)) { - revert ERC6909InvalidSender(address(0)); + function _setOperator(address owner, address spender, bool approved) internal virtual { + if (owner == address(0)) { + revert ERC6909InvalidApprover(address(0)); } - _update(from, address(0), id, amount); + if (spender == address(0)) { + revert ERC6909InvalidSpender(address(0)); + } + _operatorApprovals[owner][spender] = approved; + emit OperatorSet(owner, spender, approved); } /** From 8f239eabb29d15fcef39dad1958b9584bfb9103f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 27 Jan 2025 16:55:12 +0100 Subject: [PATCH 46/47] testing --- test/token/ERC6909/ERC6909.behavior.js | 39 +++++++++++++++----------- test/token/ERC6909/ERC6909.test.js | 32 ++++++++++++++++++--- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js index d8989e243a1..bfac00b8f46 100644 --- a/test/token/ERC6909/ERC6909.behavior.js +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -40,16 +40,14 @@ function shouldBehaveLikeERC6909() { }); describe('setOperator', function () { - beforeEach(async function () { - this.tx = await this.token.connect(this.holder).setOperator(this.operator, true); - }); - - it('emits an an OperatorSet event', async function () { - await expect(this.tx).to.emit(this.token, 'OperatorSet').withArgs(this.holder, this.operator, true); - }); + it('emits an an OperatorSet event and updated the value', async function () { + await expect(this.token.connect(this.holder).setOperator(this.operator, true)) + .to.emit(this.token, 'OperatorSet') + .withArgs(this.holder, this.operator, true); - it('should be reflected in isOperator call', async function () { + // operator for holder await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true; + // not operator for other account await expect(this.token.isOperator(this.alice, this.operator)).to.eventually.be.false; }); @@ -59,24 +57,25 @@ function shouldBehaveLikeERC6909() { .to.emit(this.token, 'OperatorSet') .withArgs(this.holder, this.operator, false); }); - }); - describe('approve', function () { - beforeEach(async function () { - this.tx = await this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenAmount); + it('cannot set address(0) as an operator', async function () { + await expect(this.token.setOperator(ethers.ZeroAddress, true)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSpender') + .withArgs(ethers.ZeroAddress); }); + }); - it('emits an Approval event', async function () { - await expect(this.tx) + describe('approve', function () { + it('emits an Approval event and updates allowance', async function () { + await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenAmount)) .to.emit(this.token, 'Approval') .withArgs(this.holder, this.operator, firstTokenId, firstTokenAmount); - }); - it('is reflected in allowance', async function () { + // approved await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal( firstTokenAmount, ); - // not operator for other account + // other account is not approved await expect(this.token.allowance(this.alice, this.operator, firstTokenId)).to.eventually.be.equal(0); }); @@ -86,6 +85,12 @@ function shouldBehaveLikeERC6909() { .withArgs(this.holder, this.operator, firstTokenId, 0); await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(0); }); + + it('cannot give allowance to address(0)', async function () { + await expect(this.token.connect(this.holder).approve(ethers.ZeroAddress, firstTokenId, firstTokenAmount)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSpender') + .withArgs(ethers.ZeroAddress); + }); }); describe('transfer', function () { diff --git a/test/token/ERC6909/ERC6909.test.js b/test/token/ERC6909/ERC6909.test.js index a72f5245b94..7c635b49193 100644 --- a/test/token/ERC6909/ERC6909.test.js +++ b/test/token/ERC6909/ERC6909.test.js @@ -46,6 +46,20 @@ describe('ERC6909', function () { }); }); + describe('_transfer', function () { + it('reverts when transferring from the zero address', async function () { + await expect(this.token.$_transfer(ethers.ZeroAddress, this.holder, 1n, 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSender') + .withArgs(ethers.ZeroAddress); + }); + + it('reverts when transferring to the zero address', async function () { + await expect(this.token.$_transfer(this.holder, ethers.ZeroAddress, 1n, 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver') + .withArgs(ethers.ZeroAddress); + }); + }); + describe('_burn', function () { it('reverts with a zero from address', async function () { await expect(this.token.$_burn(ethers.ZeroAddress, tokenId, burnValue)) @@ -71,10 +85,20 @@ describe('ERC6909', function () { }); }); - it('reverts when transferring from the zero address', async function () { - await expect(this.token.$_transfer(ethers.ZeroAddress, this.holder, 1n, 1n)) - .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSender') - .withArgs(ethers.ZeroAddress); + describe('_approve', function () { + it('reverts when the owner is the zero address', async function () { + await expect(this.token.$_approve(ethers.ZeroAddress, this.recipient, 1n, 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidApprover') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('_setOperator', function () { + it('reverts when the owner is the zero address', async function () { + await expect(this.token.$_setOperator(ethers.ZeroAddress, this.operator, true)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidApprover') + .withArgs(ethers.ZeroAddress); + }); }); }); }); From d1297b08f32e8844371743eea6175db3a324edcd Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:57:14 -0500 Subject: [PATCH 47/47] fix apostrophe --- contracts/token/ERC6909/draft-ERC6909.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol index 6ba51dac17a..e821d4b3b22 100644 --- a/contracts/token/ERC6909/draft-ERC6909.sol +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -155,7 +155,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { } /** - * @dev Sets `amount` as the allowance of `spender` over the `owner` s `id` tokens. + * @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. @@ -179,7 +179,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { } /** - * @dev Approve `spender` to operate on all of `owner` tokens + * @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. @@ -203,7 +203,7 @@ contract ERC6909 is Context, ERC165, IERC6909 { } /** - * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * @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.