Merge branch 'master' into typo-fixes

pull/5489/head^2
Hadrien Croubois 3 days ago committed by GitHub
commit 602dc92c1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/brown-seals-sing.md
  2. 5
      .changeset/brown-turkeys-marry.md
  3. 5
      .changeset/chilly-guests-jam.md
  4. 5
      .changeset/dirty-bananas-shake.md
  5. 5
      .changeset/proud-cooks-do.md
  6. 5
      .changeset/ten-hats-begin.md
  7. 27
      contracts/account/utils/draft-ERC4337Utils.sol
  8. 12
      contracts/interfaces/README.adoc
  9. 26
      contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sol
  10. 14
      contracts/token/ERC20/utils/SafeERC20.sol
  11. 27
      contracts/token/ERC6909/README.adoc
  12. 224
      contracts/token/ERC6909/draft-ERC6909.sol
  13. 52
      contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol
  14. 76
      contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol
  15. 34
      contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol
  16. 2
      contracts/utils/cryptography/EIP712.sol
  17. 1
      docs/modules/ROOT/nav.adoc
  18. 47
      docs/modules/ROOT/pages/erc6909.adoc
  19. 9
      hardhat/async-test-sanity.js
  20. 22
      scripts/upgradeable/upgradeable.patch
  21. 75
      test/account/utils/draft-ERC4337Utils.test.js
  22. 15
      test/account/utils/draft-ERC7579Utils.t.sol
  23. 36
      test/token/ERC20/utils/SafeERC20.test.js
  24. 216
      test/token/ERC6909/ERC6909.behavior.js
  25. 104
      test/token/ERC6909/ERC6909.test.js
  26. 49
      test/token/ERC6909/extensions/ERC6909ContentURI.test.js
  27. 58
      test/token/ERC6909/extensions/ERC6909Metadata.test.js
  28. 53
      test/token/ERC6909/extensions/ERC6909TokenSupply.test.js
  29. 9
      test/utils/introspection/SupportsInterface.behavior.js

@ -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.

@ -17,7 +17,7 @@ library ERC4337Utils {
using Packing for *;
/// @dev Address of the entrypoint v0.7.0
IEntryPoint internal constant ENTRYPOINT = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032);
IEntryPoint internal constant ENTRYPOINT_V07 = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032);
/// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) return this value on success.
uint256 internal constant SIG_VALIDATION_SUCCESS = 0;
@ -163,29 +163,4 @@ library ERC4337Utils {
function paymasterData(PackedUserOperation calldata self) internal pure returns (bytes calldata) {
return self.paymasterAndData.length < 52 ? Calldata.emptyBytes() : self.paymasterAndData[52:];
}
/// @dev Deposit ether into the entrypoint.
function depositTo(address to, uint256 value) internal {
ENTRYPOINT.depositTo{value: value}(to);
}
/// @dev Withdraw ether from the entrypoint.
function withdrawTo(address payable to, uint256 value) internal {
ENTRYPOINT.withdrawTo(to, value);
}
/// @dev Add stake to the entrypoint.
function addStake(uint256 value, uint32 unstakeDelaySec) internal {
ENTRYPOINT.addStake{value: value}(unstakeDelaySec);
}
/// @dev Unlock stake on the entrypoint.
function unlockStake() internal {
ENTRYPOINT.unlockStake();
}
/// @dev Withdraw unlocked stake from the entrypoint.
function withdrawStake(address payable to) internal {
ENTRYPOINT.withdrawStake(to);
}
}

@ -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
@ -84,4 +88,12 @@ are useful to interact with third party contracts that implement them.
{{IERC6372}}
{{IERC6909}}
{{IERC6909ContentURI}}
{{IERC6909Metadata}}
{{IERC6909TokenSupply}}
{{IERC7674}}

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

@ -42,6 +42,20 @@ library SafeERC20 {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
*/
function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
*/
function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
return _callOptionalReturnBool(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.

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

@ -48,7 +48,9 @@ abstract contract EIP712 is IERC5267 {
ShortString private immutable _name;
ShortString private immutable _version;
// slither-disable-next-line constable-states
string private _nameFallback;
// slither-disable-next-line constable-states
string private _versionFallback;
/**

@ -13,6 +13,7 @@
** xref:erc721.adoc[ERC-721]
** xref:erc1155.adoc[ERC-1155]
** xref:erc4626.adoc[ERC-4626]
** xref:erc6909.adoc[ERC-6909]
* xref:governance.adoc[Governance]

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

@ -59,7 +59,7 @@ index ff596b0c3..000000000
-<!-- Make sure that you have reviewed the OpenZeppelin Contracts Contributor Guidelines. -->
-<!-- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CONTRIBUTING.md -->
diff --git a/README.md b/README.md
index fa7b4e31e..4799b6376 100644
index 60d0a430a..0e4f91a6d 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,9 @@
@ -110,7 +110,7 @@ index fa7b4e31e..4799b6376 100644
}
```
diff --git a/contracts/package.json b/contracts/package.json
index 845e8c403..8dc181b91 100644
index 3682eadeb..4f870d094 100644
--- a/contracts/package.json
+++ b/contracts/package.json
@@ -1,5 +1,5 @@
@ -118,7 +118,7 @@ index 845e8c403..8dc181b91 100644
- "name": "@openzeppelin/contracts",
+ "name": "@openzeppelin/contracts-upgradeable",
"description": "Secure Smart Contract library for Solidity",
"version": "5.0.2",
"version": "5.2.0",
"files": [
@@ -13,7 +13,7 @@
},
@ -140,7 +140,7 @@ index 845e8c403..8dc181b91 100644
+ }
}
diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol
index 77c4c8990..602467f40 100644
index bcb67c87a..7195c3bbd 100644
--- a/contracts/utils/cryptography/EIP712.sol
+++ b/contracts/utils/cryptography/EIP712.sol
@@ -4,7 +4,6 @@
@ -151,7 +151,7 @@ index 77c4c8990..602467f40 100644
import {IERC5267} from "../../interfaces/IERC5267.sol";
/**
@@ -28,28 +27,18 @@ import {IERC5267} from "../../interfaces/IERC5267.sol";
@@ -28,30 +27,18 @@ import {IERC5267} from "../../interfaces/IERC5267.sol";
* NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain
* separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the
* separator from the immutable values, which is cheaper than accessing a cached version in cold storage.
@ -177,14 +177,16 @@ index 77c4c8990..602467f40 100644
- ShortString private immutable _name;
- ShortString private immutable _version;
- // slither-disable-next-line constable-states
- string private _nameFallback;
- // slither-disable-next-line constable-states
- string private _versionFallback;
+ string private _name;
+ string private _version;
/**
* @dev Initializes the domain separator and parameter caches.
@@ -64,29 +53,23 @@ abstract contract EIP712 is IERC5267 {
@@ -66,29 +53,23 @@ abstract contract EIP712 is IERC5267 {
* contract upgrade].
*/
constructor(string memory name, string memory version) {
@ -222,7 +224,7 @@ index 77c4c8990..602467f40 100644
}
/**
@@ -125,6 +108,10 @@ abstract contract EIP712 is IERC5267 {
@@ -127,6 +108,10 @@ abstract contract EIP712 is IERC5267 {
uint256[] memory extensions
)
{
@ -233,7 +235,7 @@ index 77c4c8990..602467f40 100644
return (
hex"0f", // 01111
_EIP712Name(),
@@ -139,22 +126,62 @@ abstract contract EIP712 is IERC5267 {
@@ -141,22 +126,62 @@ abstract contract EIP712 is IERC5267 {
/**
* @dev The name parameter for the EIP712 domain.
*
@ -307,10 +309,10 @@ index 77c4c8990..602467f40 100644
}
}
diff --git a/package.json b/package.json
index c4b358e10..96ab2559c 100644
index f9e7d9205..c35020d51 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
@@ -34,7 +34,7 @@
},
"repository": {
"type": "git",

@ -4,16 +4,15 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { packValidationData, UserOperation } = require('../../helpers/erc4337');
const { MAX_UINT48 } = require('../../helpers/constants');
const time = require('../../helpers/time');
const ADDRESS_ONE = '0x0000000000000000000000000000000000000001';
const fixture = async () => {
const [authorizer, sender, factory, paymaster, other] = await ethers.getSigners();
const [authorizer, sender, factory, paymaster] = await ethers.getSigners();
const utils = await ethers.deployContract('$ERC4337Utils');
const SIG_VALIDATION_SUCCESS = await utils.$SIG_VALIDATION_SUCCESS();
const SIG_VALIDATION_FAILED = await utils.$SIG_VALIDATION_FAILED();
return { utils, authorizer, sender, factory, paymaster, other, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED };
return { utils, authorizer, sender, factory, paymaster, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED };
};
describe('ERC4337Utils', function () {
@ -21,6 +20,12 @@ describe('ERC4337Utils', function () {
Object.assign(this, await loadFixture(fixture));
});
describe('entrypoint', function () {
it('v0.7.0', async function () {
await expect(this.utils.$ENTRYPOINT_V07()).to.eventually.equal(entrypoint);
});
});
describe('parseValidationData', function () {
it('parses the validation data', async function () {
const authorizer = this.authorizer;
@ -285,68 +290,4 @@ describe('ERC4337Utils', function () {
});
});
});
describe('stake management', function () {
const unstakeDelaySec = 3600n;
beforeEach(async function () {
await this.authorizer.sendTransaction({ to: this.utils, value: ethers.parseEther('1') });
});
it('deposit & withdraw', async function () {
await expect(entrypoint.balanceOf(this.utils)).to.eventually.equal(0n);
// deposit
await expect(this.utils.$depositTo(this.utils, 42n)).to.changeEtherBalances(
[this.utils, entrypoint],
[-42n, 42n],
);
await expect(entrypoint.balanceOf(this.utils)).to.eventually.equal(42n);
// withdraw
await expect(this.utils.$withdrawTo(this.other, 17n)).to.changeEtherBalances(
[entrypoint, this.other],
[-17n, 17n],
);
await expect(entrypoint.balanceOf(this.utils)).to.eventually.equal(25n); // 42 - 17
});
it('stake, unlock & withdraw stake', async function () {
await expect(entrypoint.deposits(this.utils)).to.eventually.deep.equal([0n, false, 0n, 0n, 0n]);
// stake
await expect(this.utils.$addStake(42n, unstakeDelaySec)).to.changeEtherBalances(
[this.utils, entrypoint],
[-42n, 42n],
);
await expect(entrypoint.deposits(this.utils)).to.eventually.deep.equal([0n, true, 42n, unstakeDelaySec, 0n]);
// unlock
const unlockTx = this.utils.$unlockStake();
await expect(unlockTx).to.changeEtherBalances([this.utils, entrypoint], [0n, 0n]); // no ether movement
const timestamp = await time.clockFromReceipt.timestamp(unlockTx);
await expect(entrypoint.deposits(this.utils)).to.eventually.deep.equal([
0n,
false,
42n,
unstakeDelaySec,
timestamp + unstakeDelaySec,
]);
// wait
await time.increaseBy.timestamp(unstakeDelaySec);
// withdraw stake
await expect(this.utils.$withdrawStake(this.other)).to.changeEtherBalances(
[this.utils, entrypoint, this.other],
[0n, -42n, 42n],
);
await expect(entrypoint.deposits(this.utils)).to.eventually.deep.equal([0n, false, 0n, 0n, 0n]);
});
});
});

@ -20,8 +20,6 @@ contract SampleAccount is IAccount, Ownable {
using ERC4337Utils for *;
using ERC7579Utils for *;
IEntryPoint internal constant ENTRY_POINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032));
event Log(bool duringValidation, Execution[] calls);
error UnsupportedCallType(CallType callType);
@ -33,7 +31,7 @@ contract SampleAccount is IAccount, Ownable {
bytes32 userOpHash,
uint256 missingAccountFunds
) external override returns (uint256 validationData) {
require(msg.sender == address(ENTRY_POINT), "only from EP");
require(msg.sender == address(ERC4337Utils.ENTRYPOINT_V07), "only from EP");
// Check signature
if (userOpHash.toEthSignedMessageHash().recover(userOp.signature) != owner()) {
revert OwnableUnauthorizedAccount(_msgSender());
@ -81,7 +79,7 @@ contract SampleAccount is IAccount, Ownable {
}
function execute(Mode mode, bytes calldata executionCalldata) external payable {
require(msg.sender == address(this) || msg.sender == address(ENTRY_POINT), "not auth");
require(msg.sender == address(this) || msg.sender == address(ERC4337Utils.ENTRYPOINT_V07), "not auth");
(CallType callType, ExecType execType, , ) = mode.decodeMode();
@ -105,7 +103,6 @@ contract ERC7579UtilsTest is Test {
using ERC4337Utils for *;
using ERC7579Utils for *;
IEntryPoint private constant ENTRYPOINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032));
address private _owner;
uint256 private _ownerKey;
address private _account;
@ -166,7 +163,7 @@ contract ERC7579UtilsTest is Test {
userOps[0].signature = abi.encodePacked(r, s, v);
vm.recordLogs();
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary));
assertEq(_recipient1.balance, 1 wei);
assertEq(_recipient2.balance, 1 wei);
@ -224,7 +221,7 @@ contract ERC7579UtilsTest is Test {
abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector)
)
);
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary));
_collectAndPrintLogs(false);
}
@ -282,7 +279,7 @@ contract ERC7579UtilsTest is Test {
abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector)
)
);
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary));
_collectAndPrintLogs(true);
}
@ -378,7 +375,7 @@ contract ERC7579UtilsTest is Test {
}
function hashUserOperation(PackedUserOperation calldata useroperation) public view returns (bytes32) {
return useroperation.hash(address(ENTRYPOINT), block.chainid);
return useroperation.hash(address(ERC4337Utils.ENTRYPOINT_V07), block.chainid);
}
function _collectAndPrintLogs(bool includeTotalValue) internal {

@ -60,12 +60,24 @@ describe('SafeERC20', function () {
.withArgs(this.token);
});
it('returns false on trySafeTransfer', async function () {
await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n))
.to.emit(this.mock, 'return$trySafeTransfer')
.withArgs(false);
});
it('reverts on transferFrom', async function () {
await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
.withArgs(this.token);
});
it('returns false on trySafeTransferFrom', async function () {
await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n))
.to.emit(this.mock, 'return$trySafeTransferFrom')
.withArgs(false);
});
it('reverts on increaseAllowance', async function () {
// Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason)
await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason();
@ -94,12 +106,24 @@ describe('SafeERC20', function () {
.withArgs(this.token);
});
it('returns false on trySafeTransfer', async function () {
await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n))
.to.emit(this.mock, 'return$trySafeTransfer')
.withArgs(false);
});
it('reverts on transferFrom', async function () {
await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
.withArgs(this.token);
});
it('returns false on trySafeTransferFrom', async function () {
await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n))
.to.emit(this.mock, 'return$trySafeTransferFrom')
.withArgs(false);
});
it('reverts on increaseAllowance', async function () {
await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n))
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
@ -357,11 +381,23 @@ function shouldOnlyRevertOnErrors() {
.withArgs(this.mock, this.receiver, 10n);
});
it('returns true on trySafeTransfer', async function () {
await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 10n))
.to.emit(this.mock, 'return$trySafeTransfer')
.withArgs(true);
});
it("doesn't revert on transferFrom", async function () {
await expect(this.mock.$safeTransferFrom(this.token, this.owner, this.receiver, 10n))
.to.emit(this.token, 'Transfer')
.withArgs(this.owner, this.receiver, 10n);
});
it('returns true on trySafeTransferFrom', async function () {
await expect(this.mock.$trySafeTransferFrom(this.token, this.owner, this.receiver, 10n))
.to.emit(this.mock, 'return$trySafeTransferFrom')
.withArgs(true);
});
});
describe('approvals', function () {

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

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

Loading…
Cancel
Save