浏览代码

Move ERC721 and ERC1155 receiver checks to dedicate libraries (#4845)

Co-authored-by: Ernesto García <ernestognw@gmail.com>
Hadrien Croubois 1 年之前
父节点
当前提交
7eba10dd1e

+ 5 - 0
.changeset/poor-chefs-cheat.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`ERC721Utils` and `ERC1155Utils`: Add reusable libraries with functions to perform acceptance checks on `IERC721Receiver` and `IERC1155Receiver` implementers.

+ 3 - 69
contracts/token/ERC1155/ERC1155.sol

@@ -4,8 +4,8 @@
 pragma solidity ^0.8.20;
 
 import {IERC1155} from "./IERC1155.sol";
-import {IERC1155Receiver} from "./IERC1155Receiver.sol";
 import {IERC1155MetadataURI} from "./extensions/IERC1155MetadataURI.sol";
+import {ERC1155Utils} from "./utils/ERC1155Utils.sol";
 import {Context} from "../../utils/Context.sol";
 import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol";
 import {Arrays} from "../../utils/Arrays.sol";
@@ -203,9 +203,9 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER
             if (ids.length == 1) {
                 uint256 id = ids.unsafeMemoryAccess(0);
                 uint256 value = values.unsafeMemoryAccess(0);
-                _doSafeTransferAcceptanceCheck(operator, from, to, id, value, data);
+                ERC1155Utils.checkOnERC1155Received(operator, from, to, id, value, data);
             } else {
-                _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, values, data);
+                ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, data);
             }
         }
     }
@@ -374,72 +374,6 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER
         emit ApprovalForAll(owner, operator, approved);
     }
 
-    /**
-     * @dev Performs an acceptance check by calling {IERC1155-onERC1155Received} on the `to` address
-     * if it contains code at the moment of execution.
-     */
-    function _doSafeTransferAcceptanceCheck(
-        address operator,
-        address from,
-        address to,
-        uint256 id,
-        uint256 value,
-        bytes memory data
-    ) private {
-        if (to.code.length > 0) {
-            try IERC1155Receiver(to).onERC1155Received(operator, from, id, value, data) returns (bytes4 response) {
-                if (response != IERC1155Receiver.onERC1155Received.selector) {
-                    // Tokens rejected
-                    revert ERC1155InvalidReceiver(to);
-                }
-            } catch (bytes memory reason) {
-                if (reason.length == 0) {
-                    // non-IERC1155Receiver implementer
-                    revert ERC1155InvalidReceiver(to);
-                } else {
-                    /// @solidity memory-safe-assembly
-                    assembly {
-                        revert(add(32, reason), mload(reason))
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * @dev Performs a batch acceptance check by calling {IERC1155-onERC1155BatchReceived} on the `to` address
-     * if it contains code at the moment of execution.
-     */
-    function _doSafeBatchTransferAcceptanceCheck(
-        address operator,
-        address from,
-        address to,
-        uint256[] memory ids,
-        uint256[] memory values,
-        bytes memory data
-    ) private {
-        if (to.code.length > 0) {
-            try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, values, data) returns (
-                bytes4 response
-            ) {
-                if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
-                    // Tokens rejected
-                    revert ERC1155InvalidReceiver(to);
-                }
-            } catch (bytes memory reason) {
-                if (reason.length == 0) {
-                    // non-IERC1155Receiver implementer
-                    revert ERC1155InvalidReceiver(to);
-                } else {
-                    /// @solidity memory-safe-assembly
-                    assembly {
-                        revert(add(32, reason), mload(reason))
-                    }
-                }
-            }
-        }
-    }
-
     /**
      * @dev Creates an array in memory with only one value for each of the elements provided.
      */

+ 87 - 0
contracts/token/ERC1155/utils/ERC1155Utils.sol

@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {IERC1155Receiver} from "../IERC1155Receiver.sol";
+import {IERC1155Errors} from "../../../interfaces/draft-IERC6093.sol";
+
+/**
+ * @dev Library that provide common ERC-1155 utility functions.
+ *
+ * See https://eips.ethereum.org/EIPS/eip-1155[ERC-1155].
+ */
+library ERC1155Utils {
+    /**
+     * @dev Performs an acceptance check for the provided `operator` by calling {IERC1155-onERC1155Received}
+     * on the `to` address. The `operator` is generally the address that initiated the token transfer (i.e. `msg.sender`).
+     *
+     * The acceptance call is not executed and treated as a no-op if the target address is doesn't contain code (i.e. an EOA).
+     * Otherwise, the recipient must implement {IERC1155Receiver-onERC1155Received} and return the acceptance magic value to accept
+     * the transfer.
+     */
+    function checkOnERC1155Received(
+        address operator,
+        address from,
+        address to,
+        uint256 id,
+        uint256 value,
+        bytes memory data
+    ) internal {
+        if (to.code.length > 0) {
+            try IERC1155Receiver(to).onERC1155Received(operator, from, id, value, data) returns (bytes4 response) {
+                if (response != IERC1155Receiver.onERC1155Received.selector) {
+                    // Tokens rejected
+                    revert IERC1155Errors.ERC1155InvalidReceiver(to);
+                }
+            } catch (bytes memory reason) {
+                if (reason.length == 0) {
+                    // non-IERC1155Receiver implementer
+                    revert IERC1155Errors.ERC1155InvalidReceiver(to);
+                } else {
+                    /// @solidity memory-safe-assembly
+                    assembly {
+                        revert(add(32, reason), mload(reason))
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @dev Performs a batch acceptance check for the provided `operator` by calling {IERC1155-onERC1155BatchReceived}
+     * on the `to` address. The `operator` is generally the address that initiated the token transfer (i.e. `msg.sender`).
+     *
+     * The acceptance call is not executed and treated as a no-op if the target address is doesn't contain code (i.e. an EOA).
+     * Otherwise, the recipient must implement {IERC1155Receiver-onERC1155Received} and return the acceptance magic value to accept
+     * the transfer.
+     */
+    function checkOnERC1155BatchReceived(
+        address operator,
+        address from,
+        address to,
+        uint256[] memory ids,
+        uint256[] memory values,
+        bytes memory data
+    ) internal {
+        if (to.code.length > 0) {
+            try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, values, data) returns (
+                bytes4 response
+            ) {
+                if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
+                    // Tokens rejected
+                    revert IERC1155Errors.ERC1155InvalidReceiver(to);
+                }
+            } catch (bytes memory reason) {
+                if (reason.length == 0) {
+                    // non-IERC1155Receiver implementer
+                    revert IERC1155Errors.ERC1155InvalidReceiver(to);
+                } else {
+                    /// @solidity memory-safe-assembly
+                    assembly {
+                        revert(add(32, reason), mload(reason))
+                    }
+                }
+            }
+        }
+    }
+}

+ 4 - 32
contracts/token/ERC721/ERC721.sol

@@ -4,8 +4,8 @@
 pragma solidity ^0.8.20;
 
 import {IERC721} from "./IERC721.sol";
-import {IERC721Receiver} from "./IERC721Receiver.sol";
 import {IERC721Metadata} from "./extensions/IERC721Metadata.sol";
+import {ERC721Utils} from "./utils/ERC721Utils.sol";
 import {Context} from "../../utils/Context.sol";
 import {Strings} from "../../utils/Strings.sol";
 import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol";
@@ -158,7 +158,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
      */
     function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
         transferFrom(from, to, tokenId);
-        _checkOnERC721Received(from, to, tokenId, data);
+        ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
     }
 
     /**
@@ -311,7 +311,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
      */
     function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
         _mint(to, tokenId);
-        _checkOnERC721Received(address(0), to, tokenId, data);
+        ERC721Utils.checkOnERC721Received(_msgSender(), address(0), to, tokenId, data);
     }
 
     /**
@@ -384,7 +384,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
      */
     function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual {
         _transfer(from, to, tokenId);
-        _checkOnERC721Received(from, to, tokenId, data);
+        ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
     }
 
     /**
@@ -452,32 +452,4 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
         }
         return owner;
     }
-
-    /**
-     * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target address. This will revert if the
-     * recipient doesn't accept the token transfer. The call is not executed if the target address is not a contract.
-     *
-     * @param from address representing the previous owner of the given token ID
-     * @param to target address that will receive the tokens
-     * @param tokenId uint256 ID of the token to be transferred
-     * @param data bytes optional data to send along with the call
-     */
-    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private {
-        if (to.code.length > 0) {
-            try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
-                if (retval != IERC721Receiver.onERC721Received.selector) {
-                    revert ERC721InvalidReceiver(to);
-                }
-            } catch (bytes memory reason) {
-                if (reason.length == 0) {
-                    revert ERC721InvalidReceiver(to);
-                } else {
-                    /// @solidity memory-safe-assembly
-                    assembly {
-                        revert(add(32, reason), mload(reason))
-                    }
-                }
-            }
-        }
-    }
 }

+ 48 - 0
contracts/token/ERC721/utils/ERC721Utils.sol

@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {IERC721Receiver} from "../IERC721Receiver.sol";
+import {IERC721Errors} from "../../../interfaces/draft-IERC6093.sol";
+
+/**
+ * @dev Library that provide common ERC-721 utility functions.
+ *
+ * See https://eips.ethereum.org/EIPS/eip-721[ERC-721].
+ */
+library ERC721Utils {
+    /**
+     * @dev Performs an acceptance check for the provided `operator` by calling {IERC721-onERC721Received}
+     * on the `to` address. The `operator` is generally the address that initiated the token transfer (i.e. `msg.sender`).
+     *
+     * The acceptance call is not executed and treated as a no-op if the target address is doesn't contain code (i.e. an EOA).
+     * Otherwise, the recipient must implement {IERC721Receiver-onERC721Received} and return the acceptance magic value to accept
+     * the transfer.
+     */
+    function checkOnERC721Received(
+        address operator,
+        address from,
+        address to,
+        uint256 tokenId,
+        bytes memory data
+    ) internal {
+        if (to.code.length > 0) {
+            try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) {
+                if (retval != IERC721Receiver.onERC721Received.selector) {
+                    // Token rejected
+                    revert IERC721Errors.ERC721InvalidReceiver(to);
+                }
+            } catch (bytes memory reason) {
+                if (reason.length == 0) {
+                    // non-IERC721Receiver implementer
+                    revert IERC721Errors.ERC721InvalidReceiver(to);
+                } else {
+                    /// @solidity memory-safe-assembly
+                    assembly {
+                        revert(add(32, reason), mload(reason))
+                    }
+                }
+            }
+        }
+    }
+}

+ 299 - 0
test/token/ERC1155/utils/ERC1155Utils.test.js

@@ -0,0 +1,299 @@
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+const { RevertType } = require('../../../helpers/enums');
+const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
+
+const firstTokenId = 1n;
+const secondTokenId = 2n;
+const firstTokenValue = 1000n;
+const secondTokenValue = 1000n;
+
+const RECEIVER_SINGLE_MAGIC_VALUE = '0xf23a6e61';
+const RECEIVER_BATCH_MAGIC_VALUE = '0xbc197c81';
+
+const deployReceiver = (
+  revertType,
+  returnValueSingle = RECEIVER_SINGLE_MAGIC_VALUE,
+  returnValueBatched = RECEIVER_BATCH_MAGIC_VALUE,
+) => ethers.deployContract('$ERC1155ReceiverMock', [returnValueSingle, returnValueBatched, revertType]);
+
+const fixture = async () => {
+  const [eoa, operator, owner] = await ethers.getSigners();
+  const utils = await ethers.deployContract('$ERC1155Utils');
+
+  const receivers = {
+    correct: await deployReceiver(RevertType.None),
+    invalid: await deployReceiver(RevertType.None, '0xdeadbeef', '0xdeadbeef'),
+    message: await deployReceiver(RevertType.RevertWithMessage),
+    empty: await deployReceiver(RevertType.RevertWithoutMessage),
+    customError: await deployReceiver(RevertType.RevertWithCustomError),
+    panic: await deployReceiver(RevertType.Panic),
+    nonReceiver: await ethers.deployContract('CallReceiverMock'),
+    eoa,
+  };
+
+  return { operator, owner, utils, receivers };
+};
+
+describe('ERC1155Utils', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  describe('onERC1155Received', function () {
+    it('succeeds when called by an EOA', async function () {
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.eoa,
+          firstTokenId,
+          firstTokenValue,
+          '0x',
+        ),
+      ).to.not.be.reverted;
+    });
+
+    it('succeeds when data is passed', async function () {
+      const data = '0x12345678';
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.correct,
+          firstTokenId,
+          firstTokenValue,
+          data,
+        ),
+      ).to.not.be.reverted;
+    });
+
+    it('succeeds when data is empty', async function () {
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.correct,
+          firstTokenId,
+          firstTokenValue,
+          '0x',
+        ),
+      ).to.not.be.reverted;
+    });
+
+    it('reverts when receiver returns invalid value', async function () {
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.invalid,
+          firstTokenId,
+          firstTokenValue,
+          '0x',
+        ),
+      )
+        .to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
+        .withArgs(this.receivers.invalid);
+    });
+
+    it('reverts when receiver reverts with message', async function () {
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.message,
+          firstTokenId,
+          firstTokenValue,
+          '0x',
+        ),
+      ).to.be.revertedWith('ERC1155ReceiverMock: reverting on receive');
+    });
+
+    it('reverts when receiver reverts without message', async function () {
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.empty,
+          firstTokenId,
+          firstTokenValue,
+          '0x',
+        ),
+      )
+        .to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
+        .withArgs(this.receivers.empty);
+    });
+
+    it('reverts when receiver reverts with custom error', async function () {
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.customError,
+          firstTokenId,
+          firstTokenValue,
+          '0x',
+        ),
+      )
+        .to.be.revertedWithCustomError(this.receivers.customError, 'CustomError')
+        .withArgs(RECEIVER_SINGLE_MAGIC_VALUE);
+    });
+
+    it('reverts when receiver panics', async function () {
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.panic,
+          firstTokenId,
+          firstTokenValue,
+          '0x',
+        ),
+      ).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO);
+    });
+
+    it('reverts when receiver does not implement onERC1155Received', async function () {
+      await expect(
+        this.utils.$checkOnERC1155Received(
+          this.operator,
+          this.owner,
+          this.receivers.nonReceiver,
+          firstTokenId,
+          firstTokenValue,
+          '0x',
+        ),
+      )
+        .to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
+        .withArgs(this.receivers.nonReceiver);
+    });
+  });
+
+  describe('onERC1155BatchReceived', function () {
+    it('succeeds when called by an EOA', async function () {
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.eoa,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          '0x',
+        ),
+      ).to.not.be.reverted;
+    });
+
+    it('succeeds when data is passed', async function () {
+      const data = '0x12345678';
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.correct,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          data,
+        ),
+      ).to.not.be.reverted;
+    });
+
+    it('succeeds when data is empty', async function () {
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.correct,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          '0x',
+        ),
+      ).to.not.be.reverted;
+    });
+
+    it('reverts when receiver returns invalid value', async function () {
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.invalid,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          '0x',
+        ),
+      )
+        .to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
+        .withArgs(this.receivers.invalid);
+    });
+
+    it('reverts when receiver reverts with message', async function () {
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.message,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          '0x',
+        ),
+      ).to.be.revertedWith('ERC1155ReceiverMock: reverting on batch receive');
+    });
+
+    it('reverts when receiver reverts without message', async function () {
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.empty,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          '0x',
+        ),
+      )
+        .to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
+        .withArgs(this.receivers.empty);
+    });
+
+    it('reverts when receiver reverts with custom error', async function () {
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.customError,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          '0x',
+        ),
+      )
+        .to.be.revertedWithCustomError(this.receivers.customError, 'CustomError')
+        .withArgs(RECEIVER_SINGLE_MAGIC_VALUE);
+    });
+
+    it('reverts when receiver panics', async function () {
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.panic,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          '0x',
+        ),
+      ).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO);
+    });
+
+    it('reverts when receiver does not implement onERC1155BatchReceived', async function () {
+      await expect(
+        this.utils.$checkOnERC1155BatchReceived(
+          this.operator,
+          this.owner,
+          this.receivers.nonReceiver,
+          [firstTokenId, secondTokenId],
+          [firstTokenValue, secondTokenValue],
+          '0x',
+        ),
+      )
+        .to.be.revertedWithCustomError(this.utils, 'ERC1155InvalidReceiver')
+        .withArgs(this.receivers.nonReceiver);
+    });
+  });
+});

+ 94 - 0
test/token/ERC721/utils/ERC721Utils.test.js

@@ -0,0 +1,94 @@
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+const { RevertType } = require('../../../helpers/enums');
+const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
+
+const tokenId = 1n;
+
+const RECEIVER_MAGIC_VALUE = '0x150b7a02';
+
+const deployReceiver = (revertType, returnValue = RECEIVER_MAGIC_VALUE) =>
+  ethers.deployContract('$ERC721ReceiverMock', [returnValue, revertType]);
+
+const fixture = async () => {
+  const [eoa, operator, owner] = await ethers.getSigners();
+  const utils = await ethers.deployContract('$ERC721Utils');
+
+  const receivers = {
+    correct: await deployReceiver(RevertType.None),
+    invalid: await deployReceiver(RevertType.None, '0xdeadbeef'),
+    message: await deployReceiver(RevertType.RevertWithMessage),
+    empty: await deployReceiver(RevertType.RevertWithoutMessage),
+    customError: await deployReceiver(RevertType.RevertWithCustomError),
+    panic: await deployReceiver(RevertType.Panic),
+    nonReceiver: await ethers.deployContract('CallReceiverMock'),
+    eoa,
+  };
+
+  return { operator, owner, utils, receivers };
+};
+
+describe('ERC721Utils', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  describe('onERC721Received', function () {
+    it('succeeds when called by an EOA', async function () {
+      await expect(this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.eoa, tokenId, '0x')).to
+        .not.be.reverted;
+    });
+
+    it('succeeds when data is passed', async function () {
+      const data = '0x12345678';
+      await expect(this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.correct, tokenId, data))
+        .to.not.be.reverted;
+    });
+
+    it('succeeds when data is empty', async function () {
+      await expect(this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.correct, tokenId, '0x'))
+        .to.not.be.reverted;
+    });
+
+    it('reverts when receiver returns invalid value', async function () {
+      await expect(this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.invalid, tokenId, '0x'))
+        .to.be.revertedWithCustomError(this.utils, 'ERC721InvalidReceiver')
+        .withArgs(this.receivers.invalid);
+    });
+
+    it('reverts when receiver reverts with message', async function () {
+      await expect(
+        this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.message, tokenId, '0x'),
+      ).to.be.revertedWith('ERC721ReceiverMock: reverting');
+    });
+
+    it('reverts when receiver reverts without message', async function () {
+      await expect(this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.empty, tokenId, '0x'))
+        .to.be.revertedWithCustomError(this.utils, 'ERC721InvalidReceiver')
+        .withArgs(this.receivers.empty);
+    });
+
+    it('reverts when receiver reverts with custom error', async function () {
+      await expect(
+        this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.customError, tokenId, '0x'),
+      )
+        .to.be.revertedWithCustomError(this.receivers.customError, 'CustomError')
+        .withArgs(RECEIVER_MAGIC_VALUE);
+    });
+
+    it('reverts when receiver panics', async function () {
+      await expect(
+        this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.panic, tokenId, '0x'),
+      ).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO);
+    });
+
+    it('reverts when receiver does not implement onERC721Received', async function () {
+      await expect(
+        this.utils.$checkOnERC721Received(this.operator, this.owner, this.receivers.nonReceiver, tokenId, '0x'),
+      )
+        .to.be.revertedWithCustomError(this.utils, 'ERC721InvalidReceiver')
+        .withArgs(this.receivers.nonReceiver);
+    });
+  });
+});