commit
602dc92c1c
@ -0,0 +1,5 @@ |
||||
--- |
||||
'openzeppelin-solidity': minor |
||||
--- |
||||
|
||||
`SafeERC20`: Add `trySafeTransfer` and `trySafeTransferFrom` that do not revert and return false if the transfer is not successful. |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'openzeppelin-solidity': minor |
||||
--- |
||||
|
||||
`ER6909TokenSupply`: Add an extension of ERC6909 which tracks total supply for each token id. |
@ -1,5 +0,0 @@ |
||||
--- |
||||
'openzeppelin-solidity': minor |
||||
--- |
||||
|
||||
`ERC4337Utils`: Add functions to manage deposit and stake on the paymaster. |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'openzeppelin-solidity': minor |
||||
--- |
||||
|
||||
`ERC6909ContentURI`: Add an extension of ERC6909 which adds content URI functionality. |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'openzeppelin-solidity': minor |
||||
--- |
||||
|
||||
`ERC6909Metadata`: Add an extension of ERC6909 which adds metadata functionality. |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'openzeppelin-solidity': minor |
||||
--- |
||||
|
||||
`ERC6909`: Add a standard implementation of ERC6909. |
@ -0,0 +1,26 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {ERC6909Metadata} from "../../../../token/ERC6909/extensions/draft-ERC6909Metadata.sol"; |
||||
|
||||
contract ERC6909GameItems is ERC6909Metadata { |
||||
uint256 public constant GOLD = 0; |
||||
uint256 public constant SILVER = 1; |
||||
uint256 public constant THORS_HAMMER = 2; |
||||
uint256 public constant SWORD = 3; |
||||
uint256 public constant SHIELD = 4; |
||||
|
||||
constructor() { |
||||
_setDecimals(GOLD, 18); |
||||
_setDecimals(SILVER, 18); |
||||
// Default decimals is 0 |
||||
_setDecimals(SWORD, 9); |
||||
_setDecimals(SHIELD, 9); |
||||
|
||||
_mint(msg.sender, GOLD, 10 ** 18); |
||||
_mint(msg.sender, SILVER, 10_000 ** 18); |
||||
_mint(msg.sender, THORS_HAMMER, 1); |
||||
_mint(msg.sender, SWORD, 10 ** 9); |
||||
_mint(msg.sender, SHIELD, 10 ** 9); |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
= ERC-6909 |
||||
|
||||
[.readme-notice] |
||||
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc6909 |
||||
|
||||
This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-6909[ERC-6909 Minimal Multi-Token Interface]. |
||||
|
||||
The ERC consists of four interfaces which fulfill different roles--the interfaces are as follows: |
||||
|
||||
. {IERC6909}: Base interface for a vanilla ERC6909 token. |
||||
. {IERC6909ContentURI}: Extends the base interface and adds content URI (contract and token level) functionality. |
||||
. {IERC6909Metadata}: Extends the base interface and adds metadata functionality, which exposes a name, symbol, and decimals for each token id. |
||||
. {IERC6909TokenSupply}: Extends the base interface and adds total supply functionality for each token id. |
||||
|
||||
Implementations are provided for each of the 4 interfaces defined in the ERC. |
||||
|
||||
== Core |
||||
|
||||
{{ERC6909}} |
||||
|
||||
== Extensions |
||||
|
||||
{{ERC6909ContentURI}} |
||||
|
||||
{{ERC6909Metadata}} |
||||
|
||||
{{ERC6909TokenSupply}} |
@ -0,0 +1,224 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {IERC6909} from "../../interfaces/draft-IERC6909.sol"; |
||||
import {Context} from "../../utils/Context.sol"; |
||||
import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; |
||||
|
||||
/** |
||||
* @dev Implementation of ERC-6909. |
||||
* See https://eips.ethereum.org/EIPS/eip-6909 |
||||
*/ |
||||
contract ERC6909 is Context, ERC165, IERC6909 { |
||||
mapping(address owner => mapping(uint256 id => uint256)) private _balances; |
||||
|
||||
mapping(address owner => mapping(address operator => bool)) private _operatorApprovals; |
||||
|
||||
mapping(address owner => mapping(address spender => mapping(uint256 id => uint256))) private _allowances; |
||||
|
||||
error ERC6909InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 id); |
||||
error ERC6909InsufficientAllowance(address spender, uint256 allowance, uint256 needed, uint256 id); |
||||
error ERC6909InvalidApprover(address approver); |
||||
error ERC6909InvalidReceiver(address receiver); |
||||
error ERC6909InvalidSender(address sender); |
||||
error ERC6909InvalidSpender(address spender); |
||||
|
||||
/// @inheritdoc IERC165 |
||||
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { |
||||
return interfaceId == type(IERC6909).interfaceId || super.supportsInterface(interfaceId); |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909 |
||||
function balanceOf(address owner, uint256 id) public view virtual override returns (uint256) { |
||||
return _balances[owner][id]; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909 |
||||
function allowance(address owner, address spender, uint256 id) public view virtual override returns (uint256) { |
||||
return _allowances[owner][spender][id]; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909 |
||||
function isOperator(address owner, address spender) public view virtual override returns (bool) { |
||||
return _operatorApprovals[owner][spender]; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909 |
||||
function approve(address spender, uint256 id, uint256 amount) public virtual override returns (bool) { |
||||
_approve(_msgSender(), spender, id, amount); |
||||
return true; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909 |
||||
function setOperator(address spender, bool approved) public virtual override returns (bool) { |
||||
_setOperator(_msgSender(), spender, approved); |
||||
return true; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909 |
||||
function transfer(address receiver, uint256 id, uint256 amount) public virtual override returns (bool) { |
||||
_transfer(_msgSender(), receiver, id, amount); |
||||
return true; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909 |
||||
function transferFrom( |
||||
address sender, |
||||
address receiver, |
||||
uint256 id, |
||||
uint256 amount |
||||
) public virtual override returns (bool) { |
||||
address caller = _msgSender(); |
||||
if (sender != caller && !isOperator(sender, caller)) { |
||||
_spendAllowance(sender, caller, id, amount); |
||||
} |
||||
_transfer(sender, receiver, id, amount); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* @dev Creates `amount` of token `id` and assigns them to `account`, by transferring it from address(0). |
||||
* Relies on the `_update` mechanism |
||||
* |
||||
* Emits a {Transfer} event with `from` set to the zero address. |
||||
* |
||||
* NOTE: This function is not virtual, {_update} should be overridden instead. |
||||
*/ |
||||
function _mint(address to, uint256 id, uint256 amount) internal { |
||||
if (to == address(0)) { |
||||
revert ERC6909InvalidReceiver(address(0)); |
||||
} |
||||
_update(address(0), to, id, amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Moves `amount` of token `id` from `from` to `to` without checking for approvals. |
||||
* |
||||
* This internal function is equivalent to {transfer}, and can be used to |
||||
* e.g. implement automatic token fees, slashing mechanisms, etc. |
||||
* |
||||
* Emits a {Transfer} event. |
||||
* |
||||
* NOTE: This function is not virtual, {_update} should be overridden instead. |
||||
*/ |
||||
function _transfer(address from, address to, uint256 id, uint256 amount) internal { |
||||
if (from == address(0)) { |
||||
revert ERC6909InvalidSender(address(0)); |
||||
} |
||||
if (to == address(0)) { |
||||
revert ERC6909InvalidReceiver(address(0)); |
||||
} |
||||
_update(from, to, id, amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Destroys a `amount` of token `id` from `account`. |
||||
* Relies on the `_update` mechanism. |
||||
* |
||||
* Emits a {Transfer} event with `to` set to the zero address. |
||||
* |
||||
* NOTE: This function is not virtual, {_update} should be overridden instead |
||||
*/ |
||||
function _burn(address from, uint256 id, uint256 amount) internal { |
||||
if (from == address(0)) { |
||||
revert ERC6909InvalidSender(address(0)); |
||||
} |
||||
_update(from, address(0), id, amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Transfers `amount` of token `id` from `from` to `to`, or alternatively mints (or burns) if `from` |
||||
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding |
||||
* this function. |
||||
* |
||||
* Emits a {Transfer} event. |
||||
*/ |
||||
function _update(address from, address to, uint256 id, uint256 amount) internal virtual { |
||||
address caller = _msgSender(); |
||||
|
||||
if (from != address(0)) { |
||||
uint256 fromBalance = _balances[from][id]; |
||||
if (fromBalance < amount) { |
||||
revert ERC6909InsufficientBalance(from, fromBalance, amount, id); |
||||
} |
||||
unchecked { |
||||
// Overflow not possible: amount <= fromBalance. |
||||
_balances[from][id] = fromBalance - amount; |
||||
} |
||||
} |
||||
if (to != address(0)) { |
||||
_balances[to][id] += amount; |
||||
} |
||||
|
||||
emit Transfer(caller, from, to, id, amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Sets `amount` as the allowance of `spender` over the `owner`'s `id` tokens. |
||||
* |
||||
* This internal function is equivalent to `approve`, and can be used to e.g. set automatic allowances for certain |
||||
* subsystems, etc. |
||||
* |
||||
* Emits an {Approval} event. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `owner` cannot be the zero address. |
||||
* - `spender` cannot be the zero address. |
||||
*/ |
||||
function _approve(address owner, address spender, uint256 id, uint256 amount) internal virtual { |
||||
if (owner == address(0)) { |
||||
revert ERC6909InvalidApprover(address(0)); |
||||
} |
||||
if (spender == address(0)) { |
||||
revert ERC6909InvalidSpender(address(0)); |
||||
} |
||||
_allowances[owner][spender][id] = amount; |
||||
emit Approval(owner, spender, id, amount); |
||||
} |
||||
|
||||
/** |
||||
* @dev Approve `spender` to operate on all of `owner`'s tokens |
||||
* |
||||
* This internal function is equivalent to `setOperator`, and can be used to e.g. set automatic allowances for |
||||
* certain subsystems, etc. |
||||
* |
||||
* Emits an {OperatorSet} event. |
||||
* |
||||
* Requirements: |
||||
* |
||||
* - `owner` cannot be the zero address. |
||||
* - `spender` cannot be the zero address. |
||||
*/ |
||||
function _setOperator(address owner, address spender, bool approved) internal virtual { |
||||
if (owner == address(0)) { |
||||
revert ERC6909InvalidApprover(address(0)); |
||||
} |
||||
if (spender == address(0)) { |
||||
revert ERC6909InvalidSpender(address(0)); |
||||
} |
||||
_operatorApprovals[owner][spender] = approved; |
||||
emit OperatorSet(owner, spender, approved); |
||||
} |
||||
|
||||
/** |
||||
* @dev Updates `owner`'s allowance for `spender` based on spent `amount`. |
||||
* |
||||
* Does not update the allowance value in case of infinite allowance. |
||||
* Revert if not enough allowance is available. |
||||
* |
||||
* Does not emit an {Approval} event. |
||||
*/ |
||||
function _spendAllowance(address owner, address spender, uint256 id, uint256 amount) internal virtual { |
||||
uint256 currentAllowance = allowance(owner, spender, id); |
||||
if (currentAllowance < type(uint256).max) { |
||||
if (currentAllowance < amount) { |
||||
revert ERC6909InsufficientAllowance(spender, currentAllowance, amount, id); |
||||
} |
||||
unchecked { |
||||
_allowances[owner][spender][id] = currentAllowance - amount; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,52 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.20; |
||||
|
||||
import {ERC6909} from "../draft-ERC6909.sol"; |
||||
import {IERC6909ContentURI} from "../../../interfaces/draft-IERC6909.sol"; |
||||
|
||||
/** |
||||
* @dev Implementation of the Content URI extension defined in ERC6909. |
||||
*/ |
||||
contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { |
||||
string private _contractURI; |
||||
mapping(uint256 id => string) private _tokenURIs; |
||||
|
||||
/// @dev Event emitted when the contract URI is changed. See https://eips.ethereum.org/EIPS/eip-7572[ERC-7572] for details. |
||||
event ContractURIUpdated(); |
||||
|
||||
/// @dev See {IERC1155-URI} |
||||
event URI(string value, uint256 indexed id); |
||||
|
||||
/// @inheritdoc IERC6909ContentURI |
||||
function contractURI() public view virtual override returns (string memory) { |
||||
return _contractURI; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909ContentURI |
||||
function tokenURI(uint256 id) public view virtual override returns (string memory) { |
||||
return _tokenURIs[id]; |
||||
} |
||||
|
||||
/** |
||||
* @dev Sets the {contractURI} for the contract. |
||||
* |
||||
* Emits a {ContractURIUpdated} event. |
||||
*/ |
||||
function _setContractURI(string memory newContractURI) internal virtual { |
||||
_contractURI = newContractURI; |
||||
|
||||
emit ContractURIUpdated(); |
||||
} |
||||
|
||||
/** |
||||
* @dev Sets the {tokenURI} for a given token of type `id`. |
||||
* |
||||
* Emits a {URI} event. |
||||
*/ |
||||
function _setTokenURI(uint256 id, string memory newTokenURI) internal virtual { |
||||
_tokenURIs[id] = newTokenURI; |
||||
|
||||
emit URI(newTokenURI, id); |
||||
} |
||||
} |
@ -0,0 +1,76 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
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; |
||||
string symbol; |
||||
uint8 decimals; |
||||
} |
||||
|
||||
mapping(uint256 id => TokenMetadata) private _tokenMetadata; |
||||
|
||||
/// @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`. |
||||
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; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909Metadata |
||||
function symbol(uint256 id) public view virtual override returns (string memory) { |
||||
return _tokenMetadata[id].symbol; |
||||
} |
||||
|
||||
/// @inheritdoc IERC6909Metadata |
||||
function decimals(uint256 id) public view virtual override returns (uint8) { |
||||
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); |
||||
} |
||||
} |
@ -0,0 +1,34 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
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 ERC6909TokenSupply is ERC6909, IERC6909TokenSupply { |
||||
mapping(uint256 id => uint256) private _totalSupplies; |
||||
|
||||
/// @inheritdoc IERC6909TokenSupply |
||||
function totalSupply(uint256 id) public view virtual override returns (uint256) { |
||||
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); |
||||
|
||||
if (from == address(0)) { |
||||
_totalSupplies[id] += amount; |
||||
} |
||||
if (to == address(0)) { |
||||
unchecked { |
||||
// amount <= _balances[id][from] <= _totalSupplies[id] |
||||
_totalSupplies[id] -= amount; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
= ERC-6909 |
||||
|
||||
ERC-6909 is a draft EIP that draws on ERC-1155 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. |
||||
|
||||
== Changes from ERC-1155 |
||||
|
||||
There are three main changes from ERC-1155 which are as follows: |
||||
|
||||
. 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 |
||||
|
||||
We'll use ERC-6909 to track multiple items in a game, each having their own unique attributes. All item types will 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 mint 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/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. |
||||
|
||||
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 |
||||
---- |
@ -1,3 +1,10 @@ |
||||
process.on('unhandledRejection', reason => { |
||||
throw new Error(reason); |
||||
// If the reason is already an Error object, throw it directly to preserve the stack trace.
|
||||
if (reason instanceof Error) { |
||||
throw reason; |
||||
} else { |
||||
// If the reason is not an Error (e.g., a string, number, or other primitive),
|
||||
// create a new Error object with the reason as its message.
|
||||
throw new Error(`Unhandled rejection: ${reason}`); |
||||
} |
||||
}); |
||||
|
@ -0,0 +1,216 @@ |
||||
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 firstTokenSupply = 2000n; |
||||
const secondTokenSupply = 3000n; |
||||
const amount = 100n; |
||||
|
||||
describe('like an ERC6909', function () { |
||||
describe('balanceOf', function () { |
||||
describe("when accounts don't own tokens", function () { |
||||
it('return zero', async function () { |
||||
await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.be.equal(0n); |
||||
await expect(this.token.balanceOf(this.holder, secondTokenId)).to.eventually.be.equal(0n); |
||||
await expect(this.token.balanceOf(this.other, randomTokenId)).to.eventually.be.equal(0n); |
||||
}); |
||||
}); |
||||
|
||||
describe('when accounts own some tokens', function () { |
||||
beforeEach(async function () { |
||||
await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply); |
||||
await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply); |
||||
}); |
||||
|
||||
it('returns amount owned by the given address', async function () { |
||||
await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.be.equal(firstTokenSupply); |
||||
await expect(this.token.balanceOf(this.holder, secondTokenId)).to.eventually.be.equal(secondTokenSupply); |
||||
await expect(this.token.balanceOf(this.other, firstTokenId)).to.eventually.be.equal(0n); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('setOperator', function () { |
||||
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); |
||||
|
||||
// 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.other, this.operator)).to.eventually.be.false; |
||||
}); |
||||
|
||||
it('can unset the operator approval', async function () { |
||||
await this.token.connect(this.holder).setOperator(this.operator, true); |
||||
|
||||
// before
|
||||
await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true; |
||||
|
||||
// unset
|
||||
await expect(this.token.connect(this.holder).setOperator(this.operator, false)) |
||||
.to.emit(this.token, 'OperatorSet') |
||||
.withArgs(this.holder, this.operator, false); |
||||
|
||||
// after
|
||||
await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.false; |
||||
}); |
||||
|
||||
it('cannot set address(0) as an operator', async function () { |
||||
await expect(this.token.connect(this.holder).setOperator(ethers.ZeroAddress, true)) |
||||
.to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSpender') |
||||
.withArgs(ethers.ZeroAddress); |
||||
}); |
||||
}); |
||||
|
||||
describe('approve', function () { |
||||
it('emits an Approval event and updates allowance', async function () { |
||||
await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenSupply)) |
||||
.to.emit(this.token, 'Approval') |
||||
.withArgs(this.holder, this.operator, firstTokenId, firstTokenSupply); |
||||
|
||||
// approved
|
||||
await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal( |
||||
firstTokenSupply, |
||||
); |
||||
// other account is not approved
|
||||
await expect(this.token.allowance(this.other, this.operator, firstTokenId)).to.eventually.be.equal(0n); |
||||
}); |
||||
|
||||
it('can unset the approval', async function () { |
||||
await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, 0n)) |
||||
.to.emit(this.token, 'Approval') |
||||
.withArgs(this.holder, this.operator, firstTokenId, 0n); |
||||
await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(0n); |
||||
}); |
||||
|
||||
it('cannot give allowance to address(0)', async function () { |
||||
await expect(this.token.connect(this.holder).approve(ethers.ZeroAddress, firstTokenId, firstTokenSupply)) |
||||
.to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSpender') |
||||
.withArgs(ethers.ZeroAddress); |
||||
}); |
||||
}); |
||||
|
||||
describe('transfer', function () { |
||||
beforeEach(async function () { |
||||
await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply); |
||||
await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply); |
||||
}); |
||||
|
||||
it('transfers to the zero address are blocked', async function () { |
||||
await expect(this.token.connect(this.holder).transfer(ethers.ZeroAddress, firstTokenId, firstTokenSupply)) |
||||
.to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver') |
||||
.withArgs(ethers.ZeroAddress); |
||||
}); |
||||
|
||||
it('reverts when insufficient balance', async function () { |
||||
await expect(this.token.connect(this.holder).transfer(this.recipient, firstTokenId, firstTokenSupply + 1n)) |
||||
.to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientBalance') |
||||
.withArgs(this.holder, firstTokenSupply, firstTokenSupply + 1n, firstTokenId); |
||||
}); |
||||
|
||||
it('emits event and transfers tokens', async function () { |
||||
await expect(this.token.connect(this.holder).transfer(this.recipient, firstTokenId, amount)) |
||||
.to.emit(this.token, 'Transfer') |
||||
.withArgs(this.holder, this.holder, this.recipient, firstTokenId, amount); |
||||
|
||||
await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount); |
||||
await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount); |
||||
}); |
||||
}); |
||||
|
||||
describe('transferFrom', function () { |
||||
beforeEach(async function () { |
||||
await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply); |
||||
await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply); |
||||
}); |
||||
|
||||
it('transfer from self', async function () { |
||||
await expect(this.token.connect(this.holder).transferFrom(this.holder, this.recipient, firstTokenId, amount)) |
||||
.to.emit(this.token, 'Transfer') |
||||
.withArgs(this.holder, this.holder, this.recipient, firstTokenId, amount); |
||||
|
||||
await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount); |
||||
await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount); |
||||
}); |
||||
|
||||
describe('with approval', async function () { |
||||
beforeEach(async function () { |
||||
await this.token.connect(this.holder).approve(this.operator, firstTokenId, amount); |
||||
}); |
||||
|
||||
it('reverts when insufficient allowance', async function () { |
||||
await expect( |
||||
this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount + 1n), |
||||
) |
||||
.to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientAllowance') |
||||
.withArgs(this.operator, amount, amount + 1n, firstTokenId); |
||||
}); |
||||
|
||||
it('should emit transfer event and update approval (without an Approval event)', async function () { |
||||
await expect( |
||||
this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount - 1n), |
||||
) |
||||
.to.emit(this.token, 'Transfer') |
||||
.withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount - 1n) |
||||
.to.not.emit(this.token, 'Approval'); |
||||
|
||||
await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal(1n); |
||||
}); |
||||
|
||||
it("shouldn't reduce allowance when infinite", async function () { |
||||
await this.token.connect(this.holder).approve(this.operator, firstTokenId, ethers.MaxUint256); |
||||
|
||||
await this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount); |
||||
|
||||
await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal( |
||||
ethers.MaxUint256, |
||||
); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
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, firstTokenSupply); |
||||
}); |
||||
|
||||
it('operator can transfer', async function () { |
||||
await expect(this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount)) |
||||
.to.emit(this.token, 'Transfer') |
||||
.withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount); |
||||
|
||||
await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount); |
||||
await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount); |
||||
}); |
||||
|
||||
it('operator transfer does not reduce allowance', async function () { |
||||
// Also give allowance
|
||||
await this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenSupply); |
||||
|
||||
await expect(this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount)) |
||||
.to.emit(this.token, 'Transfer') |
||||
.withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount); |
||||
|
||||
await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal( |
||||
firstTokenSupply, |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
shouldSupportInterfaces(['ERC6909']); |
||||
}); |
||||
} |
||||
|
||||
module.exports = { |
||||
shouldBehaveLikeERC6909, |
||||
}; |
@ -0,0 +1,104 @@ |
||||
const { ethers } = require('hardhat'); |
||||
const { expect } = require('chai'); |
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); |
||||
|
||||
const { shouldBehaveLikeERC6909 } = require('./ERC6909.behavior'); |
||||
|
||||
async function fixture() { |
||||
const [holder, operator, recipient, other] = await ethers.getSigners(); |
||||
const token = await ethers.deployContract('$ERC6909'); |
||||
return { token, holder, operator, recipient, other }; |
||||
} |
||||
|
||||
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 () { |
||||
await 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 () { |
||||
await expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
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)) |
||||
.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 () { |
||||
await 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 () { |
||||
await expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue - burnValue); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
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); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,49 @@ |
||||
const { ethers } = require('hardhat'); |
||||
const { expect } = require('chai'); |
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); |
||||
|
||||
async function fixture() { |
||||
const token = await ethers.deployContract('$ERC6909ContentURI'); |
||||
return { token }; |
||||
} |
||||
|
||||
describe('ERC6909ContentURI', function () { |
||||
beforeEach(async function () { |
||||
Object.assign(this, await loadFixture(fixture)); |
||||
}); |
||||
|
||||
describe('contractURI', function () { |
||||
it('is empty string be default', async function () { |
||||
await expect(this.token.contractURI()).to.eventually.equal(''); |
||||
}); |
||||
|
||||
it('is settable by internal setter', async function () { |
||||
await this.token.$_setContractURI('https://example.com'); |
||||
await 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 () { |
||||
it('is empty string be default', async function () { |
||||
await 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
|
||||
await 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, 'URI') |
||||
.withArgs('https://example.com/1', 1n); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,58 @@ |
||||
const { ethers } = require('hardhat'); |
||||
const { expect } = require('chai'); |
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); |
||||
|
||||
async function fixture() { |
||||
const token = await ethers.deployContract('$ERC6909Metadata'); |
||||
return { token }; |
||||
} |
||||
|
||||
describe('ERC6909Metadata', function () { |
||||
beforeEach(async function () { |
||||
Object.assign(this, await loadFixture(fixture)); |
||||
}); |
||||
|
||||
describe('name', function () { |
||||
it('is empty string be default', async function () { |
||||
await expect(this.token.name(1n)).to.eventually.equal(''); |
||||
}); |
||||
|
||||
it('can be set by dedicated setter', async function () { |
||||
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
|
||||
await expect(this.token.name(2n)).to.eventually.equal(''); |
||||
}); |
||||
}); |
||||
|
||||
describe('symbol', function () { |
||||
it('is empty string be default', async function () { |
||||
await expect(this.token.symbol(1n)).to.eventually.equal(''); |
||||
}); |
||||
|
||||
it('can be set by dedicated setter', async function () { |
||||
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
|
||||
await expect(this.token.symbol(2n)).to.eventually.equal(''); |
||||
}); |
||||
}); |
||||
|
||||
describe('decimals', function () { |
||||
it('is 0 by default', async function () { |
||||
await expect(this.token.decimals(1n)).to.eventually.equal(0); |
||||
}); |
||||
|
||||
it('can be set by dedicated setter', async function () { |
||||
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
|
||||
await expect(this.token.decimals(2n)).to.eventually.equal(0); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,53 @@ |
||||
const { ethers } = require('hardhat'); |
||||
const { expect } = require('chai'); |
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); |
||||
|
||||
const { shouldBehaveLikeERC6909 } = require('../ERC6909.behavior'); |
||||
|
||||
async function fixture() { |
||||
const [holder, operator, recipient, other] = await ethers.getSigners(); |
||||
const token = await ethers.deployContract('$ERC6909TokenSupply'); |
||||
return { token, holder, operator, recipient, other }; |
||||
} |
||||
|
||||
describe('ERC6909TokenSupply', function () { |
||||
beforeEach(async function () { |
||||
Object.assign(this, await loadFixture(fixture)); |
||||
}); |
||||
|
||||
shouldBehaveLikeERC6909(); |
||||
|
||||
describe('totalSupply', function () { |
||||
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 () { |
||||
await this.token.$_mint(this.holder, 1n, 17n); |
||||
await expect(this.token.totalSupply(1n)).to.eventually.be.equal(17n); |
||||
}); |
||||
|
||||
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.recipient, 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); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue